依赖
在pom.xml中添加依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.13</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
然后在配置文件中添加配置:
# 开启注解(默认为true)
spring.aop.auto=true
# true使用cglib代理,false使用JDK动态代理(默认为false)
spring.aop.proxy-target-class=true
该配置可以不加,并不会影响执行的结果。
若不使用cglib代理,则默认使用JDK动态代理。
在创建代理上,JDK效率更高。但在调用代理方法时,JDK使用的是反射机制,cglib使用FastClass机制,cglib效率更高。
但注意即使spring.aop.proxy-target-class
设置为false
,如果目标类没有生命接口,则Spring将自动使用CGLib动态代理。
开发
首先定义一个程序员类:
package com.example.test.component;
import org.springframework.stereotype.Component;
@Component
public class Programmer {
public void develop() {
System.out.println("开发");
}
}
该类只有一个方法develop()
。
使用@Component
注解将其放入了Spring容器中。
但在执行develop()
之前,我们首先要打开编译器;执行develop()
之后,我们要关闭编译器。
现在使用切面来开发。先定义一个环境类,增加@Component
注解添加到Spring容器。然后在其下添加2个方法:
@Component
public class Auxiliary {
public void openCompiler() {
System.out.println("打开编译器");
}
public void closeCompiler() {
System.out.println("关闭编译器");
}
}
我们需要在Programmer.develop()
前后分别调用openCompiler()
和closeCompiler()
。
接下来为Auxiliary
类添加切面:
- 添加
@Aspect
注解来将该类声明为切面类。 - 添加一个代理方法
work()
,需要使用@Pointcut
来指明该方法的切点位置。在这里,Auxiliary.work()
代表了Programmer.develop()
。 - 在
openCompiler()
前添加@Before("work()")
,用于指明该方法需在代理方法执行前执行。 - 在
closeCompiler()
前添加@After("work()")
,用于指明该方法需在代理方法执行后执行。
package com.example.test.component;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class Auxiliary {
@Pointcut("execution(* com.example.test.component.Programmer.develop(..))")
public void work() {}
@Before("work()")
public void openCompiler() {
System.out.println("打开编译器");
}
@After("work()")
public void closeCompiler() {
System.out.println("关闭编译器");
}
}
然后将Programmer
放入一个controller中进行调用:
@Autowired
private Programmer programmer;
@GetMapping("/test")
public void test() {
programmer.develop();
}
可以看到控制台的输出:
打开编译器
开发
关闭编译器
切点语法
使用@Pointcut()
来指定切点。其语法为:
@Pointcut("execution( 修饰符 返回值类型 包名.类名.方法名(参数类型..))")
其中可使用*
代表任意类型。
例如:
// Programmer类
@Pointcut("execution( com.example.test.component.Programmer)");
// Programmer类下所有的方法
@Pointcut("execution( * com.example.test.component.Programmer.*(..))");
// Programmer类下所有的public方法
@Pointcut("execution( public * com.example.test.component.Programmer.*(..))");
// Programmer类下public且返回值类型为String的方法
@Pointcut("execution( public String com.example.test.component.Programmer.*(..))");
// Programmer类下public且返回值类型为String的所有参数类型的develop方法
@Pointcut("execution( public String com.example.test.component.Programmer.develop(..))");
// Programmer类下public且返回值类型为String的第一个参数为String类型的develop方法
@Pointcut("execution( public String com.example.test.component.Programmer.develop(String,..))");
// Programmer类下public且返回值类型为String的第一个参数为String,第二个参数为Long类型的develop方法
@Pointcut("execution( public String com.example.test.component.Programmer.develop(String, Long))");
各通知的调用时机:通知
针对代理被调用时的时机,有如下几种注解:
@before
: 前置通知,在代理调用之前执行。@After
:后置通知,在代理调用之后执行。发生异常依然会执行。@AfterReturning
: 返回通知,在方法返回结果之后执行。其时机在@After
之前。发生异常不会被执行。@AfterThrowing
:异常通知,在方法抛出异常之后执行。其时机在@After
之前。@Around
:环绕通知,将切点方法放入自身方法体内执行,且自身方法必须返回切点方法的执行结果Object
。发生异常则之后的@Around
不会被执行。
其中较为特殊的是@Around
。
以上所有注解修饰的方法都可传入JoinPoint
及其子类型参数。通过JoinPoint
可获取到参数等信息。
@Around
@Around
的作用是将切点方法放入自身方法体内执行。因此可以在切点方法执行前或者执行后添加各种自定义操作。
@Around
需传入ProceedingJoinPoint
参数(派生于JoinPoint
)并调用ProceedingJoinPoint.proceed()
才能实现切点方法的调用。不传参切点方法不会被调用。
对应地,ProceedingJoinPoint
参数只能用于@Around
。
特别注意,@Around
修饰的方法必须返回ProceedingJoinPoint.proceed()
的结果Object
,否则请求结果将不会被返回给请求者。
例如:
package com.example.test.component;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class Auxiliary {
@Pointcut("execution(* com.example.test.component.Programmer.develop(..))")
public void work() {}
@Before("work()")
public void openCompiler() {
System.out.println("打开编译器");
}
@After("work()")
public void closeCompiler() {
System.out.println("关闭编译器");
}
@AfterReturning("work()")
public void checkInCode() {
System.out.println("提交代码");
}
@AfterThrowing("work()")
public void throwException() {
System.out.println("出现异常");
}
@Around("work()")
public Object aroundCompiler(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("喝杯咖啡");
Object result = joinPoint.proceed();
System.out.println("喝杯水");
return result;
}
}
上面还加入了@AfterReturning
和@AfterThrowing
通知。
调用接口后,可以看到控制台的输出:
喝杯咖啡
打开编译器
开发
提交代码
关闭编译器
喝杯水
即@Around
的时机早于@Before
,晚于@After
。
@AfterThrowing
当逻辑出现异常时,@After
依然会正常执行。而@AfterReturning
以及之后的@Around
不会被执行。
现在修改Programmer.develop()
:
@Component
public class Programmer {
public void develop() {
System.out.println("开发");
Integer sum = null;
sum++;
}
}
当执行Programmer.develop()
时会抛出异常。
调用接口后,可以看到控制台的输出:
喝杯咖啡
打开编译器
开发
异常
关闭编译器
在通知中获取请求的属性
所有的通知都可以传入JoinPoint
参数并获取到请求的属性。
获取请求对象
@Before(value = "pointcut()")
public void pointcut(JoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
}
获取到请求对象后即可拿到所有请求相关的属性。
修改执行参数
对于@Around
,可以传入ProceedingJoinPoint
参数(派生于JoinPoint
)并调用ProceedingJoinPoint.proceed()
才能实现切点方法的调用。
现在要在切点方法执行前对其参数进行修改,对其所有的Integer
类型参数+1。可以这样操作:
@Around("work()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("喝杯咖啡");
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if(args[i] instanceof Integer) {
args[i] = (Integer)args[i] + 1;
}
}
Object result = joinPoint.proceed(args);
System.out.println("喝杯水");
return result;
}
关键在于ProceedingJoinPoint.proceed()
可传入Object[]
型参数,其定义为:
Object proceed() throws Throwable;
Object proceed(Object[] var1) throws Throwable;
传入后将会替代原本的参数值。
注意述拦截的请求类型是get还是post,参数以?
及&
形式还是以form-data或json形式传入,JoinPoint.getArgs()
都可以获取到。json形式传入时会以String
类型放在args[0]
中。
执行后返回的Object
结果需要由@Around
所修饰的方法返回。
属性获取
对于一个POST
方法,使用form-data方式传入v1
和v2
2个参数。
@PostMapping("/myTest")
public void myTest(Integer v1, Integer v2) {}
加一个切点,令其匹配测试接口:
@Aspect
@Component
public class AspectTest {
private static final Logger log = LoggerFactory.getLogger(AspectTest.class);
@Pointcut("execution(public * com.example.test.controller.TestController.*(..))")
public void pointcut() {}
}
首先在@Around
中修改其参数值,令其+1:
@Around(value = "pointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Integer) {
args[i] = (Integer) args[i] + 1;
}
}
System.out.println("所有参数+1");
Object result = joinPoint.proceed(args);
return result;
}
然后在@Before
中输出其属性值:
@Before("pointcut()")
public void beforeAdvice(JoinPoint joinPoint) throws Throwable {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 记录下请求内容
log.info("请求类型:" + request.getMethod()); // GET/POST等
log.info("请求URL:" + request.getRequestURL());
log.info("请求IP: " + request.getRemoteAddr());
log.info("包名.类名: " + signature.getDeclaringTypeName());
log.info("方法名: " + signature.getName());
log.info("方法名: " + method.getName());
// 参数名+参数值
Enumeration eParameterNames = request.getParameterNames();
while (eParameterNames.hasMoreElements()) {
String name = (String) eParameterNames.nextElement();
String value = request.getParameter(name);
// 参数值为request中的原始参数值
log.info("参数名:" + name + ", 原始参数值:" + value);
}
// 参数名+参数值
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] sParamNames = u.getParameterNames(method);
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
// 请求参数类型判断过滤,特殊类型不输出
if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse || args[i] instanceof MultipartFile) {
continue;
}
// 参数值为传入的args参数值,可能已经被修改过
log.info("参数名:" + sParamNames[i] + ", 传入的args参数值 :" + JSON.toJSONString(args[i]));
}
}
请求接口,可看到控制台输出:
所有参数+1
请求类型:POST
请求URL:http://127.0.0.1:8080/test/myTest
请求IP: 127.0.0.1
包名.类名: com.example.test.controller.TestController
方法名: myTest
方法名: myTest
参数名:v1, 原始参数值:1
参数名:v2, 原始参数值:2
参数名:v1, 传入的args参数值 :2
参数名:v2, 传入的args参数值 :3