巴黎共享滑板車亂象多 河中打撈竟成新興工作

摘錄自2019年10月26日民視新聞報導

巴黎目前有十多家業者、提供了大概2萬輛電動滑板車,可以自由租借,帶來方便的同時,也帶來了混亂,街頭隨處可見被人亂停、亂丟的電動滑板車,還有不少直接被扔進塞納河。負責清理、回收這些電動滑板車的「巡邏隊」,成了新興工作。

塞納河畔有人用力拉著繩索,他們可不是在釣魚,而是在「釣電動滑板車」,這家電動滑板車公司的員工,3個鐘頭從河裡打撈了足足15輛。電動滑板車公司員工表示:「就像那輛的手把卡在岸邊,我們不可能直接把它拉起來,你必須下到水裡,把手把轉向才能將它弄起來。」

 

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

寮國湄公河首座發電水壩啟動 大批民眾抗議

摘錄自2019年10月29日民視新聞報導

寮國湄公河下游的第一座水力發電水壩、沙耶武里水壩29日正式啟動,就迎來了大批抗議民眾,他們擔心水壩會對當地環境,造成難以預測的嚴重損害,尤其是加劇糧食跟水源匱乏的危機,大批民眾群聚泰國與鄰近寮國交界處的黎府,跟著師父唸經祈福,抗議沙耶武里水壩正式啟動。

而隨著這座水壩正式運作,湄公河的命運也步入了重要轉折點。當地環保人士表示,「當沙耶武里水壩正式發電時,湄公河將發生變化,我們將無法預測這種變化的嚴重程度。」

雖然相關單位打包票,這項工程會保護到湄公河脆弱的生態系統,但環保組織卻反咬建商根本就是「工程先行,研究後補」。另外水壩的安全也令人憂心。去年7月,寮國東南部阿速坡省就發生水壩大潰堤,50億噸的洪水滾滾流向下游,造成40人死亡,失蹤至少上百人,6600人流離失所。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

不畏油價跌 美電動車 12 月創最高單月銷售紀錄

儘管美國汽油零售價格跟隨國際原油同步下挫到每加侖 2 美元的低水準,2014 年美國電動車銷售強勁增加 23%,12 月並創下單月最佳紀錄;顯見電動車已蔚為一股持久的趨勢,不受油價影響。   根據電動車專業網站 InsideEVs 統計,2014 年 12 月美國賣出 12,874 輛電動車,為統計以來最高單月銷售紀錄。至於 2014 年全年,電動車銷售勁揚 23%,達到 119,710 輛。   就個別品牌來看,全美電動車銷售冠軍是日產 Leaf,2014 年總計售出 30,200 輛,遙遙領先亞軍通用汽車雪佛蘭 Volt 的 18,805 輛。起價介於 3.5 萬到 4 萬美元的特斯拉 Model S 排名第 3,全年銷量為 17,300 輛,市占不及 15%。不過,在 12 月 Model S 首度擊敗售價較低的日產 Leaf(約 3 萬美元)。   2014 年美國電動車市場唯一新上市車款是 BMW,分別在 5 月及 8 月推出 i3 與 i8 ,加總的銷量僅 6,647 輛。展望 2015 年,將有數款新型電動車上路,尤其是在休旅車和越野車市場。而特斯拉的 Model X 則預計 2015 年下半年登場,是今年最受期待的電動車。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

產能尚未滿載 特斯拉要到 2020 年才可能獲利

特斯拉執行長 Elon Musk 在世界汽車業大會 (Automotive News World Congress) 中表示,以目前狀況看來,特斯拉的電動車在 2020 年前都無法開始獲利。   根據華爾街日報報導,特斯拉的 Model 3 電動車在現有會計原則下,至少要等到 2020 年產能滿載時才能正式轉盈。Model 3 是特斯拉功能性較少的電動車,一台可能售價約介於 3 萬到 4 萬美元之間,比起其他 Model S 系列一台動輒超過 10 萬美元來說相對便宜,Model 3 預計在 2017 年推出。特斯拉的股價盤後下跌 7%,來到 189.95 美元。   對於電動車的發展,Musk 也要求其他汽車公司盡速推出自家電動車。特斯拉目前電動車銷售量約為 35,000 台,預計到 2025 年可賣出數百萬台。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

Tesla 股價大跌 穆斯克荷包失血 28 億美元

