詳解二分查找算法

我周圍的人幾乎都認為二分查找很簡單,但事實真的如此嗎?二分查找真的很簡單嗎?並不簡單。看看 Knuth 大佬(發明 KMP 算法的那位)怎麼說的:

Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky…

這句話可以這樣理解:思路很簡單,細節是魔鬼。

本文就來探究幾個最常用的二分查找場景:尋找一個數、尋找左側邊界、尋找右側邊界。

而且,我們就是要深入細節,比如while循環中的不等號是否應該帶等號,mid 是否應該加一等等。分析這些細節的差異以及出現這些差異的原因,保證你能靈活準確地寫出正確的二分查找算法。

一、二分查找的框架

int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;

    while(...) {
        int mid = (right + left) / 2;
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) {
            right = ...
        }
    }
    return ...;
}

分析二分查找的一個技巧是:不要出現 else,而是把所有情況用 else if 寫清楚,這樣可以清楚地展現所有細節。本文都會使用 else if,旨在講清楚,讀者理解后可自行簡化。

其中…標記的部分,就是可能出現細節問題的地方,當你見到一個二分查找的代碼時,首先注意這幾個地方。後文用實例分析這些地方能有什麼樣的變化。

另外聲明一下,計算 mid 時需要技巧防止溢出,建議寫成: mid = left + (right – left) / 2,本文暫時忽略這個問題。

二、尋找一個數(基本的二分搜索)

這個場景是最簡單的,可能也是大家最熟悉的,即搜索一個數,如果存在,返回其索引,否則返回 -1。

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1; // 注意

    while(left <= right) { // 注意
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid - 1; // 注意
        }
    return -1;
}

1. 為什麼 while 循環的條件中是 <=,而不是 < ?

答:因為初始化 right 的賦值是 nums.length – 1,即最後一個元素的索引,而不是 nums.length。

這二者可能出現在不同功能的二分查找中,區別是:前者相當於兩端都閉區間 [left, right],後者相當於左閉右開區間 [left, right),因為索引大小為 nums.length 是越界的。

我們這個算法中使用的是 [left, right] 兩端都閉的區間。這個區間就是每次進行搜索的區間,我們不妨稱為「搜索區間」(search space)。

什麼時候應該停止搜索呢?當然,找到了目標值的時候可以終止:

    if(nums[mid] == target)
        return mid; 

但如果沒找到,就需要 while 循環終止,然後返回 -1。那 while 循環什麼時候應該終止?搜索區間為空的時候應該終止,意味着你沒得找了,就等於沒找到嘛。

while(left <= right)的終止條件是 left == right + 1,寫成區間的形式就是 [right + 1, right],或者帶個具體的数字進去 [3, 2],可見這時候搜索區間為空,因為沒有数字既大於等於 3 又小於等於 2 的吧。所以這時候 while 循環終止是正確的,直接返回 -1 即可。

while(left < right)的終止條件是 left == right,寫成區間的形式就是 [right, right],或者帶個具體的数字進去 [2, 2],這時候搜索區間非空,還有一個數 2,但此時 while 循環終止了。也就是說這區間 [2, 2] 被漏掉了,索引 2 沒有被搜索,如果這時候直接返回 -1 就可能出現錯誤。

當然,如果你非要用 while(left < right) 也可以,我們已經知道了出錯的原因,就打個補丁好了:

//...
while(left < right) {
    // ...
}
return nums[left] == target ? left : -1;

2. 為什麼 left = mid + 1,right = mid – 1?我看有的代碼是 right = mid 或者 left = mid,沒有這些加加減減,到底怎麼回事,怎麼判斷?

答:這也是二分查找的一個難點,不過只要你能理解前面的內容,就能夠很容易判斷。

剛才明確了「搜索區間」這個概念,而且本算法的搜索區間是兩端都閉的,即 [left, right]。那麼當我們發現索引 mid 不是要找的 target 時,如何確定下一步的搜索區間呢?

當然是去搜索 [left, mid – 1] 或者 [mid + 1, right] 對不對?因為 mid 已經搜索過,應該從搜索區間中去除。

3. 此算法有什麼缺陷?

答:至此,你應該已經掌握了該算法的所有細節,以及這樣處理的原因。但是,這個算法存在局限性。

比如說給你有序數組 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 2,沒錯。但是如果我想得到 target 的左側邊界,即索引 1,或者我想得到 target 的右側邊界,即索引 3,這樣的話此算法是無法處理的。

這樣的需求很常見。你也許會說,找到一個 target 索引,然後向左或向右線性搜索不行嗎?可以,但是不好,因為這樣難以保證二分查找對數級的時間複雜度了。

我們後續的算法就來討論這兩種二分查找的算法。

三、尋找左側邊界的二分搜索

直接看代碼,其中的標記是需要注意的細節:

int left_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0;
    int right = nums.length; // 注意

    while (left < right) { // 注意
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            right = mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid; // 注意
        }
    }
    return left;
}

1. 為什麼 while(left < right) 而不是 <= ?

答:用相同的方法分析,因為初始化 right = nums.length 而不是 nums.length – 1 。因此每次循環的「搜索區間」是 [left, right) 左閉右開。

while(left < right) 終止的條件是 left == right,此時搜索區間 [left, left) 恰巧為空,所以可以正確終止。

2. 為什麼沒有返回 -1 的操作?如果 nums 中不存在 target 這個值,怎麼辦?

答:因為要一步一步來,先理解一下這個「左側邊界」有什麼特殊含義:

對於這個數組,算法會返回 1。這個 1 的含義可以這樣解讀:nums 中小於 2 的元素有 1 個。

比如對於有序數組 nums = [2,3,5,7], target = 1,算法會返回 0,含義是:nums 中小於 1 的元素有 0 個。如果 target = 8,算法會返回 4,含義是:nums 中小於 8 的元素有 4 個。

綜上可以看出,函數的返回值(即 left 變量的值)取值區間是閉區間 [0, nums.length],所以我們簡單添加兩行代碼就能在正確的時候 return -1:

while (left < right) {
    //...
}
// target 比所有數都大
if (left == nums.length) return -1;
// 類似之前算法的處理方式
return nums[left] == target ? left : -1;

3. 為什麼 left = mid + 1,right = mid ?和之前的算法不一樣?

答:這個很好解釋,因為我們的「搜索區間」是 [left, right) 左閉右開,所以當 nums[mid] 被檢測之後,下一步的搜索區間應該去掉 mid 分割成兩個區間,即 [left, mid) 或 [mid + 1, right)。

4. 為什麼該算法能夠搜索左側邊界?

答:關鍵在於對於 nums[mid] == target 這種情況的處理:

    if (nums[mid] == target)
        right = mid;

可見,找到 target 時不要立即返回,而是縮小「搜索區間」的上界 right,在區間 [left, mid) 中繼續搜索,即不斷向左收縮,達到鎖定左側邊界的目的。

5. 為什麼返回 left 而不是 right?

答:返回left和right都是一樣的,因為 while 終止的條件是 left == right。

四、尋找右側邊界的二分查找

尋找右側邊界和尋找左側邊界的代碼差不多,只有兩處不同,已標註:

int right_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length;

    while (left < right) {
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            left = mid + 1; // 注意
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    return left - 1; // 注意

1. 為什麼這個算法能夠找到右側邊界?

答:類似地,關鍵點還是這裏:

    if (nums[mid] == target) {
        left = mid + 1;

當 nums[mid] == target 時,不要立即返回,而是增大「搜索區間」的下界 left,使得區間不斷向右收縮,達到鎖定右側邊界的目的。

2. 為什麼最後返回 left – 1 而不像左側邊界的函數,返回 left?而且我覺得這裏既然是搜索右側邊界,應該返回 right 才對。

答:首先,while 循環的終止條件是 left == right,所以 left 和 right 是一樣的,你非要體現右側的特點,返回 right – 1 好了。

至於為什麼要減一,這是搜索右側邊界的一個特殊點,關鍵在這個條件判斷:

    if (nums[mid] == target) {
        left = mid + 1;
        // 這樣想: mid = left - 1

因為我們對 left 的更新必須是 left = mid + 1,就是說 while 循環結束時,nums[left] 一定不等於 target 了,而 nums[left – 1]可能是target。

至於為什麼 left 的更新必須是 left = mid + 1,同左側邊界搜索,就不再贅述。

3. 為什麼沒有返回 -1 的操作?如果 nums 中不存在 target 這個值,怎麼辦?

答:類似之前的左側邊界搜索,因為 while 的終止條件是 left == right,就是說 left 的取值範圍是 [0, nums.length],所以可以添加兩行代碼,正確地返回 -1:

while (left < right) {
    // ...
}
if (left == 0) return -1;
return nums[left-1] == target ? (left-1) : -1;

五、最後總結

先來梳理一下這些細節差異的因果邏輯:

第一個,最基本的二分查找算法:

因為我們初始化 right = nums.length - 1
所以決定了我們的「搜索區間」是 [left, right]
所以決定了 while (left <= right)
同時也決定了 left = mid+1 和 right = mid-1

因為我們只需找到一個 target 的索引即可
所以當 nums[mid] == target 時可以立即返回

第二個,尋找左側邊界的二分查找:

因為我們初始化 right = nums.length
所以決定了我們的「搜索區間」是 [left, right)
所以決定了 while (left < right)
同時也決定了 left = mid+1 和 right = mid

因為我們需找到 target 的最左側索引
所以當 nums[mid] == target 時不要立即返回
而要收緊右側邊界以鎖定左側邊界

第三個,尋找右側邊界的二分查找:

因為我們初始化 right = nums.length
所以決定了我們的「搜索區間」是 [left, right)
所以決定了 while (left < right)
同時也決定了 left = mid+1 和 right = mid

因為我們需找到 target 的最右側索引
所以當 nums[mid] == target 時不要立即返回
而要收緊左側邊界以鎖定右側邊界

又因為收緊左側邊界時必須 left = mid + 1
所以最後無論返回 left 還是 right,必須減一

如果以上內容你都能理解,那麼恭喜你,二分查找算法的細節不過如此。

通過本文,你學會了:

1. 分析二分查找代碼時,不要出現 else,全部展開成 else if 方便理解。

2. 注意「搜索區間」和 while 的終止條件,如果存在漏掉的元素,記得在最後檢查。

3. 如需要搜索左右邊界,只要在 nums[mid] == target 時做修改即可。搜索右側時需要減一。

就算遇到其他的二分查找變形,運用這幾點技巧,也能保證你寫出正確的代碼。LeetCode Explore 中有二分查找的專項練習,其中提供了三種不同的代碼模板,現在你再去看看,很容易就知道這幾個模板的實現原理了。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

從零開始寫一個Exporter

前言

上一篇文章中已經給大家整體的介紹了開源監控系統Prometheus,其中Exporter作為整個系統的Agent端,通過HTTP接口暴露需要監控的數據。那麼如何將用戶指標通過Exporter的形式暴露出來呢?比如說在線,請求失敗數,異常請求等指標可以通過Exporter的形式暴露出來,從而基於這些指標做告警監控。

 

演示環境

$ uname -a
Darwin 18.6.0 Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; root:xnu-4903.261.4~2/RELEASE_X86_64 x86_64
$ go version
go version go1.12.4 darwin/amd64

 

四類指標介紹

Prometheus定義了4種不同的指標類型:Counter(計數器),Gauge(儀錶盤),Histogram(直方圖),Summary(摘要)。

其中Exporter返回的樣本數據中會包含數據類型的說明,例如:

# TYPE node_network_carrier_changes_total counter
node_network_carrier_changes_total{device="br-01520cb4f523"} 1

這四類指標的特徵為:

Counter:只增不減(除非系統發生重啟,或者用戶進程有異常)的計數器。常見的監控指標如http_requests_total, node_cpu都是Counter類型的監控指標。一般推薦在定義為Counter的指標末尾加上_total作為後綴。

Gauge:可增可減的儀錶盤。Gauge類型的指標側重於反應系統當前的狀態。因此此類指標的數據可增可減。常見的例如node_memory_MemAvailable_bytes(可用內存)。

HIstogram:分析數據分佈的直方圖。显示數據的區間分佈。例如統計請求耗時在0-10ms的請求數量和10ms-20ms的請求數量分佈。

Summary: 分析數據分佈的摘要。显示數據的中位數,9分數等。

 

實戰

接下來我將用Prometheus提供的Golang SDK 編寫包含上述四類指標的Exporter,示例的編寫修改自SDK的example。由於example中示例比較複雜,我會精簡一下,盡量讓大家用最小的學習成本能夠領悟到Exporter開發的精髓。第一個例子會演示Counter和Gauge的用法,第二個例子演示Histogram和Summary的用法。

Counter和Gauge用法演示:

package main

import (
    "flag"
    "log"
    "net/http"

    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var addr = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.")

func main() {
    flag.Parse()
    http.Handle("/metrics", promhttp.Handler())
    log.Fatal(http.ListenAndServe(*addr, nil))
}

上述代碼就是一個通過0.0.0.0:8080/metrics 暴露golang信息的原始Exporter,沒有包含任何的用戶自定義指標信息。接下來往裡面添加Counter和Gauge類型指標:

 1 func recordMetrics() {
 2     go func() {
 3         for {
 4             opsProcessed.Inc()
 5             myGague.Add(11)
 6             time.Sleep(2 * time.Second)
 7         }
 8     }()
 9 }
10 
11 var (
12     opsProcessed = promauto.NewCounter(prometheus.CounterOpts{
13         Name: "myapp_processed_ops_total",
14         Help: "The total number of processed events",
15     })
16     myGague = promauto.NewGauge(prometheus.GaugeOpts{
17         Name: "my_example_gauge_data",
18         Help: "my example gauge data",
19         ConstLabels:map[string]string{"error":""},
20     })
21 )

在上面的main函數中添加recordMetrics方法調用。curl 127.0.0.1:8080/metrics 能看到自定義的Counter類型指標myapp_processed_ops_total 和 Gauge 類型指標my_example_gauge_data。

# HELP my_example_gauge_data my example gauge data
# TYPE my_example_gauge_data gauge
my_example_gauge_data{error=""} 44
# HELP myapp_processed_ops_total The total number of processed events
# TYPE myapp_processed_ops_total counter
myapp_processed_ops_total 4

其中#HELP 是代碼中的Help字段信息,#TYPE 說明字段的類型,例如my_example_gauge_data是gauge類型指標。my_example_gauge_data是指標名稱,大括號括起來的error是該指標的維度,44是該指標的值。需要特別注意的是第12行和16行用的是promauto包的NewXXX方法,例如:

func NewCounter(opts prometheus.CounterOpts) prometheus.Counter {
    c := prometheus.NewCounter(opts)
    prometheus.MustRegister(c)
    return c
}

可以看到該函數是會自動調用MustRegister方法,如果用的是prometheus包的NewCounter則需要再自行調用MustRegister註冊收集的指標。其中Couter類型指標有以下的內置接口:

type Counter interface {
    Metric
    Collector

    // Inc increments the counter by 1. Use Add to increment it by arbitrary
    // non-negative values.
    Inc()
    // Add adds the given value to the counter. It panics if the value is <
    // 0.
    Add(float64)
}

可以通過Inc()接口給指標直接進行+1操作,也可以通過Add(float64)給指標加上某個值。還有繼承自Metric和Collector的一些描述接口,這裏不做展開。

Gauge類型的內置接口有:

type Gauge interface {
    Metric
    Collector

    // Set sets the Gauge to an arbitrary value.
    Set(float64)
    // Inc increments the Gauge by 1. Use Add to increment it by arbitrary
    // values.
    Inc()
    // Dec decrements the Gauge by 1. Use Sub to decrement it by arbitrary
    // values.
    Dec()
    // Add adds the given value to the Gauge. (The value can be negative,
    // resulting in a decrease of the Gauge.)
    Add(float64)
    // Sub subtracts the given value from the Gauge. (The value can be
    // negative, resulting in an increase of the Gauge.)
    Sub(float64)

    // SetToCurrentTime sets the Gauge to the current Unix time in seconds.
    SetToCurrentTime()
}

需要注意的是Gauge提供了Sub(float64)的減操作接口,因為Gauge是可增可減的指標。Counter因為是只增不減的指標,所以只有加的接口。

 

Histogram和Summary用法演示:

 1 package main
 2 
 3 import (
 4     "flag"
 5     "fmt"
 6     "log"
 7     "math"
 8     "math/rand"
 9     "net/http"
10     "time"
11 
12     "github.com/prometheus/client_golang/prometheus"
13     "github.com/prometheus/client_golang/prometheus/promhttp"
14 )
15 
16 var (
17     addr              = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.")
18     uniformDomain     = flag.Float64("uniform.domain", 0.0002, "The domain for the uniform distribution.")
19     normDomain        = flag.Float64("normal.domain", 0.0002, "The domain for the normal distribution.")
20     normMean          = flag.Float64("normal.mean", 0.00001, "The mean for the normal distribution.")
21     oscillationPeriod = flag.Duration("oscillation-period", 10*time.Minute, "The duration of the rate oscillation period.")
22 )
23 
24 var (
25     rpcDurations = prometheus.NewSummaryVec(
26         prometheus.SummaryOpts{
27             Name:       "rpc_durations_seconds",
28             Help:       "RPC latency distributions.",
29             Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
30         },
31         []string{"service","error_code"},
32     )
33     rpcDurationsHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
34         Name:    "rpc_durations_histogram_seconds",
35         Help:    "RPC latency distributions.",
36         Buckets: prometheus.LinearBuckets(0, 5, 20),
37     })
38 )
39 
40 func init() {
41     // Register the summary and the histogram with Prometheus's default registry.
42     prometheus.MustRegister(rpcDurations)
43     prometheus.MustRegister(rpcDurationsHistogram)
44     // Add Go module build info.
45     prometheus.MustRegister(prometheus.NewBuildInfoCollector())
46 }
47 
48 func main() {
49     flag.Parse()
50 
51     start := time.Now()
52 
53     oscillationFactor := func() float64 {
54         return 2 + math.Sin(math.Sin(2*math.Pi*float64(time.Since(start))/float64(*oscillationPeriod)))
55     }
56 
57     go func() {
58         i := 1
59         for {
60             time.Sleep(time.Duration(75*oscillationFactor()) * time.Millisecond)
61             if (i*3) > 100 {
62                 break
63             }
64             rpcDurations.WithLabelValues("normal","400").Observe(float64((i*3)%100))
65             rpcDurationsHistogram.Observe(float64((i*3)%100))
66             fmt.Println(float64((i*3)%100), " i=", i)
67             i++
68         }
69     }()
70 
71     go func() {
72         for {
73             v := rand.ExpFloat64() / 1e6
74             rpcDurations.WithLabelValues("exponential", "303").Observe(v)
75             time.Sleep(time.Duration(50*oscillationFactor()) * time.Millisecond)
76         }
77     }()
78 
79     // Expose the registered metrics via HTTP.
80     http.Handle("/metrics", promhttp.Handler())
81     log.Fatal(http.ListenAndServe(*addr, nil))
82 }

第25-32行定義了一個Summary類型指標,其中有service和errro_code兩個維度。第33-37行定義了一個Histogram類型指標,從0開始,5為寬度,有20個直方。也就是0-5,6-10,11-15 …. 等20個範圍統計。

其中直方圖HIstogram指標的相關結果為:

 1 # HELP rpc_durations_histogram_seconds RPC latency distributions.
 2 # TYPE rpc_durations_histogram_seconds histogram
 3 rpc_durations_histogram_seconds_bucket{le="0"} 0
 4 rpc_durations_histogram_seconds_bucket{le="5"} 1
 5 rpc_durations_histogram_seconds_bucket{le="10"} 3
 6 rpc_durations_histogram_seconds_bucket{le="15"} 5
 7 rpc_durations_histogram_seconds_bucket{le="20"} 6
 8 rpc_durations_histogram_seconds_bucket{le="25"} 8
 9 rpc_durations_histogram_seconds_bucket{le="30"} 10
10 rpc_durations_histogram_seconds_bucket{le="35"} 11
11 rpc_durations_histogram_seconds_bucket{le="40"} 13
12 rpc_durations_histogram_seconds_bucket{le="45"} 15
13 rpc_durations_histogram_seconds_bucket{le="50"} 16
14 rpc_durations_histogram_seconds_bucket{le="55"} 18
15 rpc_durations_histogram_seconds_bucket{le="60"} 20
16 rpc_durations_histogram_seconds_bucket{le="65"} 21
17 rpc_durations_histogram_seconds_bucket{le="70"} 23
18 rpc_durations_histogram_seconds_bucket{le="75"} 25
19 rpc_durations_histogram_seconds_bucket{le="80"} 26
20 rpc_durations_histogram_seconds_bucket{le="85"} 28
21 rpc_durations_histogram_seconds_bucket{le="90"} 30
22 rpc_durations_histogram_seconds_bucket{le="95"} 31
23 rpc_durations_histogram_seconds_bucket{le="+Inf"} 33
24 rpc_durations_histogram_seconds_sum 1683
25 rpc_durations_histogram_seconds_count 33

xxx_count反應當前指標的記錄總數,xxx_sum表示當前指標的總數。不同的le表示不同的區間,後面的数字是從開始到這個區間的總數。例如le=”30″後面的10表示有10個樣本落在0-30區間,那麼26-30這個區間一共有多少個樣本呢,只需要用len=”30″ – len=”25″,即2個。也就是27和30這兩個點。

Summary相關的結果如下:

 1 # HELP rpc_durations_seconds RPC latency distributions.
 2 # TYPE rpc_durations_seconds summary
 3 rpc_durations_seconds{error_code="303",service="exponential",quantile="0.5"} 7.176288428497417e-07
 4 rpc_durations_seconds{error_code="303",service="exponential",quantile="0.9"} 2.6582266087185467e-06
 5 rpc_durations_seconds{error_code="303",service="exponential",quantile="0.99"} 4.013935374172691e-06
 6 rpc_durations_seconds_sum{error_code="303",service="exponential"} 0.00015065426336339398
 7 rpc_durations_seconds_count{error_code="303",service="exponential"} 146
 8 rpc_durations_seconds{error_code="400",service="normal",quantile="0.5"} 51
 9 rpc_durations_seconds{error_code="400",service="normal",quantile="0.9"} 90
10 rpc_durations_seconds{error_code="400",service="normal",quantile="0.99"} 99
11 rpc_durations_seconds_sum{error_code="400",service="normal"} 1683
12 rpc_durations_seconds_count{error_code="400",service="normal"} 33

其中sum和count指標的含義和上面Histogram一致。拿第8-10行指標來說明,第8行的quantile 0.5 表示這裏指標的中位數是51,9分數是90。

 

自定義類型

如果上面Counter,Gauge,Histogram,Summary四種內置指標都不能滿足我們要求時,我們還可以自定義類型。只要實現了Collect接口的方法,然後調用MustRegister即可:

func MustRegister(cs ...Collector) {
    DefaultRegisterer.MustRegister(cs...)
}

type Collector interface {
    Describe(chan<- *Desc)
    Collect(chan<- Metric)
}

 

總結

文章通過Prometheus內置的Counter(計數器),Gauge(儀錶盤),Histogram(直方圖),Summary(摘要)演示了Exporter的開發,最後提供了自定義類型的實現方法。

 

參考

https://prometheus.io/docs/guides/go-application/

https://yunlzheng.gitbook.io/prometheus-book/parti-prometheus-ji-chu/promql/prometheus-metrics-types

https://songjiayang.gitbooks.io/prometheus/content/concepts/metric-types.html

 

 

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

SpringBoot啟動流程分析(六):IoC容器依賴注入

SpringBoot系列文章簡介

SpringBoot源碼閱讀輔助篇:

  Spring IoC容器與應用上下文的設計與實現

SpringBoot啟動流程源碼分析:

  1. SpringBoot啟動流程分析(一):SpringApplication類初始化過程
  2. SpringBoot啟動流程分析(二):SpringApplication的run方法
  3. SpringBoot啟動流程分析(三):SpringApplication的run方法之prepareContext()方法
  4. SpringBoot啟動流程分析(四):IoC容器的初始化過程
  5. SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現
  6. SpringBoot啟動流程分析(六):IoC容器依賴注入

筆者註釋版Spring Framework與SpringBoot源碼git傳送門:請不要吝嗇小星星

  1. spring-framework-5.0.8.RELEASE
  2. SpringBoot-2.0.4.RELEASE

一、前言

  前面我們對IoC容器的初始化過程進行了詳細的分析,這個初始化過程完成的主要工作是在IoC容器中建立BeanDefinition數據映射。在此過程中並沒有看到IoC容器對Bean依賴關係進行注入,接下來分析一下IoC容器是怎樣對Bean的依賴關係進行注入的。

  前面在refresh()–>invokeBeanFactoryPostProcessors(beanFactory);方法中已經完成了IoC容器的初始化並已經載入了我們定義的Bean的信息(BeanDefinition),現在我們開始分析依賴注入的原理。首先需要說明的是依賴注入在用戶第一次向IoC容器索要Bean時觸發,當然也有例外,我們可以在BeanDefinition中中通過控制lazy-init屬性來讓容器完成對Bean的預實例化。這個預實例化實際上也是一個依賴注入的過程,但它是在初始化過程中完成的。

 

二、源碼分析

2.1、getBean()的過程

  接着前面看refresh()方法,這已經是refresh()方法的第三篇博文了,別迷糊我們還沒走出refresh()方法。

 1 // AbstractApplicationContext類
 2 @Override
 3 public void refresh() throws BeansException, IllegalStateException {
 4     synchronized (this.startupShutdownMonitor) {
 5         ...
 6         try {
 7             ...
 8             // Instantiate all remaining (non-lazy-init) singletons.
 9             finishBeanFactoryInitialization(beanFactory);
10             ...
11         }
12         ...
13     }
14 }
15 // AbstractApplicationContext類
16 protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
17     ...
18     // Instantiate all remaining (non-lazy-init) singletons.
19     // 實例化所有剩餘的(non-lazy-init)單例。
20     beanFactory.preInstantiateSingletons();
21 }
22 // DefaultListableBeanFactory類
23 @Override
24 public void preInstantiateSingletons() throws BeansException {
25     ...
26     // Iterate over a copy to allow for init methods which in turn register new bean definitions.
27     // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
28     List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
29     // Trigger initialization of all non-lazy singleton beans...
30     for (String beanName : beanNames) {
31         RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
32         if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
33             if (isFactoryBean(beanName)) {
34                 Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
35                 if (bean instanceof FactoryBean) {
36                     final FactoryBean<?> factory = (FactoryBean<?>) bean;
37                     boolean isEagerInit;
38                     if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
39                         isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
40                                         ((SmartFactoryBean<?>) factory)::isEagerInit,
41                                 getAccessControlContext());
42                     } else {
43                         isEagerInit = (factory instanceof SmartFactoryBean &&
44                                 ((SmartFactoryBean<?>) factory).isEagerInit());
45                     }
46                     if (isEagerInit) {
47                         getBean(beanName);
48                     }
49                 }
50             } else {
51                 // 這裏就是觸發依賴注入的地方
52                 getBean(beanName);
53             }
54         }
55     }
56     ...
57 }

   跟蹤其調用棧,看到上面第52行的getBean(beanName);方法,我們再梳理一下getBean()方法,前面總結過該方法在IoC容器的頂層接口BeanFactory中定義,然後在IoC容器的具體產品DefaultListableBeanFactory類的基類AbstractBeanFactory實現了getBean()方法。接着看代碼。

 1 // AbstractBeanFactory類
 2 @Override
 3 public Object getBean(String name) throws BeansException {
 4     return doGetBean(name, null, null, false);
 5 }
 6 @Override
 7 public <T> T getBean(String name, @Nullable Class<T> requiredType) throws BeansException {
 8     return doGetBean(name, requiredType, null, false);
 9 }
10 @Override
11 public Object getBean(String name, Object... args) throws BeansException {
12     return doGetBean(name, null, args, false);
13 }
14 public <T> T getBean(String name, @Nullable Class<T> requiredType, @Nullable Object... args)
15         throws BeansException {
16     return doGetBean(name, requiredType, args, false);
17 }

  從上面代碼可知大致可分為兩種獲取Bean的參數,一種是按名獲取,一種是按類獲取。但是最終都進入到了doGetBean()方法。

  1 // AbstractBeanFactory類
  2 protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
  3         @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
  4 
  5     // bean獲取過程:先獲取bean名字
  6     // 會把帶有&前綴的去掉,或者去aliasMap中找這個是不是別名,最終確定bean的id是什麼
  7     final String beanName = transformedBeanName(name);
  8     Object bean;
  9 
 10     // 1.檢查緩存中或者實例工廠中是否有對應的實例
 11     // 因為在創建單例bean的時候會存在依賴注入的情況,而在創建依賴的時候為了避免循環依賴
 12     // Spring在創建bean的時候不會等bean創建完成就會將bean的ObjectFactory提早曝光
 13     // 也就是將ObjectFactory加入到緩存中,一旦下一個要創建的bean需要依賴上個bean則直接使用ObjectFactory
 14     // 2.spring 默認是單例的,如果能獲取到直接返回,提高效率。
 15     // Eagerly check singleton cache for manually registered singletons.
 16     Object sharedInstance = getSingleton(beanName);
 17     if (sharedInstance != null && args == null) {
 18         if (logger.isDebugEnabled()) {
 19             if (isSingletonCurrentlyInCreation(beanName)) {
 20                 logger.debug("Returning eagerly cached instance of singleton bean '" + beanName +
 21                         "' that is not fully initialized yet - a consequence of a circular reference");
 22             }
 23             else {
 24                 logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
 25             }
 26         }
 27         // 用於檢測bean的正確性,同時如果獲取的是FactoryBean的話還需要調用getObject()方法獲取最終的那個bean實例
 28         bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
 29     }
 30 
 31     else {
 32         // Fail if we're already creating this bean instance:
 33         // We're assumably within a circular reference.
 34         if (isPrototypeCurrentlyInCreation(beanName)) {
 35             throw new BeanCurrentlyInCreationException(beanName);
 36         }
 37 
 38         // Check if bean definition exists in this factory.
 39         //這裏對IoC容器中的BeanDefinition是否存在進行檢查,檢查是否能在當前的BeanFactory中取得需要的Bean。
 40         // 如果當前的工廠中取不到,則到雙親BeanFactory中去取。如果當前的雙親工廠取不到,那就順着雙親BeanFactory
 41         // 鏈一直向上查找。
 42         BeanFactory parentBeanFactory = getParentBeanFactory();
 43         if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
 44             // Not found -> check parent.
 45             String nameToLookup = originalBeanName(name);
 46             if (parentBeanFactory instanceof AbstractBeanFactory) {
 47                 // 遞歸調用父bean的doGetBean查找
 48                 return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
 49                         nameToLookup, requiredType, args, typeCheckOnly);
 50             }
 51             else if (args != null) {
 52                 // Delegation to parent with explicit args.
 53                 return (T) parentBeanFactory.getBean(nameToLookup, args);
 54             }
 55             else {
 56                 // No args -> delegate to standard getBean method.
 57                 return parentBeanFactory.getBean(nameToLookup, requiredType);
 58             }
 59         }
 60 
 61         if (!typeCheckOnly) {
 62             markBeanAsCreated(beanName);
 63         }
 64 
 65         try {
 66             //這裏根據Bean的名字取得BeanDefinition
 67             final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
 68             checkMergedBeanDefinition(mbd, beanName, args);
 69 
 70             // Guarantee initialization of beans that the current bean depends on.
 71             //獲取當前Bean的所有依賴Bean,這裡會觸發getBean的遞歸調用。知道取到一個沒有任何依賴的Bean為止。
 72             String[] dependsOn = mbd.getDependsOn();
 73             if (dependsOn != null) {
 74                 for (String dep : dependsOn) {
 75                     if (isDependent(beanName, dep)) {
 76                         throw new BeanCreationException(mbd.getResourceDescription(), beanName,
 77                                 "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
 78                     }
 79                     registerDependentBean(dep, beanName);
 80                     try {
 81                         getBean(dep);
 82                     }
 83                     catch (NoSuchBeanDefinitionException ex) {
 84                         throw new BeanCreationException(mbd.getResourceDescription(), beanName,
 85                                 "'" + beanName + "' depends on missing bean '" + dep + "'", ex);
 86                     }
 87                 }
 88             }
 89 
 90             // 這裏通過createBean方法創建singleton Bean的實例 這裏還有一個回調函數
 91             // Create bean instance.
 92             if (mbd.isSingleton()) {
 93                 sharedInstance = getSingleton(beanName, () -> {
 94                     try {
 95                         // 最後在getSingleton中又會調用這個方法
 96                         // TODO createBean的入口
 97                         return createBean(beanName, mbd, args);
 98                     }
 99                     catch (BeansException ex) {
100                         // Explicitly remove instance from singleton cache: It might have been put there
101                         // eagerly by the creation process, to allow for circular reference resolution.
102                         // Also remove any beans that received a temporary reference to the bean.
103                         destroySingleton(beanName);
104                         throw ex;
105                     }
106                 });
107                 bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
108             }
109             // 這裡是創建prototype bean的地方
110             else if (mbd.isPrototype()) {
111                 // It's a prototype -> create a new instance.
112                 Object prototypeInstance = null;
113                 try {
114                     beforePrototypeCreation(beanName);
115                     prototypeInstance = createBean(beanName, mbd, args);
116                 }
117                 finally {
118                     afterPrototypeCreation(beanName);
119                 }
120                 bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
121             }
122 
123             else {
124                 String scopeName = mbd.getScope();
125                 final Scope scope = this.scopes.get(scopeName);
126                 if (scope == null) {
127                     throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
128                 }
129                 try {
130                     Object scopedInstance = scope.get(beanName, () -> {
131                         beforePrototypeCreation(beanName);
132                         try {
133                             return createBean(beanName, mbd, args);
134                         }
135                         finally {
136                             afterPrototypeCreation(beanName);
137                         }
138                     });
139                     bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
140                 }
141                 catch (IllegalStateException ex) {
142                     throw new BeanCreationException(beanName,
143                             "Scope '" + scopeName + "' is not active for the current thread; consider " +
144                             "defining a scoped proxy for this bean if you intend to refer to it from a singleton",
145                             ex);
146                 }
147             }
148         }
149         catch (BeansException ex) {
150             cleanupAfterBeanCreationFailure(beanName);
151             throw ex;
152         }
153     }
154 
155     // Check if required type matches the type of the actual bean instance.
156     //這裏對創建的Bean進行類型檢查,如果沒有問題,就返回這個新創建的Bean,這個Bean已經是包含了依賴關係的Bean
157     if (requiredType != null && !requiredType.isInstance(bean)) {
158         try {
159             T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
160             if (convertedBean == null) {
161                 throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
162             }
163             return convertedBean;
164         }
165         catch (TypeMismatchException ex) {
166             if (logger.isDebugEnabled()) {
167                 logger.debug("Failed to convert bean '" + name + "' to required type '" +
168                         ClassUtils.getQualifiedName(requiredType) + "'", ex);
169             }
170             throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
171         }
172     }
173     return (T) bean;
174 }

  這個就是依賴注入的入口了,依賴注入是在容器的BeanDefinition數據已經建立好的前提下進行的。“程序=數據+算法”,很經典的一句話,前面我們詳細介紹了BeanDefinition的註冊過程,BeanDefinition就是數據。如上面代碼所示,doGetBean()方法不涉及複雜的算法,但是這個過程也不是很簡單,因為我們都知道,對於IoC容器的使用,Spring提供了很多的配置參數,每一個配置參數實際上就代表了一個IoC容器的實現特徵,這些特徵很多都需要在依賴注入的過程或者對Bean進行生命周期管理的過程中完成。雖然我們可以簡單的將IoC容器描述成一個ConcurrentHashMap,ConcurrentHashMap只是它的數據結構而不是IoC容器的全部。

