當大潮退去,才知道誰在裸泳。關注公眾號【BAT的烏托邦】開啟專欄式學習,拒絕淺嘗輒止。本文 https://www.yourbatman.cn 已收錄,裡面一併有Spring技術棧、MyBatis、中介軟體等小而美的專欄供以學習哦。
前言
各位小夥伴大家好,我是A哥。這是一篇“插隊”進來的文章,源於我公眾號下面的這句評論:
官方管這兩種模式分別叫:Full @Configuration
和lite @Bean mode
,口語上我習慣把它稱為Spring配置的Full模式和Lite模式更易溝通。
的確,我很簡單的“調研”了一下,知曉Spring配置中Lite模式
和Full模式
的幾乎沒有(或者說真的很少吧)。按照我之前的理論,大多人都不知道的技術(知識點)那肯定是不流行的。但是:不流行不代表不重要,不流行不代表不值錢,畢竟高薪往往只有少數人才能擁有。
什麼OPP、OOP、AOP程式設計,其實我最喜歡的和推崇的是面向工資程式設計。當然前提是夠硬(收回你邪惡的笑容),沒有金剛鑽,不攬瓷器活。
聽我這麼一忽悠,是不是對這塊內容還饒有興味了,這不它來了嘛。
版本約定
本文內容若沒做特殊說明,均基於以下版本:
- JDK:
1.8
- Spring Framework:
5.2.2.RELEASE
正文
最初的Spring只支援xml方式配置Bean,從Spring 3.0
起支援了一種更優的方式:基於Java類的配置方式,這一下子讓我們Javaer可以從標籤語法裡解放了出來。畢竟作為Java程式設計師,我們擅長的是寫Java類,而非用標籤語言去寫xml檔案。
我對Spring配置的Full/Lite模式的關注和記憶深刻,源自於一個小小故事:某一年我在看公司的專案時發現,資料來源配置類裡有如下一段配置程式碼:
@Configuration
public class DataSourceConfig {
...
@Bean
public DataSource dataSource() {
...
return dataSource;
}
@Bean(name = "transactionManager")
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
...
}
作為當時還是Java萌新的我,非常的費解。自然的對此段程式碼產生了較大的好奇(其實是質疑):在準備DataSourceTransactionManager
這個Bean時呼叫了dataSource()
方法,根據我“非常紮實”的JavaSE基礎知識,它肯定會重新走一遍dataSource()
方法,從而產生一個新的資料來源例項,那麼你的事務管理器管理的不就是一個“全新資料來源”麼?談何事務呢?
為了驗證我的猜想,我把斷點打到dataSource()
方法內部開始除錯,但讓我“失望”的是:此方法並沒有執行兩次。這在當時是震驚了我的,甚至一度懷疑自己引以為豪的Java基礎了。所以我四處詢問,希望得到一個“解釋”,但奈何,問了好幾圈,那會沒有一人能給我一個合理的說法,只知道那麼用是沒有問題的。
很明顯,現在再回頭來看當時的這個質疑是顯得有些“無知”的,這個“難題”困擾了我很久,直到我前2年開始深度研究Spring原始碼才讓此難題迎刃而解,當時那種豁然開朗的感覺真好呀。
基本概念
關於配置類的核心概念,在這裡先予以解釋。
@Configuration和@Bean
Spring新的配置體系中最為重要的構件是:@Configuration
標註的類,@Bean
標註的方法。
// @since 3.0
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(annotation = Component.class)
String value() default "";
// @since 5.2
boolean proxyBeanMethods() default true;
}
用@Configuration
註解標註的類表明其主要目的是作為bean定義的源。此外,@Configuration
類允許通過呼叫同一類中的其他@Bean
method方法來定義bean之間的依賴關係(下有詳解)。
// @since 3.0
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
@AliasFor("name")
String[] value() default {};
@AliasFor("value")
String[] name() default {};
@Deprecated
Autowire autowire() default Autowire.NO;
// @since 5.1
boolean autowireCandidate() default true;
String initMethod() default "";
String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
}
@Bean
註解標註在方法上,用於指示方法例項化、配置和初始化要由Spring IoC容器管理的新物件。對於熟悉Spring的<beans/>
XML配置的人來說,@Bean
註解的作用與<bean/>
元素相同。您可以對任何Spring的@Component元件使用@Bean
註釋的方法代替(注意:這是理論上,實際上比如使用@Controller標註的元件就不能直接使用它代替)。
需要注意的是,通常來說,我們均會把@Bean
標註的方法寫在@Configuration
標註的類裡面來配合使用。
簡單粗暴理解:
@Configuration
標註的類等同於一個xml檔案,@Bean
標註的方法等同於xml檔案裡的一個<bean/>
標籤
使用舉例
@Configuration
public class AppConfig {
@Bean
public User user(){
User user = new User();
user.setName("A哥");
user.setAge(18);
return user;
}
}
public class Application {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
User user = context.getBean(User.class);
System.out.println(user.getClass());
System.out.println(user);
}
}
輸出:
class com.yourbatman.fullliteconfig.User
User{name='A哥', age=18}
Full模式和Lite模式
Full模式和Lite模式均是針對於Spring配置類而言的,和xml配置檔案無關。值得注意的是:判斷是Full模式 or Lite模式的前提是,首先你得是個容器元件。至於一個例項是如何“晉升”成為容器元件的,可以用註解也可以沒有註解,本文就不展開討論了,這屬於Spring的基礎知識。
Lite模式
當@Bean
方法在沒有使用@Configuration
註釋的類中宣告時,它們被稱為在Lite模式下處理。它包括:在@Component
中宣告的@Bean
方法,甚至只是在一個非常普通的類中宣告的Bean方法,都被認為是Lite版的配置類。@Bean
方法是一種通用的工廠方法(factory-method
)機制。
和Full模式的@Configuration
不同,Lite模式的@Bean
方法不能宣告Bean之間的依賴關係。因此,這樣的@Bean
方法不應該呼叫其他@Bean方法。每個這樣的方法實際上只是一個特定Bean引用的工廠方法(factory-method),沒有任何特殊的執行時語義。
何時為Lite模式
官方定義為:在沒有標註@Configuration
的類裡面有@Bean
方法就稱為Lite模式的配置。透過原始碼再看這個定義是不完全正確的,而應該是有如下case均認為是Lite模式的配置類:
- 類上標註有
@Component
註解 - 類上標註有
@ComponentScan
註解 - 類上標註有
@Import
註解 - 類上標註有
@ImportResource
註解 - 若類上沒有任何註解,但類記憶體在@Bean方法
以上case的前提均是類上沒有被標註@Configuration
,在Spring 5.2之後新增了一種case也算作Lite模式:
- 標註有
@Configuration(proxyBeanMethods = false)
,注意:此值預設是true哦,需要顯示改為false才算是Lite模式
細心的你會發現,自Spring5.2(對應Spring Boot 2.2.0)開始,內建的幾乎所有的@Configuration
配置類都被修改為了@Configuration(proxyBeanMethods = false)
,目的何為?答:以此來降低啟動時間,為Cloud Native繼續做準備。
優缺點
優點:
- 執行時不再需要給對應類生成CGLIB子類,提高了執行效能,降低了啟動時間
- 可以該配置類當作一個普通類使用嘍:也就是說@Bean方法 可以是private、可以是final
缺點:
- 不能宣告@Bean之間的依賴,也就是說不能通過方法呼叫來依賴其它Bean
- (其實這個缺點還好,很容易用其它方式“彌補”,比如:把依賴Bean放進方法入參裡即可)
程式碼示例
主配置類:
@ComponentScan("com.yourbatman.fullliteconfig.liteconfig")
@Configuration
public class AppConfig {
}
準備一個Lite模式的配置:
@Component
// @Configuration(proxyBeanMethods = false) // 這樣也是Lite模式
public class LiteConfig {
@Bean
public User user() {
User user = new User();
user.setName("A哥-lite");
user.setAge(18);
return user;
}
@Bean
private final User user2() {
User user = new User();
user.setName("A哥-lite2");
user.setAge(18);
// 模擬依賴於user例項 看看是否是同一例項
System.out.println(System.identityHashCode(user()));
System.out.println(System.identityHashCode(user()));
return user;
}
public static class InnerConfig {
@Bean
// private final User userInner() { // 只在lite模式下才好使
public User userInner() {
User user = new User();
user.setName("A哥-lite-inner");
user.setAge(18);
return user;
}
}
}
測試用例:
public class Application {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 配置類情況
System.out.println(context.getBean(LiteConfig.class).getClass());
System.out.println(context.getBean(LiteConfig.InnerConfig.class).getClass());
String[] beanNames = context.getBeanNamesForType(User.class);
for (String beanName : beanNames) {
User user = context.getBean(beanName, User.class);
System.out.println("beanName:" + beanName);
System.out.println(user.getClass());
System.out.println(user);
System.out.println("------------------------");
}
}
}
結果輸出:
1100767002
313540687
class com.yourbatman.fullliteconfig.liteconfig.LiteConfig
class com.yourbatman.fullliteconfig.liteconfig.LiteConfig$InnerConfig
beanName:userInner
class com.yourbatman.fullliteconfig.User
User{name='A哥-lite-inner', age=18}
------------------------
beanName:user
class com.yourbatman.fullliteconfig.User
User{name='A哥-lite', age=18}
------------------------
beanName:user2
class com.yourbatman.fullliteconfig.User
User{name='A哥-lite2', age=18}
------------------------
小總結
- 該模式下,配置類本身不會被CGLIB增強,放進IoC容器內的就是本尊
- 該模式下,對於內部類是沒有限制的:可以是Full模式或者Lite模式
- 該模式下,配置類內部不能通過方法呼叫來處理依賴,否則每次生成的都是一個新例項而並非IoC容器內的單例
- 該模式下,配置類就是一普通類嘛,所以@Bean方法可以使用
private/final
等進行修飾(static自然也是闊儀的)
Full模式
在常見的場景中,@Bean
方法都會在標註有@Configuration
的類中宣告,以確保總是使用“Full模式”,這麼一來,交叉方法引用會被重定向到容器的生命週期管理,所以就可以更方便的管理Bean依賴。
何時為Full模式
標註有@Configuration
註解的類被稱為full模式的配置類。自Spring5.2後這句話改為下面這樣我覺得更為精確些:
- 標註有
@Configuration
或者@Configuration(proxyBeanMethods = true)
的類被稱為Full模式的配置類 - (當然嘍,proxyBeanMethods屬性的預設值是true,所以一般需要Full模式我們只需要標個註解即可)
優缺點
優點:
- 可以支援通過常規Java呼叫相同類的@Bean方法而保證是容器內的Bean,這有效規避了在“Lite模式”下操作時難以跟蹤的細微錯誤。特別對於萌新程式設計師,這個特點很有意義
缺點:
- 執行時會給該類生成一個CGLIB子類放進容器,有一定的效能、時間開銷(這個開銷在Spring Boot這種擁有大量配置類的情況下是不容忽視的,這也是為何Spring 5.2新增了
proxyBeanMethods
屬性的最直接原因) - 正因為被代理了,所以@Bean方法 不可以是private、不可以是final
程式碼示例
主配置:
@ComponentScan("com.yourbatman.fullliteconfig.fullconfig")
@Configuration
public class AppConfig {
}
準備一個Full模式的配置:
@Configuration
public class FullConfig {
@Bean
public User user() {
User user = new User();
user.setName("A哥-lite");
user.setAge(18);
return user;
}
@Bean
protected User user2() {
User user = new User();
user.setName("A哥-lite2");
user.setAge(18);
// 模擬依賴於user例項 看看是否是同一例項
System.out.println(System.identityHashCode(user()));
System.out.println(System.identityHashCode(user()));
return user;
}
public static class InnerConfig {
@Bean
// private final User userInner() { // 只在lite模式下才好使
public User userInner() {
User user = new User();
user.setName("A哥-lite-inner");
user.setAge(18);
return user;
}
}
}
測試用例:
public class Application {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 配置類情況
System.out.println(context.getBean(FullConfig.class).getClass());
System.out.println(context.getBean(FullConfig.InnerConfig.class).getClass());
String[] beanNames = context.getBeanNamesForType(User.class);
for (String beanName : beanNames) {
User user = context.getBean(beanName, User.class);
System.out.println("beanName:" + beanName);
System.out.println(user.getClass());
System.out.println(user);
System.out.println("------------------------");
}
}
}
結果輸出:
550668305
550668305
class com.yourbatman.fullliteconfig.fullconfig.FullConfig$$EnhancerBySpringCGLIB$$70a94a63
class com.yourbatman.fullliteconfig.fullconfig.FullConfig$InnerConfig
beanName:userInner
class com.yourbatman.fullliteconfig.User
User{name='A哥-lite-inner', age=18}
------------------------
beanName:user
class com.yourbatman.fullliteconfig.User
User{name='A哥-lite', age=18}
------------------------
beanName:user2
class com.yourbatman.fullliteconfig.User
User{name='A哥-lite2', age=18}
------------------------
小總結
- 該模式下,配置類會被CGLIB增強(生成代理物件),放進IoC容器內的是代理
- 該模式下,對於內部類是沒有限制的:可以是Full模式或者Lite模式
- 該模式下,配置類內部可以通過方法呼叫來處理依賴,並且能夠保證是同一個例項,都指向IoC內的那個單例
- 該模式下,@Bean方法不能被
private/final
等進行修飾(很簡單,因為方法需要被複寫嘛,所以不能私有和final。defualt/protected/public都可以哦),否則啟動報錯(其實IDEA編譯器在編譯器就提示可以提示你了):
Exception in thread "main" org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: @Bean method 'user2' must not be private or final; change the method's modifiers to continue
Offending resource: class path resource [com/yourbatman/fullliteconfig/fullconfig/FullConfig.class]
at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:72)
at org.springframework.context.annotation.BeanMethod.validate(BeanMethod.java:50)
at org.springframework.context.annotation.ConfigurationClass.validate(ConfigurationClass.java:220)
at org.springframework.context.annotation.ConfigurationClassParser.validate(ConfigurationClassParser.java:211)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:326)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:242)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:275)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:95)
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:706)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:532)
at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:89)
at com.yourbatman.fullliteconfig.Application.main(Application.java:11)
使用建議
瞭解了Spring配置類的Full模式和Lite模式,那麼在工作中我該如何使用呢?這裡A哥給出使用建議,僅供參考:
- 如果是在公司的業務功能/服務上做開發,使用Full模式
- 如果你是個容器開發者,或者你在開發中介軟體、通用元件等,那麼使用Lite模式是一種更被推薦的方式,它對Cloud Native更為友好
思考題?
通過new AnnotationConfigApplicationContext(AppConfig.class)
直接放進去的類,它會成為一個IoC的元件嗎?若會,那麼它是Full模式 or Lite模式呢?是個固定的結果還是也和其標註的註解有關呢?
本思考題不難,自己試驗一把便知,建議多動手~
總結
本文結合程式碼示例闡述了Spring配置中Full模式和Lite模式,以及各自的定義和優缺點。對於一般的小夥伴,掌握本文就夠用了,並且足夠你面試中吹x。但A哥系列文章一般不止於“表面”嘛,下篇文章將從原理層面告訴你Spring是如何來巧妙的處理這兩種模式的,特別是會結合Spring 5.2.0
新特性,以及對比Spring 5.2.0
的實現和之前版本有何不同,你課訂閱我的公眾號保持關注。