電動車大廠特斯拉(Tesla) 股價重創,不僅投資人損失慘重,公司創辦人穆斯克(Elon Musk)財富更是大失血。根據經濟日報報導,Tesla 股價 2014 年 9 月至今大跌 34%,穆斯克財富也蒸發 28 億美元。   穆斯克表示,Tesla 在中國的銷售低迷,使曾經超搶手的 Tesla 股票價格 14 日大跌 11.56 美元,跌幅約 6%,若和去年 9 月每股 291.42 美元的高價相比,Tesla 股票在這段期間崩跌掉 3 分之 1 的價值。   穆斯克則擁有逾 2,800 萬股的 Tesla 股票,相當流通股總數的 4 分之 1 左右,這意味股價大跌讓穆斯克 28 億美元的財富煙消雲散。不過,雖然上述損失數字很可觀,但穆斯克本身可能不太意外,他在去年 9 月曾警告說,Tesla 的股價「有點太高」,當時投資人不予理會,Tesla 股價還接著上漲至每股 281.19 美元。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

實時web應用方案——SignalR(.net core)

何為實時

先從理論上解釋一下兩者的區別。

大多數傳統的web應用是這樣的:客戶端發起http請求到服務端,服務端返回對應的結果。像這樣:

 

也就是說,傳統的web應用都是客戶端主動發起請求到服務端。

那麼實時web應用呢?它不需要主動發起請求,服務端可以主動推送信息到客戶端。

舉栗子的話,實時聊天工具、web遊戲等都可以算是實時應用。

什麼是SignalR

如果想做一個實時應用,最好用web socket。很早以前我也寫過web socket的實現方式,但不夠全面,這裏再補上一篇。

來說說signalR,它是一款開源的實時框架,可以使用三種方式實現通信(long polling、server sent events、web socket)。它很好的整合了底層技術,讓我們可以不用關注底層技術實現而把精力聚焦在業務實現上。一個完整的signalR包括客戶端和服務端,服務端支持net core/net framework,還支持大部分客戶端,比如瀏覽器和桌面應用。

回落機制

為了兼容不同瀏覽器(客戶端)和服務端,signalR採用了回落機制,使得它可以根據情況協商使用不同的底層傳輸方式。假如瀏覽器不支持web socket,就自動降級使用sse,再不行就long polling。當然,也可以禁用這種機制,指定其中一種。

三種通信方式

long polling(長輪詢)

長輪詢是客戶端發起請求到服務端,服務器有數據就會直接返回。如果沒有數據就保持連接並且等待,一直到有新的數據返回。如果請求保持到一段時間仍然沒有返回,這時候就會超時,然後客戶端再次發起請求。

這種方式優點就是簡單,缺點就是資源消耗太多,基本是不考慮的。

server sent events(sse)

如果使用了sse,服務器就擁有了向客戶端推送的能力,這些信息和流信息差不多,期間會保持連接。

這種方式優點還是簡單,也支持自動重連,綜合來講比long polling好用。缺點也很明顯,不支持舊的瀏覽器不說,還只能發送本文信息,而且瀏覽器對sse還有連接數量的限制(6個)。

web socket

web socket允許客戶端和服務端同時向對方發送消息(也就是雙工通信),而且不限制信息類型。雖然瀏覽器同樣有連接數量限制(可能是50個),但比sse強得多。理論上最優先使用。

進入正題

開始之前,還需要了解RPC和Hub的概念。

RPC:全程Remote Procedure Call,字面意思遠程服務調用,可以像調用本地方法一樣調用遠程服務。前端可以調用後端方法,後端也可以調用前端方法。

Hub:基於RPC,接受從客戶端發過來的消息,也同時負責把服務端的消息發送給客戶端。客戶端可以調用Hub裏面的方法,服務端可以通過Hub調用客戶端裏面的方法。

好了,概念已經理解清楚了,接下來上代碼。

在項目里新增Hub類:

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRDemo.Server
{
    public class SignalRHub : Hub
    {
        /// <summary>
        /// 客戶連接成功時觸發
        /// </summary>
        /// <returns></returns>
        public override async Task OnConnectedAsync()
        {
            var cid = Context.ConnectionId;

            //根據id獲取指定客戶端
            var client = Clients.Client(cid);

            //向指定用戶發送消息
            await client.SendAsync("Self", cid);

            //像所有用戶發送消息
            await Clients.All.SendAsync("AddMsg", $"{cid}加入了聊天室");
        }
    }
}

