参考:官方文档
本文使用的是SpringBoot框架!!!
Spring
从2.0
版本开始引入AOP
(面向切面编程)。
AOP
在Spring Framework
中的作用是:
- 提供声明式的企业服务,
Spring
提供的声明式事务管理就是其中最重要的一个服务。 - 让用户能够实现自定义的切面,应用
AOP
丰富他们的OOP
应用。
在SpringBoot
中,为了使用AOP
功能,需要引入spring-boot-starter-aop
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1 AOP中的概念
首先,学习AOP
中重要的几个概念:
- Aspect(切面):切面指的是跨越多个类的关注点。例如,在
Spring
事务管理中,事务管理就是跨越了多个类的关注点,它是一种公共的需求,可以模块化的。在Spring AOP
中,切面通常可以通过XML
配置或者使用@Aspect
注解来声明。 - Join Point(连接点):连接点指的是程序执行过程中的一个点。比如,程序中执行的一个方法或者处理的一个异常。在
Spring AOP
中,连接点通常指的是一个方法的执行。 - Advice(通知):通知指的是在特定的连接点上被切面执行的一个动作。通知通常包括前置通知、后置通知、环绕通知这几种类型。很多
AOP
框架,包括Spring
,通过拦截器来实现通知,并在连接点附近维持一个拦截器链。 - Pointcut(切入点):切入点指的是匹配连接点的谓词,通常使用切入点表达式来描述切入点。通知绑定切入点,并在切入点匹配的那些连接点上被执行。
Spring
默认使用AspectJ
的切入点表达式来描述切入点。 - Target Object(目标对象):目标对象指的是被加上通知的对象。因为
Spring AOP
是使用的代理模式实现的,因此目标对象指的是代理对象。 - AOP Proxy(AOP代理):
Spring
框架实现AOP
是使用的JDK
动态代理或者CGLIB
代理。
2 通知的类型
Spring AOP
提供了以下几种通知类型:
- Before Advice(前置通知):在连接点之前执行的通知,但是不能阻止连接点的执行(除非跑出异常)。
- After Returning Advice(正常返回通知):连接点正常返回后执行的通知。
- After Throwing Advice(异常返回通知):在连接点抛出异常,并返回后执行的通知。
- After (Finally) Advice(返回通知):无论是连接点正常返回还是抛出异常,都会执行的通知。
- Around Advice(环绕通知):环绕通知是围绕在连接点前后的通知,这是最为强大的通知,能够在连接点前后自定义一些操作。环绕通知还需要负责决定是继续执行连接点(调用ProceedingJoinPoint的proceed方法)还是中断执行,并返回自己的返回值或者抛出异常。
建议根据需要选择使用能够满足需求的同时且功能最简单的通知。不要一上来就选择功能最强大的环绕通知。
3 支持@AspectJ
@AspectJ
指的是采用在Java
类上通过注解的方式来声明一个切面的方式。@AspectJ
风格是AspectJ 5
发行版引入的新风格。Spring
使用AspectJ
提供的库来解析和匹配切入点。但是AOP
运行时仍然是纯Spring AOP
,并且不依赖于AspectJ
编译器或织如。
3.1 开启@AspectJ支持
为了使用@AspectJ
支持的功能,首先需要在配置类上使用@EnableAspectJAutoProxy
注解,开始AOP
功能:
@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}
这相当于基于xml
配置中使用的<aop:aspectj-autoproxy/>
。
3.2 声明一个Aspect(切面)
当开启了AOP
功能,任何在应用上下文中被@Aspect
注解注释的类都将被Spring AOP
框架自动的发现并加载为一个Aspect
(切面)。
下面的例子声明了一个叫做CustomAspect
的切面:
package com.tao.aop.aspect;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;
@Component
@Aspect
public class CustomAspect {
}
注意:一个
@Aspect
注解并不能在Spring
上下文中注册这个bean
,需要添加@Component
注解来注册bean
,不然这个切面不会生效。
被@Aspect
注释的类就是一个切面类。这个切面类可以有成员变量和方法,也可以包含切入点、通知。
3.3 声明一个Pointcut(切入点)
切入点是用来匹配那些执行切面的连接点的。因为Spring AOP
只支持方法执行类的连接点,所以可以将切入点看作是用来匹配那些方法的表达式。
定义一个切入点涉及两个要素:
- 切入点签名:通常是一个返回值为
void
的函数。 - 切入点表达式:通常会用来匹配执行切面的方法。
通常,使用@Pointcut
注解注释一个返回值为void
的函数,就定义了一个切入点:
@Pointcut("execution(* transfer(..))") // 切点表达式
private void anyOldTransfer() {} // 切点签名
3.3.1 Spring AOP支持的切入点标志符
Spring AOP
在切入点表达式中支持以下几种AspectJ
切入点标志符(PCD):
- execution:用于匹配方法执行的连切入点。
- within:用来匹配指定类中的全部方法或指定包中的某些类中的全部方法。
- this:当代理对象
bean
是指定类的实例时匹配。 - target:当被代理的应用程序对象是指定类的实例时匹配。
- args:当执行的方法的参数是指定类型时匹配。
- @target:当被代理对象上有指定类型的注解时匹配。
- @args:当方法参数上有指定类型的注解时匹配。
- @within:当类上有指定注解时匹配,和
@target
有点类似。 - @annotation:当匹配的方法上有指定类型的注解时匹配。
Spring AOP
还支持一个叫做 **bean
**的切入点标志符,当被代理的应用程序对象是Spring
容器中bean
的名称时匹配,在bean
切入点标志符中可以使用&&
、||
、!
来组合表达式。
3.3.2 切入点表达式的组合
可以通过使用&&
、||
、!
来组合切入点表达式,用来表示更加复杂的匹配规则。
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
第三个切入点表达式就是由第一个和第二个切入点表达式通过&&
组合而成的。
3.3.3 共享通用的切入点表达式
在应用中,我们可以通过定义一个通用的类,并在其中定义一些通用的切入点表达式来达到共享和代码重用的作用。
package com.xyz.someapp.aop.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* 匹配web层。
* 匹配com.xyz.someapp.web包及其子包下的所有类的所有方法。
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}
/**
* 匹配service层。
* 匹配com.xyz.someapp.service包及其子包下的所有类的所有方法。
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {}
/**
* 匹配dao层。
* 匹配com.xyz.someapp.dao包及其子包下的所有类的所有方法。
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {}
/**
* 匹配com.xyz.someapp包及其子包下的service包下的任意类的返回值为任意值的参数任意的任意方法。
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {}
/**
* 匹配com.xyz.someapp.dao包下的任意类的返回值为任意值的参数任意的任意方法。
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
使用的时候,可以把它看作工具类一样来使用:
@Aspect
public class BeforeExample {
@Before("com.xyz.someapp.aop.aspect.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
3.3.4 例子
在Spring AOP
中用的最多的切入点标志符就是execution
,用来匹配方法执行,下面是它的声明格式:
execution(修饰符? 返回值类型 类类型?方法名(参数) 异常?)
除了返回值类型、方法名、参数这3个是必须的,其他的都是可选的。
下面是切入点的一些例子。
- 匹配所有
public
方法
execution(public * *(..))
- 匹配所有以
set
开头的方法
execution(* set*(..))
- 匹配
AccountService
类中所有方法
execution(* com.xyz.service.AccountService.*(..))
- 匹配
service
包下的所有类中的所有方法
execution(* com.xyz.service.*.*(..))
- 匹配
service
包及其子包下的所有类中的所有方法
execution(* com.xyz.service..*.*(..))
其他的一些例子:
service
包下的任何连接点(在Spring AOP
中指的是方法)
within(com.xyz.service.*)
service
包及其子包下的任何连接点(在Spring AOP
中指的是方法)
within(com.xyz.service..*)
- 当代理对象实现了
AccountService
接口时匹配
this(com.xyz.service.AccountService)
- 当被代理对象实现了
AccountService
接口时匹配
target(com.xyz.service.AccountService)
- 当连接点(在
Spring AOP
中指的是方法)的参数只有一个并且实现了Serializable
时匹配
args(java.io.Serializable)
- 当被代理对象有
@Transactional
注解时匹配
@target(org.springframework.transaction.annotation.Transactional)
- 当连接点所在的类上有
@Transactional
注解时匹配,和@target
很像
@within(org.springframework.transaction.annotation.Transactional)
- 当方法上有
@Transactional
注解时匹配
@annotation(org.springframework.transaction.annotation.Transactional)
- 当方法只有一个参数并且参数上有
@Classified
注解时匹配
@args(com.xyz.security.Classified)
- 匹配
Spring
上下文中叫做tradeService
的bean
bean(tradeService)
- 匹配
Spring
上下文中名称以Service
结尾的bean
bean(*Service)
3.4 声明Advice(通知)
通知又分为前置通知、后置通知、环绕通知三大类。
3.4.1 前置通知
可以使用@Before
来声明一个前置通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
也可以在@Before
中直接书写切入点表达式:
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() {
// ...
}
}
3.4.2 后置通知
后置通知又分为3种:
- 正常返回通知
- 异常返回通知
- 返回通知
3.4.2.1 正常返回通知
可以使用@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() {
// ...
}
}
有时候,希望在正常返回通知中获得返回的值,可以通过returning
属性绑定返回值:
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
属性中指定的名称和函数参数的变量名要保持一致。
3.4.2.2 异常返回通知
可以使用@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
属性来绑定:
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(Throwable ex) {
// ...
}
}
注意:
throwing
属性中指定的名称和函数参数的变量名要保持一致。
3.4.2.3 返回通知
可以使用@After
来声明一个返回通知:
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() {
// ...
}
}
3.4.3 环绕通知
可以使用@Around
注解来声明以个环绕通知。
环绕通知方法的第一个参数必须是ProceedingJoinPoint
类型。在通知方法里面,通过调用ProceedingJoinPoint
的对象的proceed()
方法来触发连接点方法的执行。
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 {
// 开始时间
long beginTime = System.currentTimeMillis();
// 连接点方法的执行
Object retVal = pjp.proceed();
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
logger.info("请求处理响应时间:" + time + " 毫秒");
return retVal;
}
}
3.4.4 通知的参数
可以向通知传递一些参数。
3.4.4.1 传递JoinPoint
任何通知都可以传递一个类型为org.aspectj.lang.JoinPoint
的参数作为第一个参数(注意,环绕通知的第一个参数必须是ProceedingJoinPoint
类型,它是JoinPoint
的子类,他俩都是接口)。
JoinPoint
提供了一些有用的方法:
// 返回目标方法的参数
getArgs();
// 返回代理对象
getThis();
// 返回目标对象
getTarget();
// 返回目标方法的签名,包含了方法的描述信息
getSignature();
// 打印目标方法的有用信息
toString();
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(JoinPoint joinPoint) {
// ...
}
}
3.4.4.2 传递参数给通知
如果需要将被执行函数的参数传入通知里面,可以通过args
来绑定。
下面的例子描述了一个前置通知,通过args
将被执行函数的类型为Account
的参数account
绑定并传入通知里面:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(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
也可以以相同的方式绑定参数。
3.4.4.3 参数名
因为在Java
反射中参数名是无法获得的,所以Spring AOP
使用以下策略来确定参数名:
- 用户可以通过
argNames
属性显示的指定参数名。如果显示的指定了,那么Spring AOP
将按照指定的参数名来解析。
@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
中指定它:
@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
属性:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
- 如果没有指定
argNames
,那么Spring AOP
将会从该类的调用信息中查找并确定参数名称。该类在编译的时候至少要有调试信息-g:vars
。 - 如果在编译代码时没有必要的调试信息,
Spring AOP
就会尝试推断绑定变量与参数的配对(例如,如果切入点表达式中只绑定了一个变量,而通知方法只接受一个参数,那么这种配对是明显的)。如果给定可用信息,变量绑定是不明确的,则抛出一个AmbiguousBindingException
异常。 - 如果以上策略都失败了,那么会抛出
IllegalArgumentException
异常。
3.4.4.4 在通知中传递参数给被调函数
在下面的环绕通知中,传递参数newPattern
给被调用函数:
@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});
}
3.4.5 通知的顺序
如果当多个通知都在同一个连接点上运行时,它们的顺序又将是如何的呢?
Spring AOP
遵循AspectJ
的优先级顺序策略:
- 在前置通知中,优先级高的先执行。
- 在后置通知中,优先级高的后执行。
- 如果当两个通知定义在不同的两个切面类中并且作用于同一个连接点,除非显示指定它们的优先级,否则优先级将是未定义的。可以通过让切面类实现
org.springframework.core.Ordered
接口或者在类上面使用@Order
注解来指定优先级,值越小,优先级越高。 - 如果两个通知定义在同一个切面类中并且作用于同一个连接点呢?这种情况下通过
Java
反射是无法确定优先级的,最好的办法就是将这两个通知放入不同的切面类中,然后通过@Order
指定优先级。
3.4.6 为被代理对象引入新方法
3.5 基于xml配置的切面