來阿里有段時間了,談談這幾個月最真實的感受

作者:黃小斜

文章來源:微信公眾號【程序員江湖】

 

閱讀本文大概需要 4 分鐘。

 

 

有段時間沒寫過原創了,想了各種理由,發現其實理由就一個,沒時間。

 

我來阿里,已經幾個月了。這段時間,最大的感受就是累。我是在今年的四月份加入阿里的。

 

當初我沒有參加阿里巴巴的實習,而是選擇了直接進行校園招聘,這也是因為當時我對實習的部門不感興趣,於是在校招的時候我就選擇了自己感興趣的部門,也就是現在我所在的螞蟻金服。

 

之前就聽說過阿里的工作強度,可能會比較大,但是,當我在真正來到這家公司的時候,才發現996,並不是虛言,這倒不是說制度上規定的 996 ,而是因為,你手上的工作,是很難做完的,所以導致你不得不用加班的方式來完成,996 最終的目的只有一個,就是你要把手上的工作做完,對其他人有一個交代。

 

剛來一個月的時候只其實我是非常不適應的,畢竟從學校到公司,整個變化是顛覆性的,雖然之前有過實習經歷,但是是比較划水的,和正式工作差別還是比較大的,第一個月大抵的狀態就是,每天九點多上班,晚上九點及以後下班,這是生活規律上的不適應。

 

除此之外,在工作上也會有一些不適應,雖然前期給我的活兒都比較的零散,只要求這裏修修補補,那裡加點東西。這倒是其次。

 

 

主要的原因還是因為我對業務線的研發流程不太了解,以及對大家在做的事情不太了解,這會導致我在工作上遇到很多困難。因為我會聽不懂大家在說什麼。所以第一個月,更多的是思考。

 

如何去提高自己的效率,如何去融入這個公司,如何去熟悉部門的業務和技術戰,自己感覺是一個比較外向的人,所以一旦有問題就會諮詢,我的同事,我的師兄。

 

阿里的師兄是一種文化。就像武俠小說里的同門師兄弟一樣,師兄會帶領着你去做那些工作,並且讓你去熟悉整個部門的一些事情。我的師兄是一個寡言少語的人,但是技術能力非常強,也很有責任心。他總是對總是對工作要求特別高,不放過任何一個問題,不漏掉任何一行問題代碼。

 

在第一個月的時間里。我學習了很多阿里巴巴的一些中間件技術。其實這些技術。在學校複習的時候,多少也聽說過一些,看過一些文章。只不過外面用的都是開源的東西,而在阿里內部,更多的是自研的產品,比如說,消息中間件,分佈式事務,數據庫中間件,等等等等。這些東西,在平時的學習和研發中,是經常會用到的,如果你在學校或者是在其他小公司,可能根本沒有機會接觸到這些東西。

 

 

作為一個Java工程師,在這家公司的職責可不止是寫代碼,你需要熟悉整個研發流程,從系統設計,代碼開發,測試聯調,發布上線,問題排查都是你的職責,其實這很鍛煉人,這也是為什麼加班會這麼多的原因,寫代碼只佔你日常工作的一小部分時間,你需要花更多時間在解決各類問題上。

 

阿里對校招生有一個培訓,不同部門可能不太一樣,螞蟻的培訓長達一個月,這個月應該也是我覺得成長最快,過得最快樂的一個月,其實無非就是上課,拓展和各類活動,這段時間認識了很多人,其中也有很多大牛,大家一起上課,一起做項目,同甘共苦度過一個月,這樣的經歷也是非常值得紀念的。

 

培訓結束后,我也開始承擔更多的工作,當自己逐漸習慣這種節奏之後,才感覺自己逐漸在融入這家公司,每次搞懂一個業務問題或者技術問題都會覺得自己在成長,當肩頭上承擔更多責任的時候,同時也承擔了更多壓力,如果不能調整好自己的心態,我想在這家公司是很難待久的。

 

可能你在其他互聯網公司也會感受到相似的壓力,但這就是大部分互聯網公司的現狀,追求效率,追求極致,我們身在其中,就必須適應環境,尊重遊戲規則,馬上又是新一年的校招季,去年這個時候,這個公眾號才剛剛誕生,轉眼一年時間,多的是更多思考,希望公眾號的文章對你們有會有更多幫助。

 

下一篇文章應該也不會讓你們等太久。

 

 

文能碼字,武能coding,是我黃小斜,不是黃老邪噢。

 

 

推薦閱讀:

 

焦慮的 BAT、不安的編程語言,揭秘程序員技術圈生存現狀!

 

 

 

為什麼有些大公司技術弱爆了?

 

 

 

  

 

​你點的每個好看,我都認真當成了喜歡

【精選推薦文章】

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

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

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

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

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

SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現

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

自定義Starter:

  SpringBoot應用篇(一):自定義starter

