使用 ProxyFactoryBean 创建 AOP 代理

如果您使用 Spring IoC 容器(ApplicationContextBeanFactory)来管理您的业务对象(您应该这样做!),您需要使用 Spring 的 AOP FactoryBean 实现之一。(请记住,工厂 bean 引入了一层间接性,允许它创建不同类型的对象。)

Spring AOP 支持也在幕后使用工厂 bean。

在 Spring 中创建 AOP 代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean。这提供了对切入点、任何适用的建议及其排序的完全控制。但是,如果您不需要这种控制,则有更简单的选项。

基础

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

使用ProxyFactoryBean或其他 IoC 意识类创建 AOP 代理的最重要优势之一是建议和切入点也可以由 IoC 管理。这是一个强大的功能,它使某些难以用其他 AOP 框架实现的方法成为可能。例如,建议本身可以引用应用程序对象(除了目标,目标应该在任何 AOP 框架中都可用),从依赖注入提供的所有可插拔性中受益。

JavaBean 属性

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

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

  • proxyTargetClass:如果要代理目标类,而不是目标类的接口,则为true。如果此属性值设置为true,则会创建 CGLIB 代理(但另请参见基于 JDK 和 CGLIB 的代理)。

  • optimize:控制是否对通过 CGLIB 创建的代理应用积极优化。除非您完全了解相关 AOP 代理如何处理优化,否则您不应该随意使用此设置。目前仅用于 CGLIB 代理。它对 JDK 动态代理没有影响。

  • frozen:如果代理配置被frozen,则不再允许更改配置。这既有轻微的优化作用,也有助于在代理创建后,您不希望调用者能够通过Advised接口操作代理。此属性的默认值为false,因此允许更改(例如添加其他建议)。

  • exposeProxy: 确定当前代理是否应该在 ThreadLocal 中公开,以便目标可以访问它。如果目标需要获取代理并且 exposeProxy 属性设置为 true,则目标可以使用 AopContext.currentProxy() 方法。

ProxyFactoryBean 的其他特定属性包括:

  • proxyInterfaces: 一个 String 接口名称数组。如果未提供此数组,则使用目标类的 CGLIB 代理(但另请参见 基于 JDK 和 CGLIB 的代理)。

  • interceptorNames: 一个 String 数组,包含要应用的 Advisor、拦截器或其他建议名称。顺序很重要,遵循先到先得的原则。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。

    这些名称是当前工厂中的 bean 名称,包括祖先工厂中的 bean 名称。您不能在此处提及 bean 引用,因为这样做会导致 ProxyFactoryBean 忽略建议的单例设置。

    您可以将拦截器名称附加一个星号 (*)。这样做会导致应用所有名称以星号之前的部分开头的顾问 bean。您可以在 使用“全局”顾问 中找到使用此功能的示例。

  • singleton: 工厂是否应该返回单个对象,无论 getObject() 方法被调用多少次。几个 FactoryBean 实现提供这样的方法。默认值为 true。如果您想使用有状态的建议 - 例如,用于有状态的混合 - 请使用原型建议以及 false 的单例值。

基于 JDK 和 CGLIB 的代理

本节作为 ProxyFactoryBean 如何选择为特定目标对象(将被代理)创建基于 JDK 的代理或基于 CGLIB 的代理的权威文档。

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

如果要代理的目标对象的类(以下简称目标类)没有实现任何接口,则会创建一个基于 CGLIB 的代理。这是最简单的场景,因为 JDK 代理是基于接口的,没有接口意味着 JDK 代理甚至不可能。您可以插入目标 bean 并通过设置 interceptorNames 属性来指定拦截器列表。请注意,即使 ProxyFactoryBeanproxyTargetClass 属性已设置为 false,也会创建一个基于 CGLIB 的代理。(这样做没有意义,最好从 bean 定义中删除,因为它充其量是多余的,最坏的情况是令人困惑。)

如果目标类实现了一个(或多个)接口,则创建的代理类型取决于 ProxyFactoryBean 的配置。

如果 `ProxyFactoryBean` 的 `proxyTargetClass` 属性设置为 `true`,则会创建一个基于 CGLIB 的代理。这很有意义,并且符合最小惊讶原则。即使 `ProxyFactoryBean` 的 `proxyInterfaces` 属性已设置为一个或多个完全限定的接口名称,但 `proxyTargetClass` 属性设置为 `true` 也会导致基于 CGLIB 的代理生效。