為了讓外部可以訪問,我們還需要一個控制器。在控制器里聲明隨便建一個:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using SignalRDemo.Server;
using System.Threading.Tasks;

namespace SignalRDemo.Controllers
{
    public class HomeController : Controller
    {
        private readonly IHubContext<SignalRHub> _countHub;

        public HomeController(IHubContext<SignalRHub> countHub)
        {
            _countHub = countHub;
        }

        /// <summary>
        /// 發送信息
        /// </summary>
        /// <param name="msg"></param>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task Send(string msg, string id)
        {
            await _countHub.Clients.All.SendAsync("AddMsg", $"{id}:{msg}");
        }
    }
}

再然後進入StartUp設置端點:

endpoints.MapHub<SignalRHub>("/hub");

完成以後,配置signalr客戶端:

setupConn = () => {
    conn = new signalR.HubConnectionBuilder()
        .withUrl("/hub")
        .build();

    conn.on("AddMsg", (obj) => {
        $('#msgPanel').append(`<p>${obj}</p>`);
    });

    conn.on("Finished", () => {
        conn.stop();
        $('#msgPanel').text('log out!');
    });

    conn.on("Self", (obj) => {
        $('#userId').text(obj);
    });

    conn.start()
        .catch(err => console.log(err));
}

要注意withUrl裏面的路徑就是之前設置好的端點。

運行效果:

 

 

 Hub還支持組操作,比如:

//將用戶添加到A組
await
Groups.AddToGroupAsync(Context.ConnectionId, "GroupA");
//將用戶踢出A組
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "GroupA");
//向A組所有成員廣播消息
await Clients.Group("GroupA").SendAsync("AddMsg", "群組消息");

更多操作請參考官方文檔。

本文演示demo的源碼見git,地址:https://gitee.com/muchengqingxin/SignalRDemo.git

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

十、深度優先 && 廣度優先

原文地址

一、什麼是“搜索”算法?

  • 算法是作用於具體數據結構之上的,深度優先搜索算法和廣度優先搜索算法都是基於“圖”這種數據結構的。
  • 因為圖這種數據結構的表達能力很強,大部分涉及搜索的場景都可以抽象成“圖”。
  • 圖上的搜索算法,最直接的理解就是,在圖中找出從一個頂點出發,到另一個頂點的路徑。
  • 具體方法有很多,兩種最簡單、最“暴力”的方法為深度優先、廣度優先搜索,還有A、 IDA等啟髮式搜索算法。
  • 圖有兩種主要存儲方法,鄰接表和鄰接矩陣。
  • 以無向圖,採用鄰接表存儲為例:
public class Graph {
    // 頂點的個數
    private int v;
    // 每個頂點後面有個鏈表
    private LinkedList<Integer>[] adj;

    public Graph(int v) {
        this.v = v;
        adj = new LinkedList[v];
        for (int i = 0; i < v; i++) {
            adj[i] = new LinkedList<>();
        }
    }

    /**
     * 添加邊
     * @param s 頂點
     * @param t 頂點
     */
    public void addEdge(int s,int t){
        // 無向圖一條邊存兩次(聯想微信好友)
        adj[s].add(t);
        adj[t].add(s);
    }
}

二、廣度優先搜索(BFS)

  • 廣度優先搜索(Breadth-First-Search),簡稱為 BFS。
  • 它是一種“地毯式”層層推進的搜索策略,即先查找離起始頂點最近的,然後是次近的,依次往外搜索

2.1、實現過程

/**
 * 圖的廣度優先搜索,搜索一條從 s 到 t 的路徑。
 * 這樣求得的路徑就是從 s 到 t 的最短路徑。
 *
 * @param s 起始頂點
 * @param t 終止頂點
 */
