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");
}
}