Spring擴展:替換IOC容器中的Bean組件 -- @Replace注解


1、背景:

    工作中是否有這樣的場景?一個軟件系統會同時存在多個不同版本,比如我現在做的IM系統,同時又作為公司的技術輸出給其他銀行,不同的銀行有自己的業務實現(登陸驗證、用戶信息查詢等)。或者你的工程里依賴了其他第三方的jar,這些jar包里的組件都是通過Spring容器來管理的,如果你想修改某個類里面的部分邏輯,怎么辦呢?是否可以考慮下直接把Spring容器里的某個組件(Bean)替換成你自己實現的Bean?

2、原理&實現

2.1 先看看Spring開放給我們的擴展

    Spring框架超強的擴展性毋庸置疑,我們可以通過BeanPostProcessor來簡單替換容器中的Bean(Spring中主要有兩種后處理器:BeanFactoryPostProcessor和BeanPostProcessor)。

@Component
public class MyBeanPostProcessor implements ApplicationContextAware, BeanPostProcessor {
    private ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (beanName.equals("defaultConfig")) {
            // 如果遇到需要替換的Bean,我們直接換成自己實現的Bean即可(這里可以把就得removeBeanDefinition,然后注冊新的registerBeanDefinition)
            // 這里的myConfig要繼承自defaultConfig,否則引用的地方會報錯
            return applicationContext.getBean("myConfig");
        }
        return bean;
    }
}

優點:

  • 直接利用Spring原生的擴展,可以平滑升級
  • 實現簡單,易操作好理解,對於只需要替換少數幾個Bean的情況下推薦這種方式

缺點:

  • beanName硬編碼在代碼里,雖然可以把替換關系配置在properties里,但是在多版本部署,替換Bean較多時,維護這種關系將是一種負擔
  • 僅僅是替換了Bean對象,對於容器中元數據如BeanDefinition等等均是原對象的,存在一定局限性

2.2 更靈活一點的替換方式

    Spring容器是可以直接動態注冊Bean的,而且如果注冊的Bean與容器中現有的Bean同名,則會直接替換現有Bean,這樣可以繞過Spring啟動時解析BeanDefinition的checkCandidate檢查。更重要的是其他引用原來Bean的組件的地方都會替換成新注冊的Bean,所以這種功能是能直接滿足我們上面場景的。

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(DynamicRegisteringBean.class);
        springApplication.setAllowBeanDefinitionOverriding(true);
        ConfigurableApplicationContext ctx = springApplication.run(args);
        System.out.println(((DefaultConfig) ctx.getBean("defaultConfig")).getName());
        ctx.getBean(UseBean.class).printName();
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MyConfig.class);
        beanDefinitionBuilder.addPropertyValue("name", "this is a DynamicConfig");
        BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) ctx;
        // 動態注入同名Bean,會直接覆蓋之前的Bean,並且容器中其他Bean對當前Bean的引用也會被更新
        beanDefinitionRegistry.registerBeanDefinition("defaultConfig", beanDefinitionBuilder.getBeanDefinition());
        System.out.println(((DefaultConfig) ctx.getBean("defaultConfig")).getName());
        ctx.getBean(UseBean.class).printName();
    }

上面代碼中MyConfig繼承了DefaultConfig,並重寫了getName方法,且MyConfig默認是沒有被@Component注解修飾的,完整代碼可參考:https://github.com/hiccup234/spring-ext/tree/master/src/test/java/top/hiccup/spring/ext/test/replace/dynamic
運行結果如下:

相比2.1中通過BeanPostProcessor來實現替換Bean組件,動態注冊的方法會更靈活一些,也不存在元數據不匹配的問題,但是又引入一個新的問題:需要我們自己手動創建BeanDefinition,這相當於要應用程序去關注和完成解析Bean的工作,使得Spring對應用程序的侵入性變高。

2.3 更優雅一點的替換方式

    Spring實際上就是一個容器,底層其實就是一個ConcurrentHashMap。如果要替換Map中的Entry,再次調用put方法設置相同的key不同的value就可以了。同理,如果要替換Spring容器中的Bean組件,那么我們重新定義一個同名的Bean並注冊進去就可以了。當然直接申明兩個同名的Bean是過不了Spring中ClassPathBeanDefinitionScanner的檢查的,這時候需要我們做一點點擴展。

實現自己的ClassPathBeanDefinitionScanner

目前的想法是直接重寫checkCandidate方法,通過判斷Bean的類上是否有@Replace注解,來決定是否通過檢查。

依次往上擴展就到了ConfigurationClassPostProcessor,這是Spring中非常重要的一個容器后置處理器BeanFactoryPostProcessor(上面我們用的是Bean后處理器:BeanPostProcessor),重寫processConfigBeanDefinitions方法就可以引入自己實現的ClassPathBeanDefinitionScanner。具體細節可以參考:https://github.com/hiccup234/spring-ext.git

3、使用示例

    直接在項目中增加如下坐標(Maven中央倉庫),目前這個版本是對Spring的5.2.2.RELEASE做擴展,新版本的Spring其相對3.X、4.X有部分代碼變動。

<dependency>
    <groupId>top.hiccup</groupId>
    <artifactId>spring-ext</artifactId>
    <version>5.2.2.0-SNAPSHOT</version>
</dependency>

對Spring Boot中的SpringApplication做一點擴展,將上面擴展的ConfigurationClassPostProcessor類以RootBeanDefinition注冊到容器中。

聲明一個自己的類,然后繼承需要替換的Bean的類型(這樣就可以重寫原Bean中的某些方法,從而添加自己的處理邏輯),然后用@Replace("defaultConfig")修飾,如下:

通過ExtSpringApplication啟動,可以看到,實際Spring容器中的Bean已經替換成我們自己實現的Bean組件了。

另一種方式:
通過spring.factories的SPI機制,避免擴展SpringApplication,引入jar包后能夠直接獲得@Replace的功能。

在BootExtApplicationContextInitializer里對beanDefinitionRegistry注冊RootBeanDefinition(org.springframework.context.annotation.internalConfigurationAnnotationProcessor)

    @Override
    public void initialize(ConfigurableApplicationContext context) {
        if (context instanceof BeanDefinitionRegistry) {
            BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) context;
            beanDefinitionRegistry.registerBeanDefinition(AnnotationConfigUtils.CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME, new RootBeanDefinition(ExtConfigurationClassPostProcessor.class));
        } else {
            logger.error("Can`t use spring-ext.jar");
        }
    }


免責聲明!

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



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