对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

使用 ProxyFactoryBean 创建 AOP 代理

如果你为你的业务对象使用 Spring IoC 容器(ApplicationContextBeanFactory)(而且你应该这样做!),那么你应当使用 Spring AOP 的某个 FactoryBean 实现。(请记住,工厂 bean 引入了一层间接性,使其能够创建不同类型的对象。)spring-doc.cadn.net.cn

Spring AOP 支持在底层也使用了工厂 Bean。

在 Spring 中创建 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean。这可以完全控制 切入点(pointcuts)、所应用的通知(advice)以及它们的执行顺序。然而,如果你不需要如此精细的控制, 还有更简单的选项可供选择。spring-doc.cadn.net.cn

基础

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

使用 ProxyFactoryBean 或其他感知 IoC 的类来创建 AOP 代理,其中一个最重要的好处是通知(advice)和切入点(pointcut)也可以由 IoC 容器进行管理。这是一个强大的特性,使得某些在其他 AOP 框架中难以实现的方法成为可能。例如,一个通知本身可以引用应用程序对象(除了目标对象之外,而目标对象在任何 AOP 框架中都应该是可用的),从而充分利用依赖注入所提供的全部可插拔性。spring-doc.cadn.net.cn

JavaBean 属性

与 Spring 提供的大多数 FactoryBean 实现一样,ProxyFactoryBean 类本身也是一个 JavaBean。它的属性用于:spring-doc.cadn.net.cn

某些关键属性继承自 org.springframework.aop.framework.ProxyConfig (Spring 中所有 AOP 代理工厂的超类)。这些关键属性包括以下内容:spring-doc.cadn.net.cn

  • proxyTargetClass:如果要代理目标类本身而非目标类的接口,则设为 true。如果此属性值设置为 true,则会创建 CGLIB 代理(另请参阅基于 JDK 和 CGLIB 的代理)。spring-doc.cadn.net.cn

  • optimize:控制是否对通过 CGLIB 创建的代理应用激进的优化。 除非你完全理解相关 AOP 代理如何处理优化,否则不应随意使用此设置。 该选项目前仅用于 CGLIB 代理,对 JDK 动态代理无效。spring-doc.cadn.net.cn

  • frozen:如果一个代理配置被设置为frozen(冻结),则不再允许对该配置进行修改。这既可作为一种轻微的优化手段,也适用于那些不希望调用者在代理创建之后通过Advised接口操纵代理的情况。此属性的默认值为false,因此允许进行更改(例如添加额外的通知)。spring-doc.cadn.net.cn

  • exposeProxy:确定是否应将当前代理暴露在 ThreadLocal 中,以便目标对象可以访问它。如果目标对象需要获取代理,并且 exposeProxy 属性被设置为 true,则目标对象可以使用 AopContext.currentProxy() 方法。spring-doc.cadn.net.cn

ProxyFactoryBean 特有的其他属性包括以下内容:spring-doc.cadn.net.cn

  • proxyInterfaces:一个 String 类型的接口名称数组。如果未提供此参数,则将为目标类使用 CGLIB 代理(另请参阅基于 JDK 和 CGLIB 的代理)。spring-doc.cadn.net.cn

  • interceptorNames:一个 String 类型的数组,包含要应用的 Advisor、拦截器或其他通知(advice)的名称。顺序很重要,遵循先到先服务的原则。也就是说,列表中的第一个拦截器将最先有机会拦截方法调用。spring-doc.cadn.net.cn

    这些名称是当前工厂中的 bean 名称,包括从父级工厂继承而来的 bean 名称。此处不能引用 bean 引用,因为这样做会导致 ProxyFactoryBean 忽略通知(advice)的单例(singleton)设置。spring-doc.cadn.net.cn

    您可以在拦截器名称后附加一个星号(*)。这样会将所有名称以星号前部分开头的 Advisor Bean 应用到目标上。您可以在使用“全局”Advisor一节中找到使用此功能的示例。spring-doc.cadn.net.cn

  • singleton:工厂是否应始终返回同一个对象,无论 getObject() 方法被调用多少次。多个 FactoryBean 实现都提供了这样的方法。默认值为 true。如果你希望使用有状态的通知(advice)——例如用于有状态的混入(mixins)——请将原型(prototype)通知与 false 的 singleton 值一起使用。spring-doc.cadn.net.cn

