Spring的一个关键组件是AOP框架。虽然Spring IoC容器不依赖于AOP(意味着如果您不想使用AOP,则不需要使用AOP),AOP补充了Spring IoC,以提供非常强大的中间件解决方案。
AOP在Spring Framework中用于:
- 提供声明性企业服务,尤其是作为EJB声明性服务的替代品。最重要的此类服务是 声明式事务管理。
- 让用户实现自定义切面,用AOP补充OOP的使用
AOP概念
Spring AOP是用纯Java实现的。不需要特殊的编译过程。Spring AOP不需要控制类加载器层次结构,因此适合在servlet容器或应用程序服务器中使用。
Spring AOP目前仅支持方法执行连接点(通知在Spring bean上执行方法)。虽然可以在不破坏核心Spring AOP API的情况下添加对字段拦截的支持,但未实现字段拦截。如果您需要通知字段访问和更新连接点,请考虑使用AspectJ等语言。
Spring AOP默认使用标准的JDK动态代理作为AOP代理。这允许代理任何接口(或一组接口)。
下面是一些AOP的术语。
Aspect(切面):跨多个类的关注点的模块化。事务管理是企业Java应用程序中横切关注点的一个很好的例子。在Spring AOP中,切面是通过使用常规类(基于模式的方法)或使用@Aspect注释(@AspectJ样式)注释的常规类来实现的。(模块化)
Join point(连接点):程序执行过程中的点,如方法的执行或异常的处理。在Spring AOP中,连接点总是表示方法执行(值方法的一处,一般是方法执行前、执行后或抛出异常后)。
Advice(通知):切面在特定连接点采取的操作。不同类型的通知包括“围绕”、“之前”和“之后”的通知。(通知类型将在后面讨论。)许多AOP框架,包括Spring,将通知建模为拦截器,并在连接点周围维护拦截器链。(指需要连接的具体操作)
Pointcut(切入点):匹配连接点的谓词。通知与切入点表达式相关联,并在与切入点匹配的任何连接点运行(例如,执行具有特定名称的方法)。由切入点表达式匹配的连接点的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。(指一串表达式,spring会根据这个表达式找到对应的切面)
Introduction(引入):代表类型声明其他方法或字段。Spring AOP允许您向任何通知的对象引入新接口(和相应的实现)。例如,您可以使用简介使bean实现 IsModified接口,以简化缓存。
Target object(目标对象):一个或多个切面通知的对象。也被称为“通知对象”。由于Spring AOP是通过使用运行时代理实现的,所以这个对象始终是一个代理对象。
AOP proxy(AOP代理):AOP框架为了实现切面(通知方法执行等)而创建的对象。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。
Weaving(织入):将切面与其他应用程序类型或对象链接以创建通知对象。这可以在编译时、加载时或运行时完成。Spring AOP和其他纯Java AOP框架一样,在运行时进行织入。
Spring AOP包括以下类型的通知:
- Before advice:通知在连接点之前运行,但是不能阻止执行流继续到连接点(除非它引发异常)。
- After returning advice:在连接点正常完成后运行的通知(例如,如果方法返回而不抛出异常)。
- After throwing advice:如果方法通过抛出异常退出,则执行通知。
- After (finally) advice:不管连接点以何种方式退出(正常返回或异常返回),都要执行通知。
- Around advice:围绕连接点(如方法调用)的通知。这是最有力的通知。围绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续到连接点,还是通过返回自己的返回值或抛出异常来缩短通知的方法执行。
基于XML格式的AOP支持
Spring提供了对使用新的aop命名空间标记定义方面的支持,要使用aop命名空间标记需要导入spring-aop模式。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- bean definitions here -->
</beans>
在Spring配置中,所有切面和通知元素都必须放在<aop:config>元素中(在应用程序上下文配置中可以有多于一个<aop:config>元素)。<aop:config>元素可以包含切入点、通知和切面元素(注意,这些元素必须按顺序声明)。
声明一个切面(aspect)
您可以使用<aop:aspect>元素声明切面,并使用ref属性引用支持bean ,如以下示例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
声明切入点(pointcut)
您可以在<aop:config>元素中声明一个命名的切入点,允许跨越多个方面和顾问共享切入点定义。
表示服务层中任何业务服务执行的切入点可以定义如下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>
如果使用基于java模式的声明样式,则可以引用切入点表达式中类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法如下:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.myapp.SystemArchitecture.businessService()"/>
</aop:config>
支持的切入点指示符
Spring AOP支持以下切入点指示符(PCD)用于切入点表达式:
execution:用于匹配方法执行连接点。这是使用Spring AOP时使用的主要切入点指示符。
- execution:用于匹配方法执行连接点。这是使用Spring AOP时使用的主要切入点指示符。
- within:限制匹配某些类型中的连接点(使用Spring AOP时在匹配类型中声明的方法的执行)。
- this:限制与连接点的匹配(使用Spring AOP时执行方法),其中bean引用(Spring AOP代理)是给定类型的实例。
- target:限制匹配连接点(使用Spring AOP时执行方法),其中目标对象(被代理的应用程序对象)是给定类型的实例。
- args:限制与连接点的匹配(使用Spring AOP时执行方法),其中参数是给定类型的实例。
- @target:限制与连接点的匹配(使用Spring AOP时执行方法),其中执行对象的类具有给定类型的注释。
- @args:限制与连接点的匹配(使用Spring AOP时执行方法),其中传递的实际参数的运行时类型具有给定类型的注释。
- @within:限制匹配到具有给定注释的类型中的连接点(使用Spring AOP时执行在具有给定注释的类型中声明的方法)。
- @annotation:限制连接点的匹配,其中连接点的主题(在Spring AOP中执行的方法)具有给定的注释。
Spring AOP用户可能最常使用执行切入点指示符。执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
除返回类型模式(ret-type-pattern)、名称模式和参数模式之外的所有部分都是可选的。返回类型模式确定方法的返回类型必须是什么才能匹配连接点,* 是最常用作返回类型模式,它匹配任何返回类型。仅当方法返回给定类型时,完全限定类型名称才匹配。名称模式与方法名称匹配,您可以将 * 通配符用作名称模式的全部或部分。如果指定声明类型模式,请包含尾部以 . 将其连接到名称模式组件。参数模式稍微复杂一些:()匹配一个不带参数的方法,而(…)匹配任何数量(零个或多个)参数。该()模式匹配一个采用任何类型的一个参数的方法。 (,String)匹配一个带有两个参数的方法。第一个可以是任何类型,而第二个必须是String。(更多语言语义内容:https://www.eclipse.org/aspectj/doc/released/progguide/semantics-pointcuts.html)
以下示例显示了一些常见的切入点表达式:
执行任何公共方法:
execution(public * *(..))
执行名称以set开头的任何方法:
execution(* set*(..))
执行AccountService定义的任何方法:
execution(* com.xyz.service.AccountService.*(..))
执行service包中定义的任何方法:
execution(* com.xyz.service.*.*(..))
执行service包或其子包中定义的任何方法:
execution(* com.xyz.service..*.*(..))
然后可以切面内声明切入点与声明顶级切入点,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
当组合切入点子表达式时,&在XML文档中很尴尬,因此您可以使用 and、or 或者 not来代替,而不是关键字&、||和!。例如:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service..(..)) and this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
声明通知(around)
通知与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或围绕运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。
在之前通知(Before Advice)
在匹配方法执行之前运行通知。它是通过使用<aop:aspect>元素在<aop:before>内声明的,如下面的示例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
这里dataAccessOperation是定义在顶级的切入点id。要改为内联切入点,请使用pointcut-ref属性替换为切入点表达式,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
method属性标识提供通知正文的方法(doAccessCheck)。此方法必须为包含通知的切面元素引用的bean定义。在执行数据访问操作(由切入点表达式匹配的方法执行连接点)之前,调用方面bean上的doAccessCheck方法。
返回后通知(After Returning Advice)
当匹配的方法正常执行完成后运行通知。在<aop:aspect>中以与前面的建议相同的方式声明它。您也可以在通知主体中获取返回值。为此,使用returning属性指定应该向其传递返回值的参数的名称,如下面的示例所示:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
doAccessCheck方法必须声明一个名为retVal的参数。例如,可以如下声明方法签名:
public void doAccessCheck(Object retVal) {
//...
}
在异常抛出之后(After Throwing Advice)
当通过抛出异常退出匹配的方法时将执行通知。与前面一样,它通过在<aop:aspect>中使用after-throwing 元素声明。也可以在通知体内获得抛出的异常。为此,使用throwing属性指定应将异常传递到方法的参数的名称,如以下示例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions方法必须声明一个名为dataAccessEx的参数。例如,方法签名可以声明如下:
public void doRecoveryActions(DataAccessException dataAccessEx){
//...
}
最后通知(After (Finally) Advice)
无论匹配的方法如何退出,通知将在(最后)之后运行。您可以使用该after元素声明它,如以下示例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
</aop:aspect>
围绕通知(Around Advice)
它有机会在方法执行之前和之后都执行工作,并确定何时、如何以及即使方法实际上开始执行。环绕建议通常用于以线程安全的方式在方法执行之前和之后共享状态(例如,启动和停止计时器)。应该始终使用最不强大的通知形式来满足您的需求。如果在通知能够完成之前,不建议使用围绕通知。
您可以使用该<aop:around>元素声明围绕的通知。通知方法的第一个参数必须是ProceedingJoinPoint类型的。在通知的主体内,对ProceedingJoinPoint调用proceed()会导致底层方法执行,proceed()方法也可以用Object[]接收。数组中的值用作方法执行时的参数。以下示例显示如何在XML中声明通知:
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut-ref="businessService"
method="doBasicProfiling"/>
...
</aop:aspect>
doBasicProfiling通知的实现如以下示例所示:
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start ...
Object retVal = pjp.proceed();
// stop ...
return retVal;
}
通知参数
如果您希望显式指定通知方法的参数名称(不依赖于前面描述的检测策略),可以使用arg-names 通知元素的属性来实现。以下示例显示如何在XML中指定参数名称:
<aop:before
pointcut="com.xyz.lib.Pointcuts.anyPublicMethod()"
method="audit"
arg-names="auditable"/>
arg-names属性接受以逗号分隔的参数名称列表。
通知类型
Spring提供了几种通知类型,并且可以扩展以支持任意通知类型。本节介绍基本概念和标准建议类型。
拦截通知
Spring中最基本的通知类型是围绕通知进行拦截。
Spring符合AOP Alliance接口,用于使用方法拦截的围绕通知。实现MethodInterceptor和实现通知的类也应该实现以下接口:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
以下示例显示了一个简单的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与任何around通知一样,可以返回不同的值或抛出异常,而不是调用proceed方法。然而,我们并不希望您这样做。
在之前通知
以下清单显示了MethodBeforeAdvice界面:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
请注意,返回类型是void。通知可以在连接点执行之前插入自定义行为但不能更改返回值。如果before advice抛出异常,则会中止拦截器链的进一步执行。异常传播回拦截器链。如果未作任何处理它将直接传递给客户端。否则,它将被AOP代理包装在未检查的异常中。
以下示例显示了Spring中的before建议,该建议计算所有方法调用:
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;
}
}
before通知可以与任何切入点一起使用。
抛出通知
如果连接点引发异常,则在返回连接点后调用抛出通知。请注意org.springframework.aop.ThrowsAdvice接口不包含任何方法。它是一个标记接口,用于标识给定对象实现一个或多个类型化throws通知方法。这些方法应该是以下形式:
afterThrowing([Method, args, target], subclassOfThrowable)
只需要最后一个参数。方法签名可以有一个或四个参数,具体取决于通知方法是否对方法和参数感兴趣。
如果抛出RemoteException(包括子类)异常,则调用以下通知:
public class RemoteThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
}
与前面的建议不同,下一个示例声明了四个参数,以便它可以访问被调用的方法,方法参数和目标对象。如果抛出ServletException异常,则调用以下建议:
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
如果throws-advice方法本身抛出异常,它将覆盖原始异常(即,它会更改抛出给用户的异常)。覆盖异常通常是RuntimeException,它与任何方法签名兼容。但是,如果throws-advice方法抛出已检查的异常,则它必须与目标方法的已声明异常匹配,因此在某种程度上与特定目标方法签名相关联。不要抛出与目标方法签名不兼容的未声明的已检查异常!
返回后通知
在Spring中返回后的通知必须实现 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;
}
}
(这里省略了适用与类的Introduction通知,需要了解请查看官网文档)
aop:advisor标签
“advisors”的概念来自Spring中定义的AOP支持,并且在AspectJ中没有直接的等价物。advisor就像一个小小的独立的切面,只有一条通知。通知本身由bean表示,并且必须实现上面描述的通知接口之一。advisor可以利用AspectJ切入点表达式。
Spring使用<aop:advisor>元素支持顾问概念。您最常见的是它与事务性通知一起使用,它在Spring中也有自己的命名空间支持。以下示例显示了一个advisor:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
除了在前面的示例中使用的 pointcut-ref属性之外,还可以使用pointcut属性内联地定义切入点表达式。
为了定义advisor的优先级,以便通知能够参与排序,使用order属性定义Advisor的Ordered值。
(更多内容请查看官网文档)