欢迎光临

我们一直在努力
当前位置:首页 > 编程技术 >

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

日期:
后台-插件-广告管理-首页/栏目/内容广告位一(PC)
后台-插件-广告管理-首页/栏目/内容广告位一(手机)
目录
  • 动态替换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 annotationhttp:// = 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;
            }
        }
    }
    [!--empirenews.page--]

    有点儿不一样的是这是一个配置类,将它放置到 Spring 的自动扫描路径上,就可以自动扫描 classpath 下 @FakeBejsanFor 指定的类,并将其加载为 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 = http://www.cppcns.com/ruanjian/java/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("找到了需要进行替换的类。。。。。。。。。。。");
        js        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 {
        }
    [!--empirenews.page--]

    配置文件中的格式采用下面的样式 :

    replaceClass=gov.df.fap.service.OldTaskBO#gov.df.newmodel.service.NewTaskBO

    在启动的时候,会找到容器中的老的bean,将其remove掉,然后手动注册新的bean到容器中。

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

    后台-插件-广告管理-首页/栏目/内容广告位二(PC)
    后台-插件-广告管理-首页/栏目/内容广告位二(手机)
    后台-插件-广告管理-内容广告位三(PC)
    后台-插件-广告管理-内容广告位三(手机)

    相关阅读

    • Spring深入分析容器接口作用

    • 目录1.容器接口有哪些2.BeanFactory能干嘛3.ApplicationContext有哪些扩展功能3.1 MessWWuzIefbageSource3.2 ResourcePatternResolver3.3 EnvironmentCapable3.4 Applicati
    后台-插件-广告管理-内容广告位四(PC)
    后台-插件-广告管理-内容广告位四(手机)

    热门文章

    后台-插件-广告管理-侧边广告位一(PC)
    后台-插件-广告管理-侧边广告位一(手机)
    • HTML 表单组件实例代码

    • HTML 表单用于搜集不同类型的用户输入。下文通过代码给大家分享html 表单组件实例代码,感兴趣的朋友参考下吧 废话不多说了,直接给大家贴代码了,具体代码如下所示: <!DOCTYPE
    • html2canvas 将html代码转为图片的使用方法

    • 转换代码到图片使用 html2canvas,这是一个非常著名的从浏览器网页截图的开源库,使用很方便,功能也很强大。 使用 html2canvas http:// html2canvas 的使用非常简单,简单
    • HTML网页中插入视频的方法小结

    • 现在如果要在页面中使用video标签,需要考虑三种情况,支持Ogg Theora或者VP8(如果这玩意儿没出事的话)的(Opera、Mozilla、Chrome),支持H.264的(Safari、IE 9、Chrome),都不支持的(IE6、
    • HTML实现文本框只读不能修改其中的内容

    • 废话不多说了,直接给大家贴代码了,具体代码如下所示: <!--方法1:>http:// 当鼠标放不上就离开焦点 --> <input type="text" name="input1" value=http://www.cppcns.com/web
    • 移动端专用的meta标签设置大全

    • 前言 之前学习前端中,对meta标签的了解仅仅只是这一句。 <meta charset="UTF-8"> 但是打开任意的网站,其head标签内都有一列的meta标签。比如我们我们网站,但是自己却很不熟
    后台-插件-广告管理-侧边广告位二(PC)
    后台-插件-广告管理-侧边广告位二(手机)

    最新文章

    • 在Asp.net core项目中使用WebSocket

    • 今天小试了一下在ASP.NET core中使用websocket,这里记录一下: 在 Startup 类的 Configure 方法中添加 WebSocket 中间件。 app.UseWebSockets(); 它也可以传入一些参数 app.Us
    • Vue快速理解事件绑定是什么

    • 目录一、监听事件二、事件修饰符1、stop修饰符阻止事件冒泡2、capture修饰符3、self修饰符4、prevent修饰符5、键盘事件修饰符6、鼠标事件修饰符一、监听事件 监听事件一般
    • C#实现模拟ATM自动取款机功能

    • 目录(1)关于用户帐号的类:Account(2)关于银行数据库的类:BankDatabase(3)关于ATM屏幕显示的类:Screen(4)关于ATM键盘的类:Keypad(5)关于进钞、出钞口的类:DepositSlot(6)关于ATM
    • Java设计模式之抽象工厂模式浅析讲解

    • 1.介绍 当系统准备为用户提供一系列相关对象,又不想让用户代码和这些对象形成耦合时,就可以使用抽象工厂模式。 2.如何实现 1)抽象产品--Car 2)具体产品--BYDCar、TSLCar 3)抽象
    • 如何动态替换Spring容器中的Bean

    • 目录动态替换Spring容器中的Bean原因方案实现Spring中的bean替换问题动态替换Spring容器中的Bean 原因 最近在编写单测时,发现使用 Mock 工具预定义 Service 中方法的行为特
    • C#优雅的实现INotifyPropertyChanged接口

    • INotifyPropertyChanged接口在wpF或WinFrom程序中使用还是经常用到,常用于通知界面属性变更。标准写法如下: class NotifandroidyObject : INotifyPropertyChanged {
    后台-插件-广告管理-侧边广告位三(PC)
    后台-插件-广告管理-侧边广告位三(手机)