spring中那些讓你愛不釋手的代碼技巧(續(xù)集)
大家好,我是蘇三,又和大家見面了。
最近聽說井上雄彥已經(jīng)官宣:《灌籃高手》要出電影版了,這條消息讓我激動不已。

籃球,青春,熱血,愛情,友情,遺憾。。。爺青回。

前言
上一篇文章《spring中這些能升華代碼的技巧,可能會讓你愛不釋手》發(fā)表之后,受到了不少讀者的好評,很多讀者都在期待續(xù)集。今天非常高興的通知大家,你們要的續(xù)集來了。本文繼續(xù)總結(jié)我認為spring中還不錯的知識點,希望對您有所幫助。
一. @Conditional的強大之處
不知道你們有沒有遇到過這些問題:
某個功能需要根據(jù)項目中有沒有某個jar判斷是否開啟該功能。
某個bean的實例化需要先判斷另一個bean有沒有實例化,再判斷是否實例化自己。
某個功能是否開啟,在配置文件中有個參數(shù)可以對它進行控制。
如果你有遇到過上述這些問題,那么恭喜你,本節(jié)內(nèi)容非常適合你。
@ConditionalOnClass
問題1可以用@ConditionalOnClass
注解解決,代碼如下:
如果項目中存在B類,則會實例化A類。如果不存在B類,則不會實例化A類。
有人可能會問:不是判斷有沒有某個jar嗎?怎么現(xiàn)在判斷某個類了?
?直接判斷有沒有該jar下的某個關(guān)鍵類更簡單。
?
這個注解有個升級版的應(yīng)用場景:比如common工程中寫了一個發(fā)消息的工具類mqTemplate,業(yè)務(wù)工程引用了common工程,只需再引入消息中間件,比如rocketmq的jar包,就能開啟mqTemplate的功能。而如果有另一個業(yè)務(wù)工程,通用引用了common工程,如果不需要發(fā)消息的功能,不引入rocketmq的jar包即可。
這個注解的功能還是挺實用的吧?
@ConditionalOnBean
問題2可以通過@ConditionalOnBean
注解解決,代碼如下:
實例A只有在實例B存在時,才能實例化。
@ConditionalOnProperty
問題3可以通過@ConditionalOnProperty
注解解決,代碼如下:
在applicationContext.properties文件中配置參數(shù):
各參數(shù)含義:
prefix 表示參數(shù)名的前綴,這里是demo
name 表示參數(shù)名
havingValue 表示指定的值,參數(shù)中配置的值需要跟指定的值比較是否相等,相等才滿足條件
matchIfMissing 表示是否允許缺省配置。
這個功能可以作為開關(guān),相比EnableXXX注解的開關(guān)更優(yōu)雅,因為它可以通過參數(shù)配置是否開啟,而EnableXXX注解的開關(guān)需要在代碼中硬編碼開啟或關(guān)閉。
其他的Conditional注解
當然,spring用得比較多的Conditional注解還有:ConditionalOnMissingClass
、ConditionalOnMissingBean
、ConditionalOnWebApplication
等。
下面用一張圖整體認識一下@Conditional
家族。

自定義Conditional
說實話,個人認為springboot自帶的Conditional系列已經(jīng)可以滿足我們絕大多數(shù)的需求了。但如果你有比較特殊的場景,也可以自定義自定義Conditional。
第一步,自定義注解:
第二步,實現(xiàn)Condition接口:
第三步,使用@MyConditionOnProperty注解。
Conditional的奧秘就藏在ConfigurationClassParser
類的processConfigurationClass
方法中:

這個方法邏輯不復(fù)雜:

先判斷有沒有使用Conditional注解,如果沒有直接返回false
收集condition到集合中
按
order
排序該集合遍歷該集合,循環(huán)調(diào)用
condition
的matchs
方法。
二. 如何妙用@Import?
有時我們需要在某個配置類中引入另外一些類,被引入的類也加到spring容器中。這時可以使用@Import
注解完成這個功能。
如果你看過它的源碼會發(fā)現(xiàn),引入的類支持三種不同類型。
但是我認為最好將普通類和@Configuration注解的配置類分開講解,所以列了四種不同類型:

