SpringAOP功能和和目标
Spring AOP目前仅支持方法执行连接点(建议在Spring bean上执行方法)。
虽然可以在不破坏核心Spring AOP API的情况下添加对字段拦截的支持,但未实现字段拦截。如果您需要建议字段访问和更新连接点,请考虑使用AspectJ等语言。
AOP代理
Spring AOP是基于代理的。
Spring AOP默认是用JDK动态代理(要求必须要有接口)。代理类的接口不是必须的。默认情况下如果没有实现接口,则是用Cglib代理。也可以强制使用Cglib
@AspectJ支持
启用@AspectJ支持
使用Java启用@AspectJ支持@Configuration
,请添加@EnableAspectJAutoProxy
注释,如以下示例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
使用XML配置启用@AspectJ支持
要使用基于XML的配置启用@AspectJ支持,请使用该aop:aspectj-autoproxy
元素,如以下示例所示:
<aop:aspectj-autoproxy/>
声明AspectJ
启用@AspectJ支持后,Spring会自动检测并用于配置Spring AOP
这两个示例中的第一个示例在应用程序上下文中显示了一个常规bean定义,该定义指向具有@Aspect
注释的bean类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
两个示例中的第二个示出了NotVeryUsefulAspect
类定义,该注释使用org.aspectj.lang.annotation.Aspect
注释进行注释;
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Component //如果需要Spring发现该类,需要单独配置该注解
@Aspect
public class NotVeryUsefulAspect {
}
声明切入点
Spring AOP仅支持Spring bean的方法执行连接点,因此您可以将切入点视为匹配Spring bean上方法的执行。
切入点声明有两个部分:一个包含名称和任何参数的签名,以及一个精确确定我们感兴趣的方法执行的切入点表达式。在AOP的@AspectJ注释样式中,切入点签名由常规方法定义提供,并使用@Pointcut
注释指示切入点表达式(用作切入点签名的方法必须具有void
返回类型)。
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
支持的切入点指示符
Spring AOP支持以下AspectJ切入点指示符(PCD)用于切入点表达式:
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中执行的方法)具有给定的注释。
其他切入点类型
完整的AspectJ切入点语言支持未在Spring支持额外的切入点指示符:
call
,get
,set
,preinitialization
,staticinitialization
,initialization
,handler
,adviceexecution
,withincode
,cflow
,cflowbelow
,if
,@this
,和@withincode
。在Spring AOP解释的切入点表达式中使用这些切入点指示符会导致IllegalArgumentException
被抛出。
Spring AOP支持的切入点指示符集可以在将来的版本中进行扩展,以支持更多的AspectJ切入点指示符。
由于Spring AOP仅限制与方法执行连接点的匹配,因此前面对切入点指示符的讨论给出了比在AspectJ编程指南中找到的更窄的定义。除此之外,AspectJ本身具有基于类型的语义和,在执行的连接点,无论是this
和target
指的是相同的对象:对象执行方法。Spring AOP是一个基于代理的系统,它区分代理对象本身(绑定到this
)和代理背后的目标对象(绑定到target
)。
由于Spring的AOP框架基于代理的特性,根据定义,目标对象内的调用不会被截获。对于JDK代理,只能拦截代理上的公共接口方法调用。使用CGLIB,代理上的公共和受保护方法调用被截获(如果需要,甚至是包可见的方法)。但是,通过代理进行的常见交互应始终通过公共签名进行设计。
请注意,切入点定义通常与任何截获的方法匹配。如果切入点严格意义上是公开的,即使在通过代理进行潜在非公共交互的CGLIB代理方案中,也需要相应地定义切入点。
如果您的拦截需要包括目标类中的方法调用甚至构造函数,请考虑使用Spring驱动的本机AspectJ编织而不是Spring的基于代理的AOP框架。这构成了具有不同特征的不同AOP使用模式,因此在做出决定之前一定要熟悉编织。
结合Pointcut表达式
您可以使用&&,
||
和组合切入点表达式!
。您还可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} // 1
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {} // 2
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} //3
anyPublicOperation
如果方法执行连接点表示任何公共方法的执行,则匹配。inTrading
如果方法执行在交易模块中,则匹配。tradingOperation
如果方法执行表示交易模块中的任何公共方法,则匹配。
共享公共切入点定义
在使用企业应用程序时,开发人员通常希望从几个方面引用应用程序的模块和特定的操作集。我们建议定义一个“SystemArchitecture”方面,为此目的捕获常见的切入点表达式。这样的方面通常类似于以下示例:
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
您可以在需要切入点表达式的任何位置引用此类方面中定义的切入点。例如,要使服务层成为事务性的,您可以编写以下内容:
<aop:config>
<aop:advisor
pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
的<aop:config>
和<aop:advisor>
元件在讨论基于Schema的AOP支持。事务管理中讨论了事务元素。
例子
Spring AOP用户可能execution
最常使用切入点指示符。执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除返回类型模式(ret-type-pattern
在前面的代码片段中),名称模式和参数模式之外的所有部分都是可选的。返回类型模式确定方法的返回类型必须是什么才能匹配连接点。 *
最常用作返回类型模式。它匹配任何返回类型。仅当方法返回给定类型时,完全限定类型名称才匹配。名称模式与方法名称匹配。您可以将*
通配符用作名称模式的全部或部分。如果指定声明类型模式,请包含尾部.
以将其连接到名称模式组件。参数模式稍微复杂一些:()
匹配不带参数的方法,而(..)
匹配任何数量(零个或多个)参数。该(*)
模式匹配采用任何类型的一个参数的方法。 (*,String)
匹配一个带有两个参数的方法。第一个可以是任何类型,而第二个必须是a String
。有关更多信息,请参阅AspectJ编程指南的 语言语义部分。
以下示例显示了一些常见的切入点表达式:
-
执行任何公共方法:
execution(public * *(..))
-
执行名称以以下开头的任何方法
set
:execution(* set*(..))
-
执行
AccountService
接口定义的任何方法:execution(* com.xyz.service.AccountService.*(..))
-
执行
service
包中定义的任何方法:execution(* com.xyz.service.*.*(..))
-
执行服务包或其子包中定义的任何方法:
execution(* com.xyz.service..*.*(..))
-
服务包中的任何连接点(仅在Spring AOP中执行方法):
within(com.xyz.service.*)
-
服务包或其子包中的任何连接点(仅在Spring AOP中执行方法):
within(com.xyz.service..*)
-
代理实现
AccountService
接口的任何连接点(仅在Spring AOP中执行方法) :this(com.xyz.service.AccountService)
'this’更常用于绑定形式。请参阅有关 如何在建议正文中提供代理对象的声明建议部分。
-
目标对象实现
AccountService
接口的任何连接点(仅在Spring AOP中执行方法):target(com.xyz.service.AccountService)
'target’更常用于绑定形式。有关如何在建议体中提供目标对象的信息,请参阅“ 声明建议”部分。
-
采用单个参数的任何连接点(仅在Spring AOP中执行的方法)以及在运行时传递的参数是
Serializable
:args(java.io.Serializable)
'args’更常用于绑定形式。请参阅声明建议部分,了解如何在建议体中提供方法参数。
请注意,此示例中给出的切入点不同于
execution(* *(java.io.Serializable))
。如果参数在运行时传递,则args版本匹配Serializable
,如果方法签名声明了单个参数类型,则执行版本匹配Serializable
。 -
目标对象具有
@Transactional
注释的任何连接点(仅在Spring AOP中执行方法) :@target(org.springframework.transaction.annotation.Transactional)
您还可以在绑定表单中使用“@target”。有关如何在建议体中提供注释对象的信息,请参阅“ 声明建议”部分。
-
任何连接点(仅在Spring AOP中执行方法),其中目标对象的声明类型具有
@Transactional
注释:@within(org.springframework.transaction.annotation.Transactional)
您也可以在绑定表单中使用“@within”。有关如何在建议体中提供注释对象的信息,请参阅“ 声明建议”部分。
-
任何连接点(仅在Spring AOP中执行方法),其中执行方法具有
@Transactional
注释:@annotation(org.springframework.transaction.annotation.Transactional)
您还可以在绑定表单中使用“@annotation”。有关如何在建议体中提供注释对象的信息,请参阅“ 声明建议”部分。
-
任何连接点(仅在Spring AOP中执行的方法),它接受一个参数,并且传递的参数的运行时类型具有
@Classified
注释:@args(com.xyz.security.Classified)
您也可以在绑定表单中使用“@args”。请参阅“ 声明建议”部分,了解如何在建议体中提供注释对象。
-
名为的Spring bean上的任何连接点(仅在Spring AOP中执行方法)
tradeService
:bean(tradeService)
-
具有与通配符表达式匹配的名称的Spring bean上的任何连接点(仅在Spring AOP中执行方法)
*Service
:bean(*Service)
写好切入点
在编译期间,AspectJ处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态地或动态地)匹配给定切入点是一个代价高昂的过程。(动态匹配意味着无法通过静态分析完全确定匹配,并且在代码中放置测试以确定代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ会将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点在DNF(析取范式)中重写,并且切入点的组件被排序,以便首先检查那些评估更便宜的组件。
但是,AspectJ只能使用它所说的内容。为了获得最佳匹配性能,您应该考虑他们要实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三组:kinded,scoping和contextual:
- Kinded代号选择特定类型的连接点的:
execution
,get
,set
,call
,和handler
。 - 范围界定指示符选择一组感兴趣的连接点(可能是多种类型):
within
和withincode
- 基于上下文语境指示符匹配(和任选的绑定): ,
this
,target
和@annotation
一个写得很好的切入点应至少包括前两种类型(kinded和scoping)。您可以包含上下文指示符以基于连接点上下文进行匹配,或者绑定该上下文以在建议中使用。由于额外的处理和分析,仅提供一个kinded指示符或仅提供上下文指示符,但可能会影响编织性能(使用的时间和内存)。范围界定指示符非常快速匹配,使用它们意味着AspectJ可以非常快速地解除不应进一步处理的连接点组。如果可能,一个好的切入点应该总是包含一个。
声明通知(Advice)
前置通知
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
返回后通知
返回后,匹配的方法执行正常返回。您可以使用@AfterReturning
注释声明它:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
有时候需要或获取返会的实际值。如以下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
returning
参数值和方法参数名相对应,方法执行返回时,返回值作为相应的参数值传递给增强(advice)方法。
returning
子句也限制了只能匹配到返回指定类型的值。Object 参数表示匹配任何返回值。
请注意,在返回增强使用时,不可能返回完全不同的引用。
抛出异常后通知
匹配的方法执行通过抛出异常退出。您可以使用@AfterThrowing
注释声明它,如以下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
有时候想需要在抛出特定类型异样时才会运行,并且还要在增强方法中访问抛出的异常。可以使用该 throwing
属性来限制匹配(如果需要,Throwable
否则使用 - 作为异常类型)并将抛出的异常绑定到advice参数。以下示例显示了如何执行此操作:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
最终通知
当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。事后通知必须准备好处理正常和异常返回条件。它通常用于释放资源和类似用途。下面的示例演示如何在finally advice之后使用:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
环绕通知
@Around
声明的增强方法,要求第一参数必须是 ProceedingJoinPoint
。
以下示例显示如何使用around建议:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
around通知返回的值是方法调用者看到的返回值。例如,一个简单的缓存方面可以从缓存中返回一个值(如果有的话),proceed()
如果没有则调用。请注意,proceed
可以在around建议的正文中调用一次,多次或根本不调用。所有这些都是合法的。
参数通知
访问当前 JoinPoint
任何增强方法都可以声明org.aspectj.lang.JoinPoint
类型作为第一个参数。ProceedingJoinPoint
是其JoinPoint
子类。JoinPoint
接口提供了许多有用的方法:
getArgs()
:返回方法参数。getThis()
:返回代理对象。getTarget()
:返回目标对象。getSignature()
:返回正在建议的方法的描述。toString()
:打印建议方法的有用说明。
有关更多详细信息,请参阅javadoc。
将参数传递给增强方法
我们已经看到了如何绑定返回的值或异常值(在返回之后和抛出建议之后使用)。要使参数值可用于建议体,您可以使用绑定形式args
。如果在args表达式中使用参数名称代替类型名称,则在调用通知时,相应参数的值将作为参数值传递。一个例子应该使这更清楚。假设您要建议执行以Account
对象作为第一个参数的DAO操作,并且您需要访问建议体中的帐户。你可以写下面的内容:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
args(account,..)
切入点表达式的一部分有两个目的。首先,它将匹配仅限于那些方法至少采用一个参数的方法执行,而传递给该参数的参数是一个实例Account
。其次,它Account
通过account
参数使实际对象可用于增强。
另一种编写方法是声明一个切入点,Account
当它与连接点匹配时“提供” 对象值,然后从增强中引用指定的切入点。这看起来如下:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
代理对象(this
),目标对象(target
),和说明(@within
, @target
,@annotation
,和@args
)都可以以类似的方式结合。接下来的两个示例显示如何匹配带@Auditable
注解的注解方法的执行代码:
这两个示例中的第一个显示了@Auditable
注解的定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
这两个示例中的第二个显示了与@Auditable
方法执行相匹配的增强:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
建议参数和泛型
Spring AOP可以处理类声明和方法参数中使用的泛型。假设您有一个如下所示的泛型类型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
您可以通过在要拦截方法的参数类型中键入advice参数,将方法类型的拦截限制为某些参数类型:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
此方法不适用于通用集合。因此,您无法按如下方式定义切入点:
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
为了使这项工作,我们必须检查集合中的每个元素,这是不合理的,因为我们也无法决定如何处理null
一般的值。要实现与此类似的操作,您必须键入参数Collection<?>
并手动检查元素的类型。
确定参数名称
通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称匹配。参数名称不能通过Java反射获得,因此Spring AOP使用以下策略来确定参数名称:
-
如果用户已明确指定参数名称,则使用指定的参数名称。通知和切入点注释都有一个可选
argNames
属性,您可以使用该属性指定带注释方法的参数名称。这些参数名称在运行时可用。以下示例显示如何使用该argNames
属性:@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames="bean,auditable") public void audit(Object bean, Auditable auditable) { AuditCode code = auditable.value(); // ... use code and bean }
如果第一个参数是的
JoinPoint
,ProceedingJoinPoint
或JoinPoint.StaticPart
类型,你可以从价值离开了参数的名称argNames
属性。例如,如果修改前面的建议以接收连接点对象,则该argNames
属性不需要包含它:@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames="bean,auditable") public void audit(JoinPoint jp, Object bean, Auditable auditable) { AuditCode code = auditable.value(); // ... use code, bean, and jp }
给出的第一个参数的特殊待遇
JoinPoint
,ProceedingJoinPoint
和JoinPoint.StaticPart
类型是不收取任何其它连接上下文的通知情况下,特别方便。在这种情况下,您可以省略该argNames
属性。例如,以下建议无需声明argNames
属性:@Before("com.xyz.lib.Pointcuts.anyPublicMethod()") public void audit(JoinPoint jp) { // ... use jp }
-
使用该
'argNames'
属性有点笨拙,因此如果'argNames'
未指定该属性,Spring AOP会查看该类的调试信息,并尝试从局部变量表中确定参数名称。只要使用调试信息('-g:vars'
至少)编译了类,就会出现此信息。使用此标志进行编译的后果是:(1)您的代码稍微容易理解(逆向工程),(2)类文件大小略大(通常无关紧要),(3)优化删除未使用的本地变量未由编译器应用。换句话说,通过使用此标志构建,您应该不会遇到任何困难。
如果没有调试信息,AspectJ编译器(ajc)编译了@AspectJ方面,则无需添加
argNames
属性,因为编译器会保留所需的信息。
- 如果代码编译时没有必要的调试信息,Spring AOP会尝试推断绑定变量与参数的配对(例如,如果只有一个变量绑定在切入点表达式中,并且advice方法只接受一个参数,那么配对很明显)。如果给定可用信息,变量的绑定是不明确的,
AmbiguousBindingException
则抛出a。 - 如果以上所有策略都失败了,那就
IllegalArgumentException
抛出了。
继续论证
我们之前评论过,我们将描述如何proceed
使用在Spring AOP和AspectJ中一致工作的参数编写调用。解决方案是确保建议签名按顺序绑定每个方法参数。以下示例显示了如何执行此操作:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
在许多情况下,无论如何都要执行此绑定(如前面的示例所示)。
xml声明见官方文档:
更多资料:
https://juejin.im/post/5b06bf2df265da0de2574ee1#heading-2
http://shouce.jb51.net/spring/aop.html