TIPS:
  1,我在工程中簡單寫了一個controller和一個service,我們在後面debug看看依賴注入的過程是怎麼樣的,
也不知道我能不能說清楚。希望大家看到這一塊多debug一下,debug技巧如下圖所示,寫了debug的條件,
因為這邊到處都是遞歸和回調函數,再加上有很多Spring的Bean,但是我們只關心自己的Bean,
所以就寫了這樣的過濾條件:beanName.equals("webController")||beanName.equals("webService")
@RestController
public class WebController {
    @Autowired
    private WebService webService;

    @RequestMapping("/web")
    public String web(){
        return webService.hello();
    }
}

 

  下面我們通過代碼看看獲取bean的過程。

  OK,看代碼,Object sharedInstance = getSingleton(beanName);如註釋所說,首先回去找在容器中是不是已經存在該單例。具體在哪找我們在前面的文章中已經說得很清楚了。看一下getSingleton()方法

 1 // DefaultSingletonBeanRegistry類
 2 protected Object getSingleton(String beanName, boolean allowEarlyReference) {
 3     // 由於scope是singleton,所以先從緩存中取單例對象的實例,如果取到直接返回,沒有取到加載bean
 4     Object singletonObject = this.singletonObjects.get(beanName);
 5     // 當想要獲取的bean沒有被加載,並且也沒有正在被創建的時候,主動去加載bean
 6     if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
 7         // 鎖住單例緩存區加載bean
 8         synchronized (this.singletonObjects) {
 9             // singletonObjects ,earlySingletonObjects ,singletonFactories是一個單例實例的三種存在狀態
10             // 再去earlySingletonObjects中去找
11             singletonObject = this.earlySingletonObjects.get(beanName);
12             if (singletonObject == null && allowEarlyReference) {
13                 // 去singletonFactories中去找對象的實例
14                 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
15                 if (singletonFactory != null) {
16                     singletonObject = singletonFactory.getObject();
17                     this.earlySingletonObjects.put(beanName, singletonObject);
18                     this.singletonFactories.remove(beanName);
19                 }
20             }
21         }
22     }
23     return singletonObject;
24 }

 

   在DefaultSingletonBeanRegistry類中的singletonObjects屬性就是存singleton bean的地方。

  如果getSingleton()為 null繼續往下看,會在當前的BeanFactory中獲取BeanDefinition,也就是這行方法代碼:final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);在這行代碼拿到BeanDefinition后,首先判斷是不是singleton Bean,如果是的話,開始執行創建Bean,正是return createBean(beanName, mbd, args);這行代碼。如果是原型(Prototype)Bean我們就不分析了。原型bean每次執行getBean()都會創建一個實例。接下來我們看createBean()方法。

2.2、createBean()的過程

  首先看一下create bean的過程

1,Bean實例的創建
2,為Bean實例設置屬性(屬性注入,其實就是依賴注入真正發生的地方)
3,調用Bean的初始化方法

  前面說了getBean()是依賴注入的起點,之後會調用createBean(),下面通過createBean()代碼來了解這個過程。在這個過程中,Bean對象會根據BeanDefinition定義的要求生成。

 1 // AbstractAutowireCapableBeanFactory類
 2 @Override
 3 protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
 4         throws BeanCreationException {
 5     ...
 6     try {
 7         // 驗證以及準備override的方法
 8         mbdToUse.prepareMethodOverrides();
 9     }
10     catch (BeanDefinitionValidationException ex) {
11         throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(),
12                 beanName, "Validation of method overrides failed", ex);
13     }
14     try {
15         // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
16         // createBean之前調用BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization方法
17         // 默認不做任何處理所以會返回null
18         // 但是如果我們重寫了這兩個方法,那麼bean的創建過程就結束了,這裏就為以後的annotation自動注入提供了鈎子
19         Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
20         if (bean != null) {
21             return bean;
22         }
23     }catch (Throwable ex) {
24         throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName,
25                 "BeanPostProcessor before instantiation of bean failed", ex);
26     }
27     try {
28         // 實際執行createBean的是doCreateBean()方法
29         Object beanInstance = doCreateBean(beanName, mbdToUse, args);
30         if (logger.isDebugEnabled()) {
31             logger.debug("Finished creating instance of bean '" + beanName + "'");
32         }
33         return beanInstance;
34     }
35     catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) {
36         // A previously detected exception with proper bean creation context already,
37         // or illegal singleton state to be communicated up to DefaultSingletonBeanRegistry.
38         throw ex;
39     }
40     catch (Throwable ex) {
41         throw new BeanCreationException(
42                 mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex);
43     }
44 }

   接着往下看doCreateBean()方法。

  1 // AbstractAutowireCapableBeanFactory類
  2 protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
  3         throws BeanCreationException {
  4     // BeanWrapper是用來持有創建出來的Bean對象
  5     // Instantiate the bean.
  6     BeanWrapper instanceWrapper = null;
  7     // 如果是單例,先把緩存中的同名Bean清除
  8     if (mbd.isSingleton()) {
  9         instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
 10     }
 11     // 這裡是創建Bean的地方,由createBeanInstance完成。
 12     // TODO 完成Bean初始化過程的第一步:創建實例
 13     if (instanceWrapper == null) {
 14         instanceWrapper = createBeanInstance(beanName, mbd, args);
 15     }
 16     final Object bean = instanceWrapper.getWrappedInstance();
 17     Class<?> beanType = instanceWrapper.getWrappedClass();
 18     if (beanType != NullBean.class) {
 19         mbd.resolvedTargetType = beanType;
 20     }
 21 
 22     // Allow post-processors to modify the merged bean definition.
 23     synchronized (mbd.postProcessingLock) {
 24         if (!mbd.postProcessed) {
 25             try {
 26                 applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
 27             }
 28             catch (Throwable ex) {
 29                 throw new BeanCreationException(mbd.getResourceDescription(), beanName,
 30                         "Post-processing of merged bean definition failed", ex);
 31             }
 32             mbd.postProcessed = true;
 33         }
 34     }
 35 
 36     // Eagerly cache singletons to be able to resolve circular references
 37     // even when triggered by lifecycle interfaces like BeanFactoryAware.
 38     // 是否自動解決循環引用
 39     // 當bean條件為: 單例&&允許循環引用&&正在創建中這樣的話提早暴露一個ObjectFactory
 40     boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
 41             isSingletonCurrentlyInCreation(beanName));
 42     if (earlySingletonExposure) {
 43         if (logger.isDebugEnabled()) {
 44             logger.debug("Eagerly caching bean '" + beanName +
 45                     "' to allow for resolving potential circular references");
 46         }
 47         // 把ObjectFactory放進singletonFactories中
 48         // 這裡在其他bean在創建的時候會先去singletonFactories中查找有沒有beanName到ObjectFactory的映射
 49         // 如果有ObjectFactory就調用它的getObject方法獲取實例
 50         // 但是在這裏就可以對一個bean進行保證,代理等等AOP就可以在getEarlyBeanReference這裏實現
 51         addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
 52     }
 53 
 54     // Initialize the bean instance.
 55     Object exposedObject = bean;
 56     try {
 57         // TODO 完成Bean初始化過程的第二步:為Bean的實例設置屬性
 58         // Bean依賴注入發生的地方
 59         // 對bean進行屬性填充,如果存在依賴於其他的bean的屬性,則會遞歸的調用初始化依賴的bean
 60         populateBean(beanName, mbd, instanceWrapper);
 61         // TODO 完成Bean初始化過程的第三步:調用Bean的初始化方法(init-method)
 62         // 調用初始化方法,比如init-method方法指定的方法
 63         exposedObject = initializeBean(beanName, exposedObject, mbd);
 64     }
 65     catch (Throwable ex) {
 66         if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
 67             throw (BeanCreationException) ex;
 68         }
 69         else {
 70             throw new BeanCreationException(
 71                     mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
 72         }
 73     }
 74 
 75     if (earlySingletonExposure) {
 76         Object earlySingletonReference = getSingleton(beanName, false);
 77         if (earlySingletonReference != null) {
 78             if (exposedObject == bean) {
 79                 exposedObject = earlySingletonReference;
 80             }
 81             else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
 82                 String[] dependentBeans = getDependentBeans(beanName);
 83                 Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
 84                 for (String dependentBean : dependentBeans) {
 85                     if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
 86                         actualDependentBeans.add(dependentBean);
 87                     }
 88                 }
 89                 if (!actualDependentBeans.isEmpty()) {
 90                     throw new BeanCurrentlyInCreationException(beanName,
 91                             "Bean with name '" + beanName + "' has been injected into other beans [" +
 92                             StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
 93                             "] in its raw version as part of a circular reference, but has eventually been " +
 94                             "wrapped. This means that said other beans do not use the final version of the " +
 95                             "bean. This is often the result of over-eager type matching - consider using " +
 96                             "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
 97                 }
 98             }
 99         }
100     }
101 
102     // Register bean as disposable.
103     try {
104         // 註冊銷毀方法,比如:可以在配置bean的時候指定destory-method方法
105         registerDisposableBeanIfNecessary(beanName, bean, mbd);
106     }
107     catch (BeanDefinitionValidationException ex) {
108         throw new BeanCreationException(
109                 mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
110     }
111 
112     return exposedObject;
113 }

  結合上面的代碼,我們再來看一下創建Bean的三個步驟,是不是有點豁然開朗的感覺。別著急繼續往下看。

1,Bean實例的創建,instanceWrapper = createBeanInstance(beanName, mbd, args);
2,為Bean實例設置屬性,populateBean(beanName, mbd, instanceWrapper);
3,調用Bean的初始化方法,exposedObject = initializeBean(beanName, exposedObject, mbd);

 