普通類
這種引入方式是最簡單的,被引入的類會被實例化bean對象。
通過@Import
注解引入A類,spring就能自動實例化A對象,然后在需要使用的地方通過@Autowired
注解注入即可:
是不是挺讓人意外的?不用加@Bean
注解也能實例化bean。
@Configuration注解的配置類
這種引入方式是最復(fù)雜的,因為@Configuration
注解還支持多種組合注解,比如:
@Import
@ImportResource
@PropertySource
等。
通過@Import
注解引入@Configuration注解的配置類,會把該配置類相關(guān)@Import
、@ImportResource
、@PropertySource
等注解引入的類進行遞歸,一次性全部引入。
由于文章篇幅有限不過多介紹了,這里留點懸念,后面會出一篇文章專門介紹@Configuration
注解,因為它實在太太太重要了。
實現(xiàn)ImportSelector接口的類
這種引入方式需要實現(xiàn)ImportSelector
接口:
這種方式的好處是selectImports
方法返回的是數(shù)組,意味著可以同時引入多個類,還是非常方便的。
實現(xiàn)ImportBeanDefinitionRegistrar接口的類
這種引入方式需要實現(xiàn)ImportBeanDefinitionRegistrar
接口:
這種方式是最靈活的,能在registerBeanDefinitions
方法中獲取到BeanDefinitionRegistry
容器注冊對象,可以手動控制BeanDefinition
的創(chuàng)建和注冊。
當然@import
注解非常人性化,還支持同時引入多種不同類型的類。
這四種引入類的方式各有千秋,總結(jié)如下:
普通類,用于創(chuàng)建沒有特殊要求的bean實例。
@Configuration注解的配置類,用于層層嵌套引入的場景。
實現(xiàn)ImportSelector接口的類,用于一次性引入多個類的場景,或者可以根據(jù)不同的配置決定引入不同類的場景。
實現(xiàn)ImportBeanDefinitionRegistrar接口的類,主要用于可以手動控制BeanDefinition的創(chuàng)建和注冊的場景,它的方法中可以獲取BeanDefinitionRegistry注冊容器對象。
在ConfigurationClassParser
類的processImports
方法中可以看到這三種方式的處理邏輯:

最后的else方法其實包含了:普通類和@Configuration注解的配置類兩種不同的處理邏輯。
三. @ConfigurationProperties賦值
我們在項目中使用配置參數(shù)是非常常見的場景,比如,我們在配置線程池的時候,需要在applicationContext.propeties
文件中定義如下配置:
方法一:通過@Value
注解讀取這些配置。
這種方式使用起來非常簡單,但建議在使用時都加上:
,因為:
后面跟的是默認值,比如:@Value("${thread.pool.corePoolSize:5}"),定義的默認核心線程數(shù)是5。
?假如有這樣的場景:business工程下定義了這個ThreadPoolConfig類,api工程引用了business工程,同時job工程也引用了business工程,而ThreadPoolConfig類只想在api工程中使用。這時,如果不配置默認值,job工程啟動的時候可能會報錯。
?
如果參數(shù)少還好,多的話,需要給每一個參數(shù)都加上@Value
注解,是不是有點麻煩?
此外,還有一個問題,@Value
注解定義的參數(shù)看起來有點分散,不容易辨別哪些參數(shù)是一組的。
這時,@ConfigurationProperties
就派上用場了,它是springboot中新加的注解。
第一步,先定義ThreadPoolProperties類
第二步,使用ThreadPoolProperties類
使用@ConfigurationProperties
注解,可以將thread.pool
開頭的參數(shù)直接賦值到ThreadPoolProperties類的同名參數(shù)中,這樣省去了像@Value
注解那樣一個個手動去對應(yīng)的過程。
這種方式顯然要方便很多,我們只需編寫xxxProperties類,spring會自動裝配參數(shù)。此外,不同系列的參數(shù)可以定義不同的xxxProperties類,也便于管理,推薦優(yōu)先使用這種方式。
它的底層是通過:ConfigurationPropertiesBindingPostProcessor
類實現(xiàn)的,該類實現(xiàn)了BeanPostProcessor
接口,在postProcessBeforeInitialization
方法中解析@ConfigurationProperties
注解,并且綁定數(shù)據(jù)到相應(yīng)的對象上。
綁定是通過Binder
類的bindObject
方法完成的:

