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

目录
  • 动态替换Spring容器中的Bean
    • 原因
    • 方案
    • 实现
  • 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到容器中。

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

(0)

相关推荐

  • 创建动态代理对象bean,并动态注入到spring容器中的操作

    使用过Mybatis的同学,应该都知道,我们只需要编写mybatis对应的接口和mapper XML文件即可,并不需要手动编写mapper接口的实现.这里mybatis就用到了JDK动态代理,并且将生成的接口代理对象动态注入到Spring容器中. 这里涉及到几个问题.也许有同学会有疑问,我们直接编写好类,加入@Component等注解不是可以注入了吗?或者在配置类(@Configuration)中直接声明该Bean类型不也可以注入吗? 但具体到mybatis,这里我们用的是接口.由于spring

  • Spring 框架中注入或替换方法实现

    无状态 Bean 的作用域是 singleton 单实例,如果我们向 singleton 的 Bean A 注入 prototype 的 Bean B,并希望每次调用 Bean A 的 getBeanB() 时都能返回一个新的 Bean B ,这样的要求使用传统的注入方式是无法实现的 . 因为 singleton 的 Bean 注入关联 Bean 的动作只发生一次,虽然 Bean B 的作用域是 prototype 类型,但通过 getBeanB() 返回的对象还是最开始注入的那个 bean B

  • Spring容器中已经存在的Bean替换示例

    目录 一.背景 二.需求 三.实现思路 四.实现步骤 1.模拟第三方jar包实现并加入Spring容器中 2.自己提供一个实现 3.替换掉jar包默认的实现 4.进行测试 一.背景 我们在开发的过程中,经常会引入别人写的jar包实现某些功能.而别人的jar包一般都自动注入Spring容器中,假设别人都是通过@Bean或@Component注入的,并且没有加入@ConditionalXXX等注解,导致自己无法替换掉别人的实现,假设这个时候我就是想替换掉,那么该如何实现呢? 二.需求 由上图可知,我

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

    目录 动态替换Spring容器中的Bean 原因 方案 实现 Spring中bean替换问题 动态替换Spring容器中的Bean 原因 最近在编写单测时,发现使用 Mock 工具预定义 Service 中方法的行为特别难用,而且无法精细化的实现自定义的行为,因此想要在 Spring 容器运行过程中使用自定义 Mock 对象,该对象能够代替实际的 Bean 的给定方法. 方案 创建一个 Mock 注解,并且在 Spring 容器注册完所有的 Bean 之后,解析 classpath 下所有引入该

  • Spring容器中添加bean的5种方式

    目录 @Configuration + @Bean @Componet + @ComponentScan @Import注解导入 @Import直接导入类 @Import + ImportSelector @Import + ImportBeanDefinitionRegistrar @Import + DeferredImportSelector 使用FactoryBean接口 使用 BeanDefinitionRegistryPostProcessor 小结 我们知道平时在开发中使用Spri

  • SpringBoot普通类获取spring容器中bean的操作

    前言 在spring框架中,是无法在普通类中通过注解注入实例的,因为sping框架在启动的时候,就会将标明交给spring容器管理的类进行实例化,并梳理他们彼此的依赖关系,进行注入,没有交给spring容器管理的普通类,是不会进行注入的,即使你使用了注入的相关注解.这个时候,如果我们需要在普通类中获取spring容器中的实例,就需要一些特定的方法,这里将整理一下如何在springboot中实现这样的方法. 创建springboot工程demo 项目结构图示 项目结构说明 service包下为de

  • Java 如何从spring容器中获取注入的bean对象

    1.使用场景 控制层调用业务层时,控制层需要拿到业务层在spring容器中注入的对象 2.代码实现 import org.apache.struts2.ServletActionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.suppo

  • Spring通过工具类实现获取容器中的Bean

    目录 1. Aware 接口 2. BeanFactoryAware 3. TienChin 项目实践 1. Aware 接口 小伙伴们知道,Spring 容器最大的特点在于所有的 Bean 对于 Spring 容器的存在是没有意识的,因此我们常说理论上你可以无缝将 Spring 容器切换为其他容器(然而在现实世界中,我们其实没有这样的选择,除了 Spring 容器,难道还有更好用的?). 当然这只是一个理论,在实际开发中,我们往往要用到 Spring 容器为我们提供的诸多资源,例如想要获取到容

  • 使用spring容器在初始化Bean时前和后的操作

    目录 spring容器初始化Bean操作 @PostConstruct和@PreDestroy注解 在XML中定义init-method和destory-method方法 Bean实现InitializingBean和DisposableBean接口 Spring bean 初始化顺序 1.概述 2.InitializingBean vs init-method 3.@PostConstruct 4.小结一下吧 spring容器初始化Bean操作 在某些情况下,Spring容器在初始化Bean的

  • spring在IoC容器中装配Bean详解

    1.Spring配置概述 1.1.概述 Spring容器从xml配置.java注解.spring注解中读取bean配置信息,形成bean定义注册表: 根据bean定义注册表实例化bean: 将bean实例放入bean缓存池: 应用程序使用bean. 1.2.基于xml的配置 (1)xml文件概述 xmlns------默认命名空间 xmlns:xsi-------标准命名空间,用于指定自定义命名空间的schema文件 xmlns:xxx="aaaaa"-------自定义命名空间,xx

  • 浅谈spring容器中bean的初始化

    当我们在spring容器中添加一个bean时,如果没有指明它的scope属性,则默认是singleton,也就是单例的. 例如先声明一个bean: public class People { private String name; private String sex; public String getName() { return name; } public void setName(String name) { this.name = name; } public String get

随机推荐