2.2.1、createBeanInstance():Bean實例的創建

  看代碼

 1 // AbstractAutowireCapableBeanFactory類
 2 protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
 3     // Make sure bean class is actually resolved at this point.
 4     // 確認需要創建的Bean的實例的類可以實例化
 5     Class<?> beanClass = resolveBeanClass(mbd, beanName);
 6 
 7     if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {
 8         throw new BeanCreationException(mbd.getResourceDescription(), beanName,
 9                 "Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
10     }
11 
12     Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
13     if (instanceSupplier != null) {
14         return obtainFromSupplier(instanceSupplier, beanName);
15     }
16 
17     // 當有工廠方法的時候使用工廠方法初始化Bean,就是配置的時候指定FactoryMethod屬性,類似註解中的@Bean把方法的返回值作為Bean
18     if (mbd.getFactoryMethodName() != null)  {
19         return instantiateUsingFactoryMethod(beanName, mbd, args);
20     }
21 
22     // Shortcut when re-creating the same bean...
23     boolean resolved = false;
24     boolean autowireNecessary = false;
25     if (args == null) {
26         synchronized (mbd.constructorArgumentLock) {
27             if (mbd.resolvedConstructorOrFactoryMethod != null) {
28                 resolved = true;
29                 autowireNecessary = mbd.constructorArgumentsResolved;
30             }
31         }
32     }
33     if (resolved) {
34         if (autowireNecessary) {
35             // 如果有有參數的構造函數,構造函數自動注入
36             // 這裏spring會花費大量的精力去進行參數的匹配
37             return autowireConstructor(beanName, mbd, null, null);
38         }
39         else {
40             // 如果沒有有參構造函數,使用默認構造函數構造
41             return instantiateBean(beanName, mbd);
42         }
43     }
44 
45     // Need to determine the constructor...
46     // 使用構造函數進行實例化
47     Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
48     if (ctors != null ||
49             mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
50             mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args))  {
51         return autowireConstructor(beanName, mbd, ctors, args);
52     }
53 
54     // No special handling: simply use no-arg constructor.
55     // 使用默認的構造函數對Bean進行實例化
56     return instantiateBean(beanName, mbd);
57 }

 

  我們可以看到在instantiateBean()方法中生成了Bean所包含的Java對象,這個對象的生成有很多種不同的方式,可以通過工廠方法生成,也可以通過容器的autowire特性生成,這些生成方式都是由BeanDefinition決定的。對於上面我們的WebController和WebService兩個類是通過最後一行,使用默認的構造函數進行Bean的實例化。

  接着看instantiateBean()方法。

 1 // AbstractAutowireCapableBeanFactory類
 2 protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
 3     // 使用默認的實例化策略對Bean進行實例化,默認的實例化策略是CglibSubclassingInstantiationStrategy,
 4     // 也就是常說的CGLIB來對Bean進行實例化。PS:面試官常問的字節碼增強
 5     try {
 6         Object beanInstance;
 7         final BeanFactory parent = this;
 8         if (System.getSecurityManager() != null) {
 9             beanInstance = AccessController.doPrivileged((PrivilegedAction<Object>) () ->
10                     getInstantiationStrategy().instantiate(mbd, beanName, parent),
11                     getAccessControlContext());
12         }
13         else {
14             // getInstantiationStrategy()會返回CglibSubclassingInstantiationStrategy類的實例
15             beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
16         }
17         BeanWrapper bw = new BeanWrapperImpl(beanInstance);
18         initBeanWrapper(bw);
19         return bw;
20     }
21     catch (Throwable ex) {
22         throw new BeanCreationException(
23                 mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
24     }
25 }

  這裏使用CGLIB進行Bean的實例化。CGLIB是一個常用的字節碼生成器的類庫,其提供了一系列的API來提供生成和轉換Java字節碼的功能。在Spring AOP中同樣也是使用的CGLIB對Java的字節碼進行增強。在IoC容器中,使用SimpleInstantiationStrategy類。這個類是Spring用來生成Bean對象的默認類,它提供了兩種實例化Java對象的方法,一種是通過BeanUtils,它使用的是JVM的反射功能,一種是通過CGLIB來生成。

  getInstantiationStrategy()方法獲取到CglibSubclassingInstantiationStrategy實例,instantiate()是CglibSubclassingInstantiationStrategy類的父類SimpleInstantiationStrategy實現的。

  繼續看代碼

 1 // SimpleInstantiationStrategy類
 2 @Override
 3 public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
 4     // Don't override the class with CGLIB if no overrides.
 5     // 如果BeanFactory重寫了Bean內的方法,則使用CGLIB,否則使用BeanUtils
 6     if (!bd.hasMethodOverrides()) {
 7         // 如果bean沒有需要動態替換的方法就直接反射進行創建實例
 8         Constructor<?> constructorToUse;
 9         synchronized (bd.constructorArgumentLock) {
10             constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod;
11             if (constructorToUse == null) {
12                 final Class<?> clazz = bd.getBeanClass();
13                 if (clazz.isInterface()) {
14                     throw new BeanInstantiationException(clazz, "Specified class is an interface");
15                 }
16                 try {
17                     if (System.getSecurityManager() != null) {
18                         constructorToUse = AccessController.doPrivileged(
19                                 (PrivilegedExceptionAction<Constructor<?>>) clazz::getDeclaredConstructor);
20                     } else {
21                         constructorToUse = clazz.getDeclaredConstructor();
22                     }
23                     bd.resolvedConstructorOrFactoryMethod = constructorToUse;
24                 } catch (Throwable ex) {
25                     throw new BeanInstantiationException(clazz, "No default constructor found", ex);
26                 }
27             }
28         }
29         // 通過BeanUtils進行實例化,這個BeanUtils的實例化通過Constructor類實例化Bean
30         // 在BeanUtils中可以看到具體的調用ctor.newInstances(args)
31         return BeanUtils.instantiateClass(constructorToUse);
32     } else {
33         // Must generate CGLIB subclass.
34         // TODO 使用CGLIB實例化對象
35         return instantiateWithMethodInjection(bd, beanName, owner);
36     }
37 }

  在SpringBoot中我們一般採用@Autowire的方式進行依賴注入,很少採用像SpringMVC那種在xml中使用<lookup-method>或者<replaced-method>等標籤的方式對注入的屬性進行override,所以在上面的代碼中if(!bd.hasMethodOverrides())中的判斷為true,會採用BeanUtils的實例化方式。

 

2.2.2、populateBean();屬性設置(依賴注入)

  看代碼

 1 protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
 2     if (bw == null) {
 3         if (mbd.hasPropertyValues()) {
 4             throw new BeanCreationException(
 5                     mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
 6         }
 7         else {
 8             // Skip property population phase for null instance.
 9             return;
10         }
11     }
12     // Give any InstantiationAwareBeanPostProcessors the opportunity to modify the
13     // state of the bean before properties are set. This can be used, for example,
14     // to support styles of field injection.
15     boolean continueWithPropertyPopulation = true;
16     // 調用InstantiationAwareBeanPostProcessor  Bean的後置處理器,在Bean注入屬性前改變BeanDefinition的信息
17     if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
18         for (BeanPostProcessor bp : getBeanPostProcessors()) {
19             if (bp instanceof InstantiationAwareBeanPostProcessor) {
20                 InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
21                 if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
22                     continueWithPropertyPopulation = false;
23                     break;
24                 }
25             }
26         }
27     }
28     if (!continueWithPropertyPopulation) {
29         return;
30     }
31     // 這裏取得在BeanDefinition中設置的property值,這些property來自對BeanDefinition的解析
32     // 用於在配置文件中通過<property>配置的屬性並且显示在配置文件中配置了autowireMode屬性
33     PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
34     if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME ||
35             mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
36         MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
37 
38         // Add property values based on autowire by name if applicable.
39         // 這裏對autowire注入的處理,autowire by name
40         if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
41             autowireByName(beanName, mbd, bw, newPvs);
42         }
43 
44         // Add property values based on autowire by type if applicable.
45         // 這裏對autowire注入的處理, autowire by type
46         // private List<Test> tests;
47         if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
48             autowireByType(beanName, mbd, bw, newPvs);
49         }
50 
51         pvs = newPvs;
52     }
53     boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
54     boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);
55     if (hasInstAwareBpps || needsDepCheck) {
56         if (pvs == null) {
57             pvs = mbd.getPropertyValues();
58         }
59         PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
60         if (hasInstAwareBpps) {
61             for (BeanPostProcessor bp : getBeanPostProcessors()) {
62                 if (bp instanceof InstantiationAwareBeanPostProcessor) {
63                     InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
64                     // TODO @Autowire @Resource @Value @Inject 等註解的依賴注入過程
65                     pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
66                     if (pvs == null) {
67                         return;
68                     }
69                 }
70             }
71         }
72         if (needsDepCheck) {
73             checkDependencies(beanName, mbd, filteredPds, pvs);
74         }
75     }
76     if (pvs != null) {
77         // 注入配置文件中<property>配置的屬性
78         applyPropertyValues(beanName, mbd, bw, pvs);
79     }
80 }

  上面方法中的31-52行以及78行的applyPropertyValues()方法基本都是用於SpringMVC中採用xml配置Bean的方法。所以我們不做介紹了。看註釋知道幹嘛的就行了。我們主要看的是pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);這行代碼,這行代碼是真正執行採用@Autowire @Resource @Value @Inject 等註解的依賴注入過程。

  接着往下看

 1 // AutowiredAnnotationBeanPostProcessor類
 2 @Override
 3 public PropertyValues postProcessPropertyValues(
 4         PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {
 5     // 遍歷,獲取@Autowire,@Resource,@Value,@Inject等具備註入功能的註解
 6     InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
 7     try {
 8         // 屬性注入
 9         metadata.inject(bean, beanName, pvs);
10     } catch (BeanCreationException ex) {
11         throw ex;
12     } catch (Throwable ex) {
13         throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
14     }
15     return pvs;
16 }

  AutowiredAnnotationBeanPostProcessor類實現了postProcessPropertyValues()方法。findAutowiringMetadata(beanName, bean.getClass(), pvs);方法會尋找在當前類中的被@Autowire,@Resource,@Value,@Inject等具備註入功能的註解的屬性。

  debug看一下結果,如下圖所示,成功得到了@Autowire註解的屬性webService。

  metadata.inject(bean, beanName, pvs);方法開始執行注入的邏輯。

 1 // AutowiredAnnotationBeanPostProcessor類
 2 @Override
 3 protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
 4     // 需要注入的字段
 5     Field field = (Field) this.member;
 6     // 需要注入的屬性值
 7     Object value;
 8     if (this.cached) {
 9         value = resolvedCachedArgument(beanName, this.cachedFieldValue);
10     } else {
11         // @Autowired(required = false),當在該註解中設置為false的時候,如果有直接注入,沒有跳過,不會報錯。
12         DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
13         desc.setContainingClass(bean.getClass());
14         Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
15         Assert.state(beanFactory != null, "No BeanFactory available");
16         TypeConverter typeConverter = beanFactory.getTypeConverter();
17         try {
18             // 通過BeanFactory 解決依賴關係
19             // 比如在webController中注入了webService,這個會去BeanFactory中去獲取webService,也就是getBean()的邏輯。
20             // 如果存在直接返回,不存在再執行createBean()邏輯。
21             // 如果在webService中依然依賴,依然會去遞歸。
22             // 這裡是一個複雜的遞歸邏輯。
23             value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
24         } catch (BeansException ex) {
25             throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
26         }
27         synchronized (this) {
28             if (!this.cached) {
29                 if (value != null || this.required) {
30                     this.cachedFieldValue = desc;
31                     registerDependentBeans(beanName, autowiredBeanNames);
32                     if (autowiredBeanNames.size() == 1) {
33                         String autowiredBeanName = autowiredBeanNames.iterator().next();
34                         if (beanFactory.containsBean(autowiredBeanName) &&
35                                 beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
36                             this.cachedFieldValue = new ShortcutDependencyDescriptor(
37                                     desc, autowiredBeanName, field.getType());
38                         }
39                     }
40                 } else {
41                     this.cachedFieldValue = null;
42                 }
43                 this.cached = true;
44             }
45         }
46     }
47     if (value != null) {
48         ReflectionUtils.makeAccessible(field);
49         field.set(bean, value);
50     }
51 }

  debug看一下field。如下圖所示正是我們的webService

  看這行代碼:value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);注意beanFactory依舊是我們熟悉的IoC容器的具體產品,也就是實現類DefaultListableBeanFactory。見到就說一遍,方便大家記住它,很重要。

  在resolveDependency()方法中經過一頓操作,最終又會來到上面的getBean()方法。以上就是依賴注入的整個過程。注意看代碼中的註釋哦。

2.2.3、initializeBean():調用Bean的初始化方法

  設置Bean的初始化方法有兩種方法,一種是在xml或者@Bean指定init-method方法。另一種是讓bean實現InitializingBean接口重寫afterPropertiesSet()方法。

 1 // AbstractAutowireCapableBeanFactory類
 2 protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
 3     if (System.getSecurityManager() != null) {
 4         AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
 5             invokeAwareMethods(beanName, bean);
 6             return null;
 7         }, getAccessControlContext());
 8     }
 9     else {
10         //在調用Bean的初始化方法之前,調用一系列的aware接口實現,把相關的BeanName,BeanClassLoader,以及BeanFactory注入到Bean中去。
11         invokeAwareMethods(beanName, bean);
12     }
13 
14     Object wrappedBean = bean;
15     if (mbd == null || !mbd.isSynthetic()) {
16         // 這些都是鈎子方法,在反覆的調用,給Spring帶來了極大的可拓展性
17         // 初始化之前調用BeanPostProcessor
18         wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
19     }
20 
21     try {
22         // 調用指定的init-method方法
23         invokeInitMethods(beanName, wrappedBean, mbd);
24     }
25     catch (Throwable ex) {
26         throw new BeanCreationException(
27                 (mbd != null ? mbd.getResourceDescription() : null),
28                 beanName, "Invocation of init method failed", ex);
29     }
30     if (mbd == null || !mbd.isSynthetic()) {
31         // 這些都是鈎子方法,在反覆的調用,給Spring帶來了極大的可拓展性
32         // 初始化之後調用BeanPostProcessor
33         wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
34     }
35 
36     return wrappedBean;
37 }

  在調用Bean的初始化方法之前,調用一系列的aware接口實現,把相關的BeanName,BeanClassLoader,以及BeanFactory注入到Bean中去。接着會執行invokeInitMethods()方法。

 1 // AbstractAutowireCapableBeanFactory類
 2 protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
 3         throws Throwable {
 4     // 除了使用init-method指定的初始化方法,還可以讓bean實現InitializingBean接口重寫afterPropertiesSet()方法
 5     boolean isInitializingBean = (bean instanceof InitializingBean);
 6     if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
 7         if (logger.isDebugEnabled()) {
 8             logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
 9         }
10         if (System.getSecurityManager() != null) {
11             try {
12                 AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
13                     ((InitializingBean) bean).afterPropertiesSet();
14                     return null;
15                 }, getAccessControlContext());
16             }
17             catch (PrivilegedActionException pae) {
18                 throw pae.getException();
19             }
20         }
21         else {
22             // 執行afterPropertiesSet()方法進行初始化
23             ((InitializingBean) bean).afterPropertiesSet();
24         }
25     }
26 
27     // 先執行afterPropertiesSet()方法,再進行init-method
28     if (mbd != null && bean.getClass() != NullBean.class) {
29         String initMethodName = mbd.getInitMethodName();
30         if (StringUtils.hasLength(initMethodName) &&
31                 !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
32                 !mbd.isExternallyManagedInitMethod(initMethodName)) {
33             invokeCustomInitMethod(beanName, bean, mbd);
34         }
35     }
36 }

  可見該方法中首先判斷Bean是否配置了init-method方法,如果有,那麼通過invokeCustomInitMethod()方法來直接調用。其中在invokeCustomInitMethod()方法中是通過JDK的反射機製得到method對象,然後調用的init-method。最終完成Bean的初始化。

 

三、總結

  SpringBoot啟動流程相關的博文到這裏就結束了,在前面的文章中,我們詳細介紹了IoC容器的設計與實現,並結合SpringBoot的啟動流程介紹了IoC容器的初始化過程,及IoC容器的依賴注入,及大家都很關心的SpringBoot是如何實現自動裝配的。關於SpringBoot的源碼分析基本就到這裏了,後面有計劃寫寫AOP的實現,以及很重要的Spring事務實現。

  小半個月時間,熬了很多個凌晨,終於寫完了SpringBoot啟動流程系列博文。有辛苦更有收穫,我是從今年才開始寫博客的,以前每當想寫的時候,總想什麼時候能力足夠了,憋個大招再寫。回過頭來看之前的想法是十分錯誤的,寫博文不只是為了分享,最重要的是在寫博客的過程中能夠系統的梳理一下某一個技術棧的知識。好記性不如爛筆頭,看別人的博客很難get到自己想要的點,所以也是讓自己持續進步的一種方式。加油,各位。

 

 

  原創不易,轉載請註明出處。

  如有錯誤的地方還請留言指正。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

高級Java工程師必備 —– 深入分析 Java IO (二)NIO

接着上一篇文章 高級Java工程師必備 —– 深入分析 Java IO (一)BIO,我們來講講NIO

多路復用IO模型

場景描述

一個餐廳同時有100位客人到店,當然到店后第一件要做的事情就是點菜。但是問題來了,餐廳老闆為了節約人力成本目前只有一位大堂服務員拿着唯一的一本菜單等待客人進行服務。

方法A: 無論有多少客人等待點餐,服務員都把僅有的一份菜單遞給其中一位客人,然後站在客人身旁等待這個客人完成點菜過程。在記錄客人點菜內容后,把點菜記錄交給後堂廚師。然後是第二位客人。。。。然後是第三位客人。很明顯,只有腦袋被門夾過的老闆,才會這樣設置服務流程。因為隨後的80位客人,再等待超時后就會離店(還會給差評)。

方法B: 老闆馬上新雇傭99名服務員,同時印製99本新的菜單。每一名服務員手持一本菜單負責一位客人(關鍵不只在於服務員,還在於菜單。因為沒有菜單客人也無法點菜)。在客人點完菜后,記錄點菜內容交給後堂廚師(當然為了更高效,後堂廚師最好也有100名)。這樣每一位客人享受的就是VIP服務咯,當然客人不會走,但是人力成本可是一個大頭哦(虧死你)。

方法C: 就是改進點菜的方式,當客人到店后,自己申請一本菜單。想好自己要點的才后,就呼叫服務員。服務員站在自己身邊後記錄客人的菜單內容。將菜單遞給廚師的過程也要進行改進,並不是每一份菜單記錄好以後,都要交給後堂廚師。服務員可以記錄號多份菜單后,同時交給廚師就行了。那麼這種方式,對於老闆來說人力成本是最低的;對於客人來說,雖然不再享受VIP服務並且要進行一定的等待,但是這些都是可接受的;對於服務員來說,基本上她的時間都沒有浪費,基本上被老闆壓桿了最後一滴油水。

