javascript
Spring的@Configuration配置类-Full和Lite模式
前言
各位小伙伴大家好,我是A哥。這是一篇“插隊”進(jìn)來的文章,源于我公眾號下面的這句評論:
官方管這兩種模式分別叫:Full @Configuration和lite @Bean mode,口語上我習(xí)慣把它稱為Spring配置的Full模式和Lite模式更易溝通。
的確,我很簡單的“調(diào)研”了一下,知曉Spring配置中Lite模式和Full模式的幾乎沒有(或者說真的很少吧)。按照我之前的理論,大多人都不知道的技術(shù)(知識點)那肯定是不流行的。但是:不流行不代表不重要,不流行不代表不值錢,畢竟高薪往往只有少數(shù)人才能擁有。
版本約定
本文內(nèi)容若沒做特殊說明,均基于以下版本:
- JDK:1.8
- Spring Framework:5.2.2.RELEASE
正文
最初的Spring只支持xml方式配置Bean,從Spring 3.0起支持了一種更優(yōu)的方式:基于Java類的配置方式,這一下子讓我們Javaer可以從標(biāo)簽語法里解放了出來。畢竟作為Java程序員,我們擅長的是寫Java類,而非用標(biāo)簽語言去寫xml文件。
我對Spring配置的Full/Lite模式的關(guān)注和記憶深刻,源自于一個小小故事:某一年我在看公司的項目時發(fā)現(xiàn),數(shù)據(jù)源配置類里有如下一段配置代碼:
@Configuration public class DataSourceConfig {...@Beanpublic DataSource dataSource() {...return dataSource;}@Bean(name = "transactionManager")public DataSourceTransactionManager transactionManager() {return new DataSourceTransactionManager(dataSource());}... }作為當(dāng)時還是Java萌新的我,非常的費解。自然的對此段代碼產(chǎn)生了較大的好奇(其實是質(zhì)疑):在準(zhǔn)備DataSourceTransactionManager這個Bean時調(diào)用了dataSource()方法,根據(jù)我“非常扎實”的JavaSE基礎(chǔ)知識,它肯定會重新走一遍dataSource()方法,從而產(chǎn)生一個新的數(shù)據(jù)源實例,那么你的事務(wù)管理器管理的不就是一個“全新數(shù)據(jù)源”麼?談何事務(wù)呢?
為了驗證我的猜想,我把斷點打到dataSource()方法內(nèi)部開始調(diào)試,但讓我“失望”的是:此方法并沒有執(zhí)行兩次。這在當(dāng)時是震驚了我的,甚至一度懷疑自己引以為豪的Java基礎(chǔ)了。所以我四處詢問,希望得到一個“解釋”,但奈何,問了好幾圈,那會沒有一人能給我一個合理的說法,只知道那么用是沒有問題的。
很明顯,現(xiàn)在再回頭來看當(dāng)時的這個質(zhì)疑是顯得有些“無知”的,這個“難題”困擾了我很久,直到我前2年開始深度研究Spring源碼才讓此難題迎刃而解,當(dāng)時那種豁然開朗的感覺真好呀。
基本概念
關(guān)于配置類的核心概念,在這里先予以解釋。
@Configuration和@Bean
Spring新的配置體系中最為重要的構(gòu)件是:@Configuration標(biāo)注的類,@Bean標(biāo)注的方法。
// @since 3.0 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Configuration {@AliasFor(annotation = Component.class)String value() default "";// @since 5.2boolean proxyBeanMethods() default true;}用@Configuration注解標(biāo)注的類表明其主要目的是作為bean定義的源。此外,@Configuration類允許通過調(diào)用同一類中的其他@Bean?method方法來定義bean之間的依賴關(guān)系(下有詳解)。
// @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 {};@DeprecatedAutowire autowire() default Autowire.NO;// @since 5.1boolean autowireCandidate() default true;String initMethod() default "";String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;}@Bean注解標(biāo)注在方法上,用于指示方法實例化、配置和初始化要由Spring IoC容器管理的新對象。對于熟悉Spring的<beans/>XML配置的人來說,@Bean注解的作用與<bean/>元素相同。您可以對任何Spring的@Component組件使用@Bean注釋的方法代替(注意:這是理論上,實際上比如使用@Controller標(biāo)注的組件就不能直接使用它代替)。
需要注意的是,通常來說,我們均會把@Bean標(biāo)注的方法寫在@Configuration標(biāo)注的類里面來配合使用。
簡單粗暴理解:@Configuration標(biāo)注的類等同于一個xml文件,@Bean標(biāo)注的方法等同于xml文件里的一個<bean/>標(biāo)簽
使用舉例
@Configuration public class AppConfig {@Beanpublic 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配置文件無關(guān)。值得注意的是:判斷是Full模式 or Lite模式的前提是,首先你得是個容器組件。至于一個實例是如何“晉升”成為容器組件的,可以用注解也可以沒有注解,本文就不展開討論了,這屬于Spring的基礎(chǔ)知識。
Lite模式
當(dāng)@Bean方法在沒有使用@Configuration注釋的類中聲明時,它們被稱為在Lite模式下處理。它包括:在@Component中聲明的@Bean方法,甚至只是在一個非常普通的類中聲明的Bean方法,都被認(rèn)為是Lite版的配置類。@Bean方法是一種通用的工廠方法(factory-method)機(jī)制。
和Full模式的@Configuration不同,Lite模式的@Bean方法不能聲明Bean之間的依賴關(guān)系。因此,這樣的@Bean方法不應(yīng)該調(diào)用其他@Bean方法。每個這樣的方法實際上只是一個特定Bean引用的工廠方法(factory-method),沒有任何特殊的運行時語義。
何時為Lite模式
官方定義為:在沒有標(biāo)注@Configuration的類里面有@Bean方法就稱為Lite模式的配置。透過源碼再看這個定義是不完全正確的,而應(yīng)該是有如下case均認(rèn)為是Lite模式的配置類:
以上case的前提均是類上沒有被標(biāo)注@Configuration,在Spring 5.2之后新增了一種case也算作Lite模式:
細(xì)心的你會發(fā)現(xiàn),自Spring5.2(對應(yīng)Spring Boot 2.2.0)開始,內(nèi)置的幾乎所有的@Configuration配置類都被修改為了@Configuration(proxyBeanMethods = false),目的何為?答:以此來降低啟動時間,為Cloud Native繼續(xù)做準(zhǔn)備。
優(yōu)缺點
優(yōu)點:
- 運行時不再需要給對應(yīng)類生成CGLIB子類,提高了運行性能,降低了啟動時間
- 可以該配置類當(dāng)作一個普通類使用嘍:也就是說@Bean方法?可以是private、可以是final
缺點:
- 不能聲明@Bean之間的依賴,也就是說不能通過方法調(diào)用來依賴其它Bean
- (其實這個缺點還好,很容易用其它方式“彌補”,比如:把依賴Bean放進(jìn)方法入?yún)⒗锛纯?#xff09;
代碼示例
主配置類:
@ComponentScan("com.yourbatman.fullliteconfig.liteconfig") @Configuration public class AppConfig { }準(zhǔn)備一個Lite模式的配置:
@Component // @Configuration(proxyBeanMethods = false) // 這樣也是Lite模式 public class LiteConfig {@Beanpublic User user() {User user = new User();user.setName("A哥-lite");user.setAge(18);return user;}@Beanprivate 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("------------------------");}} }結(jié)果輸出:
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} ------------------------小總結(jié)
- 該模式下,配置類本身不會被CGLIB增強(qiáng),放進(jìn)IoC容器內(nèi)的就是本尊
- 該模式下,對于內(nèi)部類是沒有限制的:可以是Full模式或者Lite模式
- 該模式下,配置類內(nèi)部不能通過方法調(diào)用來處理依賴,否則每次生成的都是一個新實例而并非IoC容器內(nèi)的單例
- 該模式下,配置類就是一普通類嘛,所以@Bean方法可以使用private/final等進(jìn)行修飾(static自然也是闊儀的)
Full模式
在常見的場景中,@Bean方法都會在標(biāo)注有@Configuration的類中聲明,以確保總是使用“Full模式”,這么一來,交叉方法引用會被重定向到容器的生命周期管理,所以就可以更方便的管理Bean依賴。
何時為Full模式
標(biāo)注有@Configuration注解的類被稱為full模式的配置類。自Spring5.2后這句話改為下面這樣我覺得更為精確些:
- 標(biāo)注有@Configuration或者@Configuration(proxyBeanMethods = true)的類被稱為Full模式的配置類
- (當(dāng)然嘍,proxyBeanMethods屬性的默認(rèn)值是true,所以一般需要Full模式我們只需要標(biāo)個注解即可)
優(yōu)缺點
優(yōu)點:
- 可以支持通過常規(guī)Java調(diào)用相同類的@Bean方法而保證是容器內(nèi)的Bean,這有效規(guī)避了在“Lite模式”下操作時難以跟蹤的細(xì)微錯誤。特別對于萌新程序員,這個特點很有意義
缺點:
- 運行時會給該類生成一個CGLIB子類放進(jìn)容器,有一定的性能、時間開銷(這個開銷在Spring Boot這種擁有大量配置類的情況下是不容忽視的,這也是為何Spring 5.2新增了proxyBeanMethods屬性的最直接原因)
- 正因為被代理了,所以@Bean方法?不可以是private、不可以是final
代碼示例
主配置:
@ComponentScan("com.yourbatman.fullliteconfig.fullconfig") @Configuration public class AppConfig { }準(zhǔn)備一個Full模式的配置:
@Configuration public class FullConfig {@Beanpublic User user() {User user = new User();user.setName("A哥-lite");user.setAge(18);return user;}@Beanprotected 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("------------------------");}} }結(jié)果輸出:
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} ------------------------小總結(jié)
- 該模式下,配置類會被CGLIB增強(qiáng)(生成代理對象),放進(jìn)IoC容器內(nèi)的是代理
- 該模式下,對于內(nèi)部類是沒有限制的:可以是Full模式或者Lite模式
- 該模式下,配置類內(nèi)部可以通過方法調(diào)用來處理依賴,并且能夠保證是同一個實例,都指向IoC內(nèi)的那個單例
- 該模式下,@Bean方法不能被private/final等進(jìn)行修飾(很簡單,因為方法需要被復(fù)寫嘛,所以不能私有和final。defualt/protected/public都可以哦),否則啟動報錯(其實IDEA編譯器在編譯器就提示可以提示你了):
使用建議
了解了Spring配置類的Full模式和Lite模式,那么在工作中我該如何使用呢?這里A哥給出使用建議,僅供參考:
- 如果是在公司的業(yè)務(wù)功能/服務(wù)上做開發(fā),使用Full模式
- 如果你是個容器開發(fā)者,或者你在開發(fā)中間件、通用組件等,那么使用Lite模式是一種更被推薦的方式,它對Cloud Native更為友好
思考題?
通過new AnnotationConfigApplicationContext(AppConfig.class)直接放進(jìn)去的類,它會成為一個IoC的組件嗎?若會,那么它是Full模式 or Lite模式呢?是個固定的結(jié)果還是也和其標(biāo)注的注解有關(guān)呢?
本思考題不難,自己試驗一把便知,建議多動手~
總結(jié)
本文結(jié)合代碼示例闡述了Spring配置中Full模式和Lite模式,以及各自的定義和優(yōu)缺點。對于一般的小伙伴,掌握本文就夠用了,并且足夠你面試中吹x。但A哥系列文章一般不止于“表面”嘛,下篇文章將從原理層面告訴你Spring是如何來巧妙的處理這兩種模式的,特別是會結(jié)合Spring 5.2.0新特性,以及對比Spring 5.2.0的實現(xiàn)和之前版本有何不同,你課訂閱我的公眾號保持關(guān)注。
超強(qiáng)干貨來襲 云風(fēng)專訪:近40年碼齡,通宵達(dá)旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的Spring的@Configuration配置类-Full和Lite模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Boot Transact
- 下一篇: Spring AOP源码解析(二)——