public void bfs(int s, int t) {
    if (s == t) {
        return;
    }
    // visited 記錄已經被訪問的頂點,避免頂點被重複訪問。如果頂點 q 被訪問,那相應的visited[q]會被設置為true。
    boolean[] visited = new boolean[v];
    visited[s] = true;
    // queue 是一個隊列,用來存儲已經被訪問、但相連的頂點還沒有被訪問的頂點。因為廣度優先搜索是逐層訪問的,只有把第k層的頂點都訪問完成之後,才能訪問第k+1層的頂點。
    // 當訪問到第k層的頂點的時候,需要把第k層的頂點記錄下來,稍後才能通過第k層的頂點來找第k+1層的頂點。
    // 所以,用這個隊列來實現記錄的功能。
    Queue<Integer> queue = new LinkedList<>();
    queue.add(s);
    // prev 用來記錄搜索路徑。當從頂點s開始,廣度優先搜索到頂點t后,prev數組中存儲的就是搜索的路徑。
    // 不過,這個路徑是反向存儲的。prev[w]存儲的是,頂點w是從哪個前驅頂點遍歷過來的。
    // 比如,通過頂點2的鄰接表訪問到頂點3,那prev[3]就等於2。為了正向打印出路徑,需要遞歸地來打印,就是print()函數的實現方式。
    int[] prev = Arrays.stream(new int[v]).map(f -> -1).toArray();

    while (queue.size() != 0) {
        int w = queue.poll();
        LinkedList<Integer> wLinked = adj[w]; // 表示:鄰接表存儲時頂點為w,所對應的鏈表
        for (int i = 0; i < wLinked.size(); ++i) {
            int q = wLinked.get(i);
            // 判斷頂點 q 是否被訪問
            if (!visited[q]) {
                // 未被訪問
                prev[q] = w;
                if (q == t) {
                    print(prev, s, t);
                    return;
                }
                visited[q] = true;
                queue.add(q);
            }
        }
    }
}

// 遞歸打印s->t的路徑
private void print(int[] prev, int s, int t) {
    if (prev[t] != -1 && t != s) {
        print(prev, s, prev[t]);
    }
    System.out.print(t + " ");
}

原理如下:

2.2、複雜度分析

  • 最壞情況下,終止頂點 t 離起始頂點 s 很遠,需要遍歷完整個圖才能找到。
  • 這個時候,每個頂點都要進出一遍隊列,每個邊也都會被訪問一次,所以,廣度優先搜索的時間複雜度是 O(V+E)
  • 其中,V 表示頂點的個數,E 表示邊的個數。
  • 對於一個連通圖來說,也就是說一個圖中的所有頂點都是連通的,E肯定要大於等於 V-1,所以,廣度優先搜索的時間複雜度也可以簡寫為 O(E)。
  • 廣度優先搜索的空間消耗主要在幾個輔助變量 visited 數組、queue 隊列、prev 數組上。
  • 這三個存儲空間的大小都不會超過頂點的個數,所以空間複雜度是 O(V)

三、深度優先搜索(DFS)

  • 深度優先搜索(Depth-First-Search),簡稱DFS。
  • 最直觀的例子就是“走迷宮,假設站在迷宮的某個岔路口,然後想找到出口。
  • 隨意選擇一個岔路口來走,走着走着發現走不通的時候,就回退到上一個岔路口,重新選擇一條路繼續走,直到最終找到出口。這種走法就是一種深度優先搜索策略。
  • 如下圖所示,在圖中應用深度優先搜索,來找某個頂點到另一個頂點的路徑。
  • 搜索的起始頂點是 s,終止頂點是 t,在圖中尋找一條從頂點 s 到頂點 t 的路徑。
  • 用深度遞歸算法,把整個搜索的路徑標記出來了。實線箭頭表示遍歷,虛線箭頭表示回退。
  • 從圖中可以看出,深度優先搜索找出來的路徑,並不是頂點 s 到頂點 t 的最短路徑。

3.1、實現過程

// 全局變量或者類成員變量,標記是否找到終點 t
boolean found = false;

/**
 * 深度優先搜索
 *
 * @param s 起始頂點
 * @param t 終止頂點
 */
public void dfs(int s, int t) {
    found = false;
    // 標記頂點是否被訪問
    boolean[] visited = new boolean[v];
    // prev 用來記錄搜索路徑,prev[w] = a 表示 w 頂點的上一級節點為 a
    int[] prev = Arrays.stream(new int[v])
            .map(f -> -1).toArray();

    recurDfs(s, t, visited, prev);
    print(prev, s, t);
}

private void recurDfs(int w, int t, boolean[] visited, int[] prev) {
    if (found == true) {
        return;
    }
    visited[w] = true;
    if (w == t) {
        found = true;
        return;
    }
    LinkedList<Integer> wLinked = adj[w];
    for (int i = 0; i < wLinked.size(); ++i) {
        int q = wLinked.get(i);
        if (!visited[q]) {
            prev[q] = w;
            recurDfs(q, t, visited, prev);
        }
    }
}

