依赖
在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和v22个参数。
@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
本文介绍了如何在SpringBoot中使用AOP进行切面编程,包括在pom.xml中添加依赖,配置代理类型,以及如何定义切点、通知。通过一个程序员类和辅助类的示例,展示了如何在方法执行前后插入额外操作,如打开和关闭编译器。同时,详细解释了切点语法和不同类型的注解通知(@Before、@After、@AfterReturning、@AfterThrowing、@Around),并展示了如何在@Around通知中修改方法参数和处理异常情况。
349

被折叠的 条评论
为什么被折叠?