以上這段代碼會遞歸綁定數(shù)據(jù),主要考慮了三種情況:
bindAggregate
?綁定集合類bindBean
?綁定對象bindProperty
?綁定參數(shù) 前面兩種情況最終也會調(diào)用到bindProperty方法。
「此外,友情提醒一下:」
使用@ConfigurationProperties
注解有些場景有問題,比如:在apollo中修改了某個參數(shù),正常情況可以動態(tài)更新到@ConfigurationProperties
注解定義的xxxProperties類的對象中,但是如果出現(xiàn)比較復(fù)雜的對象,比如:
可能動態(tài)更新不了。
這時候該怎么辦呢?
答案是使用ApolloConfigChangeListener
監(jiān)聽器自己處理:
四. spring事務(wù)要如何避坑?
spring中的事務(wù)功能主要分為:聲明式事務(wù)
和編程式事務(wù)
。
聲明式事務(wù)
大多數(shù)情況下,我們在開發(fā)過程中使用更多的可能是聲明式事務(wù)
,即使用@Transactional
注解定義的事務(wù),因為它用起來更簡單,方便。
只需在需要執(zhí)行的事務(wù)方法上,加上@Transactional
注解就能自動開啟事務(wù):
這種聲明式事務(wù)之所以能生效,是因為它的底層使用了AOP,創(chuàng)建了代理對象,調(diào)用TransactionInterceptor
攔截器實現(xiàn)事務(wù)的功能。
?spring事務(wù)有個特別的地方:它獲取的數(shù)據(jù)庫連接放在
?ThreadLocal
中的,也就是說同一個線程中從始至終都能獲取同一個數(shù)據(jù)庫連接,可以保證同一個線程中多次數(shù)據(jù)庫操作在同一個事務(wù)中執(zhí)行。
正常情況下是沒有問題的,但是如果使用不當,事務(wù)會失效,主要原因如下:

除了上述列舉的問題之外,由于@Transactional
注解最小粒度是要被定義在方法上,如果有多層的事務(wù)方法調(diào)用,可能會造成大事務(wù)問題。

所以,建議在實際工作中少用@Transactional
注解開啟事務(wù)。
編程式事務(wù)
一般情況下編程式事務(wù)我們可以通過TransactionTemplate
類開啟事務(wù)功能。有個好消息,就是springboot
已經(jīng)默認實例化好這個對象了,我們能直接在項目中使用。
使用TransactionTemplate
的編程式事務(wù)能避免很多事務(wù)失效的問題,但是對大事務(wù)問題,不一定能夠解決,只是說相對于使用@Transactional
注解要好些。
五. 跨域問題的解決方案
關(guān)于跨域問題,前后端的解決方案還是挺多的,這里我重點說說spring的解決方案,目前有三種:

一.使用@CrossOrigin注解
該方案需要在跨域訪問的接口上加@CrossOrigin
注解,訪問規(guī)則可以通過注解中的參數(shù)控制,控制粒度更細。如果需要跨域訪問的接口數(shù)量較少,可以使用該方案。
二.增加全局配置
該方案需要實現(xiàn)WebMvcConfigurer
接口,重寫addCorsMappings
方法,在該方法中定義跨域訪問的規(guī)則。這是一個全局的配置,可以應(yīng)用于所有接口。
三.自定義過濾器
該方案通過在請求的header
中增加Access-Control-Allow-Origin
等參數(shù)解決跨域問題。
順便說一下,使用@CrossOrigin
注解 和 實現(xiàn)WebMvcConfigurer
接口的方案,spring在底層最終都會調(diào)用到DefaultCorsProcessor
類的handleInternal
方法:

