这篇文章主要讲解了“怎么动态替换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进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。