目錄
- 前言
- 閱讀對象
- 閱讀導航
- 前置知識
- 筆記正文
- 0、什么是自動配置
- 0.1 基本概念
- 0.2 SpringBoot中的【約定大于配置】
- 0.3 從SpringMVC看【約定大于配置】
- 0.4 從Redis看【約定大于配置】
- 一、@EnableAutoConfiguration源碼解析
- 二、SpringBoot常用條件注解源碼解析
- 2.1 自定義條件注解
- 2.2 @ConditionalOnClass原理解析
- 2.3 ConditionalOnBean原理解析
- 三、SpringBoot之Mybatis自動配置源碼解析
- 四、SpringBoot之AOP自動配置源碼解析
- 五、SpringBoot Jar包啟動過程源碼解析
- 學習總結
- 感謝
前言
想要搞懂自動裝配,需要當前系列前兩篇筆記內容打底。不過我上一篇筆記寫的不好,沒有什么突出的重點,主要也是當時跟著課程看了源碼之后,也沒發現什么特別的,直到在看自動裝配源碼的時候,才后知后覺遺漏了一些重要的知識點。但是總體來說不影響這一節課的筆記記錄吧。
閱讀對象
閱讀導航
系列上一篇文章:《【微服務專題】Spring啟動過程源碼解析》
前置知識
筆記正文
0、什么是自動配置
我們說起SpringBoot
,總會不自覺的聯想到自動配置
、約定大于配置
等標簽,但說實在,我是有一點困惑的,這個【約定和配置】到底是什么意思,或者說,它們具體指的是哪些內容啊?!
我在想,像我這種,只是用過那么一年多SSM/SSH
,甚至完全沒有經歷過的人來說,是很難理解的。雖然我也確實經歷過SSM/SSH
,但那時候初出茅廬,根本就沒在意過這些玩意好吧…
不過話說回來,后面再回來繼續做Java的時候,已經是SpringBoot
的時代了嘛,所以單從片面印象來說的話:SpringBoot
確實方便了很多。最最最直觀的就是啟動,給我的感覺啟動一個web應用跟簡單運行了一個main
方法一樣便捷。而在以前SSM/SSH
時代,各種眼花繚亂的xml
配置,還有那個令人煩躁的tomcat配置,就已經讓我很難受了。
好吧,不懂嘛,于是我就去稍微學習了一下。下面,我就按照我自己的理解,再拼上一些百度的、起碼能說服我自己的答案給大家說道說道。另外,也想舉一個真正直觀的例子給大家說說,【約定和配置】到底使了什么魔法
0.1 基本概念
【約定大于配置】是一種開發思想、設計理念,它強調使用默認約定和規則,減少開發者需要做的【顯式】配置。根據這一原則,大部分情況下,開發者只需關注那些【不符合默認約定】的配置,而不需要為每個細節都進行顯式配置。。
開天眼給大家說一下:既然結果是減少了需要程序員【顯示】配置的工作量,那反過來就證明了,在SpringBoot之前的時代,有一些配置是不得不(必須)需要程序員去配置的!
另外,這里說的【配置】,不是指xml
配置,而是對引入的服務的所有形式的配置,xml
只是一種介質而已,我們還可以是txt
、yml
,還有java
文件的配置方式。
我認為這個結論很重要!我后面研究案例就是往這方向去看的
綜上所述,可以這樣簡單地理解【約定】和【配置】:
- 默認規則
- 【顯示】配置(不得不設置的配置 / 沒辦法使用默認規則的配置)。這里說的【配置】,不是指具體的
xml
配置,而是對引入的服務的【所有形式】的配置,xml
只是其中一種形式而已,配置還可以是txt
、yml
,包括java
文件的配置形式。
這個理念,有如下顯著的優勢:
- 提高開發效率: 通過遵循默認約定,開發者可以快速啟動項目并進行開發,無需花費大量時間在繁瑣的配置上
- 減少決策負擔: 【約定大于配置】減少了開發者需要做出的決策,使開發過程更加流暢,不需要在每個細節上做出選擇
- 減少錯誤: 默認約定可以減少配置錯誤的機會,因為開發者只需要在特定情況下進行配置,從而降低了出錯的可能性
0.2 SpringBoot中的【約定大于配置】
我們都知道,SpringBoot是基于Spring的,嚴格來說,SpringBoot的出現其實是為了降低Spring的使用門檻。
- 使用maven的目錄結構。默認有
src-main-resources
文件夾,存放資源配置文件;src-main-java
存放項目java源碼;target
文件夾存放編譯、打包內容(其實,Maven的設計本身就是遵循【約定大于配置】的原則)
這是約定的一種,通過約定這些內容,使得項目結構統一,減少了開發人員學習成本。
試想一下,如果我們在不同的公司,他們的項目結構五花八門,甚至沒有src/main/resources,沒有src/main/java等目錄,你學習成本是不是變高了??博主之前寫過2、3年C/C++,那時候接觸的項目就是這個鳥樣子的,都是自定義資源存放目錄,打包目錄,源碼目錄,真的吐血… … 我轉戰Java很大一個原因就是奔著Java的規范性去的。當然,我現在相當后悔…
- 使用
application.properties/yml
文件設置配置內容
這也是一種約定,拒絕程序員五花八門的配置文件
- 默認使用Tomcat容器
接觸過SSM嗎?SSM項目通常來說,需要額外配置啟動一個Tomcat,然后將SSM項目打包部署到Tomcat上
而在SpringBoot中,我們啟動項目就跟運行一個普通的main
函數一樣
- 提供了大量的自動配置接口,自動配置類,注解等,幫助程序員配置。嚴格來說,自動配置接口以及注解等,都是為了自動配置類服務的。SpringBoot提供的大量自動配置類,在里面默認設置了很多值用以配置第三方接口服務,這些默認配置甚至能讓開發做到開箱即用,只有一些不得不需要用戶設置的內容才需要開發人員自己設置。這就是上面概念所謂的
開發者只需要關注那些不符合默認約定的配置
的意義。
這是SpringBoot【約定大于配置】的重要實現。我們先稍微解釋一下,這些所謂的接口、類、注解是指什么:
- 自動配置接口:AutoConfigurationImportFilter、AutoConfigurationImportListener等
- 自動配置類:DispatcherServletAutoConfiguration、HttpEncodingAutoConfiguration等
- 注解:諸如@AutoConfiguration、@AutoConfigurationPackage等
上述我只是簡單列舉了一些內容而已,其實具體的,可以看:org.springframework.boot:spring-boot-autoconfigure
這個包。里面就是SpringBoot為大家做自動配置提供的默認配置,和相關接口
0.3 從SpringMVC看【約定大于配置】
說實在,SpringMVC我不是很熟悉,所以我不是很保證自己說的是對的,在網上也沒看到非常令人信服的答案。但是關于【約定大于配置】的體現我覺得還是正確的。
有人說spring-boot-starter-web
其實就是面配置的SSM
。在SSM
時代,我們想要開發一個web應用,免不了如下配置:
- 新增
web.xml
,在里面配置前端控制器DispatcherServlet
、視圖解析器ViewResolver
、過濾器等 - 新增核心配置文件
**-servlet.xml
- Tomcat容器的配置
- 等等…
其實說需要新增這種那種xml
配置是有點狹隘的說法,我前面說過了xml
只是其中一種配置形式而已。
準確來說,傳統的SSM
項目,需要我們開發人員顯式、準確地向Spring容器注冊DispatcherServlet
、ViewResolver
、SqlSessionFactoryBean
、DriverManagerDataSource
。
如果我們沒有【顯式】地去聲明、注冊這些Bean,那我們項目直接啟動失敗!他會說找不到這個找不到那個。但是,在SpringBoot中,我們只需要在pom中引入spring-boot-starter-web
就可以了,這個jar包,會自動向Spring中注冊上述關鍵bean
。
怎么注冊?我們簡單看一下關于SpringMVC
的自動配置類。
1)WebMvcAutoConfiguration.java
:SpringMVC的自動配置入口
在這里我們無需關心上面的諸多注解的具體含義先,具體意義我會在后面的源碼解析中介紹。
但是可以預見的是,在某種情況下,spring-boot-stater-web
會進入DispatcherServletAutoConfiguration.class
里面
2)DispatcherServletAutoConfiguration.java
:在這里,有一個@Bean
,暫不關心他什么時候會被觸發,但至少可以預見的是:在某種情況下,DispatcherServletAutoConfiguration
類會向Spring
中注冊一個DispatcherServlet
類型的bean
啊,對的,本質上就是如此而已。SpringBoot的自動配置,實際上就是SpringBoot的源碼中預先寫好了一些配置類,預先定義好了一些Bean,我們在用SpringBoot時,這些配置類就已經在我們項目的依賴中了,而這些自動配置類或自動配置Bean到底生不生效,就看具體所指定的條件了。
不過還有一點需要說明,那就是SpringBoot可不是簡簡單單的給你注冊了一個默認的bean,它還會給部分【符合約定】的基礎參數設置默認的、至少有意義的、比較符合一般應用的初始值來初始化bean。這個就是SpringBoot提供的各種XxxProperties.java
。例如:WebMvcProperties
O對了,說到了這個XxxProperties.java
文件,其實它也是約定的一種體現。以前SSM
我們相當于直接配置了服務接口的一些參數;而在SpringBoot里面,我們直接配置的其實是XxxProperties
,然后再由SpringBoot使用XxxProperties
配置屬性去初始化服務接口參數。而通常這些XxxProperties
對應的是application.properties
里面的一些配置內容。
相比起直接配置服務接口參數,XxxProperties
可讀性更強,使用也更簡單。
0.4 從Redis看【約定大于配置】
省略
OK,關于SpringBoot中的【約定大于配置】就介紹到這里了。那他們是怎么實現的呢?有經驗的朋友,或者稍微細心的朋友估計也從上圖看到了,主要是靠下圖這些類、接口、注解:org.springframework.boot:spring-boot-autoconfigure
包下
一、@EnableAutoConfiguration源碼解析
二、SpringBoot常用條件注解源碼解析
SpringBoot中的常用條件注解有:
ConditionalOnBean
:是否存在某個某類或某個名字的BeanConditionalOnMissingBean
:是否缺失某個某類或某個名字的BeanConditionalOnSingleCandidate
:是否符合指定類型的Bean只有一個ConditionalOnClass
:是否存在某個類ConditionalOnMissingClass
:是否缺失某個類ConditionalOnExpression
:指定的表達式返回的是true還是falseConditionalOnJava
:判斷Java版本ConditionalOnWebApplication
:當前應用是不是一個Web應用ConditionalOnNotWebApplication
:當前應用不是一個Web應用ConditionalOnProperty
:Environment中是否存在某個屬性- 當然我們也可以利用
@Conditional
來自定義條件注解
條件注解是可以寫在類上和方法上的,如果某個條件注解寫在了自動配置類上,那該自動配置類會不會生效就要看當前條件能不能符合;或者條件注解寫在某個@Bean修飾的方法上,那這個Bean生不生效就看當前條件符不符合。
具體原理是:
- Spring在解析某個自動配置類時,會先檢查該自動配置類上是否有條件注解,如果有,則進一步判斷該條件注解所指定的條件當前能不能滿足,如果滿足了則繼續解析該配置類,如果不滿足則不進行解析了,也就是配置類所定義的Bean都得不到解析,也就是相當于沒有這些Bean了。
- 同理,Spring在解析某個@Bean的方法時,也會先判斷方法上是否有條件注解,然后進行解析,如果不滿足條件,則該Bean不會生效
2.1 自定義條件注解
SpringBoot中眾多的條件注解,都是基于Spring中的@Conditional來實現的,我們先來用一下@Conditional注解。
先來看下@Conditional注解的定義:
/*** * 指示組件只有在所有指定條件匹配時才有資格注冊。* 條件是可以在注冊bean定義之前以編程方式確定的任何狀態(有關詳細信息,請參閱條件)。* @Conditional注釋可以以以下任何一種方式使用:* 在任何直接或間接用@Component注釋的類(包括@Configuration類)上作為類型級注釋* 作為元注釋,用于組合自定義構造型注釋* 作為任何@Bean方法上的方法級注釋* 如果@Configuration類被標記為@Conditional,那么與該類關聯的所有@Bean方法、@Import注釋和@ComponentScan注釋* 將受這些條件的約束。*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {/*** All {@link Condition} classes that must {@linkplain Condition#matches match}* in order for the component to be registered.*/Class<? extends Condition>[] value();}
根據定義我們在用@Conditional注解時,需要指定一個或多個Condition的實現類,所以我們先來提供一個實現類:
public class ShenCondition implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {Map<String, Object> component = metadata.getAnnotationAttributes("org.springframework.stereotype.Component");System.out.println("下面開始打印condition里面的內容");System.out.println(component);Object value = component.get("value");return value.toString().equals("shen");}
}
上面這個實現類很簡單,就是獲取被@Conditional(ShenCondition.class)
注解的類上,獲取它@Component
上的注解值,如果值是shen
那就返回true,否則bean注冊不成功。測試用例如下:
@Conditional(ShenCondition.class)
@Component("shen")
public class ShenWorker {public String sayHello() {return "hello";}
}@SpringBootApplication(scanBasePackages = "org.example.springboottest")
public class ShenMyApplication {public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(ShenMyApplication.class);ShenWorker shen = (ShenWorker) context.getBean(ShenWorker.class);System.out.println("------------------------");System.out.println(shen.sayHello());}
}
此時運行,控制臺輸出:
把上面的ShenWorker
的@Component
值改成其他,如@Component("shenWorker")
,運行則輸出如下:
報錯了,NoSuchBeanDefinitionException
。
我想到了這里大家應該多少知道@Conditional
注解的作用了,接下來我們挑其中一兩個SpringBoot聲明的條件注解,看一下源碼,剖析一下它是怎么實現的。
2.2 @ConditionalOnClass原理解析
@ConditionalOnClass
注解的作用是:校驗是否存在某個類。
Spring這種優秀源碼,通常注釋都是很完善的,我們先來看看它們是怎么定義的:
/*** @Conditional,當且僅當指定的類在類路徑上時,才匹配成功** value()可以在@Configuration類上安全地指定,因為在加載類之前,* 會使用ASM解析注釋元數據。當放置在@Bean方法上時,需要特別注意,* 考慮在單獨的Configuration類中隔離條件,特別是如果方法的返回類型與條件的目標匹配*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {/*** 必須設置的class信息。* 因為這個注釋是通過加載類字節碼來解析的(classLoader.loadClass),* 當且僅當這個注解直接作用在bean上,而不是當這個注解被用作組合的元注釋時,* 在這里指定最終可能不在classpath上的類是安全的(不會報錯)。* 想要將此注解用作元注釋,那么就只使用name屬性*/Class<?>[] value() default {};/*** 必須設置的類全限定名*/String[] name() default {};
}
根據我們自定義注解經驗,該注解的實現肯定是在@Conditional(OnClassCondition.class)
的OnClassCondition.class
中。
順便在這里介紹一下怎么用idea追蹤代碼吧。
1)尋找關鍵實現類
首先我們知道,OnClassCondition.class
實現自Condition
接口,主要是實現里面的matches()
方法。雖然咱還不知道它是在哪里被Spring調用的,但起碼他肯定是會被調用的對吧,所以,我們先跳到OnClassCondition.class
文件,打開它的繼承圖,如下所示:
可以發現它繼承或者實現了各種各樣的接口,其中一個是Condition
。那OK
我們在回到OnClassCondition.class
文件,alft+7
打開代碼結構圖,然后在右上角點擊查看繼承
過來的屬性、方法,如下所示:
如上圖所示,綠色框內灰色的即為繼承過來,但是沒有重寫的方法。可以看到Condition
接口的matches
方法,就沒有被重寫過,那說明父類中肯定有默認的實現類,點擊一下,會跳到SpringBootCondition
類中,它的默認實現方式如下:
可以看到,最重要的是紅框內的2行代碼。點擊getMatchOutcome
發現這是一個抽象方法,所以,又回到OnClassCondition.class
文件中,查找一下看看是否實現或者重寫了該方法。最后發現如下:
重寫了,再結合之前的代碼可以判斷出來,這里就是OnClassCondition
類的邏輯實現核心。上面的代碼其實也不難,基本上通過方法名就知道啥意思了。主要原理就是:使用反射工具,根據權限定名去JVM加載類。
特別注意:@ConditionalOnClass和@ConditionalOnMissingClass注解的核心實現都是這個類的這個方法
2.3 ConditionalOnBean原理解析
點擊查看@ConditionalOnBean
注解,會發現邏輯是在OnBeanCondition.class
中,然后按照上面的思路也會發現,它跟前面的注解基本一致,實現邏輯也是在getMatchOutcome
中,如下:
這個源碼略顯復雜,但是可以通過閱讀注解接口的各種字段信息,推斷出來具體過程。總的來說就是根據bean是否存在來返回結果。
特別注意:@ConditionalOnBean和@ConditionalOnSingleCandidate,@ConditionalOnMissingBean注解的核心實現都是這個類的這個方法
三、SpringBoot之Mybatis自動配置源碼解析
在我們的項目pom中,導入如下:
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency>
我們就會看到我們項目依賴中出現以groupId
+artifactId
命名的jar包了,如下:
對的,通常第三方在寫自動配置的時候,命名風格皆是如此。而在里面的MybatisAutoConfiguration
即為Mybatis的自動配置類。通過源碼我們可以發現,在里面注冊了兩個bean,當然是有條件的:
上面的意思是,在Spring中如果沒有SqlSessionFactory
這個類型的bean,則注冊當前bean;
另一個注冊的bean如下:
最核心的一個還是,在自動配置類下面有一個這玩意:
這個 Bean的作用是,如果沒有其他顯式的注冊Mapper掃描映射器
,那就注冊一個默認的Mapper掃描映射器
,掃描路徑跟SpringBoot掃描路徑一致。說到這里,是不是有朋友對Mybatis
如何實現把接口轉換成bean這個操作感興趣呢?大家伙可以看看按照下面這個軌跡從1-9-AB,自上而下去看看,但是需要一些Spring功底。源碼入口:MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar#registerBeanDefinitions
(我有一點功底,但不算很扎實)
總的來說,它的原大致如下:(注意,這里是指默認自動配置類情況下,如果使用了@MapperScan
注解,情況略有不同)
- Mybatis自動配置類會向Spring容器注冊一個掃描器Bean定義,根據Bean定義,初始化掃描器屬性。默認掃描路徑是當前SpringBoot的掃描路徑
- Mybatis會修改掃描到Mapper接口的自動注入屬性
- Mybatis會修改注冊成為Bean的條件,原本接口類是不能夠被注冊成為Bean的,但是通過修改條件讓其通過了判斷
- 具體怎么生成的Bean我還沒去看…,睡覺了
四、SpringBoot之AOP自動配置源碼解析
未完待續…
五、SpringBoot Jar包啟動過程源碼解析
未完待續…
學習總結
感謝
感謝阿里云社區的文章《探索Spring Boot中的原則:約定大于配置》
感謝百度大佬【作者:編程師兄】的文章《SpringBoot約定大于配置到底是什么意思?》