一、前言

  上一篇文章,通過分析refresh()方法中的invokeBeanFactoryPostProcessors()方法,分析了IoC容器的初始化過程,這一節從代碼上如下所示,接上一節ConfigurationClassParser類中的parse()方法,接着分析SpringBoot的自動裝配原理。

 1 // ConfigurationClassParser類
 2 public void parse(Set<BeanDefinitionHolder> configCandidates) {
 3     this.deferredImportSelectors = new LinkedList<>();
 4     for (BeanDefinitionHolder holder : configCandidates) {
 5         BeanDefinition bd = holder.getBeanDefinition();
 6         try {
 7             // 如果是SpringBoot項目進來的,bd其實就是前面主類封裝成的 AnnotatedGenericBeanDefinition(AnnotatedBeanDefinition接口的實現類)
 8             if (bd instanceof AnnotatedBeanDefinition) {
 9                 parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
10             } else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
11                 parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
12             } else {
13                 parse(bd.getBeanClassName(), holder.getBeanName());
14             }
15         } catch (BeanDefinitionStoreException ex) {
16             throw ex;
17         } catch (Throwable ex) {
18             throw new BeanDefinitionStoreException(
19                     "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
20         }
21     }
22     // 加載默認的配置---》(對springboot項目來說這裏就是自動裝配的入口了)
23     processDeferredImportSelectors();
24 }

 

二、SpringBoot自動裝配原理。

2.1、@SpringBootApplication註解

  對這個註解詳細大家一定非常熟悉了。再來好好看看這個註解。

 1 @Target(ElementType.TYPE)
 2 @Retention(RetentionPolicy.RUNTIME)
 3 @Documented
 4 @Inherited
 5 @SpringBootConfiguration
 6 @EnableAutoConfiguration
 7 @ComponentScan(excludeFilters = {
 8         @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
 9         @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
10 public @interface SpringBootApplication {
11     ...
12 }

  接着看@EnableAutoConfiguration

1 @Target(ElementType.TYPE)
2 @Retention(RetentionPolicy.RUNTIME)
3 @Documented
4 @Inherited
5 @AutoConfigurationPackage
6 @Import(AutoConfigurationImportSelector.class)
7 public @interface EnableAutoConfiguration {
8     ...
9 }

  OK,看到@Import(AutoConfigurationImportSelector.class)導入了一個重要的類AutoConfigurationImportSelector。

2.2、AutoConfigurationImportSelector

 1 // AutoConfigurationImportSelector類
 2 //自動裝配
 3 @Override
 4 public String[] selectImports(AnnotationMetadata annotationMetadata) {
 5     if (!isEnabled(annotationMetadata)) {
 6         return NO_IMPORTS;
 7     }
 8     AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
 9             .loadMetadata(this.beanClassLoader);
10     AnnotationAttributes attributes = getAttributes(annotationMetadata);
11     //獲取所有的自動配置類(META-INF/spring.factories中配置的key為org.springframework.boot.autoconfigure.EnableAutoConfiguration的類)
12     List<String> configurations = getCandidateConfigurations(annotationMetadata,
13             attributes);
14     configurations = removeDuplicates(configurations);
15     //需要排除的自動裝配類(springboot的主類上 @SpringBootApplication(exclude = {com.demo.starter.config.DemoConfig.class})指定的排除的自動裝配類)
16     Set<String> exclusions = getExclusions(annotationMetadata, attributes);
17     checkExcludedClasses(configurations, exclusions);
18     //將需要排除的類從 configurations remove掉
19     configurations.removeAll(exclusions);
20     configurations = filter(configurations, autoConfigurationMetadata);
21     fireAutoConfigurationImportEvents(configurations, exclusions);
22     return StringUtils.toStringArray(configurations);
23 }

  至於怎麼從章節一中提到的ConfigurationClassParser類中的parse()===>processDeferredImportSelectors()==>AutoConfigurationImportSelector#selectImports(),篇幅有限不做過多介紹。

   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); 

  我們來看一下getCandidateConfigurations()方法是怎麼拿到這些自動配置類的。

// AutoConfigurationImportSelector類
1
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, 2 AnnotationAttributes attributes) { 3 List<String> configurations = SpringFactoriesLoader.loadFactoryNames( 4 getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); 5 Assert.notEmpty(configurations, 6 "No auto configuration classes found in META-INF/spring.factories. If you " 7 + "are using a custom packaging, make sure that file is correct."); 8 return configurations; 9 }

  是不是又看到一個十分熟悉的方法loadFactoryNames(),沒錯,其實我們在分析SpringBoot啟動流程的第一篇文章的時候,就已經分析了,SpringBoot是如何從META-INF/spring.factories中加載指定key的value的。ok,我們在這裏再次回顧一遍。

  看看loadFactoryNames()方法

// SpringFactoriesLoader類
1
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) { 2 String factoryClassName = factoryClass.getName(); 3 return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); 4 }

  debug,看看要從META-INF/spring.factories中加載的類的key,如下圖所示:org.springframework.boot.autoconfigure.EnableAutoConfiguration

  回到selectImports()方法,debug,跳過List<String> configurations = getCandidateConfigurations(annotationMetadata,attributes);看一下configurations

  竟然有110個,那這些類都在哪裡呢?看spring-boot-autoconfigure(當然在SpringBoot的工程中,也不止這一個依賴包中存在該配置文件)工程下的META-INF/spring.factories,我們能看到org.springframework.boot.autoconfigure.EnableAutoConfiguration定義了一大堆。

  其中還有一個com.demo.starter.config.DemoConfig是我自定義的starter。如下所示,我在測試工程中添加了自定義starter的依賴,所以SpringBoot就能掃描到。

