20200105 Spring官方文档(Core 6)
6. Spring AOP API
6.1。Spring中的Pointcut API
6.1.1。概念
Spring的切入点模型使切入点重用不受通知类型的影响。 您可以使用相同的切入点来定位不同的通知。
org.springframework.aop.Pointcut
接口是核心接口,用于将通知定向到特定的类和方法。 完整的接口如下:
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
将Pointcut
接口分为两部分,可以重用类和方法匹配的部分以及细粒度的合成操作(例如与另一个方法匹配器执行“联合”)。
ClassFilter
接口用于将切入点限制为给定的一组目标类。 如果matches()
方法始终返回true
,则将匹配所有目标类。 以下清单显示了ClassFilter接口定义:
public interface ClassFilter {
boolean matches(Class clazz);
}
MethodMatcher
接口通常更重要:
public interface MethodMatcher {
boolean matches(Method m, Class targetClass);
boolean isRuntime();
boolean matches(Method m, Class targetClass, Object[] args);
}
matchs(Method,Class)
方法用于测试此切入点是否与目标类上的给定方法匹配。 创建AOP代理时可以执行此评估,以避免需要对每个方法调用进行测试。 如果两个参数的match
方法对于给定的方法返回true
,并且MethodMatcher
的isRuntime()
方法返回true
,则在每次方法调用时都将调用三个参数的match
方法。 这样,切入点就可以在执行目标通知之前立即查看传递给方法调用的参数。
大多数MethodMatcher
实现都是静态的,这意味着它们的isRuntime()
方法返回false
。 在这种情况下,永远不会调用三参数匹配方法。
如果可能,请尝试使切入点成为静态,以允许AOP框架在创建AOP代理时缓存切入点评估的结果。
6.1.2。切入点的操作
Spring支持切入点上的操作(特别是并集和交集)。
联合表示两个切入点都匹配的方法。 交集是指两个切入点都匹配的方法。并集通常更有用。 您可以通过使用org.springframework.aop.support.Pointcuts
类中的静态方法或使用同一包中的ComposablePointcut
类来组成切入点。 但是,使用AspectJ切入点表达式通常是一种更简单的方法。
6.1.3。AspectJ表达式切入点
从2.0开始,Spring使用的最重要的切入点类型是 org.springframework.aop.aspectj.AspectJExpressionPointcut
。这是一个切入点,该切入点使用AspectJ提供的库来解析AspectJ切入点表达式字符串。
6.1.4。便捷切入点实现
静态切入点
静态切入点基于方法和目标类,并且不能考虑方法的参数。 静态切入点足以满足大多数用途,并且是最好选择。 首次调用方法时,Spring只能评估一次静态切入点。 之后,无需在每次方法调用时再次评估切入点。
正则表达式切入点
org.springframework.aop.support.JdkRegexpMethodPointcut
是通用的正则表达式切入点,它使用JDK中的正则表达式支持。
通过JdkRegexpMethodPointcut
类,您可以提供模式字符串的列表。如果其中任何一个匹配,则切入点的计算结果为true
。(因此,结果实际上是这些切入点的并集。)
<bean id="settersAndAbsquatulatePointcut"
class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
Spring提供了一个名为RegexpMethodPointcutAdvisor
的便捷类,该类使我们还可以引用一个Advice
(请记住,Advice可以是拦截器,前置通知,异常通知等)。 在幕后,Spring使用了JdkRegexpMethodPointcut
。 使用RegexpMethodPointcutAdvisor
可以简化装载,因为一个bean封装了切入点和通知,如以下示例所示:
<bean id="settersAndAbsquatulateAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref bean="beanNameOfAopAllianceInterceptor"/>
</property>
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
您可以将RegexpMethodPointcutAdvisor
与任何Advice
类型一起使用。
属性驱动的切入点
静态切入点的一种重要类型是元数据驱动的切入点。这将使用元数据属性的值(通常是源级别的元数据)。
动态切入点
动态切入点比静态切入点更昂贵。它们考虑了方法参数以及静态信息。这意味着必须在每次方法调用时对它们进行评估,并且由于参数会有所不同,因此无法缓存结果。
控制流切入点
Spring控制流切入点在概念上类似于AspectJ cflow切入点。控制流切入点与当前调用堆栈匹配。使用org.springframework.aop.support.ControlFlowPointcut
类指定控制流切入点。
6.1.5。切入点超类
因为静态切入点最有用,所以您可能应该子类化StaticMethodMatcherPointcut
。 这仅需要实现一个抽象方法(尽管您可以覆盖其他方法以自定义行为)。 下面的示例显示如何对StaticMethodMatcherPointcut进行子类化:
class TestStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method m, Class targetClass) {
// return true if custom criteria match
}
}
动态切入点也有超类。您可以将自定义切入点与任何通知类型一起使用。
6.1.6。自定义切入点
因为Spring AOP中的切入点是Java类,而不是语言功能(如AspectJ),所以您可以声明自定义切入点,无论是静态还是动态。Spring中的自定义切入点可以任意复杂。但是,如果可以的话,我们建议使用AspectJ切入点表达语言。
6.2. Spring Advice API
6.2.1。通知生命周期
每个通知都是一个Spring bean。 通知实例可以在所有被通知对象之间共享,或者对于每个通知对象都是唯一的。 这对应于每类(per-class)或每实例(per-instance)的通知。
每类通知(per-class)最常用。 适用于一般通知,例如事务顾问。 这些不依赖于代理对象的状态或添加新状态。 它们仅作用于方法和参数。
每实例通知(per-instance)都适合引入,以支持mixins。 在这种情况下,建议将状态添加到代理对象。
您可以在同一AOP代理中混合使用共享通知基于实例的通知。
6.2.2。Spring的通知类型
拦截环绕通知
Spring中最基本的通知类型是环绕通知的拦截。
对于使用方法拦截的通知,Spring符合AOP Alliance接口。 实现MethodInterceptor
和环绕通知的类也应该实现以下接口:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
invoke()
方法的MethodInvocation
参数公开了正在调用的方法,目标连接点,AOP代理以及该方法的参数。 invoke() 方法应返回调用的结果:连接点的返回值。
以下示例显示了一个简单的MethodInterceptor实现:
public class DebugInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Before: invocation=[" + invocation + "]");
Object rval = invocation.proceed();
System.out.println("Invocation returned");
return rval;
}
}
前置通知
public class CountingBeforeAdvice implements MethodBeforeAdvice {
private int count;
public void before(Method m, Object[] args, Object target) throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
异常通知
如果连接点抛出异常,则在连接点返回后调用异常通知。 Spring提供类型化的异常通知。 请注意,这意味着org.springframework.aop.ThrowsAdvice
接口不包含任何方法。 它是一个标签接口,用于标识给定的对象实现了一个或多个类型的throws
通知方法。 这些应采用以下形式:
afterThrowing([Method, args, target], subclassOfThrowable)
仅最后一个参数是必需的。方法签名可以具有一个或四个参数,具体取决于通知方法是否对该方法和参数感兴趣。接下来的两个清单显示了异常通知的示例。
public class RemoteThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
}
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
public static class CombinedThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
如果异常通知方法本身引发异常,则它将覆盖原始异常(也就是说,它将更改引发给用户的异常)。 重写异常通常是RuntimeException
,它与任何方法签名都兼容。 但是,如果异常通知方法抛出一个已检查的异常,则它必须与目标方法的已声明异常匹配,因此在某种程度上与特定的目标方法签名耦合。 不要抛出与目标方法签名不兼容的未声明的检查异常!
后置返回通知
public class CountingAfterReturningAdvice implements AfterReturningAdvice {
private int count;
public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
引入通知
Spring将引入通知视为一种特殊的拦截通知。
简介需要实现以下接口的IntroductionAdvisor
和IntroductionInterceptor
:
public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
引入通知不能与任何切入点一起使用,因为它仅适用于类,而不适用于方法级别。 您只能通过IntroductionAdvisor
使用引入通知,它具有以下方法:
public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
ClassFilter getClassFilter();
void validateInterfaces() throws IllegalArgumentException;
}
public interface IntroductionInfo {
Class[] getInterfaces();
}
getInterfaces()
方法返回此顾问程序引入的接口。
在内部使用validateInterfaces()
方法来查看引入的接口是否可以由配置的IntroductionInterceptor
实现。
6.3。Spring的Advisor API
在Spring中,Advisor是仅包含与切入点表达式关联的单个通知对象的切面。
除了引入的特殊情况外,任何顾问都可以与任何通知一起使用。 org.springframework.aop.support.DefaultPointcutAdvisor
是最常用的顾问类。 它可以与MethodInterceptor
,BeforeAdvice
或ThrowsAdvice
一起使用。
可以在同一AOP代理中的Spring中混合使用顾问和通知类型。 例如,您可以在一个代理配置中使用环绕通知的拦截,异常通知以及前置通知。 Spring自动创建必要的拦截器链。
6.4。使用ProxyFactoryBean创建AOP代理
在Spring中创建AOP代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean
。 这样可以完全控制切入点,任何适用的通知及其顺序。 但是,如果不需要这样的控制,则有一些更简单的选项比较可取。
6.4.1。基础
像其他Spring FactoryBean实现一样,ProxyFactoryBean
引入了一个间接级别。 如果定义一个名为foo的ProxyFactoryBean,则引用foo的对象将看不到ProxyFactoryBean实例本身,而是看到由ProxyFactoryBean中的getObject()
方法的实现创建的对象。 此方法创建一个包装目标对象的AOP代理。
使用ProxyFactoryBean或另一个支持IoC的类创建AOP代理的最重要好处之一是,通知切入点也可以由IoC管理。 这是一项强大的功能,可以实现某些其他AOP框架难以实现的方法。 例如,受益于依赖注入提供的所有可插入性,通知本身可以引用应用程序对象(目标对象之外,目标对象应该在任何AOP框架中可用)。
6.4.2。JavaBean属性
FactoryBean
与Spring提供的大多数实现相同,ProxyFactoryBean
类本身就是JavaBean。其属性用于:
- 指定要代理的目标。
- 指定是否使用CGLIB
一些关键属性是从org.springframework.aop.framework.ProxyConfig
(Spring中所有AOP代理工厂的超类)继承的。 这些关键属性包括:
proxyTargetClass
:如果要代理目标类而不是目标类的接口,则为true。 如果此属性值设置为true,则将创建CGLIB代理。optimize
:控制是否将积极的优化应用于通过CGLIB创建的代理。除非您完全了解相关的AOP代理如何处理优化,否则不要随意使用此设置。当前仅用于CGLIB代理。它对JDK动态代理无效。frozen
:如果代理配置为frozen,这是一个轻微的优化,对于在您不希望调用者在创建代理后能够操作代理(通过Advised接口)的情况下很有用。 此属性的默认值为false,因此允许进行更改(例如添加其他通知)。exposeProxy
:确定当前代理是否应该在ThreadLocal中公开,以便目标对象可以访问它。 如果目标对象需要获取代理,并且暴露代理属性设置为true,则目标可以使用AopContext.currentProxy()
方法。
其他ProxyFactoryBean
特定的属性包括:
proxyInterfaces
:接口名称的String数组。 如果未提供,则使用目标类的CGLIB代理。interceptorNames
:顾问,拦截器或要应用的其他通知名称的字符串数组。 顺序很重要,先到先得。 也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。
名称是当前工厂中的bean名称,包括祖先工厂中的bean名称。您不能在此提及bean引用,因为这样做会导致 ProxyFactoryBean忽略通知的单例设置。
您可以在拦截器名称后加上星号(*
)。 这样做将导致应用所有顾问Bean,其名称以要应用星号的部分开头。 您可以在使用“全局”顾问程序中找到使用此功能的示例。
singleton
:无论getObject()
方法被调用的频率如何,工厂是否应返回单个对象。 一些FactoryBean实现提供了这种方法。 默认值是true。 如果要使用带状态通知(例如,对于状态混合),请使用原型通知以及单例值false。
6.4.3。基于JDK和CGLIB的代理
在Spring的1.2.x版和2.0版之间,ProxyFactoryBean
的行为与创建基于JDK或CGLIB的代理有关。 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属性相同。但是,它的工作量大大减少,而且不容易出现错误。
创建代理的方法,参考源码:
org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy
6.4.4。代理接口
考虑一个简单的实际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 List,其中包含当前工厂中的拦截器或advisor
的Bean名称。 您可以使用前置、后置返回和异常通知对象。 advisor的顺序很重要。
您可能想知道为什么列表不包含bean引用。 这样做的原因是,如果ProxyFactoryBean
的singleton
属性设置为false,则它必须能够返回独立的代理实例。 如果任何顾问本身就是原型,则需要返回一个独立的实例,因此必须能够从工厂获得原型的实例。 保持引用是不够的。
person可以使用前面显示的bean定义代替Person实现,如下所示:
Person person = (Person) factory.getBean("person");
与普通Java对象一样,在同一IoC上下文中的其他bean可以表达对此的强类型依赖性。以下示例显示了如何执行此操作:
<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"/>
<!-- 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定义是独立的。 但是,有时能够从工厂获得未经通知的目标对象实际上可能是一个优势(例如,在某些测试方案中)。
6.4.5。代理类
如果需要,即使有接口,也可以在任何情况下强制使用CGLIB。
CGLIB代理通过在运行时生成目标类的子类来工作。Spring配置此生成的子类以将方法调用委托给原始目标。子类用于实现Decorator模式,并编织在通知中。
CGLIB代理通常应对用户透明。但是,有一些问题要考虑:
不能通知final
方法,因为它们不能被覆盖。
无需将CGLIB添加到您的类路径中。从Spring 3.2开始,CGLIB被重新打包并包含在spring-core JAR中。换句话说,基于CGLIB的AOP就像JDK动态代理一样“开箱即用”。
CGLIB代理和动态代理之间几乎没有性能差异。在这种情况下,性能不应作为决定性的考虑因素。
6.4.6。使用“全局”顾问
通过在拦截器名称后附加星号,所有具有与该星号之前的部分匹配的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"/>
6.5。简洁的代理定义
特别是在定义事务代理时,您可能会得到许多类似的代理定义。使用父子bean定义和子bean定义以及内部bean定义可以使代理定义更加简洁明了。
首先,我们为代理创建父模板,bean定义,如下所示:
<bean id="txProxyTemplate" abstract="true"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
它本身从未实例化,因此实际上可能是不完整的。然后,每个需要创建的代理都是一个子bean定义,它将代理的目标包装为内部bean定义,因为无论如何该目标都不会单独使用。以下示例显示了这样的子bean:
<bean id="myService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MyServiceImpl">
</bean>
</property>
</bean>
您可以从父模板覆盖属性。在以下示例中,我们将覆盖事务传播设置:
<bean id="mySpecialService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MySpecialServiceImpl">
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="store*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
请注意,在父bean的示例中,我们通过将abstract
属性设置为true来将父bean定义显式标记为抽象, 因此实际上可能不会实例化它。默认情况下,应用程序上下文(但不是简单的bean工厂)会预先实例化所有单例。因此,重要的是(至少对于单例bean),如果您有一个(父)bean定义仅打算用作模板,并且此定义指定了一个类,则必须确保将abstract 属性设置为true。否则,应用程序上下文实际上会尝试对其进行实例化。
6.6。 使用ProxyFactory以编程方式创建AOP代理
使用Spring以编程方式创建AOP代理很容易。这使您可以使用Spring AOP,而无需依赖Spring IoC。
由目标对象实现的接口将被自动代理。以下清单显示了使用一个拦截器和一个顾问程序为目标对象创建代理的过程:
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
第一步是构造org.springframework.aop.framework.ProxyFactory
类型的对象 。您可以使用目标对象创建此对象,如前面的示例中所示,或指定要在备用构造函数中代理的接口。
您可以添加通知(使用拦截器作为一种特殊的通知),顾问,或同时添加两者,并在ProxyFactory的生命周期内对其进行操作。如果添加 IntroductionInterceptionAroundAdvisor
,则可以使代理实现其他接口。
ProxyFactory
(继承自AdvisedSupport
)上还有便捷的方法,可让您添加其他通知类型,例如前置、异常通知。 AdvisedSupport
是ProxyFactory
和ProxyFactoryBean
的父类。
在大多数应用程序中,将AOP代理创建与IoC框架集成在一起是最佳实践。通常,建议您使用AOP从Java代码外部化配置。
6.7。操作通知对象
即使创建AOP代理,仍然可以通过使用org.springframework.aop.framework.Advised
接口来操作它们。 任何AOP代理都可以强制转换为该接口,无论它实现了哪个其他接口。 该界面包括以下方法:
Advisor[] getAdvisors();
void addAdvice(Advice advice) throws AopConfigException;
void addAdvice(int pos, Advice advice) throws AopConfigException;
void addAdvisor(Advisor advisor) throws AopConfigException;
void addAdvisor(int pos, Advisor advisor) throws AopConfigException;
int indexOf(Advisor advisor);
boolean removeAdvisor(Advisor advisor) throws AopConfigException;
void removeAdvisor(int index) throws AopConfigException;
boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;
boolean isFrozen();
getAdvisors()
方法针对已添加到工厂的每个顾问程序,拦截器或其他通知类型返回一个顾问程序。 如果添加了顾问,则此索引处返回的顾问是您添加的对象。 如果添加了拦截器或其他通知类型,Spring会将其包装在带有总是返回true的切入点的顾问程序中。 因此,如果添加了MethodInterceptor
,则为该索引返回的顾问程序是DefaultPointcutAdvisor
,它返回您的MethodInterceptor和与所有类和方法匹配的切入点。
addAdvisor()
方法可用于添加任何Advisor
。通常,拥有切入点和通知的顾问是通用的DefaultPointcutAdvisor
,您可以将其与任何通知或切入点一起使用(不能用于引入通知)。
默认情况下,即使已创建代理,也可以添加或删除顾问程序或拦截器。 唯一的限制是不可能添加或删除引入顾问,因为工厂中的现有代理不会显示接口更改。 (您可以从工厂获取新的代理来避免此问题。)
以下示例显示了将AOP代理投射到Advised接口并检查和处理其通知:
Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");
// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(new DebugInterceptor());
// Add selective advice using a pointcut
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));
assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);
根据创建代理的方式,通常可以设置 frozen
标志。 在这种情况下,Advised isFrozen()
方法返回true
,并且任何通过添加或删除来修改通知的尝试都会导致AopConfigException
。 冻结通知对象状态的功能在某些情况下很有用(例如,防止调用代码删除安全拦截器)。
6.8。使用“自动代理”功能
Spring还允许我们使用“自动代理(auto-proxy)” Bean定义,该定义可以自动代理选定的Bean定义。它建立在Spring的“bean后置处理器”基础结构上,该基础结构允许在容器加载时修改任何bean定义。
在此模型中,您在XML bean定义文件中设置了一些特殊的bean定义,以配置自动代理基础结构。这使您可以声明有资格进行自动代理的目标。您不需要使用ProxyFactoryBean
。
有两种方法可以做到这一点:
通过使用在当前上下文中引用特定bean的自动代理创建器。
自动代理创建的一种特殊情况,值得单独考虑:由源级别元数据属性驱动的自动代理创建。
6.8.1。自动代理Bean定义
本节介绍了org.springframework.aop.framework.autoproxy
包提供的自动代理创建器。
BeanNameAutoProxyCreator
BeanNameAutoProxyCreator
类是一个BeanPostProcessor
,可以自动为名称与文字值或通配符匹配的bean创建AOP代理。 以下示例显示了如何创建BeanNameAutoProxyCreator bean:
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames" value="jdk*,onlyJdk"/>
<property name="interceptorNames">
<list>
<value>myInterceptor</value>
</list>
</property>
</bean>
与ProxyFactoryBean一样,有interceptorNames
属性而不是拦截器列表,以允许原型顾问程序具有正确的行为。 名为“interceptors”的可以是顾问或任何通知类型。
一般而言,与自动代理一样,使用BeanNameAutoProxyCreator
的要点是将相同的配置一致地应用于多个对象,并且配置量最少。 将声明式事务应用于多个对象是一种流行的选择。
名称匹配的Bean定义,例如前面示例中的jdkMyBean和onlyJdk,是带有目标类的普通Bean定义。 BeanNameAutoProxyCreator自动创建一个AOP代理。 相同的通知适用于所有匹配的bean。 注意,如果使用了顾问程序(而不是前面的示例中的拦截器),则切入点可能会不同地应用于不同的bean。
DefaultAdvisorAutoProxyCreator
一个更通用,功能更强大的自动代理创建者是 DefaultAdvisorAutoProxyCreator
。这可以在当前上下文中自动应用合格的顾问程序,而无需在自动代理顾问程序的Bean定义中包括特定的Bean名称。它具有与BeanNameAutoProxyCreator一致的配置和避免重复的优点。
使用此机制涉及:
- 指定
DefaultAdvisorAutoProxyCreator
bean定义。 - 在相同或相关的上下文中指定任意数量的顾问。 请注意,这些必须是顾问程序,而不是拦截器或其他通知。 这是必要的,因为必须有一个评估的切入点,以检查每个通知是否符合候选bean定义。
DefaultAdvisorAutoProxyCreator
自动评估每个顾问程序中包含的切入点,以查看应将其应用于每个业务对象(例如示例中的businessObject1和businessObject2)的通知(如果有)。
这意味着可以将任意数量的顾问程序自动应用于每个业务对象。 如果在任何顾问程序中没有切入点与业务对象中的任何方法匹配,则该对象不会被代理。 当为新的业务对象添加Bean定义时,如有必要,它们会自动被代理。
通常,自动代理的优点是使调用者或依赖者无法获得非通知的对象。 在此ApplicationContext
上调用getBean("businessObject1")
会返回AOP代理,而不是目标业务对象。 (前面显示的"inner bean" 也提供了这一好处。)
以下示例创建一个DefaultAdvisorAutoProxyCreator bean和本节中讨论的其他元素:
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
<property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>
<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>
<bean id="businessObject1" class="com.mycompany.BusinessObject1">
<!-- Properties omitted -->
</bean>
<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>
如果要将相同的通知一致地应用于许多业务对象,则DefaultAdvisorAutoProxyCreator
非常有用。 基础结构定义到位后,您可以添加新的业务对象,而无需包括特定的代理配置。 您也可以轻松地添加其他方面(例如,跟踪或性能监视方面),而对配置的更改最少。
DefaultAdvisorAutoProxyCreator
提供过滤支持(通过使用命名约定,以便仅评估某些Advisor,从而允许在同一工厂中使用多个配置不同的AdvisorAutoProxyCreators)和排序。 顾问可以实现org.springframework.core.Ordered
接口,以确保在出现问题时可以正确排序。 前面示例中使用的TransactionAttributeSourceAdvisor
具有可配置的顺序值。 默认设置为无序。
6.9。使用TargetSource实现
Spring提供了TargetSource
的概念,以org.springframework.aop.TargetSource
接口表示。 该接口负责返回实现连接点的“目标对象”。 每当AOP代理处理方法调用时,都会向TargetSource实现请求目标实例。
使用Spring AOP的开发人员通常不需要直接与TargetSource实现一起工作,但这提供了一种强大的手段来支持池化,热部署和其他复杂的目标。例如,TargetSource通过使用池来管理实例,池可以为每个调用返回不同的目标实例。
如果未指定TargetSource,则将使用默认实现包装本地对象。 每次调用都返回相同的目标(与您期望的一样)。
本节的其余部分描述了Spring随附的标准目标源以及如何使用它们。
使用自定义目标源时,目标通常需要是原型而不是单例bean定义。这样,Spring可以在需要时创建一个新的目标实例。
6.9.1。热部署的目标源
存在org.springframework.aop.target.HotSwappableTargetSource
可以切换AOP代理的目标,同时允许调用者保留对其的引用。
更改目标源的目标会立即生效。HotSwappableTargetSource
是线程安全的。
您可以使用HotSwappableTargetSource上的swap()
方法更改目标,如以下示例所示:
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);
以下示例显示了必需的XML定义:
<bean id="initialTarget" class="mycompany.OldTarget"/>
<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
<constructor-arg ref="initialTarget"/>
</bean>
<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="swapper"/>
</bean>
前面的swap()
调用更改了可交换bean的目标。 拥有对该bean的引用的客户端不知道更改,但立即开始达到新目标。
尽管此示例未添加任何通知(使用添加到TargetSource的通知没有必要),但是可以将任何TargetSource与任意通知结合使用。
6.9.2。池化目标源
使用池目标源提供了与无状态会话EJB相似的编程模型,在无状态会话EJB中,维护了相同实例的池,方法调用将释放池中的对象。
Spring池和SLSB池之间的关键区别在于,Spring池可以应用于任何POJO。通常,与Spring一样,可以以非侵入性方式应用此服务。
Spring提供对Commons Pool 2.2的支持,该池提供了相当有效的池实现。 您需要在应用程序的类路径上使用commons-pool
Jar才能使用此功能。 您还可以将org.springframework.aop.target.AbstractPoolingTargetSource
子类化以支持任何其他池化API。
以下清单显示了一个示例配置:
<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
scope="prototype">
... properties omitted
</bean>
<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
<property name="maxSize" value="25"/>
</bean>
<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="poolTargetSource"/>
<property name="interceptorNames" value="myInterceptor"/>
</bean>
请注意,目标对象(在前面的示例中为businessObjectTarget)必须是原型。 这使PoolingTargetSource
实现可以创建目标的新实例,以根据需要扩展池。 有关其属性的信息,请参见AbstractPoolingTargetSource
的javadoc和希望使用的具体子类。 maxSize
是最基本的,并且始终保证存在。
在这种情况下,myInterceptor是需要在同一IoC上下文中定义的拦截器的名称。 但是,您无需指定拦截器即可使用池。 如果只希望池化而没有其他通知,则完全不要设置interceptorNames
属性。
您可以将Spring配置为能够将任何池化对象转为org.springframework.aop.target.PoolingConfig
接口,该接口通过引入来公开有关池的配置和当前大小的信息。 您需要定义类似于以下内容的顾问程序:
<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="poolTargetSource"/>
<property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>
通过在AbstractPoolingTargetSource
类上调用便捷方法来获得此顾问程序,因此可以使用MethodInvokingFactoryBean
。 该顾问的名称(在此处为poolConfigAdvisor
)必须位于公开池对象的ProxyFactoryBean中的拦截器名称列表中。
转换如下:
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());
通常不需要合并无状态服务对象。我们不认为它应该是默认选择,因为大多数无状态对象自然是线程安全的,并且如果缓存了资源,实例池会成问题。
通过使用自动代理,可以实现更简单的池化。 您可以设置任何自动代理创建者使用的TargetSource实现。
6.9.3。原型目标源
设置“原型”目标源类似于设置池化TargetSource
。 在这种情况下,每次方法调用都会创建目标的新实例。 尽管在现代JVM中创建新对象的成本并不高,但是注入新对象(满足其IoC依赖关系)的成本可能会更高。 因此,没有充分的理由就不应使用此方法。
为此,您可以修改前面显示的poolTargetSource定义,如下所示(为清楚起见,我们也更改了名称):
<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
<property name="targetBeanName" ref="businessObjectTarget"/>
</bean>
唯一的属性是目标Bean的名称。在TargetSource实现中使用继承 以确保命名一致。与池化目标源一样,目标bean必须是原型bean定义。
6.9.4。ThreadLocal目标源
如果需要为每个传入请求(每个线程)创建一个对象,则ThreadLocal目标源很有用。 ThreadLocal的概念提供了JDK范围的功能,可以透明地将资源与线程一起存储。 设置ThreadLocalTargetSource
几乎与针对其他类型的目标源所说明的相同,如以下示例所示:
<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
</bean>
在多线程和多类加载器环境中错误地使用ThreadLocal实例时,会带来严重的问题(可能导致内存泄漏)。 您应该始终考虑在其他一些类中包装threadlocal,并且绝对不要直接使用ThreadLocal本身(包装类中除外)。 另外,您应该始终记住正确设置和取消设置线程本地资源的正确设置和取消设置(后者仅涉及对ThreadLocal.set(null)
的调用)。 在任何情况下都应进行取消设置,因为不取消设置可能会导致出现问题。 Spring的ThreadLocal支持为您做到了这一点,应该始终考虑使用ThreadLocal实例,而无需其他适当的处理代码。
6.10。定义新的通知类型
Spring AOP被设计为可扩展的。 尽管目前在内部使用拦截实现策略,但是除了在环绕通知,前置通知,异常通知和后置返回通知,还可以支持任意通知类型。
org.springframework.aop.framework.adapter
软件包是一个SPI软件包,可以在不更改核心框架的情况下添加对新的自定义通知类型的支持。 对自定义Advice类型的唯一限制是它必须实现org.aopalliance.aop.Advice
标记接口。
有关更多信息,请参见org.springframework.aop.framework.adapter
javadoc。