您好,登錄后才能下訂單哦!
這篇文章主要講解了“怎么動態替換Spring容器中的Bean”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“怎么動態替換Spring容器中的Bean”吧!
最近在編寫單測時,發現使用 Mock 工具預定義 Service 中方法的行為特別難用,而且無法精細化的實現自定義的行為,因此想要在 Spring 容器運行過程中使用自定義 Mock 對象,該對象能夠代替實際的 Bean 的給定方法。
創建一個 Mock 注解,并且在 Spring 容器注冊完所有的 Bean 之后,解析 classpath 下所有引入該 Mock 注解的類,使用 Mock 注解標記的 Bean 替換注解中指定名稱的 Bean。
這種方式類似于 mybatis-spring 動態解析 @Mapper 注解的方法(MapperScannerRegistrar 實現了@Mapper 注解的掃描),但是不一樣的是 mybatis-spring 使用工廠類替換接口類,而我們是用 Mock 的 Bean 替換實際的 Bean。
創建 Mock 注解
/** * 為指定的 Bean 創建 Mock 對象,需要繼承原始 Bean */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface FakeBeanFor { String value(); // 需要替換的 Bean 的名稱 }
在 Spring 容器注冊完所有的 Bean 后,解析 classpath 下引入 @FakeBeanFor 注解的類,使用 @FakeBeanFor 注解標記的 Bean 替換 value 中指定名稱的 Bean。
/** * 從當前 classpath 讀取 @FakeBeanFor 注解的類,并替換指定名稱的 bean */ @Slf4j @Configuration @ConditionalOnExpression("${unitcases.enable.fake:true}") // 通過 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry 可以將 Bean 動態注入容器 // 通過 BeanFactoryAware 可以自動注入 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); // 設置過濾器。僅掃描 @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(); // 獲取類名,并創建 Class 對象 String className = definition.getBeanClassName(); Class<?> clazz = classNameToClass(className); // 解析注解上的 value FakeBeanFor annotation = clazz.getAnnotation(FakeBeanFor.class); if (annotation == null || StringUtils.isEmpty(annotation.value())) { continue; } // 使用當前加載的 @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; } // 反射通過 class 名稱獲取 Class 對象 private Class<?> classNameToClass(String className) { try { return Class.forName(className); } catch (ClassNotFoundException e) { log.error("create instance failed.", e); } return null; } } }
有點兒不一樣的是這是一個配置類,將它放置到 Spring 的自動掃描路徑上,就可以自動掃描 classpath 下 @FakeBeanFor 指定的類,并將其加載為 BeanDefinition。
在 FakeBeanConfiguration 上還配置了 ConditionalOnExpression,這樣就可以只在單測環境下的 application.properties 文件中設置指定條件使得該 Configuration 生效。
注意:
這里 unitcases.enable.fake:true 默認開啟了替換,如果想要默認關閉則需要設置 unitcases.enable.fake:false,并且在單測環境的 application.properties 文件設置 unitcases.enable.fake=true。
舉例
假設在容器中定義如下 Service:
@Service public class HelloService { public void sayHello() { System.out.println("hello real world!"); } }
在單測環境下希望能夠改變它的行為,但是又不想修改這個類本身,則可以使用 @FakeBeanFor 注解:
@FakeBeanFor("helloService") public class FakeHelloService extends HelloService { @Override public void sayHello() { System.out.println("hello fake world!"); } }
通過繼承實際的 Service,并覆蓋 Service 的原始方法,修改其行為。在單測中可以這樣使用:
@SpringBootTest @RunWith(SpringRunner.class) public class FakeHelloServiceTest { @Autowired private HelloService helloService; @Test public void testSayHello() { helloService.sayHello(); // 輸出:“hello fake world!” } }
總結:通過自定義的 Mock 對象動態替換實際的 Bean 可以實現單測環境下比較難以使用 Mock 框架實現的功能,如將原本的異步調用邏輯修改為同步調用,避免單測完成時,異步調用還未執行完成的場景。
需求:通過配置文件,能夠使得新的一個service層類替代jar包中原有的類文件。
項目原因,引用了一些成型產品的jar包,已經不能對其進行修改了。
故,考慮采用用新的類替換jar包中的類。
實現思路:在配置文件中配置新老類的全類名,讀取配置文件后,通過spring初始化bean的過程中,移除spring容器中老類的bean對象,手動注冊新對象進去,bean名稱和老對象一致即可。
jar包中的老對象是通過@Service注冊到容器中的。
新的類因為是手動配置,不需要添加任何注解。
實現的方法如下:
@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("對象" + beanName + "開始實例化"); System.out.println("類名" + bean.getClass().getName() + "是啥"); if(StringUtils.contains(value,bean.getClass().getName())){ System.out.println("找到了需要進行替換的類。。。。。。。。。。。"); boolean containsBean = defaultListableBeanFactory.containsBean(beanName); if (containsBean) { //移除bean的定義和實例 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(); //注冊新的bean定義和實例 defaultListableBeanFactory.registerBeanDefinition(beanName, BeanDefinitionBuilder.genericBeanDefinition(tar.getClass()).getBeanDefinition()); //這里要手動注入新類里面的依賴關系 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
在啟動的時候,會找到容器中的老的bean,將其remove掉,然后手動注冊新的bean到容器中。
感謝各位的閱讀,以上就是“怎么動態替換Spring容器中的Bean”的內容了,經過本文的學習后,相信大家對怎么動態替換Spring容器中的Bean這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。