為什么沒有@Configuration注解的類其中的@Bean方法也能被掃描到?
這就要從Full和lite模式來說。最初的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模式的配置類:
- 類上標注有
@Component
注解 - 類上標注有
@ComponentScan
注解 - 類上標注有
@Import
注解 - 類上標注有
@ImportResource
注解 - 若類上沒有任何注解,但類內存在@Bean方法
- 總結一句話,只要不標識@Configuration(proxyBeanMethods=true)其他都是lite模式
以上case的前提均是類上沒有被標注@Configuration
,在Spring 5.2之后新增了一種case也算作Lite模式:
- 標注有
@Configuration(proxyBeanMethods = false)
,注意:此值默認是true哦,需要顯示改為false才算是Lite模式
細心的你會發現,自Spring5.2(對應Spring Boot 2.2.0)開始,內置的幾乎所有的@Configuration
配置類都被修改為了@Configuration(proxyBeanMethods = false)
,目的何為?答:以此來降低啟動時間,為Cloud Native繼續做准備。
源碼:
abstract class ConfigurationClassUtils { ........... public static boolean checkConfigurationClassCandidate( BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) { String className = beanDef.getBeanClassName(); if (className == null || beanDef.getFactoryMethodName() != null) { return false; } AnnotationMetadata metadata; if (beanDef instanceof AnnotatedBeanDefinition && className.equals(((AnnotatedBeanDefinition) beanDef).getMetadata().getClassName())) { // Can reuse the pre-parsed metadata from the given BeanDefinition... metadata = ((AnnotatedBeanDefinition) beanDef).getMetadata(); } else if (beanDef instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) beanDef).hasBeanClass()) { // Check already loaded Class if present... // since we possibly can't even load the class file for this Class. Class<?> beanClass = ((AbstractBeanDefinition) beanDef).getBeanClass(); if (BeanFactoryPostProcessor.class.isAssignableFrom(beanClass) || BeanPostProcessor.class.isAssignableFrom(beanClass) || AopInfrastructureBean.class.isAssignableFrom(beanClass) || EventListenerFactory.class.isAssignableFrom(beanClass)) { return false; } metadata = AnnotationMetadata.introspect(beanClass); } else { try { MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className); metadata = metadataReader.getAnnotationMetadata(); } catch (IOException ex) { if (logger.isDebugEnabled()) { logger.debug("Could not find class file for introspecting configuration annotations: " + className, ex); } return false; } } Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName()); if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } else if (config != null || isConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else { return false; } // It's a full or lite configuration candidate... Let's determine the order value, if any. Integer order = getOrder(metadata); if (order != null) { beanDef.setAttribute(ORDER_ATTRIBUTE, order); } return true; } }
優缺點
優點:
- 運行時不再需要給對應類生成CGLIB子類,提高了運行性能,降低了啟動時間
- 可以該配置類當作一個普通類使用嘍:也就是說@Bean方法 可以是private、可以是final
缺點:
- 不能聲明@Bean之間的依賴,也就是說不能通過方法調用來依賴其它Bean
- (其實這個缺點還好,很容易用其它方式“彌補”,比如:把依賴Bean放進方法入參里即可)
代碼示例
主配置類:
@ComponentScan("com.yourbatman.fullliteconfig.liteconfig") @Configuration public class AppConfig { } @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模式
-
何時為Full模式
在常見的場景中,
@Bean
方法都會在標注有@Configuration
的類中聲明,以確保總是使用“Full模式”,這么一來,交叉方法引用會被重定向到容器的生命周期管理,所以就可以更方便的管理Bean依賴。 -
標注有
@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 { } @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編譯器在編譯器就提示可以提示你了) - 其實@Configuration注解就是來表示是是full模式還是lite模式,如果不想表示為full模式,個人認為完全就可以不用表示@Configuration這個注解,目前來看full模式也就是會導致配置類會被動態代理。他的好處就是在調用配置類方法獲取bean對象的時候不會直接調用方法,而是通過BeanFatroy來獲取bean對象
使用建議
了解了Spring配置類的Full模式和Lite模式,那么在工作中我該如何使用呢?這里A哥給出使用建議,僅供參考:
- 如果是在公司的業務功能/服務上做開發,使用Full模式
- 如果你是個容器開發者,或者你在開發中間件、通用組件等,那么使用Lite模式是一種更被推薦的方式,它對Cloud Native更為友好
原文地址:https://blog.csdn.net/demon7552003/article/details/107988310