@Configuration 注解的 Full 模式和 Lite 模式!
@Configuration 注解相信各位小伙伴經(jīng)常會(huì)用到,但是大家知道嗎,這個(gè)注解有兩種不同的模式,一種叫做 Full 模式,另外一種則叫做 Lite 模式。
準(zhǔn)確來(lái)說(shuō),F(xiàn)ull 模式和 Lite 模式其實(shí) Spring 容器在處理 Bean 時(shí)的兩種不同行為。
這兩種不同的模式在使用時(shí)候的表現(xiàn)完全不同,今天松哥就來(lái)和各位小伙伴捋一捋這兩種模式。
1. 概念梳理
首先我們先來(lái)看一下 Spring 官方文檔中對(duì) Full 模式和 Lite 模式的一個(gè)介紹:
圖片
截圖來(lái)自:https://docs.spring.io/spring-framework/reference/core/beans/java/basic-concepts.html
這個(gè)文檔主要講了這樣幾件事情:
- 我們可以通過(guò)在一個(gè)方法上添加 @Bean 注解,進(jìn)而將該方法的返回值暴露給 Spring 容器,在這種場(chǎng)景下,@Bean 注解實(shí)際上就是一種通用的工廠方法機(jī)制。
- 當(dāng)一個(gè)添加了 @Bean 注解的方法位于一個(gè)沒(méi)有添加 @Configuration 注解的類(lèi)里邊時(shí),那么這個(gè)添加了 @Bean 注解的方法在處理時(shí)就會(huì)按照 Lite 模式來(lái)處理。
- 當(dāng)一個(gè) Bean 被聲明在添加了 @Component 注解的類(lèi)中,那么會(huì)按照 Lite 模式來(lái)處理。
- 當(dāng)一個(gè) Bean 被聲明在一個(gè)普通的類(lèi)中時(shí)(plain old class),按照 Lite 模式來(lái)處理(這一點(diǎn)感覺(jué)和第二點(diǎn)差不多)。
- 在 Lite 模式下,@Bean 注解標(biāo)記的方法最終不會(huì)被 CGLIB 進(jìn)行代理,就是一個(gè)普通的工廠方法,因此,在 @Bean 標(biāo)記的方法中,不能調(diào)用其他 @Bean 注解標(biāo)記的方法,如果有需要,可以通過(guò)方法參數(shù)注入自己所需要的 Bean。
- 由于 Lite 模式下并不會(huì)使用 CGLIB,因此 @Bean 標(biāo)記的方法可以是 final 類(lèi)型的。
- 在大多數(shù)場(chǎng)景下,我們?cè)谝粋€(gè) @Configuration 注解標(biāo)記的類(lèi)中,使用 @Bean 注解向 Spring 容器注冊(cè)一個(gè) Bean,都是 Full 模式。
官網(wǎng)文檔的介紹還是有些抽象,接下來(lái)松哥通過(guò)具體的案例來(lái)和大家演示 Full 模式和 Lite 模式的差別。
2. Full 模式
先看 Full 模式,中文也可以稱(chēng)之為 完整 模式,我們平時(shí)使用時(shí),在一個(gè)配置類(lèi)上添加 @Configuration 注解,且不添加任何額外屬性,這就是 Full 模式了。
Full 模式最大的特點(diǎn)是會(huì)給配置類(lèi)通過(guò) CGLIB 生成一個(gè)代理,所有被 @Bean 注解標(biāo)記的方法將來(lái)都是通過(guò)代理方法進(jìn)行調(diào)用。
假設(shè)我有如下配置類(lèi):
@Configuration
public class JavaConfig {
@Bean
User user() {
return new User();
}
}
現(xiàn)在,我們?nèi)?Spring 容器獲取這個(gè)配置類(lèi):
public class JavaDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
JavaConfig config = ctx.getBean(JavaConfig.class);
System.out.println("config.getClass() = " + config.getClass());
}
}
打印結(jié)果如下:
圖片
大家看到,最終從 Spring 容器中拿到的 JavaConfig 實(shí)例并不是原始的 JavaConfig 對(duì)象,而是一個(gè)被代理的 JavaConfig 對(duì)象。
為什么要代理呢?肯定是為了實(shí)現(xiàn)某些功能。
大家看下面這個(gè)案例:
@Configuration
public class JavaConfig {
@Bean
User user() {
User user = new User();
user.setDog(dog());
return user;
}
@Bean
Dog dog() {
return new Dog();
}
}
在 Full 模式下,在 user() 方法中調(diào)用 dog() 方法的時(shí)候,調(diào)用的是一個(gè)代理對(duì)象的 dog 方法,在這個(gè)代理對(duì)象的 dog 方法中,會(huì)首先去檢查 Spring 容器中是否存在 Dog 對(duì)象,如果存在,則直接使用 Spring 容器中的 dog 對(duì)象,就不會(huì)真正去執(zhí)行 dog 方法而獲取到一個(gè)新的 dog 對(duì)象了,如果 Spring 容器中不存在 dog 對(duì)象,才會(huì)創(chuàng)建新的 dog 對(duì)象出來(lái)。
一言以蔽之,在 Full 模式下,user 中的 dog 對(duì)象和 dog 方法注冊(cè)到 Spring 容器的 dog 對(duì)象是同一個(gè)。
在 Full 模式下,由于要給當(dāng)前類(lèi)生成代理,然后去代理 @Bean 注解標(biāo)記的方法,因此,這些 @Bean 注解標(biāo)記的方法不能是 final 或者 private 類(lèi)型的,因?yàn)?final 或者 private 類(lèi)型的方法無(wú)法被重寫(xiě),也就沒(méi)法生成代理對(duì)象,如果添加了 final 或者 private 修飾符,那么會(huì)拋出如下異常:
圖片
3. Lite 模式
再來(lái)看 Lite 模式,這種模式可以認(rèn)為是一種精簡(jiǎn)模式。
怎么開(kāi)啟呢?我們可以去除配置類(lèi)上的 @Configuration 注解,或者去除之后添加 @Component 注解,又或者使用 @ComponentScan、@ImportResource、@Import 等注解標(biāo)記類(lèi),那么最終都是 Lite 模式:
@Component
public class JavaConfig {
@Bean
final User user() {
User user = new User();
user.setDog(dog());
return user;
}
@Bean
Dog dog() {
return new Dog();
}
}
此時(shí)就是 Lite 模式,現(xiàn)在我們?nèi)?Spring 容器中獲取這個(gè)配置類(lèi):
public class JavaDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
JavaConfig config = ctx.getBean(JavaConfig.class);
System.out.println("config.getClass() = " + config.getClass());
}
}
最終打印結(jié)果如下:
圖片
大家看到,我們從 Spring 容器中拿到的就是原始的對(duì)象,而不是一個(gè)被代理過(guò)的對(duì)象。因此:
- 由于 @Bean 注解標(biāo)記的方法沒(méi)有被代理,因此,該方法可以是 final 也可以是 private,運(yùn)行時(shí)都不會(huì)報(bào)錯(cuò)。
- 由于 @Bean 方法沒(méi)有被代理,因此在 user 方法中調(diào)用 dog 方法的時(shí)候,就直接調(diào)用了,這就導(dǎo)致 user 中的 dog 和最終 dog 方法注冊(cè)到 Spring 容器中的 dog 不是同一個(gè)。
針對(duì)第二點(diǎn),如果想要確保 user 中的 dog 和 Spring 容器中的 dog 是同一個(gè),那么可以通過(guò)參數(shù)將所需要的對(duì)象注入進(jìn)來(lái),類(lèi)似下面這樣:
@Component
public class JavaConfig {
@Bean
final User user(Dog dog) {
User user = new User();
user.setDog(dog);
return user;
}
@Bean
Dog dog() {
return new Dog();
}
}
當(dāng) Spring 容器調(diào)用 user 方法初始化 User 對(duì)象時(shí),發(fā)現(xiàn)該方法還有參數(shù),因此會(huì)去容器中查找這個(gè)參數(shù),找到了直接使用。
另外,我們也可以在類(lèi)上添加 @Configuration 注解,但是通過(guò)修改屬性值來(lái)啟用 Lite 模式:
@Configuration(proxyBeanMethods = false)
public class JavaConfig {
@Bean
final User user(Dog dog) {
User user = new User();
user.setDog(dog);
return user;
}
@Bean
Dog dog() {
return new Dog();
}
}
如果設(shè)置了 proxyBeanMethods 屬性為 false,那么也就是 Lite 模式了,其實(shí)我們從屬性名稱(chēng)上也能看出來(lái)端倪:是否代理 @Bean 注解標(biāo)記的方法。
4. 小結(jié)
總結(jié)一下:
- Lite 模式下,配置類(lèi)中的方法就是普通方法,可以是 final 類(lèi)型,也可以是 private。
- Lite 模式下,不需要通過(guò) CGLIB 生成動(dòng)態(tài)代理類(lèi),所以啟動(dòng)速度會(huì)快一些。
- Lite 模式下,一個(gè) @Bean 方法調(diào)用另外一個(gè) @Bean 方法,會(huì)導(dǎo)致同一個(gè) Bean 被初始化兩次。
- Full 模式下,會(huì)給配置類(lèi)生成一個(gè)動(dòng)態(tài)代理類(lèi),配置類(lèi)中的所有方法都將被動(dòng)態(tài)代理,因此配置類(lèi)中的方法不能是 final 或者 private 的。
- Full 模式下,一個(gè) @Bean 方法調(diào)用另外一個(gè) @Bean 方法,動(dòng)態(tài)代理方法會(huì)先去容器中檢查是否存在該 Bean,如果存在,則直接使用容器中的 Bean,否則才會(huì)去創(chuàng)建新的對(duì)象。
日常開(kāi)發(fā)中,我們使用較多的是 Full 模式。