使用 ProxyFactoryBean 创建 AOP 代理
如果您将 Infra IoC 容器(ApplicationContext 或 BeanFactory)用于您的业务对象(您应该这样做!),
您会希望使用 Infra AOP FactoryBean 实现之一。(请记住,工厂 bean 引入了一个间接层,使其能够创建不同类型的对象。)
| Infra AOP 支持在底层也使用了工厂 bean。 |
在 Infra 中创建 AOP 代理的基本方法是使用 infra.aop.framework.ProxyFactoryBean。
这让您可以完全控制切入点、任何适用的通知及其顺序。但是,如果您不需要这种控制,也可以选择更简单的选项。
基础
ProxyFactoryBean 与其他 Infra FactoryBean 实现一样,引入了一个间接层。
如果您定义了一个名为 foo 的 ProxyFactoryBean,引用 foo 的对象看到的不是 ProxyFactoryBean 实例本身,
而是由 ProxyFactoryBean 中的 getObject() 方法实现创建的对象。此方法创建一个包装目标对象的 AOP 代理。
使用 ProxyFactoryBean 或其他 IoC 感知类创建 AOP 代理的最重要好处之一是,通知和切入点也可以由 IoC 管理。
这是一个强大的功能,使得某些用其他 AOP 框架难以实现的方法成为可能。例如,通知本身可以引用应用程序对象
(除了目标对象,这在任何 AOP 框架中都应该是可用的),从而受益于依赖注入提供的所有可插拔性。
JavaBean 属性
与 Infra 随附的大多数 FactoryBean 实现一样,ProxyFactoryBean 类本身也是一个 JavaBean。其属性用于:
-
指定要代理的目标。
-
指定是否使用 CGLIB(稍后描述,另请参见 基于 JDK 和 CGLIB 的代理)。
一些关键属性继承自 infra.aop.framework.ProxyConfig(Infra 中所有 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:要应用的Advisor、拦截器或其他通知名称的String数组。 顺序很重要,采用先到先得的方式。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。这些名称是当前工厂中的 bean 名称,包括来自祖先工厂的 bean 名称。您不能在此处提及 bean 引用, 因为这样做会导致
ProxyFactoryBean忽略通知的单例设置。您可以在拦截器名称后附加星号(
*)。这样做会导致应用名称以星号之前的部分开头的所有顾问 bean。 您可以在 使用“全局”顾问 中找到使用此功能的示例。 -
singleton:无论调用
getObject()方法多少次,工厂是否应返回单个对象。 多个FactoryBean实现提供了这样的方法。默认值为true。如果您想使用有状态通知——例如, 用于有状态 mixin——请使用原型通知以及单例值false。
基于 JDK 和 CGLIB 的代理
本节作为权威文档,说明 ProxyFactoryBean 如何选择为特定目标对象(需要被代理的对象)
创建基于 JDK 的代理还是基于 CGLIB 的代理。
ProxyFactoryBean 在创建基于 JDK 或 CGLIB 的代理方面的行为在 Infra 的 1.2.x 版本和 2.0 版本之间发生了变化。
ProxyFactoryBean 现在在自动检测接口方面表现出与 TransactionProxyFactoryBean 类相似的语义。
|
如果要代理的目标对象的类(以下简称为目标类)未实现任何接口,则会创建基于 CGLIB 的代理。
这是最简单的场景,因为 JDK 代理是基于接口的,没有接口意味着甚至无法进行 JDK 代理。
您可以插入目标 bean 并通过设置 interceptorNames 属性指定拦截器列表。
请注意,即使 ProxyFactoryBean 的 proxyTargetClass 属性已设置为 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。这是示例中的
personTargetbean 定义。 -
用于提供通知的
Advisor和Interceptor。 -
一个 AOP 代理 bean 定义,用于指定目标对象(
personTargetbean)、要代理的接口以及要应用的通知。
以下清单显示了该示例:
<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="infra.aop.interceptor.DebugInterceptor">
</bean>
<bean id="person"
class="infra.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 名称。
您可以使用顾问、拦截器、前置、后置返回和抛出通知对象。顾问的顺序很重要。
您可能会想知道为什么列表不包含 bean 引用。其原因是,如果 ProxyFactoryBean 的 singleton 属性设置为 false,
则它必须能够返回独立的代理实例。如果任何顾问本身是原型,则需要返回一个独立的实例,因此必须能够从工厂获取原型的实例。
持有引用是不够的。
|
前面显示的 person bean 定义可以用来代替 Person 实现,如下所示:
-
Java
Person person = (Person) factory.getBean("person");
同一 IoC 上下文中的其他 bean 可以表达对它的强类型依赖,就像对普通 Java 对象一样。
以下示例展示了如何执行此操作:
<bean id="personUser" class="com.mycompany.PersonUser">
<property name="person"><ref bean="person"/></property>
</bean>
本示例中的 PersonUser 类公开了一个类型为 Person 的属性。据其所知,AOP 代理可以像“真实”人实现一样透明地使用。
但是,它的类将是一个动态代理类。可以将它转换为 Advised 接口(稍后讨论)。
您可以使用匿名内部 bean 隐藏目标和代理之间的区别,如下例所示:
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="infra.aop.interceptor.DebugInterceptor"/>
<bean id="person" class="infra.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<!-- 使用匿名内部 bean 隐藏目标 -->
<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>
这具有 Person 类型的只有一个 bean 的优点。如果我们想防止应用程序上下文的用户获取对未通知对象的引用,
或者是需要避免 Infra IoC 容器中的任何歧义,这将非常有用。还可以争辩说,ProxyFactoryBean 定义是自包含的,这是一个优势。
但是,有时能够从工厂获取未通知的目标实际上可能是一个优势(例如,用于某些测试场景)。
代理类
如果您需要代理一个类,而不是一个或多个接口,该怎么办?
想象一下,在前面的示例中,没有 Person 接口。我们需要通知一个名为 Person 的类,它没有实现任何业务接口。
在这种情况下,您可以配置 Infra 使用 CGLIB 代理,而不是动态代理。为此,请将前面显示的 ProxyFactoryBean
上的 proxyTargetClass 属性设置为 true。虽然最好根据接口而不是类进行编程,但在使用遗留代码时,
能够通知不实现接口的类可能很有用。(一般来说,Infra 不是规定性的。它使得应用良好实践变得容易,但它避免强迫采用特定方法。)
如果您愿意,即使有接口,也可以在任何情况下强制使用 CGLIB。
CGLIB 代理通过在运行时生成目标类的子类来工作。Infra 配置生成的子类以将方法调用委托给原始目标。 子类用于实现装饰器模式,并在其中编织通知。
CGLIB 代理通常对用户是透明的。但是,有一些问题需要考虑:
-
final方法不能被通知,因为它们不能被覆盖。 -
您不需要将 CGLIB 添加到类路径中。截至 Infra 3.2,CGLIB 已重新打包并包含在 infra-core JAR 中。 换句话说,基于 CGLIB 的 AOP 可以“开箱即用”,就像 JDK 动态代理一样。
CGLIB 代理和动态代理之间的性能差异很小。在这种情况下,性能不应成为决定性因素。
使用“全局”顾问
通过在拦截器名称后附加星号,所有 bean 名称与星号之前部分匹配的顾问都将添加到顾问链中。 如果您需要添加一组标准的“全局”顾问,这会很方便:
<bean id="proxy" class="infra.aop.framework.ProxyFactoryBean">
<property name="target" ref="service"/>
<property name="interceptorNames">
<list>
<value>global*</value>
</list>
</property>
</bean>
<bean id="global_debug" class="infra.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="infra.aop.interceptor.PerformanceMonitorInterceptor"/>