做 Java 开发这么多年,动态代理和 Spring 容器这两个东西分开看不难,但一旦要“在容器运行过程中动态生成一个代理对象,再把它注入到 Spring 容器里”,就特别容易踩坑。我最近在给一个老项目做接口监控增强时,正好把这个流程完整趟了一遍,从最初的@Bean手动返回代理对象,到后来用FactoryBean+BeanDefinitionRegistryPostProcessor实现全自动注册,中间翻过源码、排过循环依赖、还差点被事务注解坑到怀疑人生。今天把这套方案连同底层原理一起拆开讲清楚,希望对需要做动态代理 Bean 注入的朋友有点帮助。
如果你之前只写过@Aspect注解切面,或者只在测试代码里用Proxy.newProxyInstance()创建过代理,那这篇文章能帮你把这两件事串起来:动态代理负责生成增强对象,Spring 容器负责管理这个增强对象的生命周期。全文以一个完整的“动态代理 Bean 注册”实战为主线,覆盖 JDK 动态代理与 CGLIB 的选型、四种注入方式、Bean 生命周期源码级解析、常见踩坑与排查方法,代码可以直接抄走改改就能用。
1. 什么时候需要动态代理 Bean:三个我遇到过的真实场景
1.1 最典型的场景:给一批接口统一做性能监控
我这次的真实需求是这样的:老项目里有几十个 Service 接口,分布在不同的包里,业务代码没有统一日志埋点。领导要求上线前给所有 Service 加上耗时监控和异常告警,但不能大规模改动业务代码。
第一反应当然是写一个@Aspect切面,定义切点匹配所有 Service。但问题是,这些 Service 本身内部直接new了实现类,没有完全走 Spring 注入,一部分是静态方法调用,还有几个是历史遗留的单例手工管理。这种情况下,切面只能代理从容器里拿出来的 Bean,对于手工 new 出来的对象完全无能为力。
这时候动态代理 Bean 就有了用武之地:写一个通用的ServiceProxyFactory,在 Spring 容器扫描完 Bean 定义之后、创建 Bean 实例的过程中,动态拦截所有符合条件的 Service,生成代理对象,再把代理对象注册回容器。业务方即使拿着旧的 Bean 名称去getBean(),拿到的也是增强后的代理。
1.2 动态代理 Bean 和普通 AOP 的本质区别
我们要区分两件事。Spring AOP 做的也是动态代理,但它是“在 Bean 实例化之后,通过BeanPostProcessor包装一层代理返回”。而我们要讲的“动态代理 Bean 注入”,是“在容器运行时,主动创建出一个新的代理对象,然后将这个代理对象注册为一个新的 Bean”。
换句话说,普通的 AOP 是“对已有的 Bean 做增强”,动态代理 Bean 是“凭空生出一个新的增强型 Bean 放进容器”。这个区别直接影响了实现方式:一个靠注解和切点声明式实现,一个靠编码和容器扩展点手动实现。
1.3 还有哪些场景会用到这个能力
除了统一监控,我遇到过需要动态代理 Bean 注入的场景还有这些:
- 接口 Mock:在测试环境启动时,动态为某些远程接口生成返回假数据的代理 Bean,生产环境则不用加载,通过配置开关控制。
- 动态数据源切换:根据路由键为每个数据源生成代理 Mapper,动态决定走哪个库。
- 灰度发布:在运行时根据用户标识将请求转发到新老逻辑的代理 Bean 上,不用停机切换。
- 自定义 RPC 框架:在消费端通过动态代理生成远程服务的本地调用对象,典型的就是 Feign、Dubbo 的
@Reference注入。
这些场景的共同点都是:在编译期无法确定 Bean 的完整实现,只有在运行时才能通过规则、配置或动态逻辑来生成代理对象。
2. 选型前必须想清楚:JDK 动态代理和 CGLIB 到底用哪个
2.1 两者底层原理的差异
动态代理 Bean 的核心是“代理对象从哪来”,而 Java 生态里最常用的代理生成方式就是 JDK 动态代理和 CGLIB。很多新人搞不清,我直接说人话。
JDK 动态代理从 JDK 1.3 开始就有,它要求目标对象必须实现至少一个接口。它生成的代理对象和目标对象实现了同样的接口,但代理类本身是一个独立的类,不属于目标类的子类。外部调用时,代理对象会把方法调用转发给InvocationHandler.invoke()。它只认接口,所以如果你要代理的是一个没有实现任何接口的类,JDK 动态代理直接无能为力。
CGLIB 则走的是另一条路:它直接生成目标类的子类,覆写非 final 的方法,在覆写方法里插入增强逻辑。所以 CGLIB 不需要目标类实现接口,但也带来了一个限制——目标类和方法都不能是final的,否则无法覆写。
2.2 从方法调用链路看区别
JDK 动态代理生成代理类之后,调用过程是这样的:
// 用户调用 -> 代理类同名方法 -> InvocationHandler.invoke() -> 目标类方法CGLIB 的调用链是:
// 用户调用 -> 代理子类覆写方法 -> MethodInterceptor.intercept() -> FastClass 直接调用目标方法特别注意 CGLIB 的这条链路,它用了 FastClass 机制,通过生成索引直接跳到目标方法字节码位置,反射开销比 JDK 动态代理要小。这也是 CGLIB 在 JDK 17 之前高频调用场景下略占优势的原因。但随着 JDK 自身反射机制的不断优化,这个性能差异在大部分业务场景里已经可以忽略。
2.3 怎么选:记住这几个判断标准
选择不是看谁“更好”,而是看你的场景适配哪种。我用了这么多年,总结的标准很简单:
- 待代理对象有接口,优先用 JDK 动态代理。这是 Spring 的默认策略,相关类生成逻辑更成熟,排查问题资料也多。
- 没有接口,或者需要代理
toString()、equals()这类 Object 方法(JDK 动态代理不会代理它们),就用 CGLIB。 - 目标类是
final的,两种都没戏,只能手动包装,或者用 ByteBuddy 这种字节码库硬生成。 - 高并发 + 高频调用,实测两者差异不大,但 CGLIB 创建代理对象时由于需要生成字节码文件,首次加载时间明显更长。
Spring 容器里的@EnableAspectJAutoProxy(proxyTargetClass = true)全局开启 CGLIB,默认则是 JDK 代理优先。但在我们这篇文章讲的手动注册场景里,代理方式完全自己决定,不用受全局配置约束。
3. Spring 容器里注入动态代理 Bean 的四种写法
3.1 入门写法:在 @Configuration 里 @Bean 方法返回代理对象
最简单的方式,直接在配置类里写一个工厂方法:
@Configuration public class ProxyConfig { @Bean("userServiceProxy") public UserService userServiceProxy() { UserService target = new UserServiceImpl(); InvocationHandler handler = (proxy, method, args) -> { long start = System.currentTimeMillis(); Object result = method.invoke(target, args); System.out.println("方法 " + method.getName() + " 耗时 " + (System.currentTimeMillis() - start) + "ms"); return result; }; return (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler ); } }这是最直白的动态代理 Bean 注入方式:容器创建userServiceProxy这个 Bean 的时候,实际返回的是一个代理对象。直接注入@Autowired private UserService userServiceProxy;也能拿到这个代理。
但这种写法的问题很明显:
- 每个需要代理的接口都得手写一个
@Bean方法,接口一多,配置类爆炸。 - 如果需要代理的类不是自己写的,是在第三方 jar 包里,你连
@Bean方法都不好加。 - 无法实现“按规则批量代理”,例如“扫描某个包下所有 Service,全部代理”。
所以这种写法适合小规模、固定接口的快速调试,生产系统不建议依赖它。
3.2 进阶写法:FactoryBean 封装代理创建逻辑
FactoryBean是 Spring 提供的一个特殊 Bean:容器最终暴露给使用方的不是FactoryBean本身,而是getObject()返回的对象。这一点特别契合“动态代理 Bean”的需求。
@Component public class ProxyFactoryBean implements FactoryBean<UserService> { @Override public UserService getObject() { UserService target = new UserServiceImpl(); ProxyFactory factory = new ProxyFactory(); factory.setTarget(target); factory.addAdvice(new MethodInterceptor() { @Override public Object invoke(MethodInvocation invocation) throws Throwable { long start = System.currentTimeMillis(); Object result = invocation.proceed(); System.out.println("方法 " + invocation.getMethod().getName() + " 耗时 " + (System.currentTimeMillis() - start) + "ms"); return result; } }); return (UserService) factory.getProxy(); } @Override public Class<?> getObjectType() { return UserService.class; } @Override public boolean isSingleton() { return true; } }ProxyFactory是 Spring AOP 自带的代理工厂,它会根据目标对象自动选择 JDK 动态代理还是 CGLIB。用它可以省去自己写Proxy.newProxyInstance或者Enhancer的繁琐代码,而且能够直接跟 Spring 的 AOP 基础设施融合。
这个方案比@Bean方法优雅的地方在于:它把代理对象的创建逻辑封装到了一个类里,并且通过getObject()方法统一收口。但问题依然没有解决——还是针对某一个具体接口写的,做不到批量自动处理。
还有一个必须记住的小坑:如果容器里有FactoryBean<UserService>,那么通过@Autowired UserService拿到的是getObject()返回的代理对象;如果你想要拿FactoryBean本身,需要在注入点上用@Autowired FactoryBean<UserService>或者通过getBean("&proxyFactoryBean")来获取,注意名称前面带的这个&前缀,这个是 Spring 的约定。
3.3 高逼格写法:BeanDefinitionRegistryPostProcessor 动态注册 BeanDefinition
如果我要实现“扫描指定包下所有 XxxService 接口,自动为每个接口生成一个代理 Bean”的效果,手写@Bean和FactoryBean都顶不住,这时候就要请出BeanDefinitionRegistryPostProcessor这个容器扩展点了。
BeanDefinitionRegistryPostProcessor在 Spring 容器的“Bean 定义加载完成后、Bean 实例化之前”执行。它的核心能力就是往容器里动态注册BeanDefinition。换句话说,普通 Bean 是通过 XML、注解、Java Config 静态声明的,而用它可以做到运行时才确定 Bean 怎么注册。
结合FactoryBean的用法,注册逻辑是这样的:给每一个需要代理的接口,创建一个指向自定义FactoryBean的BeanDefinition,并把这个FactoryBean要代理的接口类型作为构造参数传进去。
@Component public class DynamicProxyBeanRegistrar implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { // 模拟扫描到的接口列表 Class<?>[] serviceInterfaces = scanServiceInterfaces(); for (Class<?> serviceInterface : serviceInterfaces) { registerProxyBean(registry, serviceInterface); } } private void registerProxyBean(BeanDefinitionRegistry registry, Class<?> serviceInterface) { GenericBeanDefinition definition = new GenericBeanDefinition(); // 注意:这里注册的是 FactoryBean,不是目标接口本身 definition.setBeanClass(ServiceProxyFactoryBean.class); // 把接口类型传给 FactoryBean 构造器 definition.getConstructorArgumentValues() .addGenericArgumentValue(serviceInterface); String beanName = lowerFirst(serviceInterface.getSimpleName()) + "Proxy"; registry.registerBeanDefinition(beanName, definition); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { // 这个阶段可以拿到所有 BeanDefinition,但还不建议直接实例化 } }对应的ServiceProxyFactoryBean需要写成接受 Class 参数的版本:
public class ServiceProxyFactoryBean implements FactoryBean<Object> { private final Class<?> targetInterface; public ServiceProxyFactoryBean(Class<?> targetInterface) { this.targetInterface = targetInterface; } @Override public Object getObject() { // 通过 ProxyFactory 生成代理,代理的接口是 targetInterface ProxyFactory factory = new ProxyFactory(); factory.setInterfaces(targetInterface); factory.addAdvice((MethodInterceptor) invocation -> { long start = System.currentTimeMillis(); Object result = invocation.proceed(); System.out.println(targetInterface.getSimpleName() + "#" + invocation.getMethod().getName() + " 耗时 " + (System.currentTimeMillis() - start) + "ms"); return result; }); return factory.getProxy(); } @Override public Class<?> getObjectType() { return targetInterface; } @Override public boolean isSingleton() { return true; } }关键点解释一下,为什么注册的 BeanDefinition 的beanClass是ServiceProxyFactoryBean而不是直接注册代理对象?因为代理对象本身无法被 Spring 完整管理,它只是一个普通 Java 对象;而FactoryBean是 Spring 容器的“一等公民”,容器会主动调用它的getObject()方法把产物暴露为 Bean。我们把“创建代理对象”这件事交给FactoryBean,把“决定什么时候注册、注册哪些”交给BeanDefinitionRegistryPostProcessor,两者配合,Spring 容器就能像管理普通 Bean 一样管理动态代理 Bean。
3.4 优雅写法:ImportBeanDefinitionRegistrar 配合启动类注解
BeanDefinitionRegistryPostProcessor是直接实现接口注册,而ImportBeanDefinitionRegistrar是 Spring 提供的“面向注解”的注册机制。平时做框架开发,比如写一个@EnableServiceProxy注解,在注解里引入一个 Registrar,开启后自动扫描并注册动态代理 Bean,这比让用户手动加@Component要友好得多。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(ServiceProxyRegistrar.class) public @interface EnableServiceProxy { String[] basePackages() default {}; }public class ServiceProxyRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { Map<String, Object> attributes = importingClassMetadata .getAnnotationAttributes(EnableServiceProxy.class.getName()); if (attributes == null) { return; } String[] basePackages = (String[]) attributes.get("basePackages"); // 按包扫描接口、生成代理 BeanDefinition for (String basePackage : basePackages) { scanAndRegister(registry, basePackage); } } }这四种写法不是互斥的,实际项目里往往组合使用。我个人的建议是:简单场景用@Bean,需要封装复用用FactoryBean,批量注册用BeanDefinitionRegistryPostProcessor,做框架和中间件用ImportBeanDefinitionRegistrar。
3.5 四种方案对比与适用场景
表格整理一下,方便大家做技术选型:
| 方案 | 代码量 | 适用场景 | 需要谨慎的点 |
|---|---|---|---|
| @Configuration + @Bean 返回代理对象 | 少 | 接口数量少、固定不变 | Bean 较多时代码冗余 |
| FactoryBean 封装 | 中 | 单一接口的代理逻辑复用 | 无法批量注册 |
| BeanDefinitionRegistryPostProcessor | 中高 | 按包/注解批量注册代理 Bean | 需要理解容器扩展点 |
| ImportBeanDefinitionRegistrar | 中高 | 做成 EnableXxx 注解给其他人用 | 需要设计注解参数 |
4. 动态代理 Bean 背后的 Spring 生命周期机制
4.1 从 BeanDefinition 到 Bean 实例
很多读者可能疑惑:为什么我用BeanDefinitionRegistryPostProcessor往容器里塞一个 BeanDefinition,Spring 容器就会自动帮我创建代理对象?这背后的关键在于 Bean 的完整生命周期。
Spring 容器启动之后,第一步是加载配置,生成一大堆消息者叫BeanDefinition的东西。BeanDefinition只描述“这个 Bean 的类是什么、依赖哪些属性、作用域是什么”,并不等于 Bean 本身。
然后,容器进入实例化阶段。当某个 Bean 被请求,或者容器在finishBeanFactoryInitialization()阶段预实例化所有非懒加载单例 Bean 时,会按照 BeanDefinition 的描述去创建实例。如果是普通的beanClass,Spring 通过反射调用构造器新建对象;如果beanClass指向一个FactoryBean,Spring 会先创建FactoryBean本身,然后调用getObject()方法,把返回对象作为最终的 Bean。
所以动态代理 Bean 的注册过程,本质上就是:
- 通过
BeanDefinitionRegistryPostProcessor向容器注册一个beanClass为ServiceProxyFactoryBean的 BeanDefinition。 - 容器推进生命周期到实例化时,创建
ServiceProxyFactoryBean实例。 - Spring 意识到它实现了
FactoryBean接口,于是调用getObject()。 getObject()内部动态生成代理对象,返回给容器。- 容器把代理对象放进单例池,之后所有通过名字或类型注入拿到的都是这个代理对象。
4.2 Spring 对“类型”的识别逻辑
用getBean(Class)按类型获取动态代理 Bean 时,一个容易踩坑的点是:Spring 是怎么判断这个 Bean 的类型是否匹配的?
答案是通过FactoryBean.getObjectType()。Spring 在getType()时不会真正创建代理对象,也不会真的调用getObject(),而是先调用getObjectType()看看它声称自己是什么类型。所以我们在ServiceProxyFactoryBean里必须正确实现getObjectType(),否则按类型注入会失败。
还有一个容易被忽略的细节:如果getObjectType()返回的是接口UserService,而容器里同时有多个实现该接口的 Bean,那么@Autowired UserService按类型注入依然会报NoUniqueBeanDefinitionException,因为 Spring 不知道应该选哪一个。这时需要配合@Qualifier,或者把代理 Bean 设置成@Primary。
4.3 动态代理 Bean 的生命周期和普通 Bean 有什么不同
动态代理 Bean 本质上还是通过FactoryBean创建出来的普通单例 Bean,所以它的生命周期和普通 Bean 几乎一致:实例化 -> 属性填充 -> 初始化 -> 使用 -> 销毁。
唯一有区别的是“实例化”这一步,普通 Bean 通过构造器反射完成,而动态代理 Bean 是在FactoryBean.getObject()里完成。这意味着,如果你在@PostConstruct或者InitializingBean.afterPropertiesSet()里对代理对象做初始化逻辑,这个逻辑应该写在getObject()之后,由使用方触发,而不是直接写在FactoryBean的初始化回调里。
举个例子,如果代理对象内部连接了外部资源,比如数据库连接池,你可能会想当然地在ServiceProxyFactoryBean的afterPropertiesSet()里创建连接。但这个连接是在getObject()返回之后才真正被使用的,而且如果代理的接口比较多,每个FactoryBean都会走一次初始化回调,容易把资源重复初始化。正确的做法是让代理对象的内部状态保持轻量,仅在需要时才建立连接。
5. 实战笔记与踩坑排查实录
5.1 代理连代理:@Transactional 和事务注解失效问题
动态生成的代理 Bean 看起来和普通 Bean 没什么两样,但如果你给代理的接口方法加了@Transactional,很可能遇到一个诡异的现象:代理 Bean 里的事务注解完全不生效。
原因要追溯到事务实现的机制。Spring 的声明式事务也是通过代理实现的,即TransactionInterceptor会在方法调用前开启事务,方法返回后提交事务。如果我们手动创建的代理是先于 Spring 的事务代理生成的,那么外部调用时,先进入我们自定义的MethodInterceptor,再进入事务拦截器。但这要求我们的代理必须是在“容器创建 Bean 的过程中”用ProxyFactory创建的,并且ProxyFactory添加了事务增强器。
如果用的是最简单的@Bean+Proxy.newProxyInstance方式生成代理,那么代理的InvocationHandler里根本没有事务逻辑,@Transactional注解自然被忽略。这不算 Spring 的缺陷,而是我们自己绕过了 AOP 自动代理机制。
解决办法有两个:
- 在手动
ProxyFactory上主动添加TransactionInterceptor。 - 更推荐的做法是,不要把动态代理 Bean 和声明式事务混在一起,事务增强交给 Spring AOP,动态代理只负责日志、监控、路由等横切逻辑。两层代理各有分工,避免互相干扰。
5.2 循环依赖:FactoryBean 之间的隐性相互引用
动态代理 Bean 如果依赖了另一个动态代理 Bean,就非常容易触发循环依赖问题。一个典型案例如下:
假设OrderService和UserService都是通过FactoryBean创建的代理 Bean,而OrderService的内部逻辑调用UserService,UserService又回调OrderService。这两个 Bean 在容器创建时会互相等待,Spring 默认的单例循环依赖处理机制是提前暴露ObjectFactory,利用三级缓存解决。但如果其中一个 Bean 是通过FactoryBean创建的,情况会变得更复杂。
三级缓存处理循环依赖时,会调用getEarlyBeanReference()方法提前暴露 Bean 的早期引用。对于FactoryBean创建的 Bean,早期暴露的其实是FactoryBean的早期引用,而不是getObject()返回的代理对象。如果别的 Bean 在依赖注入时拿到的还是早期引用,等代理对象真正生成之后,两边拿到的就不是同一个对象了。这种问题非常隐蔽,通常表现为运行时出现类转换异常或者对象状态不一致。
排查思路:先看控制台有没有BeanCurrentlyInCreationException,如果有,优先考虑重构依赖关系,让代理 Bean 尽量不在构造器里注入另一个代理 Bean。如果无法避免,可以尝试在FactoryBean内使用ObjectProvider<T>延迟获取依赖,把循环依赖的时机推后到实际调用阶段。
5.3 代理对象内的自调用:增强逻辑莫名消失
动态代理 Bean 的增强逻辑只对“从外部调用代理对象的方法”生效。如果代理对象内部的一个方法调用了另一个被增强的方法,例如:
public class UserServiceImpl { public void doA() { System.out.println("execute doA"); doB(); // 内部自调用 } public void doB() { System.out.println("execute doB"); } }外部调用doA(),代理工厂只增强了doA(),内部直接调用的doB()并不会再次经过代理。这其实不算动态代理 Bean 特有的问题,Spring AOP 也有一样的限制。解决思路有三个:一是把doB()的逻辑独立成另一个代理 Bean,二是通过ApplicationContext.getBean()获取代理对象来调用doB(),三是在代理工厂里对自调用做特殊处理(但实现复杂度较高,不推荐一般业务这么干)。
5.4 序列化问题:JDK Proxy 与 CGLIB 代理的序列化差异
在分布式场景里,如果动态代理 Bean 需要被序列化(比如放进 Redis 缓存、通过 Dubbo 传递),就不得不在意序列化的差异。
JDK 动态代理生成的代理类实现了java.io.Serializable接口,理论上可以被序列化,但前提是InvocationHandler本身也要可序列化,且被代理的目标对象不能持有不可序列化的资源。很多人在InvocationHandler里直接引用了DataSource或Connection,序列化时直接爆NotSerializableException。
CGLIB 代理生成的子类是否可序列化,取决于目标类是否继承了Serializable。如果目标类没有实现Serializable,那么代理子类也无法序列化。
实际做法:尽量不直接序列化代理对象,而是序列化代理对象背后的数据模型或者 DTO。代理对象的职责是方法拦截和增强,不应该承担数据持久化的角色。
5.5 类加载器问题:ClassCastException 在理不清的类加载器里
动态代理 Bean 还有一个容易忽视的坑:类加载器。如果你的项目运行在 Java EE 容器(如 Tomcat)里,容器使用WebAppClassLoader加载应用中引用的类。Proxy.newProxyInstance的第一个参数是目标对象的类加载器,必须与目标对象的类加载器保持一致,否则生成的代理类可能会被不同的类加载器加载,导致强制类型转换失败。
Spring 的ProxyFactory在传入目标对象时,会默认使用AopUtils里的工具方法获取正确的类加载器,通常不会出问题。但如果你是手工使用Proxy.newProxyInstance或Enhancer创建代理,一定要显式传递与目标类一致的类加载器,例如:
ClassLoader classLoader = target.getClass().getClassLoader(); // 传递 classLoader 给 Proxy.newProxyInstance 或 Enhancer这个坑在单体应用里很少暴露,一旦迁移到微服务容器化或 Java Agent 场景,就会出现偶发性的ClassCastException,而且特别难排查。
5.6 注册时机的微妙区别:在 Spring 启动的哪个阶段注册最安全
BeanDefinitionRegistryPostProcessor可以在非常早的阶段介入,但如果你注册的代理 Bean 依赖了容器里其他尚未定义的 Bean,而你又没有声明@DependsOn,很可能启动失败。更稳妥的做法是尽量依赖接口类型而不是具体实现,或者在FactoryBean中使用ApplicationContextAware延迟获取依赖。
以我的经验,注册动态代理的BeanDefinition,最安全的时机是在容器扫描完成之后的BeanFactoryPostProcessor阶段,因为这时候大部分用户 BeanDefinition 已经注册完成,但 Bean 实例尚未大量创建,不容易出现“先有实例后有定义”的顺序问题。
6. 最后分享一点我实测下来的经验
动态代理 Bean 注入这套方案,掌握之后能解决很多原本只能靠反射硬写的场景,但它确实比普通@Service注入复杂一个量级。如果只是给单体应用加几条日志,直接用 AOP 切面就够了,不必为了炫技而引入BeanDefinitionRegistryPostProcessor。如果你的场景确实是运行时动态生成 Bean,那么我的个人建议是优先用FactoryBean承载代理创建逻辑,而不是直接注册代理实例,因为FactoryBean能让 Spring 正确地管理生命周期和处理类型匹配。
我踩过的最深的一个坑,是在生产环境里突然出现“某个接口注入为 null”的问题。排查了两天,后来发现是FactoryBean的getObjectType()返回了null,Spring 在按类型注入的时候无法匹配到它。所以想提醒看到这里的朋友:无论你的代理工厂有多少花活,请至少确保getObject()、getObjectType()、isSingleton()这三个方法的行为是明确的,它们是整个机制正常运转的底线。
另外,如果你把动态代理用在类似“热更新”的场景,需要频繁注册新 Bean 并销毁旧 Bean,那么需要注意单例池和disposableBean的清理。Spring 默认不会在运行时重新扫描你新注册的 BeanDefinition,除非你手动触发DefaultListableBeanFactory.freezeConfiguration()的重新执行,或者使用ConfigurableApplicationContext.refresh()——但这个操作代价很大,不建议线上频繁使用。更可行的方法是把动态代理 Bean 的作用域设置为prototype,让容器每次都通过getObject()创建新的代理实例,避免缓存带来的不一致。
我一直觉得,动态代理 Bean 注入是一个能同时考验“Java 代理机制”和“Spring 容器生命周期”两个知识点的实践场景。能把这两块融会贯通的人,排查线上问题的时候思路会通透很多。