到店情況:併發量。到店情況不理想時,一個服務員一本菜單,當然是足夠了。所以不同的老闆在不同的場合下,將會靈活選擇服務員和菜單的配置。
客人:客戶端請求
點餐內容:客戶端發送的實際數據
服務員:操作系統內核用於IO操作的線程(內核線程)
廚師:應用程序線程(當然廚房就是應用程序進程咯)
餐單傳遞方式:包括了阻塞式和非阻塞式兩種。

  • 方法A:阻塞式/非阻塞式 同步IO
  • 方法B:使用線程進行處理的 阻塞式/非阻塞式 同步IO
  • 方法C:阻塞式/非阻塞式 多路復用IO

多路復用IO技術最適用的是“高併發”場景,所謂高併發是指1毫秒內至少同時有上千個連接請求準備好。其他情況下多路復用IO技術發揮不出來它的優勢。另一方面,使用JAVA NIO進行功能實現,相對於傳統的Socket套接字實現要複雜一些,所以實際應用中,需要根據自己的業務需求進行技術選擇。

NIO

概念

JDK 1.4中的java.nio.*包中引入新的Java I/O庫,其目的是提高速度。實際上,“舊”的I/O包已經使用NIO重新實現過,即使我們不顯式的使用NIO編程,也能從中受益。速度的提高在文件I/O和網絡I/O中都可能會發生,但本文只討論後者。

NIO我們一般認為是New I/O(也是官方的叫法),因為它是相對於老的I/O類庫新增的(其實在JDK 1.4中就已經被引入了,但這個名詞還會繼續用很久,即使它們在現在看來已經是“舊”的了,所以也提示我們在命名時,需要好好考慮),做了很大的改變。但民間跟多人稱之為Non-block I/O,即非阻塞I/O,因為這樣叫,更能體現它的特點。而下文中的NIO,不是指整個新的I/O庫,而是非阻塞I/O。

面向流與面向緩衝

Java IO和NIO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。 Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前後移動流中的數據。如果需要前後移動從流中讀取的數據,需要先將它緩存到一個緩衝區。

面向塊的 NIO一次處理一個數據塊,按塊處理數據比按流處理數據要快得多。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區里尚未處理的數據。

阻塞與非阻塞IO

Java IO的各種流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再干任何事情了。Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取,而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。 線程通常將非阻塞IO的空閑時間用於在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。

通道

通道 Channel 是對原 I/O 包中的流的模擬,可以通過它讀取和寫入數據。

通道與流的不同之處在於,流只能在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類),而通道是雙向的,可以用於讀、寫或者同時用於讀寫。

通道包括以下類型:

  • FileChannel:從文件中讀寫數據;
  • DatagramChannel:通過 UDP 讀寫網絡中數據;
  • SocketChannel:通過 TCP 讀寫網絡中數據;
  • ServerSocketChannel:可以監聽新進來的 TCP 連接,對每一個新進來的連接都會創建一個 SocketChannel。

緩衝區

發送給一個通道的所有數據都必須首先放到緩衝區中,同樣地,從通道中讀取的任何數據都要先讀到緩衝區中。也就是說,不會直接對通道進行讀寫數據,而是要先經過緩衝區。

緩衝區實質上是一個數組,但它不僅僅是一個數組。緩衝區提供了對數據的結構化訪問,而且還可以跟蹤系統的讀/寫進程。

Buffer有兩種工作模式:寫模式和讀模式。在讀模式下,應用程序只能從Buffer中讀取數據,不能進行寫操作。但是在寫模式下,應用程序是可以進行讀操作的,這就表示可能會出現臟讀的情況。所以一旦您決定要從Buffer中讀取數據,一定要將Buffer的狀態改為讀模式。

注意:ServerSocketChannel通道它只支持對OP_ACCEPT事件的監聽,所以它是不能直接進行網絡數據內容的讀寫的。所以ServerSocketChannel是沒有集成Buffer的。

緩衝區包括以下類型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

 

 

可以用三個值指定緩衝區在任意時刻的狀態:

  • position
  • limit
  • capacity

Position

您可以回想一下,緩衝區實際上就是美化了的數組。在從通道讀取時,您將所讀取的數據放到底層的數組中。 position 變量跟蹤已經寫了多少數據。更準確地說,它指定了下一個字節將放到數組的哪一個元素中。因此,如果您從通道中讀三個字節到緩衝區中,那麼緩衝區的 position 將會設置為3,指向數組中第四個元素。

同樣,在寫入通道時,您是從緩衝區中獲取數據。 position 值跟蹤從緩衝區中獲取了多少數據。更準確地說,它指定下一個字節來自數組的哪一個元素。因此如果從緩衝區寫了5個字節到通道中,那麼緩衝區的 position 將被設置為5,指向數組的第六個元素。

Limit

limit 變量表明還有多少數據需要取出(在從緩衝區寫入通道時),或者還有多少空間可以放入數據(在從通道讀入緩衝區時)。

position 總是小於或者等於 limit。

Capacity

緩衝區的 capacity 表明可以儲存在緩衝區中的最大數據容量。實際上,它指定了底層數組的大小 ― 或者至少是指定了准許我們使用的底層數組的容量。

limit 決不能大於 capacity。

 

在實際操作數據時它們有如下關係圖:

① 新建一個大小為 8 個字節的緩衝區,此時 position 為 0,而 limit = capacity = 8。capacity 變量不會改變,下面的討論會忽略它。

② 從輸入通道中讀取 5 個字節數據寫入緩衝區中,此時 position 為 5,limit 保持不變。

③ 在將緩衝區的數據寫到輸出通道之前,需要先調用 flip() 方法,這個方法將 limit 設置為當前 position,並將 position 設置為 0。

④ 從緩衝區中取 4 個字節到輸出緩衝中,此時 position 設為 4。

⑤ 最後需要調用 clear() 方法來清空緩衝區,此時 position 和 limit 都被設置為最初位置。

 

文件複製 NIO 實例

以下展示了使用 NIO 快速複製文件的實例:

public static void fastCopy(String src, String dist) throws IOException {

    /* 獲得源文件的輸入字節流 */
    FileInputStream fin = new FileInputStream(src);

    /* 獲取輸入字節流的文件通道 */
    FileChannel fcin = fin.getChannel();

    /* 獲取目標文件的輸出字節流 */
    FileOutputStream fout = new FileOutputStream(dist);

    /* 獲取輸出字節流的文件通道 */
    FileChannel fcout = fout.getChannel();

    /* 為緩衝區分配 1024 個字節 */
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

    while (true) {

        /* 從輸入通道中讀取數據到緩衝區中 */
        int r = fcin.read(buffer);

        /* read() 返回 -1 表示 EOF */
        if (r == -1) {
            break;
        }

        /* 切換讀寫 */
        buffer.flip();

        /* 把緩衝區的內容寫入輸出文件中 */
        fcout.write(buffer);

        /* 清空緩衝區 */
        buffer.clear();
    }
}

選擇器

NIO 常常被叫做非阻塞 IO,主要是因為 NIO 在網絡通信中的非阻塞特性被廣泛使用。

NIO 實現了 IO 多路復用中的 Reactor 模型,一個線程 Thread 使用一個選擇器 Selector 通過輪詢的方式去監聽多個通道 Channel 上的事件,從而讓一個線程就可以處理多個事件。

通過配置監聽的通道 Channel 為非阻塞,那麼當 Channel 上的 IO 事件還未到達時,就不會進入阻塞狀態一直等待,而是繼續輪詢其它 Channel,找到 IO 事件已經到達的 Channel 執行。

例如,當多個客戶端通過通道向服務端傳輸數據時,是通過 ByteBuffer 來傳輸,一個文件通過多次,從輸入通道中讀取 N 個字節數據寫入ByteBuffer,然後再將將緩衝區的數據寫到輸出通道,這個過程可以看成是不連續的,因為只有當緩衝區寫滿后,通過 buffer.flip() 切換成讀模式后,才開始向輸出通道寫入,所以當ByteBuffer還在寫入狀態時,服務器是不會等待這個通道的ByteBuffer寫滿,而是去處理其他客戶端Channel 為可讀的狀態,當然這個處理業務的工作可以開啟多線程來處理。

因為創建和切換線程的開銷很大,因此使用一個線程來處理多個事件而不是一個線程處理一個事件,對於 IO 密集型的應用具有很好地性能。

應該注意的是,只有套接字 Channel 才能配置為非阻塞,而 FileChannel 不能,為 FileChannel 配置非阻塞也沒有意義。

套接字 NIO 實例

package com.chenhao.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.Scanner;

import org.junit.Test;

/*
 * 一、使用 NIO 完成網絡通信的三個核心:
 * 
 * 1. 通道(Channel):負責連接
 *         
 *        java.nio.channels.Channel 接口:
 *             |--SelectableChannel
 *                 |--SocketChannel
 *                 |--ServerSocketChannel
 *                 |--DatagramChannel
 * 
 * 2. 緩衝區(Buffer):負責數據的存取
 * 
 * 3. 選擇器(Selector):是 SelectableChannel 的多路復用器。用於監控 SelectableChannel 的 IO 狀況
 * 
 */
public class TestNonBlockingNIO {
    
    //客戶端
    @Test
    public void client() throws IOException{
        //1. 獲取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        
        //2. 切換非阻塞模式
        sChannel.configureBlocking(false);
        
        //3. 分配指定大小的緩衝區
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        //4. 發送數據給服務端
        Scanner scan = new Scanner(System.in);
        
        while(scan.hasNext()){
            String str = scan.next();
            buf.put((new Date().toString() + "\n" + str).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        
        //5. 關閉通道
        sChannel.close();
    }

    //服務端
    @Test
    public void server() throws IOException{
        //1. 獲取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        
        //2. 切換非阻塞模式
        ssChannel.configureBlocking(false);
        
        //3. 綁定連接
        ssChannel.bind(new InetSocketAddress(9898));
        
        //4. 獲取選擇器
        Selector selector = Selector.open();
        
        //5. 將通道註冊到選擇器上, 並且指定“監聽接收事件”
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        //6. 輪詢式的獲取選擇器上已經“準備就緒”的事件
        //使用 select() 來監聽到達的事件,它會一直阻塞直到有至少一個事件到達。
        while(selector.select() > 0){
            
            //7. 獲取當前選擇器中所有註冊的“選擇鍵(已就緒的監聽事件)”
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            
            while(it.hasNext()){
                //8. 獲取準備“就緒”的是事件
                SelectionKey sk = it.next();
                
                //9. 判斷具體是什麼事件準備就緒
                if(sk.isAcceptable()){
                    //10. 若“接收就緒”,獲取客戶端連接
                    SocketChannel sChannel = ssChannel.accept();
                    
                    //11. 切換非阻塞模式
                    sChannel.configureBlocking(false);
                    
                    //12. 將該通道註冊到選擇器上
                    sChannel.register(selector, SelectionKey.OP_READ);
                }else if(sk.isReadable()){
                    //13. 獲取當前選擇器上“讀就緒”狀態的通道
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    
                    //14. 讀取數據
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    
                    int len = 0;
                    while((len = sChannel.read(buf)) > 0 ){
                        buf.flip();
                        System.out.println(new String(buf.array(), 0, len));
                        buf.clear();
                    }
                }
                
                //15. 取消選擇鍵 SelectionKey
                //每一個“事件關鍵字”被處理后都必須移除,否則下一次輪詢時,這個事件會被重複處理
                it.remove();
            }
        }
    }
}

NIO傳輸文件

服務器端代碼

public class Server {
    private ByteBuffer buffer = ByteBuffer.allocate(1024*1024);
        //使用Map保存每個連接,當OP_READ就緒時,根據key找到對應的文件對其進行寫入。若將其封裝成一個類,作為值保存,可以再上傳過程中显示進度等等
    Map<SelectionKey, FileChannel> fileMap = new HashMap<SelectionKey, FileChannel>();
    public static void main(String[] args) throws IOException{
        Server server = new Server();
        server.startServer();
    }
    public void startServer() throws IOException{
        Selector selector = Selector.open();
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(8888));
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服務器已開啟...");
        while (true) {
            int num = selector.select();
            if (num == 0) continue;
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                if (key.isAcceptable()) {
                    ServerSocketChannel serverChannel1 = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = serverChannel1.accept();
                    if (socketChannel == null) continue;
                    socketChannel.configureBlocking(false);
                    SelectionKey key1 = socketChannel.register(selector, SelectionKey.OP_READ);
                    InetSocketAddress remoteAddress = (InetSocketAddress)socketChannel.getRemoteAddress();
                    File file = new File(remoteAddress.getHostName() + "_" + remoteAddress.getPort() + ".txt");
                    FileChannel fileChannel = new FileOutputStream(file).getChannel();
                    fileMap.put(key1, fileChannel);
                    System.out.println(socketChannel.getRemoteAddress() + "連接成功...");
                    writeToClient(socketChannel);
                }
                else if (key.isReadable()){
                    readData(key);
                }
                // NIO的特點只會累加,已選擇的鍵的集合不會刪除,ready集合會被清空
                // 只是臨時刪除已選擇鍵集合,當該鍵代表的通道上再次有感興趣的集合準備好之後,又會被select函數選中
                it.remove();
            }
        }
    }
    private void writeToClient(SocketChannel socketChannel) throws IOException {
        buffer.clear();
        buffer.put((socketChannel.getRemoteAddress() + "連接成功").getBytes());
        buffer.flip();
        socketChannel.write(buffer);
        buffer.clear();
    }
    private void readData(SelectionKey key) throws IOException  {
        FileChannel fileChannel = fileMap.get(key);
        buffer.clear();
        SocketChannel socketChannel = (SocketChannel) key.channel();
        int num = 0;
        try {
            while ((num = socketChannel.read(buffer)) > 0) {
                buffer.flip();
                // 寫入文件
                fileChannel.write(buffer);
                buffer.clear();
                }
        } catch (IOException e) {
            key.cancel();
            e.printStackTrace();
            return;
        }
        // 調用close為-1 到達末尾
        if (num == -1) {
            fileChannel.close();
            System.out.println("上傳完畢");
            buffer.put((socketChannel.getRemoteAddress() + "上傳成功").getBytes());
            buffer.clear();
            socketChannel.write(buffer);
            key.cancel();
        }
    }
}

 

客戶端模擬三個客戶端同時向服務器發送文件

public class Client {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            // 模擬三個發端
            new Thread() {
                public void run() {
                    try {
                        SocketChannel socketChannel = SocketChannel.open();
                        socketChannel.socket().connect(new InetSocketAddress("127.0.0.1", 8888));
                        File file = new File("E:\\" + 11 + ".txt");
                        FileChannel fileChannel = new FileInputStream(file).getChannel();
                        ByteBuffer buffer = ByteBuffer.allocate(100);
                        socketChannel.read(buffer);
                        buffer.flip();
                        System.out.println(new String(buffer.array(), 0, buffer.limit(), Charset.forName("utf-8")));
                        buffer.clear();
                        int num = 0;
                        while ((num=fileChannel.read(buffer)) > 0) {
                            buffer.flip();                        
                            socketChannel.write(buffer);
                            buffer.clear();
                        }
                        if (num == -1) {
                            fileChannel.close();
                            socketChannel.shutdownOutput();
                        }
                        // 接受服務器
                        socketChannel.read(buffer);
                        buffer.flip();
                        System.out.println(new String(buffer.array(), 0, buffer.limit(), Charset.forName("utf-8")));
                        buffer.clear();
                        socketChannel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    
                };
            }.start();
            
        }
        Thread.yield();
    }
}

可見這裏我們僅僅使用了一個線程就管理了三個連接,相比以前使用阻塞的Socket要在accept函數返回后開啟線程來管理這個連接,而使用NIO我們在accept返回后,僅僅將其註冊到選擇器上,讀操作在下次檢測到有可讀的鍵的集合時就會去處理。

NIO+線程池改進

public class ThreadPoolServer extends Server{
    private ExecutorService exec = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws IOException {
        ThreadPoolServer server = new ThreadPoolServer();
        server.startServer();
    }

