专栏名称: Java基基
一个苦练基本功的 Java 公众号,所以取名 Java 基基
目录
相关文章推荐
中国电建  ·  DeepSeek视角下的中国电建:点亮世界 ... ·  昨天  
Java编程精选  ·  患者带着DeepSeek来看病,医学博主自嘲 ... ·  3 天前  
芋道源码  ·  Spring AI + ... ·  昨天  
中国电建  ·  诗词歌赋样样精彩,DeepSeek太懂中国电建了 ·  3 天前  
51好读  ›  专栏  ›  Java基基

Spring 动态代理实现新老路径的一键切换

Java基基  · 公众号  ·  · 2025-02-18 11:55

正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、 商城 、支付、工作流、大屏报表、微信公众号、 ERP CRM AI 大模型 等等功能:

  • Boot 多模块架构:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服务架构:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 双版本

来源:juejin.cn/post/
7367286576127262732


前言

本篇文章主要介绍了代码迁移开关的技术需要,以及使用 Spring 动态代理以及动态 Bean 注册的功能,实现迁移路径收束的一键控制。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

背景

众所周知,由于 usercenter 中的业务域在银行架构中,应该处于其他业务域的上层,不应被业务域服务所依赖。

但现实就是, usercenter 管理了客户在 APP 上的用户信息,其他各种场景不免需要依赖这些信息,简直"倒反天罡"。

所以,就需要把这些用户信息下沉到业务域之下的统一域,产生了这次迁移。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

加开关

虽然,本次行为,主要是对数据读写逻辑的迁移,数据库表不做迁移。但这依然是一个危险的动作。

  1. usercenter 是一个基础服务,一旦出现故障,影响面很广
  2. new-usercenter 会把一些读写行为合并,可能会有相互影响。
  3. usercenter 从直接读写数据库模型,到读写接口 DTO,并且受到接口合并的影响,势必会对业务逻辑的代码做些调整。

所以基于风险考虑,最好有一个开关,如果生产出现问题,可以快速切回旧逻辑。

而且切回以后,由于数据库表没有变化,仅需对问题数据做处理即可。

那么问题来了,怎么加开关?

之前考虑有两种方案

  • 在业务层与数据层做开关,在核心业务代码不改动的情况下,实现对数据读写的转变。
  • 在业务层上做开关,通过接口屏蔽对调用方(接口层或其他业务层类)。

最后选择了在业务层之上做了一层开关,为了是最大限度保留原代码,减少本次迁移对其的影响。

就像上面所说的风险,由于调用方式、模型、逻辑收束的变化,数据层方案,即使要做一层防腐层,也势必会对原有 inner service 造成修改。

所以在复制了 inner service 的基础上,以 new service 为新路径做迁移。

硬编码方案

好的,经过了一段长时间的复制、接入、调整,我们已经创建了新的 service,接入了 new usercenter

那剩下就简单了,就是在 Controller 调用 Service 或 Service 相互调用的地方,分别注入 inner service new service ,根据开关的值,选择调用 inner service new service

那么,第一个问题来了,一个方法,可能会被多方调用,不能每次都重复写一次吧。

所以,大家都会的,搞一个实现两个 service 共有接口的代理类来处理开关,让 Controller 只调代理类。

有第二个问题, inner service 很多,20多个。

然后方法还多,一个类可以多达十几二十个,这个乘法就很明白了。

所以,“懒惰”是第一生产力,这不能自己搞,让 Spring 来做。

动态代理方案

生成代理类

就像上面图里写的,这20几个类,共几百个方法的 Proxy Service ,得让 Spring 来写。

众所周知,Spring 代理方式有两种 JDK 以及 Cglib:

  • 基于JDK的动态代理 基于接口的动态代理,用到的类是Proxy的 newProxyInstance 静态方法创建,要求被代理对象至少实现一个接口,如果没有,则不能创建代理对象。
  • 基于cglib的动态代理 要导入cglib第三方库,使用的类是 Enhancer 的create静态方法创建,要求被代理类不能是最终类,即不能用final修饰,如String类。

因为所有的 service 都有实现接口,所以优先采用了 JDK 的方式。

Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)

核心方法需要三个参数:

  • ClassLoader :咱们直接使用接口对应的 ClassLoader 即可
  • Class[] :就是代理类需要实现的接口
  • InvocationHandler :具体实现的代理逻辑

