Skip to content

使用 ProxyFactoryBean 创建 AOP 代理

如果你在业务对象中使用 Spring IoC 容器(ApplicationContextBeanFactory)——这也是你应该做的——那么你通常会希望使用 Spring 的 AOP FactoryBean 实现之一。(记往,工厂 Bean 引入了一层间接性,允许它创建不同类型的对象。)

TIP

Spring AOP 的底层实现也大量使用了工厂 Bean。

在 Spring 中创建 AOP 代理的基本方式是使用 org.springframework.aop.framework.ProxyFactoryBean。这允许你完全控制切入点、应用的通知及其顺序。然而,如果你不需要如此精细的控制,还有更简单的替代方案。

基础知识

与其他 Spring FactoryBean 实现一样,ProxyFactoryBean 引入了一层间接性。如果你定义了一个名为 fooProxyFactoryBean,那么引用 foo 的对象看到的不是 ProxyFactoryBean 实例本身,而是由 ProxyFactoryBean 实现的 getObject() 方法所创建的对象。该方法会创建一个包装了目标对象的 AOP 代理。

使用 ProxyFactoryBean(或其他感知 IoC 容器的类)创建 AOP 代理的最重要好处之一是,通知和切入点也可以由 IoC 进行管理。这是一个强大的功能,可以实现其他 AOP 框架难以实现的方法。例如,通知本身可以引用应用程序对象(除了在任何 AOP 框架中都可用的目标对象外),从而从依赖注入提供的所有可插入性中受益。

JavaBean 属性

与 Spring 提供的显大多数 FactoryBean 实现类似,ProxyFactoryBean 类本身也是一个 JavaBean。它的属性用于:

一些核心属性继承自 org.springframework.aop.framework.ProxyConfig(Spring 中所有 AOP 代理工厂的超类):

  • proxyTargetClass:如果为 true,则代理目标类本身,而不是目标类的接口。如果设置为 true,则会创建 CGLIB 代理。
  • optimize:控制是否对通过 CGLIB 创建的代理应用激进优化。除非你完全了解相关 AOP 代理如何处理优化,否则不要轻率使用此设置。目前仅对 CGLIB 代理有效,对 JDK 动态代理没有影响。
  • frozen:如果代理配置被“冻结”,则不再允许更改配置。这既是一种轻量级性能优化,也适用于你不希望调用者在代理创建后(通过 Advised 接口)操作代理的情况。默认值为 false
  • exposeProxy:确定当前代理是否应暴露在 ThreadLocal 中,以便目标对象可以访问它。如果目标对象需要获取代理且此属性为 true,则可以使用 AopContext.currentProxy() 方法。

ProxyFactoryBean 特有的其他属性包括:

  • proxyInterfaces:接口名称的 String 数组。如果未提供,则对目标类使用 CGLIB 代理。
  • interceptorNames:要应用的 Advisor、拦截器或其他通知名称的 String 数组。顺序至关重要,遵循先到先得原则。即列表中的第一个拦截器将首先拦截调用。
    • 这些名称是当前工厂(包括父工厂)中的 Bean 名称。你不能在这里直接填入引用,因为这样做会导致 ProxyFactoryBean 忽略通知的单例设置。
    • 你可以使用星号 (*) 作为后缀。这将导致所有名称以前缀开头的 Advisor Bean 都被应用。
  • singleton:无论调用多少次 getObject()(), 工厂是否应返回单个对象。默认值为 true。如果你想使用有状态的通知(例如有状态的混入),请将此项设为 false 并结合 prototype(多例)通知。

基于 JDK 和 CGLIB 的代理

本节是关于 ProxyFactoryBean 如何选择为特定目标对象创建 JDK 代理或 CGLIB 代理的权威说明。

TIP

ProxyFactoryBean 在维护版本 1.2.x 和 2.0 之间发生了变化。它现在与 TransactionProxyFactoryBean 具有相似的自动检测接口的语义。

  1. 如果目标类没有实现任何接口,则创建 CGLIB 代理。这是最简单的场景,因为 JDK 代理是基于接口的,没有接口就意味着无法使用 JDK 代理。
  2. 如果目标类实现了一个或多个接口,具体情况取决于配置:
    • 如果 proxyTargetClass 属性为 true,则创建 CGLIB 代理。这符合“最少惊讶原则”。即使 proxyInterfaces 属性已设置,proxyTargetClass=true 也会强制使用 CGLIB。
    • 如果 proxyInterfaces 属性已设置(一个或多个全类名),则创建 JDK 代理。代理将实现该属性中指定的所有接口。如果目标类实现了更多接口,那些多出来的接口将不会被代理(即无法通过代理对象强转为那些接口)。
    • 如果 proxyInterfaces 属性未设置,但目标类实现了一个(或多个)接口,ProxyFactoryBean自动检测并创建 JDK 代理。此时所有目标类实现的接口都会被代理。这比手动列出所有接口更省力且不易出错。

