先聊一个场景:你手头有一个已经跑了好几年的 Spring Boot 项目,有一天产品提了个需求,要求给某一批服务接口动态增加逻辑——不是写死在代码里,而是根据配置中心下发的规则,在运行时决定哪些方法需要被增强、增强逻辑是什么。你第一反应可能是加个@Aspect切面,但切面是编译期写死的,没法在运行期按规则动态生成。这时候你就需要“在 Spring 容器运行过程中,手动创建动态代理对象,并把这个代理对象作为 Bean 注册进容器”,让其他业务代码能像注入普通 Service 一样注入它。
这个需求听起来偏底层,但其实并不冷门。凡是做框架封装、通用中间件、规则引擎、灰度发布、接口 Mock 的同学,大概率都会遇到。它涉及的三个核心问题分别是:怎么生成代理对象、怎么让 Spring 容器接受这个代理对象、以及怎么保证代理 Bean 的生命周期和普通 Bean 一致。这篇文章我会把这三个问题逐一拆开,给你一条能直接落地的实现路径。
1. 什么时候需要把代理对象当作 Bean 交给 Spring 管理
1.1 标准 Bean 声明方式的局限
在大多数业务项目里,我们声明 Bean 的方式基本就三种:@Component/@Service这类注解扫描,@Bean方法显式声明,或者通过@Import导入配置类。这三种方式都要求“Bean 的类型在编译期是确定的”。Spring 容器在启动阶段会读取 BeanDefinition,然后根据它去实例化、初始化、注入依赖。
但当你需要创建的对象在编译期根本不存在时,这套流程就不够用了。举个例子,你想给某个接口动态生成一个实现类,这个实现类的方法逻辑完全由运行时规则决定,甚至可能根据数据库里的配置拼装出来。你没法写死@Service或者@Bean,因为编译期压根没有这个类。唯一的办法是:在容器运行到某个阶段时,自己组装一个逻辑上完整的“Bean 描述”,再把代理对象放进去。
1.2 我在实际项目里遇到的两种典型需求
第一种是统一包装第三方 SDK。假设系统里对接了 5 个外部服务,每个服务都提供了一组 Feign Client 接口。需求方希望对这些接口统一做参数加解密、限流、审计。加切面当然可以,但如果第三方 SDK 的接口是动态加载的,或者不同租户对应不同接口实现,切面就不好使了。这时候动态创建代理 Bean,在代理里统一处理逻辑,是最干净的做法。
第二种是网关类的透传服务。我做过一个内部中间件,调用方只需要定义一个接口,中间件在运行期读取接口上的注解和配置,自动生成一个代理实现,注册到 Spring 容器,调用方直接@Autowired就能用。整个过程对业务方是透明的,他们完全不感知代理的存在。
这两种场景有一个共同点:代理对象的“类型”在编译期不可预知,但调用方希望“注入方式”和普通 Bean 完全一致。所以核心工作就变成了两件事——生成一个可用的代理实例,以及让 Spring 的依赖注入机制能够正确找到它。
提示:如果你只是想在静态代码里给某个 Bean 加日志、加事务,用
@Aspect或@Transactional就够了,不要绕这么大一圈。动态注册代理属于“非常规手段”,它解决的是常规手段覆盖不到的问题,杀鸡别用牛刀。
2. JDK 动态代理与 CGLIB 的实现差异,以及 Spring ProxyFactory 的统一
2.1 代理机制的两种底层实现
Java 世界里最常用的动态代理有两种:JDK 动态代理和 CGLIB。
JDK 动态代理要求目标对象必须实现接口。它通过Proxy.newProxyInstance(ClassLoader, Class<?>[], InvocationHandler)在运行期生成一个实现了指定接口的匿名类。所有方法调用都会被InvocationHandler.invoke拦截,你可以在调用前后插入逻辑。它的优点是原生 JDK 支持,反射性能经过长期优化;缺点是只能代理接口,不能代理类。
CGLIB 的原理完全不同。它是在运行期通过 ASM 字节码操作,生成目标类的子类,然后对子类的方法进行增强。所以 CGLIB 不要求目标对象有接口,直接代理类。代价是生成的字节码体积更大,且无法代理 final 方法和 final 类。
理解这两者的差异,对于后面“自动注入代理 Bean”非常重要。因为很多时候你手头只有一个类,没有接口;或者你连类都是运行期才动态生成的。选错机制,可能在启动阶段就报ClassCastException或者IllegalArgumentException。
2.2 框架里常见的一个“隐性坑”:代理类型与注入类型不一致
我见过不少初学 Spring AOP 的同事踩过同一个坑:一个 Service 实现了接口,类上加了@Transactional。按理说事务切面应该生效,但事务一直不生效。排查到最后发现,这个 Service 没有被接口类型注入,而是直接注入了实现类类型;而 Spring 默认对接口创建 JDK 动态代理,代理对象和实现类之间没有父子关系,直接按实现类注入就匹配不上。
这个例子看起来是注入类型的问题,但它暴露了动态代理一个非常核心的特性:动态代理生成的对象和原始对象不是同一个类型。你注入的是代理类型,而代理类型的继承体系完全由你选择的代理机制决定。如果你在运行期手动创建代理并注册到 Spring 容器,这个问题会被无限放大,因为你连“原始对象”都可能没有,完全是从零构造一个代理。
2.3 为什么我建议用 Spring 的 ProxyFactory 而不是直接 new Proxy
你当然可以直接用 JDK 动态代理或 CGLIB 生成一个对象,然后注册到容器。但实战中我不建议这样做,原因有三个:
第一,Spring 容器里已经有大量被代理过的 Bean,它们的创建流程走的是AbstractAutoProxyCreator这一套。如果你绕开它,直接 new 一个Proxy对象,这个对象就不会经过 Spring 的 AOP 链路,导致后面其他切面不会作用到这个代理 Bean 上。
第二,手动处理 JDK 动态代理和 CGLIB 的分支很繁琐。你既要检查目标对象是否有接口,又要处理不同的InvocationHandler/MethodInterceptor。而org.springframework.aop.framework.ProxyFactory内部已经做了统一封装:你只需指定代理目标(接口或类均可),它会自动选择合适的代理策略,并允许你添加多个MethodInterceptor。
第三,ProxyFactory生成的代理对象天然兼容 Spring 的 AOP 体系,比如AdvisedSupport提供的addAdvice、getInterceptorsAndDynamicInterceptionAdvice等接口,后续想动态增加切面逻辑也很方便。
所以,下面我给出的完整实现方案,会统一基于ProxyFactory。它虽然不是唯一选择,但它是 Spring 生态里最省心、最不容易出错的选择。
3. 将动态 Bean 注册进容器:核心代码与完整 Demo
3.1 注册时机:为什么不能等到别人 @Autowired 之后
在 Spring 容器的生命周期里,依赖注入发生在 Bean 创建之后、初始化之前。如果你希望某个动态代理 Bean 能在其他 Bean 注入时被正常使用,就必须保证“它已经出现在容器的依赖查找范围里”。
这里有两个经典时机可以注册 BeanDefinition:
BeanFactoryPostProcessor:容器加载完所有 BeanDefinition,但还没有实例化任何 Bean 时。此时你可以往容器里追加新的 BeanDefinition。BeanDefinitionRegistryPostProcessor:它是BeanFactoryPostProcessor的子接口,提供了一个postProcessBeanDefinitionRegistry方法。在这个方法里,你能直接拿到BeanDefinitionRegistry,调用registerBeanDefinition注册动态 Bean。这个方法执行时机比普通BeanFactoryPostProcessor还要靠前,非常合适。
我一般选择BeanDefinitionRegistryPostProcessor。原因很简单:它拿到的是BeanDefinitionRegistry,注册语义最清晰;而且在它的执行阶段,普通 Bean 都还没开始实例化,后续 Bean 创建时,Spring 已经知道容器里有这个动态代理 Bean 了,依赖注入自然能找到它。
3.2 完整示例:动态生成代理对象并注册
假设现在有这么一个业务场景:定义了一个接口MessageService,里面有两个方法send和receive。我希望在容器启动时,动态创建一个MessageService的代理 Bean,方法内部打印日志,然后交给 Spring 管理。
第一步,定义接口和业务类:
public interface MessageService { String send(String message); String receive(String channel); }第二步,写一个BeanDefinitionRegistryPostProcessor,在里面创建代理对象并注册:
import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.aop.support.AopUtils; import org.springframework.stereotype.Component; import org.aopalliance.intercept.MethodInterceptor; @Component public class DynamicProxyBeanRegistrar implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 1. 创建代理对象 ProxyFactory factory = new ProxyFactory(); factory.setInterfaces(MessageService.class); factory.addAdvice((MethodInterceptor) invocation -> { System.out.println("before method: " + invocation.getMethod().getName()); Object result = invocation.proceed(); System.out.println("after method: " + invocation.getMethod().getName()); return result; }); MessageService proxy = (MessageService) factory.getProxy(); // 2. 构造 BeanDefinition BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MessageService.class, () -> proxy); BeanDefinition beanDefinition = builder.getBeanDefinition(); // 3. 注册到容器 registry.registerBeanDefinition("dynamicMessageService", beanDefinition); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // 本示例中无需额外逻辑 } }第三步,在业务代码里像普通 Bean 一样注入使用:
@Service public class DemoService { @Autowired private MessageService messageService; public void doSomething() { String result = messageService.send("hello dynamic proxy bean"); System.out.println(result); } }这里有一个你可能没注意到的细节:BeanDefinitionBuilder.genericBeanDefinition(MessageService.class, () -> proxy)的第二参数是一个Supplier。Spring 在实例化这个 Bean 时,会直接调用这个Supplier.get()拿到实例,而不是走反射构造器。
这意味着:你可以在运行时完全控制“这个 Bean 到底长什么样”,甚至可以每次调用 Supplier 生成不同的实例。虽然 Spring 默认单例,但只要你想,也可以注册成 prototype 作用域。
3.3 为什么用 Supplier 而不是 setBeanClass + 反射实例化
有人会问:你都动态生成代理对象了,为什么不直接注册代理类的 Class?原因很简单,代理类是在运行期才由ProxyFactory动态生成的,你拿不到它的Class对象。即便你拿到了,它的构造函数参数、初始化逻辑也不可控。而Supplier这种“直接给实例”的方式,绕开了实例化策略问题,是动态 Bean 注册里最容易理解、最不容易出错的写法。
但要注意,Supplier方式有一个副作用:Spring 无法对这个 Bean 进行常规的属性填充和初始化回调。比如你在这个 Bean 里注入其他依赖、执行@PostConstruct,这些都不会自动发生。原因在于 Spring 的AbstractAutowireCapableBeanFactory在收到一个Supplier后,会优先把它当作“实例已经齐了,直接暴露”来处理。
这带来一个权衡:如果你动态代理的对象本身还需要依赖容器里的其他 Bean,就比较麻烦。我的建议是在代理对象内部通过 ApplicationContext 手动获取依赖,或者干脆让代理逻辑所需的依赖全部通过构造器传入——这也是动态代理 Bean 最推荐的做法。
4. 动态注册代理 Bean 时,Spring 生命周期最容易踩的三个坑
4.1 坑一:过早实例化引发循环依赖
很多人第一次用BeanDefinitionRegistryPostProcessor注册 Bean 时,会顺手在postProcessBeanDefinitionRegistry里直接调用registry.getBeanDefinition(...)或者beanFactory.getBean(...)。这会导致 Spring 提前实例化某些 Bean,而如果你注册的这个 Bean 又引用了同一个实例,就可能触发循环依赖。
我印象很深的一次事故:我在postProcessBeanDefinitionRegistry里创建动态代理时,为了给代理对象设置一个初始参数,提前从容器里拿了一个服务类。结果那个服务类又依赖了我正在注册的接口,启动时直接报BeanCurrentlyInCreationException。
正确做法是:在postProcessBeanDefinitionRegistry阶段,只注册 BeanDefinition,不提前触发任何 Bean 的实例化。如果需要依赖,把它交给代理对象的InvocationHandler/MethodInterceptor在运行时惰性获取,或者通过beanFactory.getBean在代理方法执行时才获取。
4.2 坑二:代理 Bean 的双重代理问题
这个坑最隐蔽。你动态创建代理 Bean 时,用ProxyFactory创建了一个代理实例。但 Spring 容器在实例化 Bean 之后,如果它发现这个 Bean 满足某些 AOP 条件,还会再套一层代理。尤其是当项目里有全局切面表达式,比如execution(* com.example..*.*(..))时,你注册的代理 Bean 极有可能被二次代理。
二次代理本身不致命,真正致命的是类型匹配错乱。假设你注册的是 JDK 动态代理(实现了MessageService),Spring 二次代理时如果也选择 JDK 动态代理,最终暴露出来的对象仍然实现了MessageService,注入没问题。但如果你的代理是 CGLIB 子类代理,而 Spring 二次代理判断目标上没有接口,又生成一个 CGLIB 子类,继承链多了两层,某些反射判断就会出乎意料地失败。
规避方案有两个:一是在ProxyFactory创建代理时,明确setProxyTargetClass(false),确保代理走 JDK 接口模式;二是给动态代理 Bean 的名称加前缀,把全局 AOP 表达式的匹配范围排除掉,比如切点表达式里限定包名,不扫描你这个动态 Bean 的注册名。
4.3 坑三:类型匹配与注入歧义
在 Spring 里,按类型注入的核心逻辑是DefaultListableBeanFactory.doResolveDependency。它会遍历容器里所有 BeanDefinition,找类型兼容的候选。动态代理 Bean 的类型来源是BeanDefinition里声明的beanClass。
如果你用genericBeanDefinition(MessageService.class, ...)注册,Spring 只知道这个 Bean 是MessageService类型。如果容器里还有其他也实现了MessageService的 Bean,@Autowired就会出现“expected single matching bean but found 2”的错误。
解决方式很简单:要么给注册的 Bean 一个清晰的名字,然后在注入处用@Qualifier("dynamicMessageService");要么在注册时把Primary属性设为 true,让动态代理 Bean 成为首选。我个人更推荐后者,因为动态代理 Bean 通常是用来“替代”某个默认实现的,设为 primary 更符合语义。
builder.setPrimary(true);这几个坑有一个共同本质:动态代理 Bean 虽然长得像 Bean,但它的创建过程绕过了 Spring 对普通 Bean 的完整管理流程。你需要手动去模拟那些被绕过的步骤,才能让它表现得像一个“守法公民”。
5. 我把这套方案落地到生产环境后的一些优化经验
5.1 用 FactoryBean 替代 PostProcessor 的场景选择
BeanDefinitionRegistryPostProcessor适合“容器启动时一次性注册多个动态 Bean”的场景。但如果你只是想把“某一个接口”的动态代理暴露给容器,且这个代理逻辑不依赖运行时配置,用FactoryBean反而更简单——你只需要实现FactoryBean<T>接口,配置一个@Bean方法返回FactoryBean实例,Spring 在容器中就会暴露getObject()返回的对象。
@Component public class MessageServiceFactoryBean implements FactoryBean<MessageService> { @Override public MessageService getObject() { ProxyFactory factory = new ProxyFactory(); factory.setInterfaces(MessageService.class); factory.addAdvice((MethodInterceptor) invocation -> { System.out.println("FactoryBean proxy: " + invocation.getMethod().getName()); return invocation.proceed(); }); return (MessageService) factory.getProxy(); } @Override public Class<?> getObjectType() { return MessageService.class; } @Override public boolean isSingleton() { return true; } }FactoryBean的优点是写起来直观,Spring 会把它当作普通 Bean 统一管理,也支持依赖注入和 AOP。缺点是它注册的 Bean 类型固定,如果你想在运行期根据配置动态决定注册哪些接口,就必须先存在对应的FactoryBean类。
所以我的选择标准很明确:类型是静态已知的,用 FactoryBean;类型是运行期动态发现的,用 BeanDefinitionRegistryPostProcessor。
5.2 代理链的叠加顺序:多个增强器如何排队
生产环境里,你给同一个代理对象加的增强不会只有一个。比如我先加了日志增强,又加了限流增强,这两个都通过ProxyFactory.addAdvice(...)添加。Spring 内部会按照添加顺序组成一个拦截器链。
这一点很容易被忽略,因为 AOP 是层层包裹的,后添加的 advice 在最外层还是最里层,直接决定了执行顺序。以我的经验,业务无关的增强(日志、监控)放前面,真正核心的业务逻辑增强放后面,这样日志能覆盖到限流逻辑的执行过程。
factory.addAdvice(new LoggingInterceptor()); factory.addAdvice(new RateLimitInterceptor());如果你想精确控制顺序,手动构造DefaultPointcutAdvisor并指定 order,比直接addAdvice更可控。
5.3 对动态代理 Bean 做缓存
容器启动时创建代理对象本身性能损耗不大,但如果你的代理工厂逻辑里包含了远程配置拉取、数据库查询、甚至规则引擎编译,那每次调用getProxy()都重新执行一遍就太浪费了。
我的做法是做一个ConcurrentHashMap<String, Object>缓存代理实例。以“接口类名 + 配置版本号”作为 key,配置发生变化时主动清缓存并重新生成代理。这样可以避免容器里同时存在多个版本不一致的代理对象,也能避免频繁创建代理带来的性能抖动。
缓存这里有一个细节:动态代理 Bean 本身一般声明为单例,但代理对象内部的状态可能随配置变化。所以缓存过期策略最好跟配置中心的通知机制绑定,而不是用定时任务去轮询,否则可能处理已过期的配置。
5.4 动态注册不要覆盖用户手动声明的 Bean
最后提醒一个非常实际的问题:动态注册的 Bean 名如果和已有 Bean 名冲突,registerBeanDefinition默认是直接覆盖。这在开发环境可能不报错,但生产环境一旦出现同名覆盖,且被覆盖的是一个关键业务 Bean,等排查到问题的代价就很大了。
所以我在注册前都会先做一次存在性检查:
public static void registerIfAbsent(BeanDefinitionRegistry registry, String beanName, BeanDefinition beanDefinition) { if (!registry.containsBeanDefinition(beanName)) { registry.registerBeanDefinition(beanName, beanDefinition); } else { log.warn("BeanDefinition [{}] already exists, skip dynamic registration.", beanName); } }如果确实需要覆盖,也要打印清晰的日志,并确保覆盖后的 Bean 满足所有注入方的契约。这种防御式的注册逻辑,能在后续迭代里省掉很多“不明原因故障”的排查时间。
以上这些经验,基本都是我在一次次事故和踩坑中总结出来的。动态代理 + 动态注册这套组合,用好了可以让框架层代码变得非常灵活,但也因为是“动态”的,排错比普通 Bean 困难得多。我的建议始终是:能用静态声明解决的,不要过度设计;确实需要动态能力时,再按照文章里提到的注册时机、类型匹配和生命周期注意点来实施,就能把风险控制在一个可接受的范围。