    @Override
    protected void readData(final SelectionKey key) throws IOException {
        // 移除掉這個key的可讀事件,已經在線程池裡面處理,如果不改變當前Key的狀態,這裏交給另外一個線程去處理,主線程下一次遍歷此KEY還是可讀事件,會重複開啟線程處理任務
        key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
        exec.execute(new Runnable() {
            @Override
            public void run() {
                ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
                FileChannel fileChannel = fileMap.get(key);
                buffer.clear();
                SocketChannel socketChannel = (SocketChannel) key.channel();
                int num = 0;
                try {
                    while ((num = socketChannel.read(buffer)) > 0) {
                        buffer.flip();
                        // 寫入文件
                        fileChannel.write(buffer);
                        buffer.clear();
                    }
                } catch (IOException e) {
                    key.cancel();
                    e.printStackTrace();
                    return;
                }
                // 調用close為-1 到達末尾
                if (num == -1) {
                    try {
                        fileChannel.close();
                        System.out.println("上傳完畢");
                        buffer.put((socketChannel.getRemoteAddress() + "上傳成功").getBytes());
                        buffer.clear();
                        socketChannel.write(buffer);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    // 只有調用cancel才會真正從已選擇的鍵的集合裏面移除,否則下次select的時候又能得到
                    // 一端close掉了,其對端仍然是可讀的,讀取得到EOF,返回-1
                    key.cancel(); 
                    return;
                }
                // Channel的read方法可能返回0,返回0並不一定代表讀取完了。
                // 工作線程結束對通道的讀取,需要再次更新鍵的ready集合,將感興趣的集合重新放在裏面
                key.interestOps(key.interestOps() | SelectionKey.OP_READ);
                // 調用wakeup,使得選擇器上的第一個還沒有返回的選擇操作立即返回即重新select
                key.selector().wakeup();
            }
        });
    }
}

推薦博客

  程序員寫代碼之外,如何再賺一份工資?

多路復用IO的優缺點

  • 不用再使用多線程來進行IO處理了(包括操作系統內核IO管理模塊和應用程序進程而言)。當然實際業務的處理中,應用程序進程還是可以引入線程池技術的
  • 同一個端口可以處理多種協議,例如,使用ServerSocketChannel的服務器端口監聽,既可以處理TCP協議又可以處理UDP協議。
  • 操作系統級別的優化:多路復用IO技術可以是操作系統級別在一個端口上能夠同時接受多個客戶端的IO事件。同時具有之前我們講到的阻塞式同步IO和非阻塞式同步IO的所有特點。Selector的一部分作用更相當於“輪詢代理器”。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

北冥有 Data,其名為鯤,鯤之大,一個 MySQL 放不下!

千萬量級的數據,用 MySQL 要怎麼存?

初學者在看到這個問題的時候,可能首先想到的是 MySQL 一張表到底能存放多少條數據?

根據 MySQL 官方文檔的介紹,MySQL 理論上限是 (232)2 條數據,然而實際操作中,往往還受限於下面兩條因素:

  1. myisam_data_pointer_size,MySQL 的 myisam_data_pointer_size 一般默認是 6,即 48 位,那麼對應的行數就是 248-1。
  2. 表的存儲大小 256TB

那有人會說,只要我的數據大小不超過上限,數據行數也不超過上限,是不是就沒有問題了?其實不盡然。

在實際項目中,一般沒有哪個項目真的觸發到 MySQL 數據的上限了,因為當數據量變大了之後,查詢速度會慢的嚇人,而一般這個時候,你的數據量離 MySQL 的理論上限還遠着呢!

傳統的企業應用一般數據量都不大,數據也都比較容易處理,但是在互聯網項目中,上千萬、上億的數據量並不鮮見。在這種時候,還要保證數據庫的操作效率,我們就不得不考慮數據庫的分庫分表了。

那麼接下來就和大家簡單聊一聊數據庫分庫分表的問題。

數據庫切分

看這個名字就知道,就是把一個數據庫切分成 N 多個數據庫,然後存放在不同的數據庫實例上面,這樣做有兩個好處:

  1. 降低單台數據庫實例的負載
  2. 可以方便的實現對數據庫的擴容

一般來說,數據庫的切分有兩種不同的切分規則:

  1. 水平切分
  2. 垂直切分

接下來我們就對這兩種不同的切分規則分別進行介紹。

水平切分

先來一張簡單的示意圖,大家感受一下什麼是水平切分:

假設我的 DB 中有 table-1、table-2 以及 table-3 三張表,水平切分就是拿着我的絕世好劍,對準黑色的線條,砍一劍或者砍 N 劍!

砍完之後,將砍掉的部分放到另外一個數據庫實例中,變成下面這樣:

這樣,原本放在一個 DB 中的 table 現在放在兩個 DB 中了,觀察之後我們發現:

  1. 兩個 DB 中表的個數都是完整的,就是原來 DB 中有幾張表,現在還是幾張。
  2. 每張表中的數據是不完整的,數據被拆分到了不同的 DB 中去了。

這就是數據庫的水平切分,也可以理解為按照數據行進行切分,即按照表中某個字段的某種規則來將表數據分散到多個庫之中,每個表中包含一部分數據。

這裏的某種規則都包含哪些規則呢?這就涉及到數據庫的分片規則問題了,這個松哥在後面的文章中也會和大家一一展開詳述。這裏先簡單說幾個常見的分片規則:

  1. 按照日期劃分:不容日期的數據存放到不同的數據庫中。
  2. 對 ID 取模:對表中的 ID 字段進行取模運算,根據取模結果將數據保存到不同的實例中。
  3. 使用一致性哈希算法進行切分。

詳細的用法,將在後面的文章中和大家仔細說。

垂直切分

先來一張簡單的示意圖,大家感受一下垂直切分:

所謂的垂直切分就是拿着我的屠龍刀,對準了黑色的線條砍。砍完之後,將不同的表放到不同的數據庫實例中去,變成下面這個樣子:

這個時候我們發現如下幾個特點:

  1. 每一個數據庫實例中的表的數量都是不完整的。
  2. 每一個數據庫實例中表的數據是完整的。

這就是垂直切分。一般來說,垂直切分我們可以按照業務來劃分,不同業務的表放到不同的數據庫實例中。

老實說,在實際項目中,數據庫垂直切分並不是一件容易的事,因為表之間往往存在着複雜的跨庫 JOIN 問題,那麼這個時候如何取捨,就要考驗架構師的水平了!

優缺點分析

通過上面的介紹,相信大家對於水平切分和垂直切分已經有所了解,優缺點其實也很明顯了,松哥再來和大家總結一下。

水平切分

  • 優點
  1. 水平切分最大的優勢在於數據庫的擴展性好,提前選好切分規則,數據庫後期可以非常方便的進行擴容。
  2. 有效提高了數據庫穩定性和系統的負載能力。拆分規則抽象好, join 操作基本可以數據庫做。
  • 缺點
  1. 水平切分后,分片事務一致性不容易解決。
  2. 拆分規則不易抽象,對架構師水平要求很高。
  3. 跨庫 join 性能較差。

垂直切分

  • 優點
  1. 一般按照業務拆分,拆分后業務清晰,可以結合微服務一起食用。
  2. 系統之間整合或擴展相對要容易很多。
  3. 數據維護相對簡單。
  • 缺點
  1. 最大的問題在於存在單庫性能瓶頸,數據表擴展不易。
  2. 跨庫 join 不易。
  3. 事務處理複雜。

結語

雖然 MySQL 中數據存儲的理論上限比較高,但是在實際開發中我們不會等到數據存不下的時候才去考慮分庫分表問題,因為在那之前,你就會明顯的感覺到數據庫的各項性能在下降,就要開始考慮分庫分表了。

好了,今天主要是向大家介紹一點概念性的東西,算是我們分佈式數據庫中間件正式出場前的一點鋪墊。

參考資料:

  1. MySQL 官方文檔

關注公眾號【江南一點雨】,專註於 Spring Boot+微服務以及前後端分離等全棧技術,定期視頻教程分享,關注后回復 Java ,領取松哥為你精心準備的 Java 乾貨!

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

WebApp 安全風險與防護課堂開課了!

本文由葡萄城技術團隊於原創並首發

轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。

 

2018 網絡安全事故頻發,從數據泄露、信息竊取,到 DDOS 攻擊、勒索病毒,不僅威脅的總數在增加,威脅態勢也變得更加多樣化,攻擊者在不斷開發新的攻擊途徑的同時,也儘力在攻擊過程中掩蓋其蹤跡,使網絡安全防護變得越發棘手。未來是萬物互聯的時代,唯有把握住網絡信息安全,才能避免被降維打擊。

為了回饋社區,我們特邀葡萄城高級架構師、安全專家Carl作為分享嘉賓,於葡萄城技術公開課上,以 WebApp 安全防護為出發點,帶你了解更多意想不到的安全防護措施與黑客攻擊手段,助你提高網絡安全意識,最終學會如何規避風險隱患,避免遭受網絡安全攻擊。

本次課程共分三節,計劃講的內容如下:

第一節:開闊眼界 – 提升安全意識

提升網絡安全意識對項目團隊中的每一個角色、每一個流程都至關重要。同時,也只有具備了網絡安全意識,才願意為數據安全投入更多的時間和精力。下面,我將為您展示部分2018年發生的網絡安全事故,這些事故造成的損失,也許遠遠超出你的想象。

2018 網絡安全事故回顧

Facebook數據泄露事件:2018年9月,Facebook因安全系統漏洞而遭受黑客攻擊,導致約5000萬用戶信息泄露。

上市公司數據堂,涉嫌侵犯數百億條公民個人信息:大數據行業知名企業數據堂在短短8個月的時間內,日均泄露公民個人信息1.3億餘條,累計傳輸數據壓縮后約為4000GB。

圓通10億快遞信息泄露: 10億條用戶數據遭公開售賣,這些數據包括寄(收)件人姓名、電話、地址等隱私信息。

萬豪酒店5億用戶開房信息泄露:萬豪酒店客房預訂數據庫遭黑客入侵,約5億名客戶的信息可能被泄露。

更多數據泄露事件

  1. 國泰航空數據泄露,940萬乘客受影響
  2. MongoDB 數據庫被入侵, 1100 萬份郵件記錄遭泄露
  3. SHEIN 數據泄露影響 642 萬用戶
  4. GovPayNet憑證系統存在漏洞,1400萬交易記錄被曝光
  5. 小米有品平台泄露個人隱私 約2000萬用戶數據遭泄露
  6. …
  7. 美國亞特蘭大市政府受到勒索軟件攻擊
  8. 美國巴爾的摩市遭遇勒索軟件攻擊,導致911緊急調度服務的計算機輔助調度(CAD)功能掉線
  9. 台積電勒索病毒事件,約造成17.6 億元的營收損失,股票市值下跌78億
  10. 很多個人電腦和中小網站都曾遭受攻擊
  11. 平昌冬奧會開幕式服務器遭到身份不明的黑客入侵
  12. GitHub遭1.35T級流量攻擊
  13. CPU數據緩存機制漏洞
  14. iOS 平台WebView組件漏洞(UIWebView/ WKWebView)跨域訪問漏洞(CNNVD-201801-515)
  15. Oracle WebLogic Server WLS核心組件遠程代碼執行漏洞
  16. 微信支付SDKXXE漏洞
  17. Apache Struts2 S2-057安全漏洞

勒索病毒事件

DDoS 攻擊

年度重大漏洞盤點

第二節:知己知彼 – 黑客如何攻擊系統

一名黑客攻擊網站的典型步驟,主要分為以下5步:

  1. 信息收集和漏洞掃描
  2. 漏洞利用
  3. 上傳木馬
  4. 獲取服務器的控制權
  5. 清理痕迹

總結:

黑客不是手動測試系統漏洞的,而是有很多強大的工具可以自動化完成

黑客不是利用系統中的一個漏洞,而是要利用一系列,不同層次的漏洞

黑客經常批量攻擊一系列網站,選取其中漏洞較多,較好利用的重點突破

第三節:十大安全風險(OWASP Top 10)

不安全的軟件正在破壞着我們的金融、醫療、國防、能源和其他重要的基礎設施。隨着我們的軟件變得愈加龐大、複雜且相互關聯,實現應用程序安全的難度也呈指數級增長。而現代軟件開發過程的飛速發展,使得快速、準確地識別軟件安全風險變得愈發的重要,OWASP 組織也因此誕生。

OWASP,即開放式Web應用程序安全項目(Open Web Application Security Project),作為一個開源的、非盈利的全球性安全組織,它提供了有關計算機和互聯網應用程序的公正、實際、有成本效益的信息,其目的是協助個人、企業和機構來發現並使用可信賴的軟件。

OWASP Top 10是由OWASP組織公布,最具權威性的“10項最嚴重的Web應用程序安全風險預警”,其就安全問題從威脅性和脆弱性兩方面進行可能性分析,並結合技術和商業影響的分析結果,輸出公認的、最嚴重的十類Web應用安全風險排名。OWASP Top 10旨在針對上述風險,提出解決方案,幫助IT公司和開發團隊規範應用程序開發流程和測試流程,提高Web產品的安全性。

OWASP敦促所有公司在其組織內採用OWASP Top 10文檔,並確保其Web應用程序最大限度地降低這些風險,採用OWASP Top 10可能是將企業內的軟件開發文化轉變為生成安全代碼文化最行之有效的一步。

OWASP Top 10包括:

  1. 注入
  2. 失效的身份認證
  3. 敏感信息泄露
  4. XML外部實體(XXE)
  5. 失效的訪問控制
  6. 安全配置錯誤
  7. 跨站腳本(XSS)
  8. 不安全的反序列化
  9. 使用含有已知漏洞的組件

10. 不足的日誌記錄和監控

講師介紹:

Carl(陳慶),葡萄城高級架構師、安全專家、葡萄城技術公開課講師。擁有15年項目開發經驗,專註於產品架構、編程技術等領域,對網絡安全有着獨到見解,曾擔任微軟TechEd講師,樂於研究各種前沿技術並分享。

請點擊該地址報名觀看直播:http://live.vhall.com/137416596

錯過本場直播?沒關係,所有直播內容我們會存放在葡萄城公開課頁面,便於您隨時觀看、學習。後續我們也會將Carl老師講的內容整理成文章,發布在社區,敬請關注。

“賦能開發者”葡萄城除了為所有開發人員提供免費的開發技巧分享、項目實戰經驗外,還提供了眾多高水準、高品質的開發工具和開發者解決方案,可有效幫助開發人員提高效率,縮短項目周期,使開發人員能更專註於業務邏輯,順利完成高質量的項目交付,歡迎您深入了解。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

分享工作中一次優化程序的過程

 

程序應用場景:

年初從總公司交接了一個評分系統,系統大概情況是80w考生,每個考生105條作答數據,作答數據主要是客觀題(單選題,多選題,判斷題),評分時間大概40分鐘左右。

需求:優化代碼,提升評分效率,優化之後評分完成在20分鐘左右。

已有代碼優化邏輯:

1.程序方面:

多線程,通過計算評分的人數,得到需要的線程數量,開啟線程分別進行評分

2.查詢數據:

成績表建立自增長Id,查詢匹配需要評分的數據用where id> =Start and id< End

3.插入數據:

採用DataTable數據導入到Sql Server,提示評分完成時保存數據效率

詳細說一下第二點的邏輯

假設數據庫有100w條需要評分的數據,這時通過計算大概得到需要開啟25個線程去同時進行評分,也就是說第一個線程去評分數據庫Id在  1~40000考生的數據,也就是說去查詢數據時,大概查詢語句select * from A where Id>=1 and Id<40000這樣能夠保證查詢出來得效率是最高的;

但是這裡會有缺陷,如果數據存在刪除過在插入肯定數據就不是連續的,也就是

where Id>=1 and Id<40000不能保證查詢得到40000條數據,數據庫表中存在的Id不一定是從1開始,所以目前這種查詢方式是很理想化的。後續這裏還需要在這快研究和學習下。

本次優化細節

代碼片段1

優化前:

 

優化后:

 

 

總結:提取線程內查詢數據存儲到內存中,從而只會查詢一次。

代碼片段2

優化前:

 

優化后:

 

總結:修改List集合取一條數據的方式,Where修改成Find,如果還需要在這裏提升查詢效率可以修改成for循環,但會導致代碼可讀性會變差。

代碼片段3

優化前

 

優化后

 

 

總結:修改DataTable表Select查詢方式,先存儲到Dictionary中,在通過Key去取對應的數據。

最後通過測試全部完成評分時間大概20分鐘左右,也算成功的完成了這個任務,可能還需要在研究研究代碼,看能否有其他地方可以改善的。

存在的疑惑:

線程運算佔用的電腦Cpu的具體的值?

是否存在最佳線程數量?

電腦Cpu處理能力越強是否也能夠提升程序的評分效率?

總結:

1.線程里反覆查詢而且不變的基礎數據放到線程外查詢存儲到變量中

2.List集合的Where查詢修改成Find查詢,極高的提升查詢效率

3.DataTable的Select改用Dictionary<string,DataRow[]>,同樣極高的提升查詢效率

這次能夠站在前人的肩膀上完成這一次代碼的優化還有學習到了很多,瑾以寫在這片文章分享個人在平時工作中的解決的一些問題,希望這次能夠通過分享此篇文章讓自己更多的去記錄和分享工作中遇到以及解決的問題,提升自己的競爭力。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

監測元素resize

前言

近來有需求要做分頁,聽起來可能有點Low。 所以我要把Low的事情做得有點逼格。
分頁本身沒啥,但是數據量起來了,比如十萬。 要是不做點處理, 那你的頁面估計爽得很,機器也爽得很。 放心,我不會讓你這麼爽。

大量數據展現方案

比較流行的當然是虛擬滾動(無限滾動)。

