如何动态替换Spring容器中的Bean

 更新时间:2022年08月27日 16:46:11   作者:凉茶方便面  
这篇文章主要介绍了如何动态替换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 框架实现的功能,如将原本的异步调用逻辑修改为同步调用,避免单测完成时,异步调用还未执行完成的场景。

Spring中bean替换问题

需求:通过配置文件,能够使得新的一个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到容器中。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。 

相关文章

  • Springboot 如何使用BindingResult校验参数

    Springboot 如何使用BindingResult校验参数

    这篇文章主要介绍了Springboot 如何使用BindingResult校验参数,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • Java编程接口回调一般用法代码解析

    Java编程接口回调一般用法代码解析

    本文的主要内容是同过实际代码向大家展示Java编程中接口回调的一般用法,具有一定参考价值,需要的朋友可以了解下
    2017-09-09
  • Spring TransactionalEventListener事务未提交读取不到数据的解决

    Spring TransactionalEventListener事务未提交读取不到数据的解决

    这篇文章主要介绍了Spring TransactionalEventListener事务未提交读取不到数据的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • java连连看游戏菜单设计

    java连连看游戏菜单设计

    这篇文章主要为大家详细介绍了java连连看游戏菜单部分的设计代码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-12-12
  • java通过AOP实现全局日志打印详解

    java通过AOP实现全局日志打印详解

    最近自己一直再看现有微服务的日志模块,发现就是使用AOP来做controller层的日志处理,加上项目在进行架构优化,这篇文章主要给大家介绍了关于java通过AOP实现全局日志打印的相关资料,需要的朋友可以参考下
    2022-01-01
  • Spring框架IOC容器底层原理详解

    Spring框架IOC容器底层原理详解

    在java当中一个类想要使用另一个类的方法,就必须在这个类当中创建这个类的对象,Spring将创建对象的权利给了IOC,在IOC当中创建了ABC三个对象,那么我们我们其他的类只需要调用集合,大大的解决了程序耦合性的问题
    2022-07-07
  • SpringCloud Ribbon与OpenFeign详解如何实现服务调用

    SpringCloud Ribbon与OpenFeign详解如何实现服务调用

    这篇文章主要介绍了SpringCloud Ribbon与OpenFeign实现服务调用的过程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-09-09
  • SpringCloud中数据认证加密的方法总结

    SpringCloud中数据认证加密的方法总结

    在当今分布式系统的日益复杂和信息传递的广泛网络化环境中,数据的加密和认证作为保障信息传递安全的关键手段,Spring Cloud,作为一套构建微服务架构的强大框架,提供了多种灵活而强大的数据加密和认证方式,本文给大家总结了SpringCloud数据认证加密的方法
    2024-03-03
  • Java使用arthas修改日志级别详解

    Java使用arthas修改日志级别详解

    在我们线上环境中,一般不会开启debug级别的日志,为了提高性能 info和warning级别的日志也一般不会打印出来,那么如果遇到线上问题,除了使用arthas定位问题,想通过查询日志来实现问题定位,如何查看logger信息,更新logger level呢,下面我们来了解arthas修改日志级别
    2022-06-06
  • Mybatis日志配置方式(slf4j、log4j、log4j2)

    Mybatis日志配置方式(slf4j、log4j、log4j2)

    这篇文章主要介绍了Mybatis日志配置方式(slf4j、log4j、log4j2),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09

最新评论