1 <dependency>
2     <groupId>com.demo</groupId>
3     <artifactId>demo-spring-boot-starter</artifactId>
4     <version>0.0.1-RELEASE</version>
5 </dependency>

   繼續看Set<String> exclusions = getExclusions(annotationMetadata, attributes);方法,該方法是排除主類上@SpringBootApplication註解上排除的自動裝配的類。比如我們在該註解上排除我們自定義starter的自動裝配的類,@SpringBootApplication(exclude = {com.demo.starter.config.DemoConfig.class})(當然也可以用excludeName進行排除),那麼在後面的configurations.removeAll(exclusions);方法中將會刪除我們的com.demo.starter.config.DemoConfig.class。

  configurations = filter(configurations, autoConfigurationMetadata);該行代碼將會過濾掉不需要裝配的類。過濾的邏輯有很多,比如我們常用的@ConditionXXX註解。如下所示:

 1 @ConditionalOnBean:容器中有指定的Bean 
 2 @ConditionalOnClass:當類路徑下有指定的類
 3 @ConditionalOnExpression:基於SpEL表達式作為判斷條件  
 4 @ConditionalOnJava:基於JVM版本作為判斷條件  
 5 @ConditionalOnJndi:在JNDI存在的條件下查找指定的位置  
 6 @ConditionalOnMissingBean:當容器中沒有指定Bean的情況下  
 7 @ConditionalOnMissingClass:當類路徑下沒有指定的類
 8 @ConditionalOnNotWebApplication:當前項目不是Web項目
 9 @ConditionalOnProperty:配置文件中指定的屬性是否有指定的值  
10 @ConditionalOnResource:類路徑下是否有指定的資源  
11 @ConditionalOnSingleCandidate:當指定Bean在容器中只有一個,或者雖然有多個但是指定首選Bean
12 @ConditionalOnWebApplication:當前項目是Web項目的條件下

   至於如何將這些類解析成BeanDefinition並註冊進beanDefinition中的,和上一節講的過程是一樣的,不再贅述了。

  debug,跳過refresh()方法中的invokeBeanFactoryPostProcessors(beanFactory);方法。如下圖所示,最終在beanFactory的BeanDefinitionMap中找到了自定義starter中的自動裝配的類。

 

  綜合本文和上一篇博文我們詳細的梳理了IoC容器的初始化過程,到此IoC容器的初始化過程就結束了。

 

  

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

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

【精選推薦文章】

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

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

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

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

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

Prometheus 入門

簡介

Prometheus 是一套開源的系統監控報警框架。它啟發於 Google 的 borgmon 監控系統,由工作在 SoundCloud 的 google 前員工在 2012 年創建,作為社區開源項目進行開發,並於 2015 年正式發布。

特點

作為新一代的監控框架,Prometheus 具有以下特點:

  • 強大的多維度數據模型:
  1. 時間序列數據通過 metric 名和鍵值對來區分。
  2. 所有的 metrics 都可以設置任意的多維標籤。
  3. 數據模型更隨意,不需要刻意設置為以點分隔的字符串。
  4. 可以對數據模型進行聚合,切割和切片操作。
  5. 支持雙精度浮點類型,標籤可以設為全 unicode。
  • 靈活而強大的查詢語句(PromQL):在同一個查詢語句,可以對多個 metrics 進行乘法、加法、連接、取分數位等操作。
  • 易於管理: Prometheus server 是一個單獨的二進制文件,可直接在本地工作,不依賴於分佈式存儲。
  • 高效:平均每個採樣點僅占 3.5 bytes,且一個 Prometheus server 可以處理數百萬的 metrics。
    使用 pull 模式採集時間序列數據,這樣不僅有利於本機測試而且可以避免有問題的服務器推送壞的 metrics。
  • 可以採用 push gateway 的方式把時間序列數據推送至 Prometheus server 端。
  • 可以通過服務發現或者靜態配置去獲取監控的 targets。
  • 有多種可視化圖形界面。
  • 易於伸縮。

組成及架構

Prometheus 生態圈中包含了多個組件,其中許多組件是可選的:

  • Prometheus Server: 用於收集和存儲時間序列數據。
  • Client Library: 客戶端庫,為需要監控的服務生成相應的 metrics 並暴露給 Prometheus server。當 Prometheus server 來 pull 時,直接返回實時狀態的 metrics。
  • Push Gateway: 主要用於短期的 jobs。由於這類 jobs 存在時間較短,可能在 Prometheus 來 pull 之前就消失了。為此,這次 jobs 可以直接向 Prometheus server 端推送它們的 metrics。這種方式主要用於服務層面的 metrics,對於機器層面的 metrices,需要使用 node exporter。
  • Exporters: 用於暴露已有的第三方服務的 metrics 給 Prometheus。
  • Alertmanager: 從 Prometheus server 端接收到 alerts 后,會進行去除重複數據,分組,並路由到對收的接受方式,發出報警。常見的接收方式有:电子郵件,pagerduty,OpsGenie, webhook 等。
  • 一些其他的工具。

下圖為 Prometheus 官方文檔中的架構圖:

從上圖可以看出,Prometheus 的主要模塊包括:Prometheus server, exporters, Pushgateway, PromQL, Alertmanager 以及圖形界面。

