如何動態替換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到容器中。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。 

推薦閱讀: