zoukankan      html  css  js  c++  java
  • Spring扩展:替换IOC容器中的Bean组件 -- @Replace注解

    1、背景:

        工作中是否有这样的场景?一个软件系统会同时存在多个不同版本,比如我现在做的IM系统,同时又作为公司的技术输出给其他银行,不同的银行有自己的业务实现(登陆验证、用户信息查询等)。或者你的工程里依赖了其他第三方的jar,这些jar包里的组件都是通过Spring容器来管理的,如果你想修改某个类里面的部分逻辑,怎么办呢?是否可以考虑下直接把Spring容器里的某个组件(Bean)替换成你自己实现的Bean?

    2、原理&实现

    2.1 先看看Spring开放给我们的扩展

        Spring框架超强的扩展性毋庸置疑,我们可以通过BeanPostProcessor来简单替换容器中的Bean(Spring中主要有两种后处理器:BeanFactoryPostProcessor和BeanPostProcessor)。

    @Component
    public class MyBeanPostProcessor implements ApplicationContextAware, BeanPostProcessor {
        private ApplicationContext applicationContext;
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            if (beanName.equals("defaultConfig")) {
                // 如果遇到需要替换的Bean,我们直接换成自己实现的Bean即可(这里可以把就得removeBeanDefinition,然后注册新的registerBeanDefinition)
                // 这里的myConfig要继承自defaultConfig,否则引用的地方会报错
                return applicationContext.getBean("myConfig");
            }
            return bean;
        }
    }
    

    优点:

    • 直接利用Spring原生的扩展,可以平滑升级
    • 实现简单,易操作好理解,对于只需要替换少数几个Bean的情况下推荐这种方式

    缺点:

    • beanName硬编码在代码里,虽然可以把替换关系配置在properties里,但是在多版本部署,替换Bean较多时,维护这种关系将是一种负担
    • 仅仅是替换了Bean对象,对于容器中元数据如BeanDefinition等等均是原对象的,存在一定局限性

    2.2 更灵活一点的替换方式

        Spring容器是可以直接动态注册Bean的,而且如果注册的Bean与容器中现有的Bean同名,则会直接替换现有Bean,这样可以绕过Spring启动时解析BeanDefinition的checkCandidate检查。更重要的是其他引用原来Bean的组件的地方都会替换成新注册的Bean,所以这种功能是能直接满足我们上面场景的。

        public static void main(String[] args) {
            SpringApplication springApplication = new SpringApplication(DynamicRegisteringBean.class);
            springApplication.setAllowBeanDefinitionOverriding(true);
            ConfigurableApplicationContext ctx = springApplication.run(args);
            System.out.println(((DefaultConfig) ctx.getBean("defaultConfig")).getName());
            ctx.getBean(UseBean.class).printName();
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MyConfig.class);
            beanDefinitionBuilder.addPropertyValue("name", "this is a DynamicConfig");
            BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) ctx;
            // 动态注入同名Bean,会直接覆盖之前的Bean,并且容器中其他Bean对当前Bean的引用也会被更新
            beanDefinitionRegistry.registerBeanDefinition("defaultConfig", beanDefinitionBuilder.getBeanDefinition());
            System.out.println(((DefaultConfig) ctx.getBean("defaultConfig")).getName());
            ctx.getBean(UseBean.class).printName();
        }
    

    上面代码中MyConfig继承了DefaultConfig,并重写了getName方法,且MyConfig默认是没有被@Component注解修饰的,完整代码可参考:https://github.com/hiccup234/spring-ext/tree/master/src/test/java/top/hiccup/spring/ext/test/replace/dynamic
    运行结果如下:

    相比2.1中通过BeanPostProcessor来实现替换Bean组件,动态注册的方法会更灵活一些,也不存在元数据不匹配的问题,但是又引入一个新的问题:需要我们自己手动创建BeanDefinition,这相当于要应用程序去关注和完成解析Bean的工作,使得Spring对应用程序的侵入性变高。

    2.3 更优雅一点的替换方式

        Spring实际上就是一个容器,底层其实就是一个ConcurrentHashMap。如果要替换Map中的Entry,再次调用put方法设置相同的key不同的value就可以了。同理,如果要替换Spring容器中的Bean组件,那么我们重新定义一个同名的Bean并注册进去就可以了。当然直接申明两个同名的Bean是过不了Spring中ClassPathBeanDefinitionScanner的检查的,这时候需要我们做一点点扩展。

    实现自己的ClassPathBeanDefinitionScanner

    目前的想法是直接重写checkCandidate方法,通过判断Bean的类上是否有@Replace注解,来决定是否通过检查。

    依次往上扩展就到了ConfigurationClassPostProcessor,这是Spring中非常重要的一个容器后置处理器BeanFactoryPostProcessor(上面我们用的是Bean后处理器:BeanPostProcessor),重写processConfigBeanDefinitions方法就可以引入自己实现的ClassPathBeanDefinitionScanner。具体细节可以参考:https://github.com/hiccup234/spring-ext.git

    3、使用示例

        直接在项目中增加如下坐标(Maven中央仓库),目前这个版本是对Spring的5.2.2.RELEASE做扩展,新版本的Spring其相对3.X、4.X有部分代码变动。

    <dependency>
        <groupId>top.hiccup</groupId>
        <artifactId>spring-ext</artifactId>
        <version>5.2.2.0-SNAPSHOT</version>
    </dependency>
    

    对Spring Boot中的SpringApplication做一点扩展,将上面扩展的ConfigurationClassPostProcessor类以RootBeanDefinition注册到容器中。

    声明一个自己的类,然后继承需要替换的Bean的类型(这样就可以重写原Bean中的某些方法,从而添加自己的处理逻辑),然后用@Replace("defaultConfig")修饰,如下:

    通过ExtSpringApplication启动,可以看到,实际Spring容器中的Bean已经替换成我们自己实现的Bean组件了。

  • 相关阅读:
    Redis学习小结
    抽屉模型
    用户提交数据的验证
    jsonp原理与实验
    文件上传
    项目
    CBV
    C++算法 线段树
    写一些奇怪的东西找到的奇怪的错误
    php安装过程出现的一些错误问题:
  • 原文地址:https://www.cnblogs.com/ocean234/p/12320633.html
Copyright © 2011-2022 走看看