其大概的工作流程是:

  1. Prometheus server 定期從配置好的 jobs 或者 exporters 中拉 metrics,或者接收來自Pushgateway 發過來的 metrics,或者從其他的 Prometheus server 中拉 metrics。
  2. Prometheus server 在本地存儲收集到的 metrics,並運行已定義好的 alert.rules,記錄新的時間序列或者向 Alertmanager 推送警報。
  3. Alertmanager 根據配置文件,對接收到的警報進行處理,發出告警。
    在圖形界面中,可視化採集數據。

相關概念

下面將對 Prometheus 中的數據模型(時間序列),metric 類型,instance 和 jobs等概念進行介紹。

數據模型

Prometheus 中存儲的數據為時間序列,是由 metric 的名字和一系列的標籤(鍵值對)唯一標識的,不同的標籤則代表不同的時間序列。

  • metric 名字:該名字應該具有語義,一般用於表示 metric 的功能,例如:http_requests_ total, 表示 http 請求的總數。其中,metric 名字由 ASCII 字符,数字,下劃線,以及冒號組成,且必須滿足正則表達式 [a-zA-Z_:][a-zA-Z0-9_:]*。
  • 標籤:使同一個時間序列有了不同維度的識別。例如 http_requests_total{method=”Get”} 表示所有 http 請求中的 Get 請求。當 method=”post” 時,則為新的一個 metric。標籤中的鍵由 ASCII 字符,数字,以及下劃線組成,且必須滿足正則表達式 [a-zA-Z_:][a-zA-Z0-9_:]*。
  • 樣本:實際的時間序列,每個序列包括一個 float64 的值和一個毫秒級的時間戳。
  • 格式: { =, …},例如:http_requests_total{method=”POST”,endpoint=”/api/tracks”}。

Metrics種類

Prometheus客戶端庫提供了四種核心Metrics類型。

Counter(計數器)

  • 說明:Counter是一個累積度量,它表示一個單調遞增的 Metrics,其值只能在重啟時遞增或重置為零
  • 場景:可以使用Counter來表示http的請求數、已完成的任務數或錯誤數、下單數。

Gauge(測量儀)

  • 說明:當前值的一次快照(snapshot)測量,可增可減。
  • 場景:磁盤使用率,當前同時在線用戶數。

Histogram(直方圖)

  • 說明:通過區間統計樣本分佈。
  • 場景:請求延遲時間的統計。例如統計 0~200ms、200ms~400ms、400ms~800ms 區間的請求數有多。

Summary(匯總)

  • 說明:根據樣本統計出百分位。
  • 場景:請求延遲時間的統計。例如統計 95%的請求延遲 < xxx ms ,99%的請求延遲 < xxx ms

instance 和 jobs

在Prometheus術語中,你可以scrape(刮擦)的端點稱為 實例,通常對應於單個進程。一組同種類型的 instances(主要用於保證可擴展性和可靠性),例如:具有四個複製instances(實例)的API服務器job作業:

  • job: api-server
    • instance 1: 1.2.3.4:5670
    • instance 2: 1.2.3.4:5671
    • instance 3: 5.6.7.8:5670
    • instance 4: 5.6.7.8:5671

當Prometheus scrape(刮擦)目標時,它會自動在scrape的時間序列上附加一些標籤,用來識別scrape的目標。

  • job:目標所屬的已配置job名稱。
  • instance: : 已刮擦的目標URL 的一部分。

