Spring的@Configuration配置類-Full和Lite模式


為什么沒有@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模式的配置類:

  1. 類上標注有@Component注解
  2. 類上標注有@ComponentScan注解
  3. 類上標注有@Import注解
  4. 類上標注有@ImportResource注解
  5. 若類上沒有任何注解,但類內存在@Bean方法
  6. 總結一句話,只要不標識@Configuration(proxyBeanMethods=true)其他都是lite模式

以上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繼續做准備。

源碼:

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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM