大數據

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

當大潮退去,才知道誰在裸泳。關注公眾號【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的實現和之前版本有何不同,你課訂閱我的公眾號保持關注。


關注A哥

  • 原創不易,碼字更不易。關注A哥的公眾號【BAT的烏托邦】,開啟有深度的專欄式學習,拒絕淺嘗輒止
  • 專欄式學習,我們是認真的(關注公眾號回覆“知識星球”領券後再輕裝入駐)
  • 加A哥好友(fsx641385712),備註“Java入群”邀你進入【Java高工、架構師】系列純純純技術群

BAT的烏托邦公眾號二維碼

Leave a Reply

Your email address will not be published. Required fields are marked *