对于最新的稳定版本,请使用 Spring Framework 7.0.6!spring-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。这可以完全控制切点、适用的任何通知及其顺序。但是,如果你不需要这样的控制,还有更简单的选项可供选择。spring-doc.cadn.net.cn

基础知识

The ProxyFactoryBean, like other Spring FactoryBean implementations, introduces a level of indirection. If you define a ProxyFactoryBean named foo, objects that reference foo do not see the ProxyFactoryBean instance itself but an object created by the implementation of the getObject() method in the ProxyFactoryBean . This method creates an AOP proxy that wraps a target object.spring-doc.cadn.net.cn

使用 ProxyFactoryBean 或其他 IoC 感知类创建 AOP 代理最重要的优势之一是:通知(advice)和切入点(pointcuts)也可以由 IoC 管理。这是一个强大的功能,这实现了某些在其他 AOP 框架中难以达成的方法。例如,通知本身可以引用应用程序对象(除了目标对象之外 —— 这在任何 AOP 框架中都应是可用的),从而受益于依赖注入(Dependency Injection)提供的所有可插拔特性。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: A String array of Advisor, interceptor, or other advice names to apply. Ordering is significant, on a first come-first served basis. That is to say that the first interceptor in the list is the first to be able to intercept the invocation.spring-doc.cadn.net.cn

    这些名称是当前工厂中的bean名称,包括来自祖先工厂的bean名称。你不能在这里提及bean引用,因为这样做会导致ProxyFactoryBean忽略建议的单例设置。spring-doc.cadn.net.cn

    您可以在拦截器名称后添加一个星号 (*)。这样做会导致所有名称以星号前部分开头的advisor bean被应用。您可以在使用“全局”顾问中找到使用此功能的示例。spring-doc.cadn.net.cn

  • 单例:无论调用多少次getObject()方法,工厂是否应始终返回同一个对象。多个FactoryBean实现类提供此方法。默认值为true。若需使用有状态通知(例如用于有状态混入),请将false设为单例值并配合使用原型通知。spring-doc.cadn.net.cn

基于JDK和CGLIB的代理

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

The behavior of the ProxyFactoryBean with regard to creating JDK- or CGLIB-based proxies changed between versions 1.2.x and 2.0 of Spring. The ProxyFactoryBean now exhibits similar semantics with regard to auto-detecting interfaces as those of the TransactionProxyFactoryBean class.

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

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

如果ProxyFactoryBeanproxyTargetClass属性被设置为true,将创建一个基于CGLIB的代理。这符合预期,并遵循最小惊讶原则。即使ProxyFactoryBeanproxyInterfaces属性被设置为一个或多个完全限定的接口名称,由于proxyTargetClass属性被设置为true,基于CGLIB的代理仍然生效。spring-doc.cadn.net.cn

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

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

代理接口

考虑一个简单的 ProxyFactoryBean 实际应用的例子。这个例子涉及: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 列表,其中包含当前工厂中拦截器或顾问的 bean 名称。您可以使用顾问、拦截器、前置、后置返回和抛出建议对象。顾问的顺序很重要。spring-doc.cadn.net.cn

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

The person bean definition shown earlier can be used in place of a Person implementation, as follows: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>

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

你可以通过使用匿名内部bean来隐藏目标和代理之间的区别。只有ProxyFactoryBean定义是不同的。包含通知只是为了完整性。以下示例展示了如何使用匿名内部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的对象。这在我们希望防止应用程序上下文的用户获得未被代理对象的引用或需要避免与Spring IoC自动装配的任何歧义时非常有用。此外,可以说ProxyFactoryBean的定义是自包含的。然而,有时能够从工厂获取未被代理的目标实际上可能是一个优势(例如,在某些测试场景中)。spring-doc.cadn.net.cn

代理类

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

想象在我们之前的示例中,没有Person接口。我们需要通知一个名为Person的类,该类没有实现任何业务接口。在这种情况下,您可以配置Spring使用CGLIB代理而不是动态代理。为此,请将proxyTargetClass属性设置为ProxyFactoryBean(如前所示)中的true。虽然最好是对接口进行编程而不是对类进行编程,但在处理遗留代码时,能够通知未实现接口的类可能会很有用。(通常,Spring并不强制规定特定的方法。尽管它使应用良好实践变得容易,但它避免强制采用特定方法。)spring-doc.cadn.net.cn

如果你想,你可以强制使用 CGLIB,即使你有接口也是如此。spring-doc.cadn.net.cn

CGLIB代理通过在运行时生成目标类的子类来工作。Spring配置这个生成的子类,使其将方法调用委托给原始目标。该子类用于实现装饰器模式,编织进通知。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名称的顾问将被添加到顾问链中。这在需要添加一组标准的“全局”顾问时会非常有用。以下示例定义了两个全局顾问: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"/>