3.2、複雜度分析

  • 深度搜索中每條邊最多會被訪問兩次,一次是遍歷,一次是回退。
  • 所以,深度優先搜索算法的時間複雜度是 O(E), E 表示邊的個數。
  • 深度優先搜索算法的消耗內存主要是 visited、 prev 數組和遞歸調用棧。
  • visited、 prev 數組的大小跟頂點的個數V成正比,遞歸調用棧的最大深度不會超過頂點的個數,所以總的空間複雜度就是 O(V)

四,兩者對比

  • 廣度優先搜索和深度優先搜索是圖上的兩種最常用、最基本的搜索算法,比起其他高級的搜索算法,比如A、 IDA等,要簡單粗暴,沒有什麼優化,所以,也被
    叫作暴力搜索算法。
  • 所以,這兩種搜索算法僅適用於狀態空間不大,也就是說圖不大的搜索。
  • 廣度優先搜索,通俗的理解就是,地毯式層層推進,從起始頂點開始,依次往外遍歷。
  • 廣度優先搜索需要藉助隊列來實現,遍歷得到的路徑就是,起始頂點到終止頂點的最短路徑。
  • 深度優先搜索用的是回溯思想,非常適合用遞歸實現。換種說法,深度優先搜索是藉助棧來實現的。
  • 在執行效率方面,深度優先和廣度優先搜索的時間複雜度都是 O(E),空間複雜度是 O(V)。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

springboot的jar為何能獨立運行

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos
內容:所有原創文章分類匯總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;

能獨立運行的jar文件

在開發springboot應用時,通過java -jar命令啟動應用是常用的方式,今天就來一起了解這個簡單操作背後的技術;

開發demo

開發一個springboot應用作為本次研究的對象,對應的版本信息如下:

  • JDK:1.8.0_211
  • springboot:2.3.1.RELEASE
  • maven:3.6.0

接下來開發springboot應用,這個應用異常簡單:

  1. springboot應用名為springbootstarterdemo,pom.xml文件內容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bolingcavalry</groupId>
    <artifactId>springbootstarterdemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springbootstarterdemo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 只有一個java類,裏面有個http接口:
package com.bolingcavalry.springbootstarterdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@SpringBootApplication
@RestController
public class SpringbootstarterdemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootstarterdemoApplication.class, args);
    }
    @RequestMapping(value = "/hello")
    public String hello(){
        return "hello " + new Date();
    }
}
  1. 編碼完成,在pom.xml所在目錄執行命令
mvn clean package -U -DskipTests
  1. 構建成功后,在target目錄下得到文件springbootstarterdemo-0.0.1-SNAPSHOT.jar
  2. 就是這個springbootstarterdemo-0.0.1-SNAPSHOT.jar,此時執行java -jar springbootstarterdemo-0.0.1-SNAPSHOT.jar就能啟動應用,如下圖:

接下來就用這個springbootstarterdemo-0.0.1-SNAPSHOT.jar來分析jar文件能夠獨立啟動的原因;

java -jar做了什麼

  • 先要弄清楚java -jar命令做了什麼,在oracle官網找到了該命令的描述:

    If the -jar option is specified, its argument is the name of the JAR file containing class and resource files for the application. The startup class must be indicated by the Main-Class manifest header in its source code.

  • 再次秀出我蹩腳的英文翻譯:

  1. 使用-jar參數時,後面的參數是的jar文件名(本例中是springbootstarterdemo-0.0.1-SNAPSHOT.jar);
  2. 該jar文件中包含的是class和資源文件;
  3. 在manifest文件中有Main-Class的定義;
  4. Main-Class的源碼中指定了整個應用的啟動類;(in its source code)
  • 小結一下:
    java -jar會去找jar中的manifest文件,在那裡面找到真正的啟動類;

探查springbootstarterdemo-0.0.1-SNAPSHOT.jar

  1. springbootstarterdemo-0.0.1-SNAPSHOT.jar是前面的springboot工程的構建結果,是個壓縮包,用常見的壓縮工具就能解壓,我這裏的環境是MacBook Pro,用unzip即可解壓;

  2. 解壓後有很多內容,我們先關注manifest相關的,下圖紅框中就是manifest文件:

  3. 打開上圖紅框中的文件,內容如下:

Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: springbootstarterdemo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter
 demoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.1.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
  1. 在上述內容可見Main-Class的值org.springframework.boot.loader.JarLauncher,這個和前面的java官方文檔對應上了,正是這個JarLauncher類的代碼中指定了真正的啟動類;

