使用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实例本身,而是由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代理(但另请参阅基于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忽略建议的单例设置。

    您可以在拦截器名称后附加一个星号(*)。这样做会导致应用所有名称以该星号前部分开头的advisor 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定义。

  • 用于提供建议的AdvisorInterceptor

  • 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名称。您可以使用顾问、拦截器、前置、返回后和抛出建议对象。顾问的排序很重要。

您可能想知道为什么列表不保存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代理可以透明地用于“真实”人员实现的位置。但是,其类将是一个动态代理类。可以将其转换为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"/>
	<!-- 使用内部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>

使用匿名内部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"/>