跳到主要内容

使用 ProxyFactoryBean 创建 AOP 代理

ChatGPT-4o-mini 中英对照 Using the ProxyFactoryBean to Create AOP Proxies Using the ProxyFactoryBean to Create AOP Proxies

如果您使用 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 的配置。

如果 ProxyFactoryBeanproxyTargetClass 属性被设置为 true,则会创建一个基于 CGLIB 的代理。这是合理的,并符合最小惊讶原则。即使 ProxyFactoryBeanproxyInterfaces 属性被设置为一个或多个完全限定的接口名称,proxyTargetClass 属性被设置为 true 的事实仍然会导致 CGLIB 基于代理的生效。

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

如果 ProxyFactoryBeanproxyInterfaces 属性尚未设置,但目标类确实实现了一个(或多个)接口,则 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>
xml

请注意,interceptorNames 属性接受一个 String 列表,该列表包含当前工厂中拦截器或顾问的 bean 名称。您可以使用顾问、拦截器、前置、后置返回和抛出通知对象。顾问的顺序是重要的。

备注

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

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

Person person = (Person) factory.getBean("person");
java

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

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

在这个例子中,PersonUser 类暴露了一个类型为 Person 的属性。就它而言,AOP 代理可以透明地替代“真实”的人实现。然而,它的类将是一个动态代理类。可以将其强制转换为 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>
xml

使用匿名内部 Bean 的好处在于只有一个 Person 类型的对象。如果我们想要防止应用上下文的用户获取对未被代理对象的引用,或者需要避免与 Spring IoC 自动装配的任何歧义,这一点非常有用。此外,可以说 ProxyFactoryBean 的定义是自包含的,这也是一个优点。然而,有时能够从工厂获取未被代理的目标实际上可能是一种优势(例如,在某些测试场景中)。

代理类

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

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

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