疑惑出現

  1. 在MANIFEST.MF文件中有這麼一行內容:
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter
 demoApplication
  1. 前面的java官方文檔中,只提到過Main-Class ,並沒有提到Start-Class
  2. Start-Class的值是SpringbootstarterdemoApplication,這是我們的java代碼中的唯一類,也只真正的應用啟動類;
  3. 所以問題就來了:理論上看,執行java -jar命令時JarLauncher類會被執行,但實際上是SpringbootstarterdemoApplication被執行了,這其中發生了什麼呢?

猜測

動手之前先猜一下,個人覺得原因應該如下:

  1. java -jar命令會啟動JarLauncher;
  2. Start-Class是給JarLauncher用的;
  3. JarLauncher根據Start-Class找到了SpringbootstarterdemoApplication,然後執行它;

分析JarLauncher

  1. 先下載SpringBoot源碼,我下載的是2.3.1版本,地址:https://github.com/spring-projects/spring-boot/releases/tag/v2.3.1.RELEASE

  2. JarLauncher所在的工程是spring-boot-loader,先弄明白JarLauncher的繼承關係,如下圖,可見JarLauncher繼承自ExecutableArchiveLauncher,而ExecutableArchiveLauncher的父類Launcher位於最頂層,是個抽象類:

  3. java -jar執行的是JarLauncher的main方法,如下,會實例化一個JarLauncher對象,然後執行其launch方法,並且將所有入參都帶入:

public static void main(String[] args) throws Exception {
	new JarLauncher().launch(args);
}
  1. 上面的launch方法在父類Launcher中:
protected void launch(String[] args) throws Exception {
    // 將jar解壓后運行的方式叫做exploded mode
    // 如果是exploded mode,就不能支持通過URL加載jar
    // 如果不是exploded mode,就可以通過URL加載jar
	if (!isExploded()) {
	    // 如果允許通過URL加載jar,就在此註冊對應的處理類
		JarFile.registerUrlProtocolHandler();
	}
	// 創建classLoader
	ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
	// jarmode是創建docker鏡像時用到的參數,使用該參數是為了生成帶有多個layer信息的鏡像
	// 這裏暫時不關注jarmode
	String jarMode = System.getProperty("jarmode");
	//如果沒有jarmode參數,launchClass的值就來自getMainClass()返回
	String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
	launch(args, launchClass, classLoader);
}
  1. 可見要重點關注的是getMainClass()方法,在看這個方法之前,我們先去關注一個重要的成員變量archive,是JarLauncher的父類ExecutableArchiveLauncher的archive,如下可見,該變量又來自方法createArchive:
public ExecutableArchiveLauncher() {
		try {
			this.archive = createArchive();
			this.classPathIndex = getClassPathIndex(this.archive);
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}
  1. 方法來自Launcher.createArchive,如下所示,可見成員變量archive實際上是個JarFileArchive對象:
protected final Archive createArchive() throws Exception {
		ProtectionDomain protectionDomain = getClass().getProtectionDomain();
		CodeSource codeSource = protectionDomain.getCodeSource();
		URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
		String path = (location != null) ? location.getSchemeSpecificPart() : null;
		if (path == null) {
			throw new IllegalStateException("Unable to determine code source archive");
		}
		File root = new File(path);
		if (!root.exists()) {
			throw new IllegalStateException("Unable to determine code source archive from " + root);
		}
		return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
	}
  1. 現在回到getMainClass()方法,可見his.archive.getManifest方法返回的是META-INF/MANIFEST.MF文件的內容,然後getValue(START_CLASS_ATTRIBUTE)方法實際上就是從META-INF/MANIFEST.MF中取得了Start-Class的屬性:
@Override
	protected String getMainClass() throws Exception {
	    // 對應的是JarFileArchive.getManifest方法,
	    // 進去后發現對應的就是JarFile.getManifest方法,
	    // JarFile.getManifest對應的就是META-INF/MANIFEST.MF文件的內容
		Manifest manifest = this.archive.getManifest();
		String mainClass = null;
		if (manifest != null) {
		    // 對應的是META-INF/MANIFEST.MF文件中的Start-Class的屬性
			mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
		}
		if (mainClass == null) {
			throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
		}
		return mainClass;
	}
  1. 從上述分析可知:getMainClass()方法返回的是META-INF/MANIFEST.MF中取得了Start-Class的屬性com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication,再次回到launch方法中,可見最終運行的代碼是launch(args, launchClass, classLoader),它的launchClass參數就是com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication:
protected void launch(String[] args) throws Exception {
		if (!isExploded()) {
			JarFile.registerUrlProtocolHandler();
		}
		ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
		String jarMode = System.getProperty("jarmode");
		// 這裏的launchClass等於"com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication"
		String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
		// 這裏就是啟動SpringbootstarterdemoApplication的地方
		launch(args, launchClass, classLoader);
	}
  1. 展開launch(args, launchClass, classLoader),最終查到了MainMethodRunner類:
public class MainMethodRunner {

	private final String mainClassName;

	private final String[] args;

	/**
	 * Create a new {@link MainMethodRunner} instance.
	 * @param mainClass the main class
	 * @param args incoming arguments
	 */
	public MainMethodRunner(String mainClass, String[] args) {
	    // mainClassName被賦值為"com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication"
		this.mainClassName = mainClass;
		this.args = (args != null) ? args.clone() : null;
	}

	public void run() throws Exception {
	    // 得到SpringbootstarterdemoApplication的Class對象
		Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
		// 得到SpringbootstarterdemoApplication的main方法對象
		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
		mainMethod.setAccessible(true);
		// 通過反射執行main方法
		mainMethod.invoke(null, new Object[] { this.args });
	}
}

終於,真相大白了;

小結

最後盡可能簡短做個小結,先看jar是如何產生的,如下圖,maven插件生成的jar文件中,有常見的class、jar,也有符合java規範的MANIFEST.MF文件,並且,還在MANIFEST.MF文件中額外生成了名為Start-Class的配置,這裏面是我們編寫的應用啟動類SpringbootstarterdemoApplication

啟動類是JarLauncher,它是如何與MANIFEST.MF文件關聯的呢?從下圖可以看出,最終是通過JarFile類的成員變量manifestSupplier關聯上的:

再來看看關鍵代碼的執行情況,如下圖:

至此,SpringBoot的jar獨立運行的基本原理已經清楚,探究的過程中,除了熟悉關鍵代碼流程,還對jar中的文件有了更多了解,如果您正在學習SpringBoot,希望本文能給您一些參考;

官方文檔

  1. 最後附上SpringBoot官方文檔,可以看到Start-Class描述信息:

  2. 上述文檔明確提到:Start-Class定義的是實際的啟動類,此時的您應該對一切都瞭然於胸,產生本該如此的感慨;

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

印度南部核電廠曾遭網攻 所幸系統未受影響

摘錄自2019年10月31日中央社報導

曾有印度媒體披露,印度核電公司(The NuclearPower Corporation of India, NCPIL)的古丹庫蘭(Kudankulam)核電廠系統曾於9月遭植入惡意軟體(malware)並受到網路攻擊。印度核電公司31日發表聲明證實,位於印度南部坦米爾那都省(TamilNadu)的古丹庫蘭核電廠曾遭網路攻擊,但聲明也強調,核電廠系統沒有受到影響。

這份聲明也證實,公司的系統被植入惡意軟體,印度電腦緊急應變小組(CERT-In)9月4日發現這種狀況後已通知公司。

聲明說,全案已由印度原子能部(Department ofAtomic Energy)專家進行調查,初步發現受感染的電腦屬於同一用戶,這名用戶的電腦曾與用於管理系統的網絡連結;受感染部份目前與關鍵內部網路隔離,且網路持續被監控中。核電廠系統沒有受到影響。

原能會對此事件也發布新聞稿表示:我國運轉中核能電廠的發電及安全相關電腦數位設備都是獨立的系統,與外界實體隔離,阻絕網路攻擊的機會,並訂有隨身碟資訊設備及門禁管制程序,以避免駭客透過網際網路以外之其他方式入侵。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

民主剛果抗伊波拉鬥士家中遇害 凶手動機不明

摘錄自2019年11月4日中央社報導

法新社報導,剛果民主共和國軍方4日表示說,一名協助傳遞對抗伊波拉(Ebola)疫情資訊的電台主持人,35歲的馬罕巴(Papy Mumbere Mahamba),他的妻子受傷,住家被縱火燒毀他的家。

這起在動盪的伊圖里省(Ituri)魯汶巴鎮(Lwemba)發生的謀殺案,凶手犯案動機不明。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案