20220507 Core - 6. Spring AOP API
前言
在本章中,将讨论底层的 Spring AOP API 。对于常见的应用程序,推荐使用带有 AspectJ 切点的 Spring AOP
Spring 中的切点 Pointcut API
概念
Spring 的切点模型支持独立于通知类型的切点重用。可以使用相同的切点针对不同的通知。
org.springframework.aop.Pointcut
是核心接口,用来针对特定的类和方法发出通知
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
将 Pointcut
接口分成两部分允许重用类和方法匹配部分以及细粒度组合操作(例如与另一个方法匹配器执行“联合”)。
ClassFilter
接口用于将切点限制到一组给定的目标类。如果 matches()
方法始终返回 true
,则匹配所有目标类。
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);
}
matches(Method, Class)
方法用于测试此切点是否与目标类上的给定方法匹配。可以在创建 AOP 代理时执行此计算,以避免需要对每个方法调用进行测试。如果两个参数的 matches
方法返回 true
,并且 MethodMatcher
的 isRuntime()
方法返回 true
,则每次方法调用时都会调用三个参数的 matches
方法。这让切点在目标通知开始之前立即查看传递给方法调用的参数。
如果两个参数的 matches
方法返回 true
并且 isRuntime
方法返回 true
,就会调用三个参数的 matches
方法校验参数
大多数 MethodMatcher
实现都是静态的,这意味着它们的 isRuntime()
方法返回 false
。在这种情况下,不会调用三参数 matches
方法。
如果可能,尽量使切点静态化,允许 AOP 框架在创建 AOP 代理时缓存切点计算的结果。
切点操作
Spring 支持切点上的操作,特别是并集(union)和交集(intersection)
并集表示任一切点匹配的方法。交集意味着两个切点都匹配的方法。并集通常更有用。
可以通过使用 org.springframework.aop.support.Pointcuts
类中的静态方法或使用相同包中的 ComposablePointcut
类来组合切点 。然而,使用 AspectJ 切点表达式通常是一种更简单的方法。
AspectJ 表达式切点
从 2.0 开始,Spring 最重要的切点类型是 org.springframework.aop.aspectj.AspectJExpressionPointcut
。这是一个切点,它使用 AspectJ 提供的库来解析 AspectJ 切点表达式字符串。
便利的切点实现
静态切点
静态切点基于方法和目标类,不考虑方法的参数。
对于大多数用途,静态切点就足够了。当方法第一次被调用时,Spring 计算一次静态切点。之后,无需在每次方法调用时再次评估切点。
正则表达式切点
org.springframework.aop.support.JdkRegexpMethodPointcut
是一个通用的正则表达式切点,它使用 JDK 中的正则表达式提供支持。
<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
可以是拦截器、before 通知、throws 通知等)。在底层,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>
属性驱动的切点
一种重要的静态切点是元数据驱动的切点。这使用元数据属性的值(通常是源码级元数据)。
动态切点
动态切点比静态切点的计算成本更高。它考虑了方法参数以及静态信息。
这意味着必须在每次方法调用时进行计算,并且结果不能被缓存,因为参数会有所不同。
控制流切点
Spring 控制流切点在概念上类似于 AspectJ cflow
切点,但功能较弱。(目前没有办法指定一个切点在另一个切点匹配的连接点之下运行)
控制流切点匹配当前调用堆栈。例如,如果连接点被 com.mycompany.web
包中的方法或 SomeCaller
类调用,它可能会触发。控制流切点是通过使用 org.springframework.aop.support.ControlFlowPointcut
类来指定的。
切点超类
Spring 提供了有用的切点超类来帮助实现自定义切点。
继承 StaticMethodMatcherPointcut
可以自定义静态切点。这只需要实现一个抽象方法。示例如下:
class TestStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method m, Class targetClass) {
// return true if custom criteria match
}
}
自定义切点
可以声明自定义切点,无论是静态的还是动态的。Spring 中的自定义切点可以是任意复杂的。但是,建议使用 AspectJ 切点表达式语言。
Spring 中的通知 Advice API
通知生命周期
每个通知都是一个 Spring bean 。通知实例可以在所有通知对象之间共享,或者对于每个通知对象都是唯一的。这对应于 per-class
或 per-instance
的通知。
最常用的是 per-class
通知。它适用于通用通知,例如事务顾问。这些不依赖于代理对象的状态或添加新状态。它们仅对方法和参数起作用。
per-instance
的通知适用于引入,以支持混合( mixin
)。这种情况下,建议将状态添加到代理对象。
可以在同一个 AOP 代理中混合使用共享和 per-instance
的通知。
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;
}
}
注意对 MethodInvocation
的 proceed()
方法的调用。这沿着拦截器链向连接点前进。大多数拦截器调用此方法并返回其返回值。但是, MethodInterceptor
与任何环绕通知一样,可以返回不同的值或抛出异常,而不是调用 proceed
方法。但是,您不应该在没有充分理由的情况下这样做。
MethodInterceptor
实现提供与其他符合 AOP 联盟的 AOP 实现的互操作性。本节剩余部分讨论的其他通知类型实现了常见的 AOP 概念,但以特定于 Spring 的方式实现。
虽然使用最具体的通知类型有优势,但如果您可能想在另一个 AOP 框架中运行切面,请坚持使用 MethodInterceptor
环绕通知。注意,切点目前无法在框架之间互操作,并且 AOP 联盟目前没有定义切点接口。
前置通知
更简单的通知类型是前置通知。它不需要 MethodInvocation
对象,因为它只在进入方法之前被调用。
前置通知的主要优点是不需要调用 proceed()
方法,因此不存在无法沿拦截器链继续下去的可能性。
MethodBeforeAdvice
接口:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
注意,返回类型是 void
。前置通知可以在连接点运行之前插入自定义行为,但不能更改返回值。
如果前置通知抛出异常,它会停止拦截器链的进一步执行。异常沿拦截器链向上传播。如果是非检查异常或在调用方法的签名上,则将其直接传递给客户端。否则,它会被 AOP 代理包裹在一个非检查异常中。
示例:
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
接口不包含任何方法。它是一个标记接口,用于标识给定对象实现一个或多个类型化的异常通知方法。
通知方法应采用以下格式:
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
}
}
最后一个示例说明了如何在处理 RemoteException
和 ServletException
的单个类中使用这两种方法。任何数量的异常通知方法都可以组合在一个类中。
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
,它与任何方法签名兼容。
但是,如果异常通知方法抛出检查异常,则它必须与目标方法的声明异常相匹配,因此在某种程度上与特定的目标方法签名耦合。*不要抛出与目标方法的签名不兼容的未声明的检查异常!***
异常通知可以与任何切点一起使用。
后置返回通知
org.springframework.aop.AfterReturningAdvice
接口:
public interface AfterReturningAdvice extends Advice {
void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable;
}
后置返回通知可以访问返回值(不能修改)、调用的方法、方法的参数和目标。
示例:
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
从 AOP 联盟 MethodInterceptor
接口继承的 invoke()
方法必须实现。也就是说,如果被调用的方法在引入的接口上,则引入拦截器负责处理方法调用——它不能调用 proceed()
。
引入通知不能与任何切点一起使用,因为它仅适用于类,而不是方法级别。只能将引入通知与 IntroductionAdvisor
一起使用
public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
ClassFilter getClassFilter();
void validateInterfaces() throws IllegalArgumentException;
}
public interface IntroductionInfo {
Class<?>[] getInterfaces();
}
没有 MethodMatcher
,因此,没有 Pointcut
与引入通知相关联。只有类过滤。
getInterfaces()
方法返回此顾问引入的接口。validateInterfaces()
方法用于内部查看引入的接口是否可以由配置的IntroductionInterceptor
实现
使用示例
向一个或多个对象引入以下接口:
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {
private boolean locked;
public void lock() {
this.locked = true;
}
public void unlock() {
this.locked = false;
}
public boolean locked() {
return this.locked;
}
public Object invoke(MethodInvocation invocation) throws Throwable {
if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
throw new LockedException();
}
return super.invoke(invocation);
}
}
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
public LockMixinAdvisor() {
super(new LockMixin(), Lockable.class);
}
}
我们可以通过使用 XML 配置中的方法或 Advised.addAdvisor()
(推荐)以编程方式应用此顾问,就像任何其他顾问一样。
Spring 中的 Advisor API
在 Spring 中,顾问是一个切面,它只包含一个与切点表达式相关联的通知对象。
除了引入的特殊情况外,顾问可以与任何通知一起使用。 org.springframework.aop.support.DefaultPointcutAdvisor
是最常用的顾问类。它可以与 MethodInterceptor
, BeforeAdvice
或 ThrowsAdvice
一起使用。
在 Spring 中可以在同一个 AOP 代理中混合顾问和通知类型。Spring 会自动创建必要的拦截器链。
使用 ProxyFactoryBean
来创建 AOP 代理
工厂 bean 引入了一个间接层,让它创建不同类型的对象
Spring AOP 在底层使用工厂 bean。
在 Spring 中创建 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean
。 这可以完全控制切点、任何适用的通知及其顺序。
基础
ProxyFactoryBean
就像其它 FactoryBean
实现一样,引入了一个间接层。如果定义了一个名为 foo
的 ProxyFactoryBean
,引用 foo
的对象不会看到 ProxyFactoryBean
实例本身,而是看到 ProxyFactoryBean
中 getObject()
方法实现创建的对象。此方法创建一个包装目标对象的 AOP 代理。
使用 ProxyFactoryBean
或 IoC 感知类来创建 AOP 代理的好处之一是通知和切点也可以由 IoC 管理。
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
中公开, 以便可以访问它。如果需要获取代理并且exposeProxy
属性设置为true
,则可以使用AopContext.currentProxy()
方法
其他特定于 ProxyFactoryBean
的属性:
-
proxyInterfaces
:接口名称String
数组。如果未提供,则使用目标类的 CGLIB 代理 -
interceptorNames
:要应用的Advisor
、拦截器或其他通知名称的String
数组。顺序很重要,先入先出,也就是说列表中的第一个拦截器是第一个被拦截调用的。名称是当前工厂中的 bean 名称,包括来自祖先工厂的 bean 名称。不能在此处使用 bean 引用,因为这样做会导致
ProxyFactoryBean
忽略通知的单例设置。可以对拦截器名称使用星号 (
*
)。这样做会导致应用名称以要应用的星号之前的部分开头的所有顾问 bean。您可以在使用“全局”顾问中找到使用此功能的示例。 -
singleton
:无论getObject()
方法被调用的频率如何,工厂是否应该返回单个对象。一些FactoryBean
实现提供了这样的方法。默认值为true
。如果你想使用有状态的通知,设置为false
——例如,对于有状态的 mixin——使用原型通知。
基于 JDK 和 CGLIB 的代理
本节作为关于 ProxyFactoryBean
如何选择为特定目标对象创建基于 JDK 的代理或基于 CGLIB 的代理的权威文档。
如果要代理的目标对象的类(以下简称为目标类)没有实现任何接口,则创建基于 CGLIB 的代理。这是最简单的情况,因为 JDK 代理是基于接口的,没有接口意味着不可能进行 JDK 代理。可以通过设置 interceptorNames
属性来指定拦截器列表。请注意,即使 ProxyFactoryBean
的 proxyTargetClass
属性已设置为 false
,也会创建基于 CGLIB 的代理。
如果目标类实现一个(或多个)接口,则创建的代理类型取决于 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
org.springframework.aop.framework.ProxyFactoryBean
代理接口
示例:
<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 名称。可以使用顾问、拦截器、前置、后置和异常通知对象。顾问的排序很重要。
前面显示的 person
bean 定义可以用来代替 Person
实现,如下所示:
Person person = (Person) factory.getBean("person");
同一个 IoC 上下文中的其他 bean 可以表达对它的强类型依赖,就像普通 Java 对象一样。以下示例显示了如何执行此操作:
<bean id="personUser" class="com.mycompany.PersonUser">
<property name="person"><ref bean="person"/></property>
</bean>
可以通过使用匿名内部 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
定义是自包含的。但是,有时能够从工厂获得未通知的目标实际上可能是一种优势(例如,在某些测试场景中)。
代理类
将 ProxyFactoryBean
的 proxyTargetClass
属性设置为 true
CGLIB 代理通过在运行时生成目标类的子类来工作。Spring 配置这个生成的子类来将方法调用委托给原始目标。子类用于实现装饰器模式,编织在通知中。
CGLIB 代理一般应该对用户透明。但是,有一些问题:
- 不能通知
final
方法,因为它们不能被覆盖 - 无需将 CGLIB 添加到您的类路径中。从 Spring 3.2 开始,CGLIB 被重新打包并包含在
spring-core
JAR 中
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 配置里的模板配置,设置 abstract="true"
<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 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>
ProxyFactory
以编程方式创建 AOP 代理
使用 Spring 以编程方式创建 AOP 代理很容易。这使您可以在不依赖 Spring IoC 的情况下使用 Spring AOP。
目标对象实现的接口会自动代理。以下清单显示了为目标对象创建代理,具有一个拦截器和一个顾问:
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
在大多数应用程序中,将 AOP 代理创建与 IoC 框架集成是最佳实践。我们建议您使用 AOP 从 Java 代码外部进行配置。
操作被通知对象
创建 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()
方法为每个已添加到工厂的顾问、拦截器或其他通知类型返回 Advisor
。如果您添加了 Advisor
,则此索引处返回的顾问就是您添加的对象。如果您添加了拦截器或其他通知类型,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
。在某些情况下,冻结通知对象状态的能力很有用(例如,防止调用删除安全拦截器的代码)。
使用“自动代理”功能
Spring 还允许我们使用 “自动代理” bean 定义,它可以自动代理选定的 bean 定义。这是建立在 Spring 的 bean 后置处理器 之上的,它允许在容器加载时修改任何 bean 定义。
您需要在 XML bean 定义文件中设置了一些特殊的 bean 定义以配置自动代理基础结构。这使您可以声明符合自动代理条件的目标。不需要使用 ProxyFactoryBean
有两种方法可以做到这一点:
- 通过使用在当前上下文中引用特定 bean 的自动代理创建器
- 自动代理创建的一个特殊情况值得考虑: 源码级元数据属性驱动的自动代理创建
参考源码
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessBeforeInstantiation
自动代理 Bean 定义
BeanNameAutoProxyCreator
BeanNameAutoProxyCreator
类是自动为名称与文本值或通配符匹配的 bean 创建 AOP 代理的 BeanPostProcessor
类。以下示例显示了如何创建 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
属性而不是拦截器列表,以允许原型顾问的正确行为。“拦截器” 可以是顾问或任何通知类型。
与一般的自动代理一样,使用 BeanNameAutoProxyCreator
的主要目的是将相同的配置一致地应用于多个对象。它是将声明式事务应用于多个对象的流行选择。
名称匹配的 bean 定义(如上例中的 jdkMyBean
和 onlyJdk
)是带有目标类的普通 bean 定义。AOP 代理由 BeanNameAutoProxyCreator
自动创建。 相同的通知适用于所有匹配的 bean 。请注意,如果使用了顾问(而不是拦截器),切点可能会以不同的方式应用于不同的 bean 。
DefaultAdvisorAutoProxyCreator
一个更通用且极其强大的自动代理创建器是 DefaultAdvisorAutoProxyCreator
。这会在当前上下文中自动应用符合条件的顾问,而无需在自动代理顾问的 bean 定义中包含特定的 bean 名称。它提供了与 BeanNameAutoProxyCreator
相同的配置一致性和避免重复的优点
使用这种机制包括:
- 指定
DefaultAdvisorAutoProxyCreator
bean 定义 - 在相同或相关的上下文中指定任意数量的顾问。注意,必须是顾问,而不是拦截器或其他通知。这是必要的,因为必须有一个切点来评估,以检查每个通知使用应该用于候选 bean 定义
DefaultAdvisorAutoProxyCreator
自动计算包括在每个顾问中的切点,看看有什么通知,应该适用于每个业务对象
自动代理通常具有使调用者或依赖项无法获得原始对象的优势。调用 ApplicationContext
的 getBean("businessObject1")
会返回一个 AOP 代理,而不是目标业务对象。
示例:
<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
支持过滤(通过使用一种命名约定,使得只有特定的顾问进行评估,其允许使用多个不同配置,在同一个工厂的 AdvisorAutoProxyCreators
)和顺序。顾问可以实现 org.springframework.core.Ordered
接口以确保正确排序。在前例中使用 TransactionAttributeSourceAdvisor
具有可配置的顺序值。默认设置是无序的。
使用 TargetSource
实现
org.springframework.aop.TargetSource
接口负责返回实现连接点的“目标对象”。每次 AOP 代理处理方法调用时,都会要求 TargetSource
实现提供一个目标实例。
使用 Spring AOP 的开发人员通常不需要直接使用 TargetSource
实现,但 TargetSource
实现提供了支持池化、热插拔和其他复杂目标的强大方法。例如,通过使用 TargetSource
池来管理实例,池可以为每次调用返回不同的目标实例。
如果未指定 TargetSource
,则使用默认实现来包装本地对象。每次调用都会返回相同的目标。
使用自定义目标源时,您的目标通常需要是原型而不是单例 bean 定义。这允许 Spring 在需要时创建一个新的目标实例。
热插拔目标源
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
可以与任意通知结合使用。
池化目标源
使用池化目标源提供了与无状态会话 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
实现。
原型目标源
设置“原型”目标源类似于设置池化 TargetSource
。在这种情况下,每次方法调用都会创建一个新的目标实例。尽管在现代 JVM 中创建新对象的成本并不高,但连接新对象(满足其 IoC 依赖性)的成本可能更高。因此,您不应在没有充分理由的情况下使用这种方法。
为此,您可以修改前面的 poolTargetSource
定义,如下所示:
<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
<property name="targetBeanName" ref="businessObjectTarget"/>
</bean>
唯一的属性是目标 bean 的名称。在 TargetSource
实现中使用继承来确保一致的命名。与池化目标源一样,目标 bean 必须是原型 bean 定义。
ThreadLocal
目标源
如果您需要为每个传入请求(即每个线程)创建一个对象,则 ThreadLocal
目标源非常有用。
<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
</bean>
定义新的通知类型
Spring AOP 被设计为可扩展的。虽然拦截实现策略目前在内部使用,但除了环绕通知、前置、异常通知和后置返回通知之后的拦截之外,还可以支持任意通知类型。
org.springframework.aop.framework.adapter
包是一个 SPI 包,可以在不更改核心框架的情况下添加对新自定义通知类型的支持。自定义 Advice
类型的唯一约束是它必须实现 org.aopalliance.aop.Advice
标记接口。