對於每次實例 scrape(刮取,Prometheus都會在以下時間序列中存儲樣本:

  • up{job=”<job-name>”, instance=”<instance-id>”}:1如果實例是健康的,即可達,或者0刮擦失敗。
  • scrape_duration_seconds{job=”<job-name>”, instance=”<instance-id>”}:刮擦持續時間。
  • scrape_samples_post_metric_relabeling{job=”<job-name>”, instance=”<instance-id>”}:應用度量標準重新標記后剩餘的樣本數。
  • scrape_samples_scraped{job=”<job-name>”, instance=”<instance-id>”}:目標暴露的樣本數。
  • scrape_series_added{job=”<job-name>”, instance=”<instance-id>”}:該刮擦中新系列的大致數量。v2.10中的新功能。

up時間序列對於實例可用性監視非常有用。

安裝和配置

安裝

你可以在官網 https://prometheus.io/download/ 下載 安裝包,解壓后使用。為了方便,我使用docker 鏡像的方式 運行Prometheus。

docker run --name prometheus -d -p 9090:9090 prom/prometheus

瀏覽器輸入http://localhost:9090 ,訪問 Prometheus 的 Web UI:

點擊菜單欄 “Status” 下的 Targets ,界面如下:

可以看大Prometheus 自身 metrics 處於UP狀態 ,說明 安裝成功。

配置

Prometheus 的配置文件 prometheus.yml 內容如下:

# 全局設置,可以被覆蓋
global:
  scrape_interval:     15s
  evaluation_interval: 15s
  
rule_files:
  # - "first.rules"
  # - "second.rules"

scrape_configs:
  - job_name: prometheus
    static_configs:
    - targets: ['localhost:9090']

global塊控制 Prometheus 的全局配置。我們有兩種選擇。第一個,scrape_interval控制Prometheus 刮擦目標的頻率。你可以為單個目標覆蓋此值。在這種情況下,全局設置是每15秒刮一次。該evaluation_interval選項控制普羅米修斯評估規則的頻率。Prometheus 使用規則創建新的時間序列並生成警報。

rule_files塊指定我們希望 Prometheus 加載的任何規則的位置。現在我們沒有規則。

最後一個塊scrape_configs控制 Prometheus 監視的資源。由於 Prometheus 還將自己的數據公開為HTTP端點,因此它可以抓取並監控自身的健康狀況。在默認配置中有一個名為 prometheus 的job,它抓取 prometheus 服務器 公開的時間序列數據。該作業包含一個靜態配置的目標,即端口9090上的本地主機。返回的時間序列數據將詳細說明Prometheus服務器的狀態和性能。

實驗

Prometheus HTTP 度量模擬器

為了演示 Prometheus 的簡單使用,這裏運行一個 Prometheus HTTP 度量模擬器。模擬一個簡單的HTTP微服務,生成Prometheus Metrics,通過 docker 運行。

docker run -p 8080:8080 pierrevincent/prom-http-simulator:0.1

它在/metrics端點下公開以下Prometheus指標:

  • http_requests_total:請求計數器,標籤endpoint和status
  • http_request_duration_milliseconds:請求延遲直方圖

可以開啟流量高峰模式,更改流量高峰模式可以通過以下方式完成:

# ON
curl -X POST http://127.0.0.1:8080/spike/on

# OFF
curl -X POST http://127.0.0.1:8080/spike/off

# RANDOM
curl -X POST http://127.0.0.1:8080/spike/random

錯誤率默認為1%。它可以更改為0到100之間的数字:

# 例如將錯誤率設置為50%
curl -H 'Content-Type: application/json' -X PUT -d '{"error_rate": 50}' http://127.0.0.1:8080/error_rate

修改Prometheus配置

需要將 HTTP 度量模擬器 的 metrics端點 配置到 Prometheus的配置文件 prometheus.yml 中。

創建一個 prometheus.yml 文件 內容如下:

global:
  scrape_interval: 5s
  evaluation_interval: 5s
  scrape_timeout: 5s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets: ['localhost:9090']
  - job_name: 'http-simulator'
    metrics_path: /metrics
    static_configs:
    - targets: ['172.16.1.232:8080']

通過docker up 命令替換 容器中的配置文件:

docker cp prometheus.yml prometheus:/etc/prometheus/

重啟容器:

docker restart prometheus

訪問 http://localhost:9090/targets ,發現已經出現了 target “http-simulator” ,並且為UP狀態。

查詢

請求率(Request Rate)查詢

查詢http請求數

http_requests_total{job="http-simulator"}

查詢成功login請求數

http_requests_total{job="http-simulator", status="200", endpoint="/login"}

查詢成功請求數,以endpoint區分

http_requests_total{job="http-simulator", status="200"}

查詢總成功請求數

sum(http_requests_total{job="http-simulator", status="200"})

查詢成功請求率,以endpoint區分

rate(http_requests_total{job="http-simulator", status="200"}[5m])

查詢總成功請求率

sum(rate(http_requests_total{job="http-simulator", status="200"}[5m]))

延遲分佈(Latency distribution)查詢

查詢http-simulator延遲分佈

http_request_duration_milliseconds_bucket{job="http-simulator"}

查詢成功login延遲分佈

http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login"}

不超過200ms延遲的成功login請求佔比

sum(http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login", le="200"}) / sum(http_request_duration_milliseconds_count{job="http-simulator", status="200", endpoint="/login"})

成功login請求延遲的99百分位

histogram_quantile(0.99, rate(http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login"}[5m]))

上面給出的這些查詢表達式,在 prometheus 的 查詢界面上自行測試下 ,這裏就不一一測試了,

總結

本篇對 Prometheus 的組成,架構和基本概念進行了介紹,並實例演示了 Prometheus 的查詢表達式的應用。本篇是 Prometheus 系列的第一篇, 後續還會有Prometheus與其他圖形界面的集成,與 springboot 應用的集成等 。

參考

https://prometheus.io/docs/introduction/overview/
https://www.ibm.com/developerworks/cn/cloud/library/cl-lo-prometheus-getting-started-and-practice/index.html

歡迎掃碼或微信搜索公眾號《程序員果果》關注我,關注有驚喜~

【精選推薦文章】

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

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

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

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

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

Spring 中的Null-Safety

之前一直在某些代碼中看到過使用@Nullable 標註過的註釋,當時也沒有在意到底是什麼意思,所以這篇文章來談談Spring中關於Null的那些事。

在Java中不允許讓你使用類型表示其null的安全性,但Spring Framework 現在在org.sprinngframework.lang包提供以下註釋,以便聲明API和字段的可空性:

  • @Nullable: 用於指定參數、返回值或者字段可以作為null的註釋。
  • @NonNull: 與上述註釋相反,表明指定參數、返回值或者字段不允許為null。(不需要@NonNullApi和@NonNullFields適用的參數/返回值和字段)
  • @NonNullApi: 包級別的註釋聲明非null作為參數和返回值。
  • @NonNullFields:包級別的註釋聲明字段默認非空

Spring Framework 本身利用了上面這幾個註釋,但它們也可以運用在任何基於Spring的Java 項目中,以聲明空安全api 和 空安全字段。尚未支持泛型和數組元素的可空性,但應也即將發布在後來的版本。Spring Null-Safety出現在Spring5中,讓我們更方便的編寫空安全的代碼,這叫做null-safety,null-safety不是讓我們逃脫不安全的代碼,而是在編譯時產生警告。 此類警告可以在運行時防止災難性空指針異常(NPE)。

@NonNull

@NonNull註釋是null-safety的所有註釋中最重要的一個,我們可以使用此註釋在期望對象引用的任何地方聲明非空約束:字段、方法參數或者方法返回值。

先來看一個例子

public class Student {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        if(name != null && name.isEmpty()){
            name = null;
        }
        this.name = name;
    }
}

上述代碼對name的校驗是有效的,但是存在一個缺陷,如果name被設置為null的話,那麼當我們使用name的時候,就會以NullPointerException來結尾。

使用@NonNull

Spring 的null-safety特性能夠允許idea或者eclipse報告這個潛在的威脅,例如,如果我們用IDEA對屬性加上@NonNull會出現如下的效果。

奇怪,並沒有什麼變化啊,沒看見有潛在的安全提示啊,那是因為你沒有在idea進行設置

設置安全檢查

如果你也沒有提示的話,可以通過如下的方式設置安全檢查

如果還不好使的話,那就在右側 configuration annotations 添加一下 @NonNull和 @Nullable 所在的jar包,如下:

添加上,打上 ✅ 即可看到如下效果。

現在fullName 已經被@NonNull 註釋添加編譯器檢查null值的功能了!

如果你不相信的話,可以把@NonNull 註釋去掉,你的鼠標再放在fullName 上,已經沒有這句提示了。

@NonNullFields

@NonNull 註解能夠幫助你確保null-safety。然而,如果此註釋直接裝飾所有的字段的話,就會污染整個代碼庫。

Spring提供了另外一個不允許為null的註解 — @NonNullFields。這個註解適合用在包級別上,通知我們的開發工具註釋包中所有的字段,默認的,不允許為null

新建一個Parent類,並在該類所屬包下創建一個名為package-info.java的類,創建的不是Java類,而是創建的file,名為package-info.java,如下

package-info.java

@NonNullFields
package com.nullsafety.demo.pojo;

import org.springframework.lang.NonNullFields;

新建一個Parent.java

public class Parent {

    private String son;
    private String age;
    private String name;

    public void setSon(String son) {
        if(son != null && son.isEmpty()){
            son = null;
        }
        this.son = son;
    }

    public void setAge(String age) {
        if(age != null && age.isEmpty()){
            age = null;
        }
        this.age = age;
    }

    public void setName(String name) {
        if(name != null && name.isEmpty()){
            name = null;
        }
        this.name = name;
    }
}

package-info.java 中的@NonNullFields能夠對Parent類中所有的屬性起作用,把鼠標放在任意一個屬性上,會出現編譯期檢查的提示

@Nullable

@NonNullFields註釋通常比@NonNull更好,因為它有助於減少樣板。 但是,有時我們想要從包級別指定的非null約束中免除某些字段,這時候就會使用到@Nullable註解

改造一下Person.java,Person.java 與pack-info.java 處於同一包下

public class Person {

    @NonNull
    private String fullName;

    @Nullable
    private String nickName;

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        if(nickName != null && nickName.isEmpty()){
            nickName = null;
        }
        this.nickName = nickName;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        if(fullName != null && fullName.isEmpty()){
            fullName = null;
        }
        this.fullName = fullName;
    }
}

在這種情況下,我們使用@Nullable註釋來覆蓋字段上@NonNullFields的語義。

@NonNullApi

@NonNullFields註釋僅適用於其名稱所示的字段。 如果我們想對方法的參數和返回值產生相同的影響,我們需要@NonNullApi。

添加 @NonNullApi和 @NonNullFields 在 configure annotations 中,並選用NonNullApi

與@NonNullFields一樣,我們需要在package-info.java 中定義@NonNullApi

package-info.java

@NonNullApi
@NonNullFields
package com.nullsafety.demo.pojo;

import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

加上如下註釋后的效果如下: 可以在返回值的時候接受到編譯期的提示。

後記

看完文章,你至少應該了解

  • 四個註解 @NonNull, @Nullable, @NonNullFields, @NonNullApi 四個註解各自的作用範圍
  • 如何設置編譯期的Null-safety檢查

歡迎關注

【精選推薦文章】

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

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

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

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

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

簡潔方便的集合處理——Java 8 stream流

背景

java 8已經發行好幾年了,前段時間java 12也已經問世,但平時的工作中,很多項目的環境還停留在java1.7中。而且java8的很多新特性都是革命性的,比如各種集合的優化、lambda表達式等,所以我們還是要去了解java8的魅力。

今天我們來學習java8的Stream,並不需要理論基礎,直接可以上手去用。

我接觸stream的原因,是我要搞一個用戶收入消費的數據分析。起初的統計篩選分組都是打算用sql語言直接從mysql里得到結果來展現的。但在操作中我們發現這樣頻繁地訪問數據庫,性能會受到很大的影響,分析速度會很慢。所以我們希望能通過訪問一次數據庫就拿到所有數據,然後放到內存中去進行數據分析統計過濾。

接着,我看了stream的API,發現這就是我想要的。

一、Stream理解

在java中我們稱Stream為『』,我們經常會用流去對集合進行一些流水線的操作。stream就像工廠一樣,只需要把集合、命令還有一些參數灌輸到流水線中去,就可以加工成得出想要的結果。這樣的流水線能大大簡潔代碼,減少操作。

二、Stream流程

原集合 —> 流  —> 各種操作(過濾、分組、統計) —> 終端操作

 

Stream流的操作流程一般都是這樣的,先將集合轉為流,然後經過各種操作,比如過濾、篩選、分組、計算。最後的終端操作,就是轉化成我們想要的數據,這個數據的形式一般還是集合,有時也會按照需求輸出count計數。下文會一一舉例。

三、API功能舉例

首先,定義一個用戶對象,包含姓名、年齡、性別和籍貫四個成員變量:

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Log4j
@Builder
public class User {
    //姓名
    private String name;
    //年齡
    private Integer age;
    //性別
    private Integer sex;
    //所在省市
    private String address;
}

 

這裏用lombok簡化了實體類的代碼。

然後創建需要的集合數據,也就是源數據:

//1.構建我們的list
List<User> list= Arrays.asList(
        new User("鋼鐵俠",40,0,"華盛頓"),
        new User("蜘蛛俠",20,0,"華盛頓"),
        new User("趙麗穎",30,1,"湖北武漢市"),
        new User("詹姆斯",35,0,"洛杉磯"),
        new User("李世民",60,0,"山西省太原市"),
        new User("蔡徐坤",20,1,"陝西西安市"),
        new User("葫蘆娃的爺爺",70,0,"山西省太原市")
);

 

3.1 過濾

1)創建流 stream() / parallelStream()

  • stream() : 串行流
  • parallelStream(): 并行流

2)filter 過濾(T-> boolean)