  1. 始終展示的是有限的固定節點。
  2. 外層創建滾動層。

一句話,就是反覆利用固定節點展現數據。

其中還有兩個點

何時需要加載新的分頁數據

  1. scrollTop , clientHeight, scrollHeight
  2. IntersectionObserver (chrome 55+)

如何知道容器寬高變化
本文就圍繞着這個展開

因為使用的是react框架,使用了 react-window, react-window就是用來展現海量數據的react列表組件。
因為項目需要,還要不通尺寸一行展現不同數量的數據。 肯定有人就說,監聽window.resize。

沒錯,監聽resize一定程度,但是window.resize, 並不能讓我知道容器本身的尺寸,當

然可以通過getComputedStyle獲取。 要是window沒有resize的情況呢。

我就想實時的知道尺寸的變化。

尺寸變化監測方案

監測元素resize這裡有幾種方案的測試和源碼。

Cross-Browser, Event-based, Element Resize Detection

思路:

如果IE,直接註冊onresize(這個點贊啊)

否則: 創建 type為text/html的object

設置position為absolute, 高度100%, 寬度100% (這樣可以獲得父容器的寬高)

設置pointer-events:none,利用點擊穿透(讓object窗體變成幽靈)

object元素的高度變化后,通知訂閱者

resize事件節流

問題:

  1. 創建object
  2. 事件處理函數掛載了元素本身上

javascript-detect-element-resize

創建三個子元素,利用scroll事件來監測變化。

原理:

https://zhuanlan.zhihu.com/p/24887312
The scroll event is fired when the document view or an element has been scrolled.
當文檔視圖或者元素滾動的時候會觸發 scroll 事件。
也就是說元素滾動的時候會觸發這個事件,那麼什麼時候元素會滾動?當元素大於其父級元素,且父級元素允許其滾動的時候,該元素可以進行滾動。
換句話說,元素可以滾動意味着父子元素大小不一致,這是這個方法的核心。
那麼我們需要讓元素大小發生改變時,使得 scrollLeft 或者 scrollTop 發生改變,從而觸發 scroll 事件,進一步得知其大小發生了改變。

visibility: hidden; opacity: 0; position: absolute;讓自己變得虛無

addEventListener(“scroll”, scrollListener, true) 在捕捉階段攔截事件,使用false無效

div.expand-trigger 變大

div.expand-trigger 變小

animationstart來監聽显示,比如style.display = ‘none’然後style.display = ‘block’

問題:

  1. 額外創建四個元素節點以及一個style節點
  2. 事件都掛載了元素本身身上,

ResizeObserver

原生自帶的方案, 兼容性並不高, resize-observer-polyfill 基於resize和MutationOberver的polyfill實現了ResizeObserver。

 const resizeObserver = new ResizeObserver(entries => {
        for (let entry of entries) {
            
            console.log(entry.target.id, `height:${entry.contentRect.height}  width:${entry.contentRect.width}`);
        }
        });
        resizeObserver.observe(document.querySelector('#my_element'));
        resizeObserver.observe(document.querySelector('#my_element2'));

此外

當然,我覺得還

  1. 定時器 + getComputedStyle 也是很低成本的實現。
  2. resize + MutationOberver 也是很簡單的方案。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

百萬級日活 App 的屏幕錄製功能是如何實現的

Android 從 4.0 開始就提供了手機錄屏方法,但是需要 root 權限,比較麻煩不容易實現。但是從 5.0 開始,系統提供給了 App 錄製屏幕的一系列方法,不需要 root 權限,只需要用戶授權即可錄屏,相對來說較為簡單。

基本上根據 官方文檔 便可以寫出錄屏的相關代碼。

屏幕錄製的基本實現步驟

在 Manifest 中申明權限
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
獲取 MediaProjectionManager 並申請權限
private val mediaProjectionManager by lazy { activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as? MediaProjectionManager }
private var mediaProjection: MediaProjection? = null
if (mediaProjectionManager == null) {
    Log.d(TAG, "mediaProjectionManager == null,當前手機暫不支持錄屏")
    showToast(R.string.phone_not_support_screen_record)
    return
}
// 申請相關權限
PermissionUtils.permission(PermissionConstants.STORAGE, PermissionConstants.MICROPHONE)
        .callback(object : PermissionUtils.SimpleCallback {
            override fun onGranted() {
                Log.d(TAG, "start record")
                mediaProjectionManager?.apply {
                    // 申請相關權限成功后,要向用戶申請錄屏對話框
                    val intent = this.createScreenCaptureIntent()
                    if (activity.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
                        activity.startActivityForResult(intent, REQUEST_CODE)
                    } else {
                        showToast(R.string.phone_not_support_screen_record)
                    }
                }
            }
            override fun onDenied() {
                showToast(R.string.permission_denied)
            }
        })
        .request()
重寫 onActivityResult() 對用戶授權進行處理
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    if (requestCode == REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            mediaProjection = mediaProjectionManager!!.getMediaProjection(resultCode, data)
            // 實測,部分手機上錄製視頻的時候會有彈窗的出現,所以我們需要做一個 150ms 的延遲
            Handler().postDelayed({
                if (initRecorder()) {
                    mediaRecorder?.start()
                } else {
                    showToast(R.string.phone_not_support_screen_record)
                }
            }, 150)
        } else {
            showToast(R.string.phone_not_support_screen_record)
        }
    }
}

private fun initRecorder(): Boolean {
    Log.d(TAG, "initRecorder")
    var result = true
    // 創建文件夾
    val f = File(savePath)
    if (!f.exists()) {
        f.mkdirs()
    }
    // 錄屏保存的文件
    saveFile = File(savePath, "$saveName.tmp")
    saveFile?.apply {
        if (exists()) {
            delete()
        }
    }
    mediaRecorder = MediaRecorder()
    val width = Math.min(displayMetrics.widthPixels, 1080)
    val height = Math.min(displayMetrics.heightPixels, 1920)
    mediaRecorder?.apply {
        // 可以設置是否錄製音頻
        if (recordAudio) {
            setAudioSource(MediaRecorder.AudioSource.MIC)
        }
        setVideoSource(MediaRecorder.VideoSource.SURFACE)
        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        setVideoEncoder(MediaRecorder.VideoEncoder.H264)
        if (recordAudio){
            setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
        }
        setOutputFile(saveFile!!.absolutePath)
        setVideoSize(width, height)
        setVideoEncodingBitRate(8388608)
        setVideoFrameRate(VIDEO_FRAME_RATE)
        try {
            prepare()
            virtualDisplay = mediaProjection?.createVirtualDisplay("MainScreen", width, height, displayMetrics.densityDpi,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, null, null)
            Log.d(TAG, "initRecorder 成功")
        } catch (e: Exception) {
            Log.e(TAG, "IllegalStateException preparing MediaRecorder: ${e.message}")
            e.printStackTrace()
            result = false
        }
    }
    return result
}   

上面可以看到,我們可以設置一系列參數,各種參數的意思就希望大家自己去觀摩官方文檔了。其中有一個比較重要的一點是我們通過 MediaProjectionManager 創建了一個 VirtualDisplay,這個 VirtualDisplay 可以理解為虛擬的呈現器,它可以捕獲屏幕上的內容,並將其捕獲的內容渲染到 Surface 上,MediaRecorder 再進一步把其封裝為 mp4 文件保存。

錄製完畢,調用 stop 方法保存數據

private fun stop() {
    if (isRecording) {
        isRecording = false
        try {
            mediaRecorder?.apply {
                setOnErrorListener(null)
                setOnInfoListener(null)
                setPreviewDisplay(null)
                stop()
                Log.d(TAG, "stop success")
            }
        } catch (e: Exception) {
            Log.e(TAG, "stopRecorder() error!${e.message}")
        } finally {
            mediaRecorder?.reset()
            virtualDisplay?.release()
            mediaProjection?.stop()
            listener?.onEndRecord()
        }
    }
}

/**
 * if you has parameters, the recordAudio will be invalid
 */
fun stopRecord(videoDuration: Long = 0, audioDuration: Long = 0, afdd: AssetFileDescriptor? = null) {
    stop()
    if (audioDuration != 0L && afdd != null) {
        syntheticAudio(videoDuration, audioDuration, afdd)
    } else {
        // saveFile
        if (saveFile != null) {
            val newFile = File(savePath, "$saveName.mp4")
            // 錄製結束后修改後綴為 mp4
            saveFile!!.renameTo(newFile)
            // 刷新到相冊
            val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
            intent.data = Uri.fromFile(newFile)
            activity.sendBroadcast(intent)
            showToast(R.string.save_to_album_success)
        }
        saveFile = null
    }

}

我們必須來看看 MediaRecorder 對 stop() 方法的註釋。

/**
 * Stops recording. Call this after start(). Once recording is stopped,
 * you will have to configure it again as if it has just been constructed.
 * Note that a RuntimeException is intentionally thrown to the
 * application, if no valid audio/video data has been received when stop()
 * is called. This happens if stop() is called immediately after
 * start(). The failure lets the application take action accordingly to
 * clean up the output file (delete the output file, for instance), since
 * the output file is not properly constructed when this happens.
 *
 * @throws IllegalStateException if it is called before start()
 */
public native void stop() throws IllegalStateException; 

根據官方文檔,stop() 如果在 prepare() 后立即調用會崩潰,但對其他情況下發生的錯誤卻沒有做過多提及,實際上,當你真正地使用 MediaRecorder 做屏幕錄製的時候,你會發現即使你沒有在 prepare() 后立即調用 stop(),也可能拋出 IllegalStateException 異常。所以,保險起見,我們最好是直接使用 try...catch... 語句塊進行包裹。

比如你 initRecorder 中某些參數設置有問題,也會出現 stop() 出錯,數據寫不進你的文件。

完畢后,釋放資源
fun clearAll() {
    mediaRecorder?.release()
    mediaRecorder = null
    virtualDisplay?.release()
    virtualDisplay = null
    mediaProjection?.stop()
    mediaProjection = null
}

無法繞過的環境聲音

上面基本對 Android 屏幕錄製做了簡單的代碼編寫,當然實際上,我們需要做的地方還不止上面這些,感興趣的可以移步到 ScreenRecordHelper 進行查看。

但這根本不是我們的重點,我們極其容易遇到這樣的情況,需要我們錄製音頻的時候錄製系統音量,但卻不允許我們把環境音量錄進去。

似乎我們前面初始化 MediaRecorder 的時候有個設置音頻源的地方,我們來看看這個 MediaRecorder.setAudioSource() 方法都支持設置哪些東西。

從官方文檔 可知,我們可以設置以下這些音頻源。由於官方註釋太多,這裏就簡單解釋一些我們支持的可以設置的音頻源。

//設定錄音來源於同方向的相機麥克風相同,若相機無內置相機或無法識別,則使用預設的麥克風
MediaRecorder.AudioSource.CAMCORDER 
//默認音頻源
MediaRecorder.AudioSource.DEFAULT  
//設定錄音來源為主麥克風
MediaRecorder.AudioSource.MIC
//設定錄音來源為語音撥出的語音與對方說話的聲音
MediaRecorder.AudioSource.VOICE_CALL
// 攝像頭旁邊的麥克風
MediaRecorder.AudioSource.VOICE_COMMUNICATION
//下行聲音
MediaRecorder.AudioSource.VOICE_DOWNLINK
//語音識別
MediaRecorder.AudioSource.VOICE_RECOGNITION
//上行聲音
MediaRecorder.AudioSource.VOICE_UPLINK

咋一看沒有我們想要的選項,實際上你逐個進行測試,你也會發現,確實如此。我們想要媒體播放的音樂,總是無法擺脫環境聲音的限制。

奇怪的是,我們使用華為部分手機的系統錄屏的時候,卻可以做到,這就感嘆於 ROM 的定製性更改的神奇,當然,千奇百怪的第三方 ROM 也一直讓我們 Android 適配困難重重。

曲線救國剝離環境聲音

既然我們通過調用系統的 API 始終無法實現我們的需求:錄製屏幕,並同時播放背景音樂,錄製好保存的視頻需要只有背景音樂而沒有環境音量,我們只好另闢蹊徑。

不難想到,我們完全可以在錄製視頻的時候不設置音頻源,這樣得到的視頻就是一個沒有任何聲音的視頻,如果此時我們再把音樂強行剪輯進去,這樣就可以完美解決用戶的需要了。

對於音視頻的混合編輯,想必大多數人都能想到的是大名鼎鼎的 FFmpeg ,但如果要自己去編譯優化得到一個穩定可使用的 FFmpge 庫的話,需要花上不少時間。更重要的是,我們為一個如此簡單的功能大大的增大我們 APK 的體積,那是萬萬不可的。所以我們需要把目光轉移到官方的 MediaExtractor 上。

從 官方文檔 來看,能夠支持到 m4a 和 aac 格式的音頻文件合成到視頻文件中,根據相關文檔我們就不難寫出這樣的代碼。

/**
 * https://stackoverflow.com/questions/31572067/android-how-to-mux-audio-file-and-video-file
 */