代理接口 (Proxying Interfaces)

让我们看一个 ProxyFactoryBean 的实际应用示例:

xml
<bean id="personTarget" class="com.mycompany.PersonImpl">
	<property name="name" value="Tony"/>
	<property name="age" value="51"/>
</bean>

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
	<property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>

<bean id="person"
	class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="proxyInterfaces" value="com.mycompany.Person"/>

	<property name="target" ref="personTarget"/>
	<property name="interceptorNames">
		<list>
			<value>myAdvisor</value>
			<value>debugInterceptor</value>
		</list>
	</property>
</bean>

在代码中使用:

java
Person person = (Person) factory.getBean("person");
kotlin
val person = factory.getBean("person") as Person

你可以通过使用匿名内部 Bean 来隐藏目标对象和代理之间的区别,从而确保容器中只有一个该类型的 Bean,避免自动装配歧义:

xml
<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="proxyInterfaces" value="com.mycompany.Person"/>
	<!-- 使用内部 Bean,而不是本地引用 -->
	<property name="target">
		<bean class="com.mycompany.PersonImpl">
			<property name="name" value="Tony"/>
		</bean>
	</property>
	<property name="interceptorNames">
		<value>myAdvisor</value>
	</property>
</bean>

代理类 (Proxying Classes)

如果你需要代理一个类而不是接口,只需将 proxyTargetClass 设为 true。虽然推荐面向接口编程,但在处理遗留代码时,代理类会非常有用。

CGLIB 代理通过在运行时生成目标类的子类来工作。Spring 配置此子类将方法调用委托给原始目标,并织入通知。

CGLIB 的局限性:

  • final 类无法被代理(无法继承)。
  • final 方法无法被通知(无法重写)。
  • private 方法无法被通知(无法重写)。

TIP

你不需要手动添加 CGLIB 依赖。CGLIB 已被重命名并包含在 spring-core JAR 包中。

使用“全局” Advisor

通过在拦截器名称后添加星号,可以将所有匹配前缀的 Advisor 添加到链中:

xml
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="target" ref="service"/>
	<property name="interceptorNames">
		<list>
			<value>global*</value>
		</list>
	</property>
</bean>

<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>

补充教学

1. 深度思考:为什么 interceptorNames 使用 String 数组?

这是很多开发者初见 ProxyFactoryBean 时的疑问。为什么要传 Bean ID 字符串,而不是直接用 <ref> 依赖注入对象?

  • 延迟加载与多例支持:如果 ProxyFactoryBean 本身是多例(Prototype),或者它包含有状态的通知,那么它需要在每次调用 getObject() 时去容器里请求全新的通知实例。如果使用 <ref>,通知会在 ProxyFactoryBean 初始化时就被固定下来,无法满足动态创建新动态实例的需求。
  • 打破循环依赖:在复杂的 AOP 配置中,使用名称引用可以有效避免因通知和目标对象互相注入导致的循环依赖问题。

2. 匿名内部 Bean:生产环境的最佳实践

在大型项目中,如果你有一个接口 UserService 和实现类 UserServiceImpl。如果你分别定义它们的 Bean,那么容器中就会出现两个符合 UserService 类型的东西:一个是原始 Bean,一个是代理 Bean。 这会导致 @Autowired UserService 时抛出 NoUniqueBeanDefinitionException推荐方案:将 UserServiceImpl 定义为 ProxyFactoryBean 的内部 <bean>,这样容器中就只会暴露代理后的对象。

3. 选择策略总结表

场景是否有接口proxyTargetClass最终结果
标准应用false (默认)JDK 动态代理
遗留/类代理不限CGLIB 代理
强制类代理trueCGLIB 代理
全反射场景不设置 proxyInterfacesJDK 动态代理 (自动代理所有接口)

4. 现代替代方案提示

虽然 ProxyFactoryBean 是理解原理的关键,但在现代 Spring Boot 项目中:

  • 我们更倾向于使用 @Aspect 配合 自动代理创建器 (AnnotationAwareAspectJAutoProxyCreator),它会自动扫描容器中的切面并应用到匹配的 Bean 上。
  • 如果你需要手动创建代理,编程式的 ProxyFactory(见后续章节)通常比繁琐的 XML ProxyFactoryBean 更受开发者青睐。

Based on Spring Framework.