最終三種方案殊途同歸,都會往header
中添加跨域需要參數(shù),只是實現(xiàn)形式不一樣而已。
六. 如何自定義starter
以前在沒有使用starter
時,我們在項目中需要引入新功能,步驟一般是這樣的:
在maven倉庫找該功能所需jar包
在maven倉庫找該jar所依賴的其他jar包
配置新功能所需參數(shù)
以上這種方式會帶來三個問題:
如果依賴包較多,找起來很麻煩,容易找錯,而且要花很多時間。
各依賴包之間可能會存在版本兼容性問題,項目引入這些jar包后,可能沒法正常啟動。
如果有些參數(shù)沒有配好,啟動服務(wù)也會報錯,沒有默認配置。
「為了解決這些問題,springboot的starter
機制應(yīng)運而生」。
starter機制帶來這些好處:
它能啟動相應(yīng)的默認配置。
它能夠管理所需依賴,擺脫了需要到處找依賴 和 兼容性問題的困擾。
自動發(fā)現(xiàn)機制,將spring.factories文件中配置的類,自動注入到spring容器中。
遵循“約定大于配置”的理念。
在業(yè)務(wù)工程中只需引入starter包,就能使用它的功能,太爽了。
下面用一張圖,總結(jié)starter的幾個要素:

接下來我們一起實戰(zhàn),定義一個自己的starter。
第一步,創(chuàng)建id-generate-starter工程:

其中的pom.xml配置如下:
第二步,創(chuàng)建id-generate-spring-boot-autoconfigure工程:

該項目當中包含:
pom.xml
spring.factories
IdGenerateAutoConfiguration
IdGenerateService
IdProperties pom.xml配置如下:
spring.factories配置如下:
IdGenerateAutoConfiguration類:
IdGenerateService類:
IdProperties類:
這樣在業(yè)務(wù)項目中引入相關(guān)依賴:
就能使用注入使用IdGenerateService的功能了
完美。
七.項目啟動時的附加功能
有時候我們需要在項目啟動時定制化一些附加功能,比如:加載一些系統(tǒng)參數(shù)、完成初始化、預(yù)熱本地緩存等,該怎么辦呢?
好消息是springboot
提供了:
CommandLineRunner
ApplicationRunner
這兩個接口幫助我們實現(xiàn)以上需求。
它們的用法還是挺簡單的,以ApplicationRunner
接口為例:
實現(xiàn)ApplicationRunner
接口,重寫run
方法,在該方法中實現(xiàn)自己定制化需求。
如果項目中有多個類實現(xiàn)了ApplicationRunner
接口,他們的執(zhí)行順序要怎么指定呢?
答案是使用@Order(n)
注解,n的值越小越先執(zhí)行。當然也可以通過@Priority
注解指定順序。
springboot項目啟動時主要流程是這樣的:

在SpringApplication
類的callRunners
方法中,我們能看到這兩個接口的具體調(diào)用:

最后還有一個問題:這兩個接口有什么區(qū)別?
CommandLineRunner接口中run方法的參數(shù)為String數(shù)組
ApplicationRunner中run方法的參數(shù)為ApplicationArguments,該參數(shù)包含了String數(shù)組參數(shù) 和 一些可選參數(shù)。
嘮嘮家常
寫著寫著又有這么多字了,按照慣例,為了避免篇幅過長,今天就先寫到這里。預(yù)告一下,后面會有AOP、BeanPostProcessor、Configuration注解等核心知識點的專題,每個主題的內(nèi)容都挺多的,可以期待一下喔。