基于 JDK 和 CGLIB 的代理

本节作为关于ProxyFactoryBean如何为特定目标对象(即需要被代理的对象)选择创建基于JDK的代理还是基于CGLIB的代理的权威文档。spring-doc.cadn.net.cn

ProxyFactoryBean 在创建基于 JDK 或 CGLIB 的代理方面的行为,在 Spring 1.2.x 版本和 2.0 版本之间发生了变化。ProxyFactoryBean 现在在自动检测接口方面的语义与 TransactionProxyFactoryBean 类的行为类似。

如果要被代理的目标对象的类(以下简称目标类)未实现任何接口,则会创建一个基于 CGLIB 的代理。这是最简单的情况,因为 JDK 代理是基于接口的,而没有接口就意味着根本无法使用 JDK 进行代理。您可以通过设置 interceptorNames 属性来注入目标 bean 并指定拦截器列表。请注意,即使将 proxyTargetClassProxyFactoryBean 属性设置为 false,也会创建基于 CGLIB 的代理。(这样做毫无意义,最好从 bean 定义中移除该设置,因为它至多是多余的,最坏的情况下还会引起混淆。)spring-doc.cadn.net.cn

如果目标类实现了一个(或多个)接口,则所创建的代理类型取决于 ProxyFactoryBean 的配置。spring-doc.cadn.net.cn

如果 proxyTargetClassProxyFactoryBean 属性被设置为 true, 则会创建一个基于 CGLIB 的代理。这种行为是合理的,并且符合“最小意外原则”(principle of least surprise)。 即使 proxyInterfacesProxyFactoryBean 属性已被设置为一个或多个完全限定的接口名称, 但由于 proxyTargetClass 属性被设为 true,仍然会启用基于 CGLIB 的代理。spring-doc.cadn.net.cn

如果 proxyInterfacesProxyFactoryBean 属性被设置为一个或多个 完全限定的接口名称,则会创建一个基于 JDK 的代理。所创建的 代理将实现 proxyInterfaces 属性中指定的所有接口。 如果目标类恰好实现了比 proxyInterfaces 属性中指定的更多的接口, 这完全没有问题,但这些额外的接口不会被返回的代理所实现。spring-doc.cadn.net.cn

如果未设置 proxyInterfacesProxyFactoryBean 属性,但目标类确实实现了一个(或多个)接口,则 ProxyFactoryBean 会自动检测到目标类实际上至少实现了一个接口,并创建一个基于 JDK 的代理。实际被代理的接口是目标类所实现的所有接口。实际上,这等同于将目标类实现的每一个接口都显式提供给 proxyInterfaces 属性。然而,这种方式工作量显著减少,且不易出现拼写错误。spring-doc.cadn.net.cn

代理接口

考虑一个正在运行的简单 ProxyFactoryBean 示例。该示例包含:spring-doc.cadn.net.cn

  • 一个被代理的目标 bean。这是示例中的 personTarget bean 定义。spring-doc.cadn.net.cn

  • 一个用于提供通知的Advisor和一个Interceptorspring-doc.cadn.net.cn

  • 一个 AOP 代理 bean 定义,用于指定目标对象(personTarget bean)、要代理的接口以及要应用的通知。spring-doc.cadn.net.cn

以下列表展示了该示例:spring-doc.cadn.net.cn

<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>

请注意,interceptorNames 属性接受一个 String 类型的列表,其中包含当前工厂中拦截器或通知器(advisor)的 bean 名称。您可以使用通知器(advisor)、拦截器(interceptor)、前置通知(before)、后置返回通知(after returning)以及异常抛出通知(throws advice)对象。通知器的顺序非常重要。spring-doc.cadn.net.cn