ClassLoader & Class[] 都很清楚, InvocationHandler 也很简单,上述的图中已经画出来了。

public class InnerServiceProxy<Timplements InvocationHandler {
    private final ZaLogger log = ZaLoggerFactory.getLogger(getClass());

    private ApplicationContext applicationContext;
    private Class interfaceClass;
    private INewService newService;
    private T originService;

    // 注入两个 service
    public Object bind(Class cls, ApplicationContext applicationContext) {
        interfaceClass = cls;
        this.applicationContext = applicationContext;
        String[] beanNames = applicationContext.getBeanNamesForType(interfaceClass);
        Arrays.stream(beanNames).forEach(name -> {
            if (name.endsWith("Proxy")) {
                log.info("跳过代理类{}", name);
                return;
            }
            T bean = applicationContext.getBean(name, interfaceClass);
            if (bean instanceof INewService) {
                log.info("{}注入迁移 Service{}", interfaceClass, bean);
                newService= (INewService) bean;
            } else {
                log.info("{}注入原始 Service{}", interfaceClass, bean);
                originService = bean;
            }
        });
        Assert.notNull(originService, "原始 Service 不能为空");
        return Proxy.newProxyInstance(cls.getClassLoader(), new Class[] {cls}, this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Objects.isNull(newService)) {
            log.info("{} 执行, 无迁移 Service, 执行原始 Service,bean={}", interfaceClass.getSimpleName(), originService);
            return method.invoke(originService, args);
        }
        Boolean migrateSwitch = applicationContext.getEnvironment().getProperty("migrate.switch", Boolean.class);
        log.info("迁移配置={}", migrateSwitch);
        if (BooleanUtils.isTrue(migrateSwitch)) {
            log.info("执行 {}, 迁移 service, config={}, bean={}", interfaceClass.getSimpleName(), migrateSwitch , newService);
            return method.invoke(newService, args);
        }
        log.info("执行 {}, 原始 service, config={}, bean={}", interfaceClass.getSimpleName(), migrateSwitch , originService);
        return method.invoke(originService, args);
    }
}

将代理类 Bean Definition 注册到 Spring 容器中

有了代理类实现,就把代理类的 Bean Definition 注册到容器中,且是涉及的所有接口。

那就需要用到Spring的扩展点: public interface BeanDefinitionRegistryPostProcessor

所有实现了该接口的类,会在 Spring 容器准备好后,就自动被 Spring 容器调用执行实现的方法。

public class InnerServiceRegistryBean implements ApplicationContextAwareBeanDefinitionRegistryPostProcessor {
    private final ZaLogger log = ZaLoggerFactory.getLogger(getClass());
    private ApplicationContext ctx;
    @Autowired
    private ResourceLoader resourceLoader;
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {// do nothing}
    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {this.ctx = ctx;}

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
        MetadataReaderFactory metaReader = new CachingMetadataReaderFactory(resourceLoader);
        Resource[] resources;
        try {
            // 扫描 inner 包下所有文件
            resources = resolver.getResources("classpath*:com/usercenter/service/inner/**/*.class");
            for (Resource resource : resources) {
                MetadataReader reader = metaReader.getMetadataReader(resource);
                String className = reader.getClassMetadata().getClassName();
                Class> cls = Class.forName(className);
                // 只需要代理实现接口        
                if (!cls.isInterface()) {
                    log.info("不是接口, 跳过代理{}", cls);
                    continue;
                }
                // 根据接口创建一个 BeanDefinition
                BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(cls);
                GenericBeanDefinition definition = (GenericBeanDefinition) builder.getRawBeanDefinition();
                // 注入属性,接口类型、以及 上下文(用于后续初始化或执行时,获取新老 bean 以及属性)
                definition.getPropertyValues().add("interfaceClass", cls);
                definition.getPropertyValues().add("applicationContext", ctx);
                // 注意,这里的实现类是 InnerServiceProxyFactory,不是 InnerServiceProxy
                definition.setBeanClass(InnerServiceProxyFactory.class);
                definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
                // 将代理类设为 Primary,所有依赖该接口的地方,优先注入该代理类的实现
                definition.setPrimary(true);
                // 固定后缀,方便后续判断是代理类
                beanDefinitionRegistry.registerBeanDefinition(cls.getSimpleName() + "Proxy"






请到「今天看啥」查看全文