比如要過濾年齡在40歲以上的用戶,就可以這樣寫:

List<User> filterList = list.stream().filter(user -> user.getAge() >= 40)
        .collect(toList());

 

filter裏面,->箭頭後面跟着的是一個boolean值,可以寫任何的過濾條件,就相當於sql中where後面的東西,換句話說,能用sql實現的功能這裏都可以實現

打印結果:

3)distinct 去重

和sql中的distinct關鍵字很相似。為了看到效果,此處在原集合中加入一個重複的人,就選擇鋼鐵俠吧,復聯4鋼鐵俠不幸遇害,大家還是比較傷心的。

List<User> list= Arrays.asList(
        new User("鋼鐵俠",40,0,"華盛頓"),
        new User("鋼鐵俠",40,0,"華盛頓"),
        new User("蜘蛛俠",20,0,"華盛頓"),
        new User("趙麗穎",30,1,"湖北武漢市"),
        new User("詹姆斯",35,0,"洛杉磯"),
        new User("李世民",60,0,"山西省太原市"),
        new User("蔡徐坤”,18,1,"陝西西安市"),
        new User("葫蘆娃的爺爺",70,0,"山西省太原市")
);

 

//distinct 去重
List<User> distinctList = filterList.stream().distinct()
        .collect(toList());

 

打印結果:

4)sorted排序