private fun syntheticAudio(audioDuration: Long, videoDuration: Long, afdd: AssetFileDescriptor) {
    Log.d(TAG, "start syntheticAudio")
    val newFile = File(savePath, "$saveName.mp4")
    if (newFile.exists()) {
        newFile.delete()
    }
    try {
        newFile.createNewFile()
        val videoExtractor = MediaExtractor()
        videoExtractor.setDataSource(saveFile!!.absolutePath)
        val audioExtractor = MediaExtractor()
        afdd.apply {
            audioExtractor.setDataSource(fileDescriptor, startOffset, length * videoDuration / audioDuration)
        }
        val muxer = MediaMuxer(newFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        videoExtractor.selectTrack(0)
        val videoFormat = videoExtractor.getTrackFormat(0)
        val videoTrack = muxer.addTrack(videoFormat)

        audioExtractor.selectTrack(0)
        val audioFormat = audioExtractor.getTrackFormat(0)
        val audioTrack = muxer.addTrack(audioFormat)

        var sawEOS = false
        var frameCount = 0
        val offset = 100
        val sampleSize = 1000 * 1024
        val videoBuf = ByteBuffer.allocate(sampleSize)
        val audioBuf = ByteBuffer.allocate(sampleSize)
        val videoBufferInfo = MediaCodec.BufferInfo()
        val audioBufferInfo = MediaCodec.BufferInfo()

        videoExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
        audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)

        muxer.start()

        // 每秒多少幀
        // 實測 OPPO R9em 垃圾手機,拿出來的沒有 MediaFormat.KEY_FRAME_RATE
        val frameRate = if (videoFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {
            videoFormat.getInteger(MediaFormat.KEY_FRAME_RATE)
        } else {
            31
        }
        // 得出平均每一幀間隔多少微妙
        val videoSampleTime = 1000 * 1000 / frameRate
        while (!sawEOS) {
            videoBufferInfo.offset = offset
            videoBufferInfo.size = videoExtractor.readSampleData(videoBuf, offset)
            if (videoBufferInfo.size < 0) {
                sawEOS = true
                videoBufferInfo.size = 0
            } else {
                videoBufferInfo.presentationTimeUs += videoSampleTime
                videoBufferInfo.flags = videoExtractor.sampleFlags
                muxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo)
                videoExtractor.advance()
                frameCount++
            }
        }
        var sawEOS2 = false
        var frameCount2 = 0
        while (!sawEOS2) {
            frameCount2++
            audioBufferInfo.offset = offset
            audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, offset)

            if (audioBufferInfo.size < 0) {
                sawEOS2 = true
                audioBufferInfo.size = 0
            } else {
                audioBufferInfo.presentationTimeUs = audioExtractor.sampleTime
                audioBufferInfo.flags = audioExtractor.sampleFlags
                muxer.writeSampleData(audioTrack, audioBuf, audioBufferInfo)
                audioExtractor.advance()
            }
        }
        muxer.stop()
        muxer.release()
        videoExtractor.release()
        audioExtractor.release()

        // 刪除無聲視頻文件
        saveFile?.delete()
    } catch (e: Exception) {
        Log.e(TAG, "Mixer Error:${e.message}")
        // 視頻添加音頻合成失敗,直接保存視頻
        saveFile?.renameTo(newFile)

    } finally {
        afdd.close()
        Handler().post {
            refreshVideo(newFile)
            saveFile = null
        }
    }
}

於是成就了錄屏幫助類 ScreenRecordHelper

經過各種兼容性測試,目前在 DAU 超過 100 萬的 APP 中穩定運行了兩個版本,於是抽出了一個工具類庫分享給大家,使用非常簡單,代碼註釋比較全面,感興趣的可以直接點擊鏈接進行訪問:https://github.com/nanchen2251/ScreenRecordHelper

使用就非常簡單了,直接把 [README] (https://github.com/nanchen2251/ScreenRecordHelper/blob/master/README.md) 貼過來吧。

Step 1. Add it in your root build.gradle at the end of repositories:
allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}       
Step 2. Add the dependency
dependencies {
    implementation 'com.github.nanchen2251:ScreenRecordHelper:1.0.2'
}
Step 3. Just use it in your project
// start screen record
if (screenRecordHelper == null) {
    screenRecordHelper = ScreenRecordHelper(this, null, PathUtils.getExternalStoragePath() + "/nanchen")
}
screenRecordHelper?.apply {
    if (!isRecording) {
        // if you want to record the audio,you can set the recordAudio as true
        screenRecordHelper?.startRecord()
    }
}

// You must rewrite the onActivityResult
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && data != null) {
        screenRecordHelper?.onActivityResult(requestCode, resultCode, data)
    }
}
    
// just stop screen record
screenRecordHelper?.apply {
    if (isRecording) {
        stopRecord()     
    }
}
Step 4. if you want to mix the audio into your video,you just should do
// parameter1 -> The last video length you want
// parameter2 -> the audio's duration
// parameter2 -> assets resource
stopRecord(duration, audioDuration, afdd)
Step 5. If you still don’t understand, please refer to the demo

由於個人水平有限,雖然目前抗住了公司產品的考驗,但肯定還有很多地方沒有支持全面,希望有知道的大佬不嗇賜教,有任何兼容性問題請直接提 issues,Thx。

參考文章:http://lastwarmth.win/2018/11/23/media-mix/
https://juejin.im/post/5afaee7df265da0ba2672608

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

不吹不黑,關於 Java 類加載器的這一點,市面上沒有任何一本圖書講到,實戰分析Tomcat的類加載器結構(使用Eclipse MAT驗證),還是Tomcat,關於類加載器的趣味實驗,重寫類加載器,實現簡單的熱替換,@Java Web 程序員,我們一起給程序開個後門吧:讓你在保留現場,服務不重啟的情況下,執行我們的調試代碼,@Java web程序員,在保留現場,服務不重啟的情況下,執行我們的調試代碼(JSP 方式)

類加載器第7彈:

實戰分析Tomcat的類加載器結構(使用Eclipse MAT驗證)

還是Tomcat,關於類加載器的趣味實驗

了不得,我可能發現了Jar 包衝突的秘密

重寫類加載器,實現簡單的熱替換

@Java Web 程序員,我們一起給程序開個後門吧:讓你在保留現場,服務不重啟的情況下,執行我們的調試代碼

@Java web程序員,在保留現場,服務不重啟的情況下,執行我們的調試代碼(JSP 方式)

 

 

一、一個程序員的思考

大家都知道,Tomcat 處理業務,靠什麼?最終是靠我們自己編寫的 Servlet。你可能說你不寫 servlet,你用 spring MVC,那也是人家幫你寫好了,你只需要配置就行。在這裏,有一個邊界,Tomcat 算容器,容器的相關 jar 包都放在它自己的 安裝目錄的 lib 下面; 我們呢,算是業務,算是webapp,我們的 servlet ,不管是自定義的,還是 spring mvc 的DispatcherServlet,都是放在我們的 war 包裏面 WEB-INF/lib下。 看過前面文章的同學是曉得的, 這二者是由不同的類加載器加載的。在 Tomcat 的實現中,會委託 webappclassloader 去加載WAR 包中的 servlet ,然後 反射生成對應的 servlet。後續有請求來了,調用生成的 servlet 的 service 方法即可。

在 org.apache.catalina.core.StandardWrapper#loadServlet 中,即負責 生成 servlet:

 

 org.apache.catalina.core.DefaultInstanceManager#newInstance(java.lang.String)
@Override
public Object newInstance(String className) throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException, ClassNotFoundException { Class<?> clazz = loadClassMaybePrivileged(className, classLoader); return newInstance(clazz.newInstance(), clazz); }

 

在上圖中,會利用 instanceManager 根據參數中指定的 servletClass 去生成 servlet 實例。newInstance 代碼如下,主要就是用 當前 context 的classloader 去加載 該 servlet,然後 反射生成 servlet 對象。

我們重點關注的是那個紅框圈出的強轉:為什麼由 webappclassloader 加載的對象,可以轉換 為 Tomcat common classloader 加載的 Servlet 呢? 按理說,兩個不同的類加載器加載的類都是互相隔離的啊,不應該拋一個 ClassCastException 嗎?說真的,我翻了不少書,從來沒提到這個,就連網上也很含糊。

 

再來一個,關於SPI的問題。  在 SPI 中(有興趣的同學可以自行查詢,網上很多,我隨便找了一篇:https://www.jianshu.com/p/46b42f7f593c),主要是由 java 社區指定規範,比如 JDBC,廠家有那麼多,mysql,oracle,postgre,大家都有自己的 jar包,要是沒有 JDBC 規範,我們估計就得針對各個廠家的實現類編程了,那遷移就麻煩了,你針對 mysql 數據庫寫的代碼,換成 oracle 的話,代碼不改是肯定不能跑的。所以, JCP組織制定了 JDBC 規範,JDBC 規範中指定了一堆的 接口,我們平時開發,只需要針對接口來編程,而實現怎麼辦,交給各廠家唄,由廠家來實現 JDBC 規範。這裏以代碼舉例,oracle.jdbc.OracleDriver 實現了 java.sql.Driver,同時,在 oracle.jdbc.OracleDriver 的 static 初始化塊中,有下面的代碼:

 

    static { try { if (defaultDriver == null) { defaultDriver = new oracle.jdbc.OracleDriver();  DriverManager.registerDriver(defaultDriver);  } // 省略
    }

其中,標紅這句,就是 Oracle Driver 要向 JDBC 接口註冊自己,java.sql.DriverManager#registerDriver(java.sql.Driver)的實現如下:

java.sql.DriverManager#registerDriver(java.sql.Driver) 

public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException { registerDriver(driver, null); }

 

可以看到,registerDriver(java.sql.Driver)  方法的參數為 java.sql.Driver,而我們傳的參數為 oracle.jdbc.OracleDriver 類型,這兩個類型,分別由不同的類加載器加載(java.sql.Driver 由 jdk 的 啟動類加載器加載,而 oracle.jdbc.OracleDriver ,如果為 web應用,則為 tomcat 的 webappclassloader 來加載,不管怎麼說,反正不是由 jdk 加載的),這樣的兩個類型,連 類加載器都不一樣,怎麼就能正常轉換呢,為啥不拋 ClassCastException?

 

 二、不同類加載器加載的類,可以轉換的關鍵

經過上面兩個例子的觀察,不知道大家發現沒, 我們都是把一個實現,轉換為一個接口。也許,這就是問題的關鍵。我們可以大膽地推測,基於類的雙親委派機制,在 加載 實現類的時候,jvm 遇到 實現類中引用到的其他類,也會觸發加載,加載的過程中,會觸發 loadClass,比如,加載 webappclassloader 在 加載 oracle.jdbc.OracleDriver 時,觸發加載 java.sql.Driver,但是 webappclassloader 明顯是不能去加載 java.sql.Driver 的,於是會委託給 jdk 的類加載,所以,最終,oracle.jdbc.OracleDriver 中 引用的 java.sql.Driver ,其實就是由 jdk 的類加載器去加載的。 而  registerDriver(java.sql.Driver driver) 中的 driver 參數的類型 java.sql.Driver 也是由 jdk 的類加載器去加載的,二者相同,所以自然可以相互轉換。

 

這裏總結一句(不一定對),在同時滿足以下幾個條件的情況下:

前置條件1、接口 jar包 中,定義一個接口 Test

前置條件2、實現 jar 包中,定義 Test 的實現類,比如 TestImpl。(但是不要在該類中包含該 接口,你說沒法編譯,那就把接口 jar包放到 classpath)

前置條件3、接口 jar 包由 interface_classLoader 加載,實現 jar 包 由 impl_classloader 加載,其中 impl_classloader 會在自己無法加載時,委派給 interface_classLoader 

 

則,定義在 實現jar 中的Test 接口的實現類,反射生成的對象,可以轉換為 Test 類型。

 

猜測說完了,就是求證過程。

 

三、求證

1、定義接口 jar 

D:\classloader_interface\ITestSample.java  

/**
* desc: * * @author : * creat_date: 2019/6/16 0016 * creat_time: 19:28 **/ public interface ITestSample { }

 

cmd下,執行:

D:\classloader_interface>javac ITestSample.java
D:\classloader_interface>jar cvf interface.jar ITestSample.class 已添加清單 正在添加: ITestSample.class(輸入 = 103) (輸出 = 86)(壓縮了 16%)

 

此時,即可在當前目錄下,生成 名為 interface.jar 的接口jar包。

 

2、定義接口的實現 jar

在不同目錄下,新建了一個實現類。

D:\classloader_impl\TestSampleImpl.java /**
 * Created by Administrator on 2019/6/25. */ public class TestSampleImpl implements ITestSample{ }

編譯,打包:

1 D:\classloader_impl>javac -cp D:\classloader_interface\interface.jar TestSampleI 2 mpl.java 3 
4 D:\classloader_impl>jar -cvf impl.jar TestSampleImpl.class 5 已添加清單 6 正在添加: TestSampleImpl.class(輸入 = 221) (輸出 = 176)(壓縮了 20%)

 

請注意上面的標紅行,不加編譯不過。

 

3、測試

測試的思路是,用一個urlclassloader 去加載 interface.jar 中的 ITestSample,用另外一個 URLClassLoader 去加載 impl.jar 中的 TestSampleImpl ,然後用java.lang.Class#isAssignableFrom 判斷後者是否能轉成前者。

 

 1 import java.lang.reflect.Method;  2 import java.net.URL;  3 import java.net.URLClassLoader;  4 
 5 /**
 6  * desc:  7  *  8  * @author : caokunliang  9  * creat_date: 2019/6/14 0014 10  * creat_time: 17:04 11  **/
12 public class MainTest { 13 
14 
15     public static void testInterfaceByOneAndImplByAnother()throws Exception{ 16         URL url = new URL("file:D:\\classloader_interface\\interface.jar"); 17         URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url}); 18         Class<?> iTestSampleClass = urlClassLoader.loadClass("ITestSample"); 19 
20 
21         URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar"); 22         URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader); 23         Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl"); 24 
25 
26         System.out.println("實現類能轉否?:"  + iTestSampleClass.isAssignableFrom(testSampleImplClass)); 27 
28  } 29 
30     public static void main(String[] args) throws Exception { 31  testInterfaceByOneAndImplByAnother(); 32  } 33 
34 }

 

打印如下:

 

4、延伸測試1

如果我們做如下改動,你猜會怎樣? 這裏的主要差別是:

改之前,urlClassloader 作為 parentClassloader: 

        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);

改之後,不傳,默認會以 jdk 的應用類加載器作為 parent:

        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl});

 

打印結果是:

Exception in thread "main" java.lang.NoClassDefFoundError: ITestSample at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:760) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:455) at java.net.URLClassLoader.access$100(URLClassLoader.java:73) at java.net.URLClassLoader$1.run(URLClassLoader.java:367) at java.net.URLClassLoader$1.run(URLClassLoader.java:361) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:360) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at MainTest.testInterfaceByOneAndImplByAnother(MainTest.java:23) at MainTest.main(MainTest.java:33) Caused by: java.lang.ClassNotFoundException: ITestSample at java.net.URLClassLoader$1.run(URLClassLoader.java:372) at java.net.URLClassLoader$1.run(URLClassLoader.java:361) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:360) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 13 more

 

 結果就是,第23行,Class<?> testSampleImplClass = implUrlClassLoader.loadClass(“TestSampleImpl”); 這裏報錯了,提示找不到 ITestSample。

這就是因為,在加載了 implUrlClassLoader 后,觸發了對 ITestSample 的隱式加載,這個隱式加載會用哪個加載器去加載呢,沒有默認指明的情況下,就是用當前的類加載器,而當前類加載器就是 implUrlClassLoader ,但是這個類加載器開始加載 ITestSample,它是遵循雙親委派的,它的parent 加載器 即為 appclassloader,(jdk的默認應用類加載器),但appclassloader 根本不能加載 ITestSample,於是還是還給 implUrlClassLoader ,但是 implUrlClassLoader  也不能加載,於是拋出異常。

 

5、延伸測試2

我們再做一個改動, 改動處和上一個測試一樣,只是這次,我們傳入了一個特別的類加載器,作為其 parentClassLoader。 它的特殊之處在於,almostSameUrlClassLoader 和 前面加載 interface.jar 的類加載器一模一樣,只是是一個新的實例。

 URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url});  URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader);

 

這次,看看結果吧,也許你猜到了?

 

這次沒報錯了,畢竟 almostSameUrlClassLoader  知道去哪裡加載 ITestSample,但是,最後的結果显示,實現類的 class 並不能 轉成 ITestSample。

 

6、延伸測試3

說實話,有些同學可能對 java.lang.Class#isAssignableFrom 不是很熟悉,我們換個你更不熟悉的,如何?

        URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar"); URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url}); URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader); Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl"); Object o = testSampleImplClass.newInstance(); Object cast = iTestSampleClass.cast(o); // 將 o 轉成 接口的那個類 System.out.println(cast);

結果:

 

如果換成下面這樣,就沒啥問題:

        URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar"); URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url}); URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader); Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl"); Object o = testSampleImplClass.newInstance(); Object cast = iTestSampleClass.cast(o); System.out.println(cast);

 

執行:

 

四、總結

大家將就看吧,第三章的測試如果仔細看下來,基本就能理解了。 其實,除了 接口這種方式,貌似 繼承 的方式也是可以的,改天再試驗下。 這一塊,不知道為啥,我是真的在網上書上沒找到,但其實很重要,改天找找虛擬機層面的實現代碼吧。 大家如果覺得有幫助,麻煩點個推薦,對於寫作的人來說,這莫過於最大的獎勵了。

 

參考:

深入探討 Java 類加載器

 

 

 

 

 

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!