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

YourBatman發表於2020-07-08

當大潮退去,才知道誰在裸泳。關注公眾號【BAT的烏托邦】開啟專欄式學習,拒絕淺嘗輒止。本文 https://www.yourbatman.cn 已收錄,裡面一併有Spring技術棧、MyBatis、中介軟體等小而美的專欄供以學習哦。

前言

各位小夥伴大家好,我是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的實現和之前版本有何不同,你課訂閱我的公眾號保持關注。

相關文章