如果 `ProxyFactoryBean` 的 `proxyInterfaces` 属性已设置为一个或多个完全限定的接口名称,则会创建一个基于 JDK 的代理。创建的代理实现 `proxyInterfaces` 属性中指定的所有接口。如果目标类恰好实现了比 `proxyInterfaces` 属性中指定的接口更多的接口,那也很好,但这些额外的接口不会由返回的代理实现。

如果 `ProxyFactoryBean` 的 `proxyInterfaces` 属性未设置,但目标类确实实现了至少一个接口,则 `ProxyFactoryBean` 会自动检测到目标类确实实现了至少一个接口,并创建一个基于 JDK 的代理。实际代理的接口是目标类实现的所有接口。实际上,这与向 `proxyInterfaces` 属性提供目标类实现的每个接口的列表相同。但是,它要少得多工作,并且不太容易出现打字错误。

代理接口

考虑一个简单的 `ProxyFactoryBean` 工作示例。此示例涉及

  • 一个被代理的目标 Bean。这是示例中的 `personTarget` Bean 定义。

  • 一个 `Advisor` 和一个 `Interceptor` 用于提供建议。

  • 一个 AOP 代理 Bean 定义,用于指定目标对象(`personTarget` Bean)、要代理的接口以及要应用的建议。

以下列表显示了示例

<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` 列表,其中包含当前工厂中拦截器或顾问的 Bean 名称。您可以使用顾问、拦截器、before、after returning 和 throws 建议对象。顾问的顺序很重要。

您可能想知道为什么列表中不包含 bean 引用。原因是,如果将 ProxyFactoryBean 的 singleton 属性设置为 false,则它必须能够返回独立的代理实例。如果任何一个顾问本身是原型,则需要返回一个独立的实例,因此需要能够从工厂获取原型实例。持有引用是不够的。

前面显示的 person bean 定义可以代替 Person 实现,如下所示

  • Java

  • Kotlin

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

同一 IoC 上下文中的其他 bean 可以对它表达强类型依赖,就像使用普通的 Java 对象一样。以下示例展示了如何做到这一点

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

本示例中的 PersonUser 类公开了一个类型为 Person 的属性。就它而言,AOP 代理可以透明地代替“真实”的 Person 实现使用。但是,它的类将是一个动态代理类。可以将其强制转换为 Advised 接口(稍后讨论)。

您可以使用匿名内部 bean 来隐藏目标和代理之间的区别。只有 ProxyFactoryBean 定义不同。建议仅出于完整性而包含。以下示例展示了如何使用匿名内部 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 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 的对象。如果我们想要阻止应用程序上下文的用户获取对未建议对象的引用,或者需要避免与 Spring IoC 自动装配的任何歧义,这将非常有用。此外,可以说,ProxyFactoryBean 定义是自包含的,这也是一个优点。但是,有时能够从工厂获取未建议的目标实际上可能是一个优势(例如,在某些测试场景中)。

代理类

如果您需要代理一个类,而不是一个或多个接口,该怎么办?

想象一下,在我们之前的示例中,没有Person接口。我们需要为一个名为Person的类提供建议,该类没有实现任何业务接口。在这种情况下,您可以配置 Spring 使用 CGLIB 代理而不是动态代理。为此,将前面显示的ProxyFactoryBean上的proxyTargetClass属性设置为true。虽然最好对接口而不是类进行编程,但在处理遗留代码时,为不实现接口的类提供建议的能力可能很有用。(总的来说,Spring 并不强制规定。虽然它使应用良好实践变得容易,但它避免了强制使用特定方法。)

如果您愿意,即使您有接口,也可以强制使用 CGLIB。

CGLIB 代理通过在运行时生成目标类的子类来工作。Spring 配置此生成的子类以将方法调用委托给原始目标。子类用于实现装饰器模式,将建议编织进来。

CGLIB 代理通常对用户来说应该是透明的。但是,有一些问题需要考虑

  • final 类不能被代理,因为它们不能被扩展。

  • final 方法不能被建议,因为它们不能被覆盖。

  • private 方法不能被建议,因为它们不能被覆盖。

  • 不可见的方法,通常是来自不同包的父类中的包私有方法,不能被建议,因为它们实际上是私有的。

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

CGLIB 代理和动态代理之间几乎没有性能差异。在这种情况下,性能不应成为决定性因素。

使用“全局”顾问

通过在拦截器名称后面追加星号,所有 bean 名称与星号之前部分匹配的顾问都会被添加到顾问链中。如果您需要添加一组标准的“全局”顾问,这将非常有用。以下示例定义了两个全局顾问

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