如果流中的元素的類實現了 Comparable 接口,即有自己的排序規則,那麼可以直接調用 sorted() 方法對元素進行排序,如: 

Comparator.comparingInt

 

反之, 需要調用 sorted((T, T) -> int) 實現 Comparator 接口。

//sorted()
List<User> sortedList = distinctList.stream().sorted(Comparator.comparingInt(User::getAge))
        .collect(toList());

 

打印結果:

結果按照年齡從小到大進行排序。

5)limit() 返回前n個元素

如果想知道這裏面年齡最小的是誰,可作如下操作:

//limit 返回前n個元素
List<User> limitList = sortedList.stream().limit(1)
        .collect(toList());

 

6)skip()

與limit恰恰相反,skip的意思是跳過,也就是去除前n個元素。

打印結果:

果然,前兩個人都被去除了,只剩下最老的葫蘆娃爺爺。

3.2 映射

1)map(T->R)

map是將T類型的數據轉為R類型的數據,比如我們想要設置一個新的list,存儲用戶所有的城市信息。

//map(T->R)
List<String> cityList = list.stream().map(User::getAddress).distinct().collect(toList());

 

打印結果:

2)flatMap(T -> Stream)

將流中的每一個元素 T 映射為一個流,再把每一個流連接成為一個流。

//flatMap(T -> Stream<R>)
List<String> flatList = new ArrayList<>();
flatList.add("唱,跳");
flatList.add("rape,籃球,music");
flatList = flatList.stream().map(s -> s.split(",")).flatMap(Arrays::stream).collect(toList());

 

打印結果:

這裏原集合中的數據由逗號分割,使用split進行拆分后,得到的是Stream<string[]>,字符串數組組成的流,要使用flatMap的

Arrays::stream

將Stream<string[]>轉為Stream,然後把流相連接,組成了完整的唱、跳、rap、籃球和music。

3.3 查找

1)allMatch(T->boolean)

檢測是否全部滿足參數行為,假如這些用戶是網吧上網的用戶名單,那就需要檢查是不是每個人都年滿18周歲了。

boolean isAdult = list.stream().allMatch(user -> user.getAge() >= 18);

 

打印結果:

true

 

2)anyMatch(T->boolean)

檢測是否有任意元素滿足給定的條件,比如,想知道同學名單里是否有女生。

//anyMatch(T -> boolean) 是否有任意一個元素滿足給定的條件
boolean isGirl = list.stream().anyMatch(user -> user.getSex() == 1);

 

