真懂Spring的@Configuration配置類?你可能自我感覺太良好

當大潮退去,才知道誰在裸泳。
作者:A哥(YourBatman)
公衆號:BAT的烏托邦(ID:BAT-utopia)
文末是否有彩蛋:有

前言

各位小夥伴大家好,我是A哥。這是一篇“插隊”進來的文章,源於我公衆號下面的這句評論:
在這裏插入圖片描述
官方管這兩種模式分別叫:Full @Configurationlite @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模式的配置類:

  1. 類上標註有@Component註解
  2. 類上標註有@ComponentScan註解
  3. 類上標註有@Import註解
  4. 類上標註有@ImportResource註解
  5. 若類上沒有任何註解,但類內存在@Bean方法

以上case的前提均是類上沒有被標註@Configuration,在Spring 5.2之後新增了一種case也算作Lite模式:

  1. 標註有@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的實現和之前版本有何不同,你課訂閱我的公衆號保持關注。


文末彩蛋

福利繼續。【BAT的烏托邦】知識星球歡迎你的加入:

知識星球二維碼
提示:先關注同名公衆號,回覆“知識星球”關鍵字,領取大額優惠券,加入星球,小衆的事情咱們小衆聊

關注我

  • 關注我的公衆號(或者加我微信),邀你進入“Java高工架構師系列”純純純技術羣,當然還有366G的自學資料
  • 每月底統計當月在看、轉發次數最多的幾位朋友,私下發紅包哈(錢不多,但你的支持我得表示感謝)
  • 文章在公衆號首發,其它平臺會慢1-2天。你也可以關注我的個人博客:https://www.yourbatman.cn

公衆號二維碼
原創不易,碼字更不易,不同意你白嫖,你的三連(在看、轉發、關注)是對A哥的最大支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章