如何動(dòng)態(tài)替換Spring容器中的Bean
動(dòng)態(tài)替換Spring容器中的Bean
原因
最近在編寫單測(cè)時(shí),發(fā)現(xiàn)使用 Mock 工具預(yù)定義 Service 中方法的行為特別難用,而且無(wú)法精細(xì)化的實(shí)現(xiàn)自定義的行為,因此想要在 Spring 容器運(yùn)行過(guò)程中使用自定義 Mock 對(duì)象,該對(duì)象能夠代替實(shí)際的 Bean 的給定方法。
方案
創(chuàng)建一個(gè) Mock 注解,并且在 Spring 容器注冊(cè)完所有的 Bean 之后,解析 classpath 下所有引入該 Mock 注解的類,使用 Mock 注解標(biāo)記的 Bean 替換注解中指定名稱的 Bean。
這種方式類似于 mybatis-spring 動(dòng)態(tài)解析 @Mapper 注解的方法(MapperScannerRegistrar 實(shí)現(xiàn)了@Mapper 注解的掃描),但是不一樣的是 mybatis-spring 使用工廠類替換接口類,而我們是用 Mock 的 Bean 替換實(shí)際的 Bean。
實(shí)現(xiàn)
創(chuàng)建 Mock 注解
/**
* 為指定的 Bean 創(chuàng)建 Mock 對(duì)象,需要繼承原始 Bean
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FakeBeanFor {
String value(); // 需要替換的 Bean 的名稱
}
在 Spring 容器注冊(cè)完所有的 Bean 后,解析 classpath 下引入 @FakeBeanFor 注解的類,使用 @FakeBeanFor 注解標(biāo)記的 Bean 替換 value 中指定名稱的 Bean。
/**
* 從當(dāng)前 classpath 讀取 @FakeBeanFor 注解的類,并替換指定名稱的 bean
*/
@Slf4j
@Configuration
@ConditionalOnExpression("${unitcases.enable.fake:true}")
// 通過(guò) BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry 可以將 Bean 動(dòng)態(tài)注入容器
// 通過(guò) BeanFactoryAware 可以自動(dòng)注入 BeanFactory
public class FakeBeanConfiguration implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
private BeanFactory beanFactory;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
log.debug("searching for classes annotated with @FakeBeanFor");
// 自定義 Scanner 掃描 classpath 下的指定注解
ClassPathFakeAnnotationScanner scanner = new ClassPathFakeAnnotationScanner(registry);
try {
List<String> packages = AutoConfigurationPackages.get(this.beanFactory); // 獲取包路徑
if (log.isDebugEnabled()) {
for (String pkg : packages) {
log.debug("Using auto-configuration base package: {}", pkg);
}
}
scanner.doScan(StringUtils.toStringArray(packages)); // 掃描所有加載的包
} catch (IllegalStateException ex) {
log.debug("could not determine auto-configuration package, automatic fake scanning disabled.", ex);
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
// empty
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
private static class ClassPathFakeAnnotationScanner extends ClassPathBeanDefinitionScanner {
ClassPathFakeAnnotationScanner(BeanDefinitionRegistry registry) {
super(registry, false);
// 設(shè)置過(guò)濾器。僅掃描 @FakeBeanFor
addIncludeFilter(new AnnotationTypeFilter(FakeBeanFor.class));
}
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
List<String> fakeClassNames = new ArrayList<>();
// 掃描全部 package 下 annotationClass 指定的 Bean
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
// 獲取類名,并創(chuàng)建 Class 對(duì)象
String className = definition.getBeanClassName();
Class<?> clazz = classNameToClass(className);
// 解析注解上的 value
FakeBeanFor annotation = clazz.getAnnotation(FakeBeanFor.class);
if (annotation == null || StringUtils.isEmpty(annotation.value())) {
continue;
}
// 使用當(dāng)前加載的 @FakeBeanFor 指定的 Bean 替換 value 里指定名稱的 Bean
if (getRegistry().containsBeanDefinition(annotation.value())) {
getRegistry().removeBeanDefinition(annotation.value());
getRegistry().registerBeanDefinition(annotation.value(), definition);
fakeClassNames.add(clazz.getName());
}
}
log.info("found fake beans: " + fakeClassNames);
return beanDefinitions;
}
// 反射通過(guò) class 名稱獲取 Class 對(duì)象
private Class<?> classNameToClass(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
log.error("create instance failed.", e);
}
return null;
}
}
}
有點(diǎn)兒不一樣的是這是一個(gè)配置類,將它放置到 Spring 的自動(dòng)掃描路徑上,就可以自動(dòng)掃描 classpath 下 @FakeBeanFor 指定的類,并將其加載為 BeanDefinition。
在 FakeBeanConfiguration 上還配置了 ConditionalOnExpression,這樣就可以只在單測(cè)環(huán)境下的 application.properties 文件中設(shè)置指定條件使得該 Configuration 生效。
注意:
- 這里 unitcases.enable.fake:true 默認(rèn)開啟了替換,如果想要默認(rèn)關(guān)閉則需要設(shè)置 unitcases.enable.fake:false,并且在單測(cè)環(huán)境的 application.properties 文件設(shè)置 unitcases.enable.fake=true。
舉例
假設(shè)在容器中定義如下 Service:
@Service
public class HelloService {
public void sayHello() {
System.out.println("hello real world!");
}
}
在單測(cè)環(huán)境下希望能夠改變它的行為,但是又不想修改這個(gè)類本身,則可以使用 @FakeBeanFor 注解:
@FakeBeanFor("helloService")
public class FakeHelloService extends HelloService {
@Override
public void sayHello() {
System.out.println("hello fake world!");
}
}
通過(guò)繼承實(shí)際的 Service,并覆蓋 Service 的原始方法,修改其行為。在單測(cè)中可以這樣使用:
@SpringBootTest
@RunWith(SpringRunner.class)
public class FakeHelloServiceTest {
@Autowired
private HelloService helloService;
@Test
public void testSayHello() {
helloService.sayHello(); // 輸出:“hello fake world!”
}
}
總結(jié):通過(guò)自定義的 Mock 對(duì)象動(dòng)態(tài)替換實(shí)際的 Bean 可以實(shí)現(xiàn)單測(cè)環(huán)境下比較難以使用 Mock 框架實(shí)現(xiàn)的功能,如將原本的異步調(diào)用邏輯修改為同步調(diào)用,避免單測(cè)完成時(shí),異步調(diào)用還未執(zhí)行完成的場(chǎng)景。
Spring中bean替換問(wèn)題
需求:通過(guò)配置文件,能夠使得新的一個(gè)service層類替代jar包中原有的類文件。
項(xiàng)目原因,引用了一些成型產(chǎn)品的jar包,已經(jīng)不能對(duì)其進(jìn)行修改了。
故,考慮采用用新的類替換jar包中的類。
實(shí)現(xiàn)思路:在配置文件中配置新老類的全類名,讀取配置文件后,通過(guò)spring初始化bean的過(guò)程中,移除spring容器中老類的bean對(duì)象,手動(dòng)注冊(cè)新對(duì)象進(jìn)去,bean名稱和老對(duì)象一致即可。
jar包中的老對(duì)象是通過(guò)@Service注冊(cè)到容器中的。
新的類因?yàn)槭鞘謩?dòng)配置,不需要添加任何注解。
實(shí)現(xiàn)的方法如下:
@Component
public class MyBeanPostProcessor implements ApplicationContextAware, BeanPostProcessor {
@Autowired
private AutowireCapableBeanFactory beanFactory;
@Autowired
private DefaultListableBeanFactory defaultListableBeanFactory;
static HashMap ReplaceClass;
static String value = null;
static {
try {
value = PropertiesLoaderUtils.loadAllProperties("你的配置文件路徑").getProperty("replaceClass");
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("properties value........"+value);
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("對(duì)象" + beanName + "開始實(shí)例化");
System.out.println("類名" + bean.getClass().getName() + "是啥");
if(StringUtils.contains(value,bean.getClass().getName())){
System.out.println("找到了需要進(jìn)行替換的類。。。。。。。。。。。");
boolean containsBean = defaultListableBeanFactory.containsBean(beanName);
if (containsBean) {
//移除bean的定義和實(shí)例
defaultListableBeanFactory.removeBeanDefinition(beanName);
}
String temp = value;
String tar_class = temp.split(bean.getClass().getName())[1].split("#")[1].split(",")[0];
System.out.println(tar_class);
try {
Class tar = Class.forName(tar_class);
Object obj = tar.newInstance();
//注冊(cè)新的bean定義和實(shí)例
defaultListableBeanFactory.registerBeanDefinition(beanName, BeanDefinitionBuilder.genericBeanDefinition(tar.getClass()).getBeanDefinition());
//這里要手動(dòng)注入新類里面的依賴關(guān)系
beanFactory.autowireBean(obj);
return obj;
} catch (Exception e) {
e.printStackTrace();
}
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
}
配置文件中的格式采用下面的樣式 :
replaceClass=gov.df.fap.service.OldTaskBO#gov.df.newmodel.service.NewTaskBO
在啟動(dòng)的時(shí)候,會(huì)找到容器中的老的bean,將其remove掉,然后手動(dòng)注冊(cè)新的bean到容器中。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
五分鐘教你手寫 SpringBoot 本地事務(wù)管理實(shí)現(xiàn)
這篇文章主要介紹了五分鐘教你手寫 SpringBoot 本地事務(wù)管理實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02
詳解Eclipse提交項(xiàng)目到GitHub以及解決代碼沖突
這篇文章主要介紹了詳解Eclipse提交項(xiàng)目到GitHub以及解決代碼沖突,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03
java中給實(shí)體對(duì)象屬性的空值賦默認(rèn)值
這篇文章主要介紹了java中給實(shí)體對(duì)象屬性的空值賦默認(rèn)值,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
spring?boot集成jasypt?并實(shí)現(xiàn)自定義加解密的詳細(xì)步驟
由于項(xiàng)目中的配置文件?配置的地方過(guò)多,現(xiàn)將配置文件統(tǒng)一放到nacos上集中管理?且密碼使用加密的方式放在配置文件中,配置文件的加密使用加密庫(kù)jasypt,本文給大家介紹spring boot集成jasypt并實(shí)現(xiàn)自定義加解密,感興趣的朋友一起看看吧2023-08-08
校驗(yàn)非空的注解@NotNull如何取得自定義的message
這篇文章主要介紹了校驗(yàn)非空的注解@NotNull如何取得自定義的message,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
JAVA8 STREAM COLLECT GROUPBY分組實(shí)例解析
這篇文章主要介紹了JAVA8 STREAM COLLECT GROUPBY分組實(shí)例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
java實(shí)現(xiàn)的二級(jí)聯(lián)動(dòng)菜單效果
這篇文章主要介紹了java實(shí)現(xiàn)的二級(jí)聯(lián)動(dòng)菜單效果,結(jié)合實(shí)例形式分析了java前臺(tái)頁(yè)面布局及與后臺(tái)交互構(gòu)造聯(lián)動(dòng)菜單的相關(guān)技巧,需要的朋友可以參考下2016-08-08
ElasticSearch學(xué)習(xí)之多條件組合查詢驗(yàn)證及示例分析
這篇文章主要為大家介紹了ElasticSearch 多條件組合查詢驗(yàn)證及示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02