打印結果:

true

 

說明集合中有女生存在。

3)noneMatch(T -> boolean)

流中是否有元素匹配給定的 T -> boolean 條件。

比如檢測有沒有來自巴黎的用戶。

boolean isLSJ = list.stream().noneMatch(user -> user.getAddress().contains("巴黎"));

 

打印結果:

true

 

打印true說明沒有巴黎的用戶。

4)findFirst( ):找到第一個元素

Optional<User> fristUser  = list.stream().findFirst();

 

打印結果:

User(name=鋼鐵俠, age=40, sex=0, address=華盛頓)

 

5)findAny():找到任意一個元素

Optional<User> anyUser  = list.stream().findAny();

 

打印結果:

User(name=鋼鐵俠, age=40, sex=0, address=華盛頓)

 

這裏我們發現findAny返回的也總是第一個元素,那麼為什麼還要進行區分呢?因為在并行流 parallelStream() 中找到的確實是任意一個元素。

Optional<User> anyParallelUser  = list.parallelStream().findAny();

 

打印結果 :

Optional[User(name=李世民, age=60, sex=0, address=山西省太原市)]

 

3.4 歸納計算

1)求用戶的總人數

long count = list.stream().collect(Collectors.counting());

 

我們可以簡寫為:

long count = list.stream().count();

 

運行結果:

 8

 

2)得到某一屬性的最大最小值

// 求最大年齡
Optional<User> max = list.stream().collect(Collectors.maxBy(
Comparator.comparing(User::getAge)));

// 求最小年齡
Optional<User> min = list.stream().collect(Collectors.minBy(
Comparator.comparing(User::getAge)));

 

運行結果:

3)求年齡總和是多少

// 求年齡總和
int totalAge = list.stream().collect(Collectors.summingInt(User::getAge));

 

運行結果:

 313

 

我們經常會用BigDecimal來記錄金錢,假設想得到BigDecimal的總和:

// 獲得列表對象金額, 使用reduce聚合函數,實現累加器
BigDecimal sum = myList.stream() .map(User::getMoney)
.reduce(BigDecimal.ZERO,BigDecimal::add);

 

4)求年齡平均值

//求年齡平均值
double avgAge = list.stream().collect(
Collectors.averagingInt(User::getAge));

 

運行結果:

 39.125

 

5)一次性得到元素的個數、總和、最大值、最小值

IntSummaryStatistics statistics = list.stream().collect(
Collectors.summarizingInt(User::getAge));

 

運行結果:

6)字符串拼接

要將用戶的姓名連成一個字符串並用逗號分割。

String names = list.stream().map(User::getName)
.collect(Collectors.joining(", "));

 

運行結果:

 鋼鐵俠, 鋼鐵俠, 蜘蛛俠, 趙麗穎, 詹姆斯, 李世民, 蔡徐坤, 葫蘆娃的爺爺

 

3.5 分組

在數據庫操作中,我們經常通過GROUP BY關鍵字對查詢到的數據進行分組,java8的流式處理也提供了分組的功能。使用Collectors.groupingBy來進行分組。

1)可以根據用戶所在城市進行分組

Map<String, List<User>> cityMap = list.stream()
.collect(Collectors.groupingBy(User::getAddress));

 

結果是一個map,key為不重複的城市名,value為屬於該城市的用戶列表。已經實現了分組。

2)二級分組,先根據城市分組再根據性別分組

Map<String, Map<Integer, List<User>>> group = list.stream().collect(
        Collectors.groupingBy(User::getAddress, // 一級分組,按所在地區
                Collectors.groupingBy(User::getSex))); // 二級分組,按性別

 

運行結果:

3)如果僅僅想統計各城市的用戶個數是多少,並不需要對應的list

按城市分組並統計人數:

Map<String, Long> cityCountMap = list.stream()
.collect(Collectors.groupingBy(User::getAddress,Collectors.counting()));

 

運行結果:

4)當然,也可以先進行過濾再分組並統計人數

Map<String,Long> map = list.stream().filter(user -> user.getAge() <= 30)
        .collect(Collectors.groupingBy(User::getAddress,Collectors.counting()));

 

運行結果:

5)partitioningBy 分區

分區與分組的區別在於,分區是按照 true 和 false 來分的,因此partitioningBy 接受的參數的 lambda 也是 T -> boolean

//根據年齡是否小於等於30來分區
Map<Boolean, List<User>> part = list.stream()
        .collect(partitioningBy(user -> user.getAge() <= 30));

 

運行結果:

總結

到目前為止,stream的功能我們已經用了很多了,感覺有點眼花繚亂卻無所不能,stream能做的事情遠遠不止這些。

我們可以多學習使用stream,把原來複雜的sql查詢,一遍又一遍地for循環的複雜代碼重構,讓代碼更簡潔易懂,可讀性強。

拓展閱讀:Redis專題(1):構建知識圖譜

Redis專題(2):Redis數據結構底層探秘

作者:楊亨

來源:宜信技術學院

【精選推薦文章】

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

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

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

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

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

詳解二分查找算法

我周圍的人幾乎都認為二分查找很簡單,但事實真的如此嗎?二分查找真的很簡單嗎?並不簡單。看看 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 乾貨!

【精選推薦文章】

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

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

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

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

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