你可能会疑惑为什么这个列表不保存 bean 的引用。原因在于,如果 ProxyFactoryBean 的 singleton 属性被设置为 false,它就必须能够返回独立的代理实例。如果其中任意一个通知器(advisor)本身是原型(prototype)作用域的,那么就需要返回该通知器的一个独立实例,因此必须能够从工厂中获取该原型的一个新实例。仅仅持有引用是不够的。

前面所示的 person bean 定义可以用来替代 Person 的实现,如下所示:spring-doc.cadn.net.cn

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

同一 IoC 容器中的其他 bean 可以像对待普通 Java 对象一样,对其声明强类型的依赖关系。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

<bean id="personUser" class="com.mycompany.PersonUser">
	<property name="person"><ref bean="person"/></property>
</bean>

本例中的 PersonUser 类暴露了一个类型为 Person 的属性。就该类而言,AOP 代理可以透明地替代“真实”的 Person 实现。然而,其实际类将是一个动态代理类,因此可以将其转换(cast)为 Advised 接口(稍后讨论)。spring-doc.cadn.net.cn

你可以通过使用匿名内部 bean 来隐藏目标对象与代理之间的区别。只有 ProxyFactoryBean 的定义有所不同。此处包含通知(advice)仅为了完整性。以下示例展示了如何使用匿名内部 bean:spring-doc.cadn.net.cn

<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 id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="proxyInterfaces" value="com.mycompany.Person"/>
	<!-- Use inner bean, not local reference to target -->
	<property name="target">
		<bean class="com.mycompany.PersonImpl">
			<property name="name" value="Tony"/>
			<property name="age" value="51"/>
		</bean>
	</property>
	<property name="interceptorNames">
		<list>
			<value>myAdvisor</value>
			<value>debugInterceptor</value>
		</list>
	</property>
</bean>

使用匿名内部 bean 的优势在于,Person 类型的对象只有一个。如果我们希望防止应用程序上下文的用户获取未被代理(un-advised)对象的引用,或者需要避免与 Spring IoC 自动装配产生任何歧义,这一点非常有用。此外,可以说还有一个优势,即 ProxyFactoryBean 的定义是自包含的。然而,在某些情况下,能够从工厂中获取未被代理的目标对象实际上可能是一种优势(例如,在某些测试场景中)。spring-doc.cadn.net.cn

代理类

如果你需要代理一个类,而不是一个或多个接口,该怎么办?spring-doc.cadn.net.cn

假设在我们之前的示例中,并不存在 Person 接口。我们需要对一个名为 Person 的类进行增强,而该类并未实现任何业务接口。在这种情况下,您可以配置 Spring 使用 CGLIB 代理,而不是动态代理。为此,请将前面所示的 proxyTargetClassProxyFactoryBean 属性设置为 true。尽管最好面向接口而非类进行编程,但在处理遗留代码时,能够对未实现接口的类进行增强的功能仍然非常有用。(总体而言,Spring 并不强制推行某种特定方式。虽然它使应用良好实践变得容易,但不会强制要求采用某种特定方法。)spring-doc.cadn.net.cn

如果需要,即使存在接口,你也可以强制在任何情况下使用 CGLIB。spring-doc.cadn.net.cn

CGLIB 代理通过在运行时生成目标类的子类来实现。Spring 将此生成的子类配置为将方法调用委托给原始目标对象。该子类用于实现装饰器(Decorator)模式,将通知(advice)织入其中。spring-doc.cadn.net.cn

CGLIB 代理对用户来说通常是透明的。然而,有一些问题需要注意:spring-doc.cadn.net.cn

无需将 CGLIB 添加到您的类路径中。CGLIB 已被重新打包并包含在 spring-core JAR 文件中。换句话说,基于 CGLIB 的 AOP 和 JDK 动态代理一样,开箱即用。

CGLIB 代理和动态代理之间的性能差异很小。 在这种情况下,性能不应该是决定性的考虑因素。spring-doc.cadn.net.cn

使用“全局”顾问

通过在拦截器名称后附加一个星号(*),所有 bean 名称与星号前部分匹配的增强器(advisor)都会被添加到增强器链中。当你需要添加一组标准的“全局”增强器时,这一功能非常有用。以下示例定义了两个全局增强器:spring-doc.cadn.net.cn

<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"/>