AOP切面编程5种注解@Before、@After、@AfterRunning、@AfterThrowing、@Around
如何匹配pointcut?
语法规则是怎样的?
springboot版本2.7.1
jdk版本1.8
需要使用到的依赖(可能不完整)
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.83_noneautotypeversion>
dependency>
<dependency>
<groupId>commons-codecgroupId>
<artifactId>commons-codecartifactId>
<version>1.15version>
dependency>
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.6version>
dependency>
这里我们拦截所有的controller,为了方便演示,给定一个controller
package com.it2.springbootmybatisplus.controller;
import com.alibaba.fastjson.JSON;
import com.it2.springbootmybatisplus.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@PostMapping("/save")
public String save(User user) {
log.info("save方法打印内容:" + JSON.toJSONString(user));
if (user.getName().equals("xiaomi")) {//如果name等于xiaomi,则制造一个异常
int a = 1 / 0;
}
return "ok";
}
}
用到的User
package com.it2.springbootmybatisplus.pojo;
import lombok.Data;
@Data
public class User {
private Integer id;
private String name;
}
package com.it2.springbootmybatisplus.config;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
@Aspect
@Slf4j
public class MyControllerAspect {
/**
* 设置AOP切点
*/
@Pointcut("execution(* com.it2.springbootmybatisplus.controller.*.*(..))")
public void mypointcut() {
}
@Before(value = "mypointcut()")
public void beforeTest(JoinPoint point) {
log.info("-----------@Before------------");
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("Before的一些操作:" + methodName + ",参数为:" + args);
log.info("-----------@Before------------");
}
/**
* 执行方法环绕
*
* @param point
*/
@Around(value = "mypointcut()")
public Object aroundTest(ProceedingJoinPoint point) throws Throwable {
log.info("-----------@around------------");
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("around的一些操作:" + methodName + ",参数为:" + args);
/**
* 可以获取请求的参数,实现@Cacheable缓存效果
*/
/**
* 打印输入的参数,这里实现效果,如果内容里包含xiaowang,则直接返回fail
*/
Object[] objects = point.getArgs();
for (int i = 0; i < objects.length; i++) {
Object obj = objects[i];
System.out.println("type:" + obj.getClass().getSimpleName() + ", value:" + obj);
if(obj.toString().contains("xiaowang")){//直接拦截,如果等于xiaowang,直接返回失败
return "fail";
}
}
log.info("-----------@around------------");
return point.proceed();
}
@After(value = "mypointcut()")
public void afterTest(JoinPoint point) {
log.info("-----------@After------------");
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("After的一些操作:" + methodName + ",参数为:" + args);
log.info("-----------@After------------");
}
/**
* 设置切入点不单独写
*
* @param point
* @param result
*/
@AfterReturning(value = "execution(* com.it2.springbootmybatisplus.controller.*.*(..))", returning = "result")
public void afterReturningTest(JoinPoint point, Object result) {
log.info("-----------@afterReturning------------");
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("afterReturning的一些操作:" + methodName + ",参数为:" + args);
System.out.println("afterReturning result:" + result);
log.info("-----------@afterReturning------------");
}
/**
* 发生异常才会执行,否则不会执行
*
* @param point
* @param exception
*/
@AfterThrowing(value = "mypointcut()", throwing = "exception")
public void afterThrowingTest(JoinPoint point, Exception exception) {
log.info("-----------@afterThrowing------------");
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("afterThrowing:" + methodName + ",参数为:" + args);
System.out.println("afterThrowing exception:" + exception);
log.info("-----------@afterThrowing------------");
}
}
通过正常的示例,我们可以发现执行顺序是@Around >> @Before >> 方法体 >> @AfterReturning >> @After
@Before: 前置通知, 在方法执行之前执行
@After: 后置通知, 在方法执行之后执行 。
@AfterRunning: 返回通知, 在方法返回结果之后执行
@AfterThrowing: 异常通知, 在方法抛出异常之后
@Around: 环绕通知, 围绕着方法执行
到这里我们已经掌握了5种Advice的切入时机,可以简单的进行切入式开发。
前面的demo我们已经知道了切入时机的5种Advice,那么它具体还有什么切入规则?语法是什么?
SpringAOP 表达式分为三种:
指示器(Designators)
通配符(WildCards)
运算符(Operators)
前面的demo里面,我们使用到的指示器是execution,那么execution是什么?还有哪些指示器?怎么使用?
前面的demo中已经使用过了,直接在方法执行时切入,不需要在方法体上做任何手脚。
execution表达式的格式
{
modidier-pattern?修饰符
ret-type-pattern 返回值
declaring-type-pattern?声明的包名
name-pattern(param-pattern)声明的方法名(参数名)
throws-pattern?声明的抛出的异常
}
上面带有?的表示可以省略,其它部分则必须填写
示例
#匹配 xxx.controller下面的内容,方法参数不限制
@Pointcut("execution(* com.it2.springbootmybatisplus.controller.*.*(..))")
#匹配返回值为int,修饰符为public,并且参数为(int,int)的方法
@Pointcut("execution(public int com.it2.springbootmybatisplus.controller.*.*(int,int))")
如果想要拦截符合条件,但是会抛出某些异常的方法,则需要在表达式后添加throws 要抛出的异常即可。
demo需求说明,给方法添加注解,使其打印执行耗时。
package com.it2.springbootmybatisplus.annotation;
import java.lang.annotation.*;
/**
* 统计执行时长
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExecutionTime {
}
package com.it2.springbootmybatisplus.annotation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 统计执行时长
*/
@Component
@Aspect
@Slf4j
public class ExecutionTimeAspect {
/**
* 设置切入点,拦截注解
*/
@Pointcut("@annotation(com.it2.springbootmybatisplus.annotation.ExecutionTime)")
public void mypointcut() {
}
@Around("mypointcut()")
public Object aroundTest(ProceedingJoinPoint proceedingJoinPoint) {
try {
Signature signature = proceedingJoinPoint.getSignature();
String className = proceedingJoinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
long s=System.currentTimeMillis();
Object object = proceedingJoinPoint.proceed();
long e=System.currentTimeMillis();
StringBuffer sb=new StringBuffer();
sb.append(className).append(".");
sb.append(methodName).append("()");
log.info("方法名:{},执行时间{}毫秒",sb.toString(),(e-s));
return object;
} catch (Throwable throwable) {
throwable.printStackTrace();
throw new RuntimeException("发生错误");
}
}
}
给方法添加这个注解,并启动服务器测试
示例
package com.it2.springbootmybatisplus.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface MyWithin {
}
package com.it2.springbootmybatisplus.annotation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Slf4j
public class MyWithinAspect {
@Around("@within(com.it2.springbootmybatisplus.annotation.MyWithin)")
public Object aroundTest(ProceedingJoinPoint proceedingJoinPoint) {
try {
Signature signature = proceedingJoinPoint.getSignature();
String className = proceedingJoinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
long s=System.currentTimeMillis();
Object object = proceedingJoinPoint.proceed();
long e=System.currentTimeMillis();
StringBuffer sb=new StringBuffer();
sb.append(className).append(".");
sb.append(methodName).append("()");
log.info("within--方法名:{},执行时间{}毫秒",sb.toString(),(e-s));
return object;
} catch (Throwable throwable) {
throwable.printStackTrace();
throw new RuntimeException("发生错误");
}
}
}
在类上使用这个注解
启动服务器,分表请求test1和test2两个方法,可以观察到使用@within注解,可以直接作用在类上。
注意: @within所跟随的注解需要必须是针对CLASS的(@Retention(RetentionPolicy.CLASS)),并且@Target必须是ElementType.TYPE。
# 拦截mybatis-plus的IService接口的所有的方法
@Around(value = "target(com.baomidou.mybatisplus.extension.service.IService)")
# 也可以具体拦截某一个类
@Around(value = "target(com.it2.springbootmybatisplus.service.BookServiceImpl)")
设置前面在指定的bean上
@Around("bean(bookMapper)")
示例
package com.it2.springbootmybatisplus.annotation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Slf4j
public class MyBeanAspect {
@Around("bean(bookMapper)")
public Object aroundTest(ProceedingJoinPoint proceedingJoinPoint) {
try {
Signature signature = proceedingJoinPoint.getSignature();
String className = proceedingJoinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
long s=System.currentTimeMillis();
Object object = proceedingJoinPoint.proceed();
long e=System.currentTimeMillis();
StringBuffer sb=new StringBuffer();
sb.append(className).append(".");
sb.append(methodName).append("()");
log.info("方法名:{},执行时间{}毫秒",sb.toString(),(e-s));
return object;
} catch (Throwable throwable) {
throwable.printStackTrace();
throw new RuntimeException("发生错误");
}
}
}
启动服务器,发起请求,调用bookMapper,可以看到bookMapper被注入了
注意: @Pointcut(“bean(beanName)”),应该指bean的名称而不是类名
略,和@target用法很像。
args参数我们更换一种写法,直接使用AspectJProxyFactory代理工厂
(1)先声明一个被拦截的目标体,分别声明了不同的参数情形
package com.it2.springbootmybatisplus.util;
public class TestService {
public void sayHello(String string) {
System.out.println("sayHello ," + string);
}
public void sayHello2(Integer a) {
System.out.println("sayHello2 ," + a);
}
public void sayHello3(Object object) {
System.out.println("sayHello3 ," + object);
}
public void sayHello4(Integer a, Integer b) {
System.out.println("sayHello4 ," + a + " + " + b + " = " + (a + b));
}
public void sayHello5() {
System.out.println("sayHello5");
}
}
(2) 定义切面拦截
package com.it2.springbootmybatisplus.util;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import java.util.Arrays;
import java.util.stream.Collectors;
@Aspect
public class MyArgAspect {
//@1:匹配只有1个参数其类型是String类型的
@Pointcut("args(String)")
public void mypointcut() {
}
@Before("mypointcut()") //引用切点
public void beforeAdvice(JoinPoint joinpoint) {
System.out.println("String请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
@Before("args(com.it2.springbootmybatisplus.pojo.User)") //直接声明切点
public void beforeAdvice2(JoinPoint joinpoint) {
System.out.println("User请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
@Before("args(com.it2.springbootmybatisplus.pojo.User2)") //直接声明切点
public void beforeAdvice3(JoinPoint joinpoint) {
System.out.println("User2请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
@Before("args(Object)") //直接声明切点,直接表明拦截Object
public void beforeAdvice4(JoinPoint joinpoint) {
System.out.println("Object请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
@Before("args(Integer,Integer)") //直接声明切点
public void beforeAdvice5(JoinPoint joinpoint) {
System.out.println("多参数请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
@Before("args(*)") //直接声明切点
public void beforeAdvice6(JoinPoint joinpoint) {
System.out.println("单个参数请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
@Before("args(*,*)") //直接声明切点
public void beforeAdvice7(JoinPoint joinpoint) {
System.out.println("两个参数请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
@Before("args(..)") //直接声明切点
public void beforeAdvice8(JoinPoint joinpoint) {
System.out.println("所有的(0个或者多个)请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
@Before("args(Integer,..)") //直接声明切点
public void beforeAdvice9(JoinPoint joinpoint) {
System.out.println("第一个参数是Integer,后面可以有0或者多个参数请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
}
自定义的Aspect切片,分别拦截String,Integer,User,User2,Object几种参数类型等
(3) 测试用例
@Test
public void testArg(){
TestService testService=new TestService();
AspectJProxyFactory proxyFactory=new AspectJProxyFactory();
proxyFactory.setTarget(testService);
proxyFactory.addAspect(MyArgAspect.class);
TestService proxy=proxyFactory.getProxy();
System.out.println("-------------String-----------------");
proxy.sayHello("xiaowang");
System.out.println("-------------Integer-----------------");
proxy.sayHello2(123);
System.out.println("--------------User----------------");
User user= new User();
user.setName("User");
user.setEmail("abc@qq.com");
proxy.sayHello3(user);
System.out.println("--------------User2----------------");
User2 user2= new User2();
user2.setName("User2");
user2.setEmail("abc@qq.com");
proxy.sayHello3(user2);
System.out.println("--------------多参数----------------");
proxy.sayHello4(3,2);
System.out.println("--------------无参数----------------");
proxy.sayHello5();
}
运行测试用例,发现Object拦截了所有(TestService单个参数方法),不同类型的参数需要不同的拦截
@Before("args(*)") //直接声明切点
public void beforeAdvice6(JoinPoint joinpoint) {
System.out.println("单个参数请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
@Before("args(*,Long)") //直接声明切点
public void beforeAdvice7(JoinPoint joinpoint) {
System.out.println("两个参数请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
@Before("args(..)") //直接声明切点
public void beforeAdvice8(JoinPoint joinpoint) {
System.out.println("所有的(0个或者多个)请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
@Before("args(Integer,..)") //直接声明切点
public void beforeAdvice9(JoinPoint joinpoint) {
System.out.println("第一个参数是Integer,后面可以有0或者多个参数请求参数:" + Arrays.stream(joinpoint.getArgs()).collect(Collectors.toList()));
}
* 匹配任意数量的字符
+ 匹配指定类及其子类
.. 一般用于匹配任意数的子包或参数
前面的demo里,我们已经使用到了通配符
&& 与
|| 或
! 非
可以将多个条件通过运算符进行组合,构成组合式pointcut
利用运算符,我们可以构建组合的pointcut
@Pointcut("bean(beanName1) || bean(beanName2)") #beanName1和beanName2其中一个匹配到
@Pointcut("bean(beanName) && @annotation(xxx.ExecutionTime)") # beanName和@xxx.ExecutionTime同时匹配到
@Pointcut("bean(beanName) && !@annotation(xxx.ExecutionTime)") #beanName被匹配到,但是不能包含@xxx.ExecutionTime注解
两段式(分离切入点和Advice)
直接声明在拦截方法声明切入点
两种声明方式都可以,没有区别。单独声明切入点,可以重复利用,但仅仅式写法上的差异,对实际执行没有任何影响。
利用AOP实现的MyCacheable,(类似@Cacheable)
springboot基础(67):利用切面编程实现自定义的@MyCacheable