文章目录
1. AOP介绍
1.1 什么是AOP
aop全称Aspect Oriented Programming,即为面向切面编程。实现对业务程序非侵入式的扩展。底层是通过动态代理实现的,具体实现有基于jdk的动态代理和基于CGLib的动态代理。
- JDK动态代理是基于反射机制实现代理接口的匿名类,在调用具体方法前调用invokeHandler来处理。有具体实现接口的情况下可以使用jdk动态代理或者cglib动态代理
- cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码文件生产子类来处理。如果没有具体实现接口的情况下,只能使用基于字节码文件的动态代理cglib
1.2 AOP中的名词介绍
先介绍一些AOP中的各种名词
- 切面(Aspect):切入点和通知的集合
- 连接点(Joinpoint):目标对象中可以被增强的所有方法
- 通知(Advice):增强的代码(逻辑),分为前置,后置,最终,异常,环绕
- 切入点(Pointcut):目标对象中经过匹配最终增强的方法
- 引入(Introduction):动态的为某个类增加和减少方法
- 目标对象(Target Object):被代理的对象
- AOP代理对象(AOP Proxy):AOP框架创建的代理对象,用于实现切面,调用方法
- 织入(Weaving):将通知应用到切入点的过程
1.3 注解介绍
- @EnableAspectJautoProxy 用于springboot启动类,代表开启注解aop功能支持
- proxyTargetClass 是否强制使用CGlib的动态代理,默认false
- exposeProxy 是否通过aop框架暴露该代理对象,aopContext能够访问
- @Aspect 用于标注切面类
- @Pointcut 用于标识切入点
- value 切入点表达式
- @Before 前置通知
- @AfterReturning 后置通知
- @AfterThrowing 异常通知
- @After 最终通知
- @Around 环绕通知,环绕通知代表了一个完整的流程,因此环绕通知和上面的四个通知任选其一使用
1.4 切入点表达式
-
execution - 根据表达式匹配,使用最多
execution([修饰符] 返回类型 [包名.类名].方法名(参数列表) [异常])
支持的通配符有
*
:匹配所有。..
:匹配多级包或者多个参数。+
表示类以及子类 -
within - 匹配方法所在的包或者类
-
this - 用于向通知方法中传入代理对象的引用
-
target - 用于向通知方法中传入目标对象的引用
-
args - 用于向通知方法中传入参数,并且匹配参数个数
-
@args - 和args都是匹配参数,但是@args要求传入切入点的参数必须标注指定注解,且不能是SOURCE源码注解,比如Lombok的
-
@within - 匹配加了某个注解的类中的所有方法
-
@target - 与@within类似,但是要求标注到类上的注解,必须为RUNTIME的
-
@annotation - 匹配加了某个注解的方法
-
bean 通过spring容器中的beName匹配
可以使用通配符*来标识以什么开头,以什么结尾
2. 测试
本文对应的源码地址:03-spring-boot-aop · master · csdn / spring-boot-csdn · GitLab (sea-clouds.cn)
pom.xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.0</version>
</dependency>
</dependencies>
2.1 简单案例
/**
* @author HLH
* @description 简单的测试
* @email [email protected]
* @date Created in 2021/8/7 上午11:25
*/
@Component // 注册组件
@Aspect // 开启aop
public class MyAspect {
/**
* 匹配所有service中的所有方法
*/
@Pointcut(value = "execution(public * xyz.hlh.boot3..service..*(..))")
public void pointCut() {}
@Before(value = "pointCut()")
public void before(JoinPoint joinPoint) {
System.out.println("前置通知");
}
@AfterReturning(value = "pointCut()", returning = "returnObj")
public void afterReturning(JoinPoint joinPoint, Object returnObj) {
System.out.println("后置通知");
}
@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
System.out.println("异常通知");
}
@After(value = "pointCut()")
public void afterThrowing(JoinPoint joinPoint) {
System.out.println("最终通知");
}
/**
* 环绕通知因为包含显示的方法调用,所以必须要有返回参数
* 因为显示调用,处理异常,所以和上面的四个通知只能选择一种使用
*/
@Around(value = "pointCut()")
public Object around(ProceedingJoinPoint pjp) {
Object result = null;
try {
System.out.println("前置处理");
// 调用方法
result = pjp.proceed(pjp.getArgs());
System.out.println("后置处理");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("异常处理");
} finally {
System.out.println("最终处理");
}
return result;
}
}
2.2 execution
用于表达式匹配
/**
* 匹配所有service中的所有方法
*/
@Pointcut(value = "execution(public * xyz.hlh.boot3..service..*(..))")
public void pointCut() {}
/**
* 匹配所有service中的所有方法
* 修饰符和抛出异常可以省略
*/
@Pointcut(value = "execution(* xyz.hlh.boot3..service..*(..))")
public void pointCut() {}
2.3 within
用于匹配切入点所在的包或者类
/**
* 匹配所有UserService类中的所有方法
*/
@Pointcut(value = "within(xyz.hlh.boot3.service.UserService)")
public void pointCut() {}
/**
* 匹配所有service包中的所有类中的所有方法
*/
@Pointcut(value = "within(xyz.hlh.boot3.service.*)")
public void pointCut1() {}
/**
* 匹配所有service包以及所有子包中的所有类中的所有方法
*/
@Pointcut(value = "within(xyz.hlh.boot3.service..*)")
public void pointCut2() {}
2.4 this
用于向通知方法中传入代理对象的引用
/**
* this 向切入点中传入代理对象,argNames指定此方法中的参数名词列表
*/
@Before(value = "pointCut() && this(proxy)", argNames = "joinPoint,proxy")
public void before(JoinPoint joinPoint, Object proxy) {
// 代理对象
System.out.println(proxy);
System.out.println("前置通知");
}
2.5 target
用于向通知方法中传入目标对象
/**
* target 向切入点中传入目标对象,argNames指定此方法中的参数名词列表
*/
@Before(value = "pointCut() && target(obj)", argNames = "joinPoint,obj")
public void before(JoinPoint joinPoint, Object obj) {
// 目标对象
System.out.println(obj);
System.out.println("前置通知");
}
2.6 args
用于向通知方法中传入参数,并且匹配参数个数
/**
* args 向切入点中传入参数(匹配参数),argNames指定此方法中的参数名词列表
* 该测试方法为修改User的方法 入参有两个 User user,Integer userId
* args中参数名词随意,但是参数个数需要匹配
* 此处通知方法中的参数类型可以指定具体的类型,spring自动强转,但是如果类型不对,会造成进不去该通知
*/
@Before(value = "pointCut() && args(a, b)", argNames = "joinPoint,a,b")
public void before(JoinPoint joinPoint, User a, Integer b) {
System.out.println(a);
System.out.println("前置通知");
}
2.7 @args
和args都是匹配参数,但是@args要求传入切入点的参数必须标注指定注解,且不能是SOURCE源码注解,比如Lombok的
/**
* @args 和args都是匹配参数,但是@args要求传入切入点的参数必须标注指定注解,且不能是SOURCE源码注解,比如Lombok的
* 该测试方法为修改User的方法 入参有两个 User user,Integer userId
* @args中指定对应的标注注解的的类型,参数个数需要匹配,不校验的参数用*代替
* 该处要求第一个参数User对象必须标注MyAnno的自定义注解
*/
@Before(value = "pointCut() && @args(xyz.hlh.boot3.annotation.MyAnno, *)")
public void before(JoinPoint joinPoint) {
System.out.println("前置通知");
}
2.8 @within
匹配加了某个注解的类中的所有方法
/**
* @within 用于匹配标书指定注解的类中的所有方法
* 匹配标注了MyAnno注解的类中的所有方法
*/
@Pointcut(value = "@within(xyz.hlh.boot3.annotation.MyAnno)")
public void pointCut() {}
2.9 @target
与@within类似,但是要求标注到类上的注解,必须为RUNTIME的
/**
* @target 用于匹配标书指定注解的类中的所有方法,要求注解必须是RUNTIME的
* 匹配UserService类,并且标注了MyAnno注解的类中的所有方法
* @target 不能单独使用,网上并没有类似说明,可能是我的springboot原因,单独使用会报错。合并使用效果正常
*/
@Pointcut("within(xyz.hlh.boot3.service.UserService) && @target(xyz.hlh.boot3.annotation.MyAnno)")
public void pointCut() {}
2.10 @annotation
匹配加了某个注解的方法
/**
* @annotation 匹配加了某个注解的方法
* 匹配标注了MyAnno注解的方法
* 此处测试方法为insertUser
*/
@Pointcut("@annotation(xyz.hlh.boot3.annotation.MyAnno)")
public void pointCut() {}
2.10 bean
通过spring容器中的beName匹配
/**
* 匹配spring容器中为指定beanName的对象中的所有方法
* 此处匹配spring容器中名称为userService的对象中的所有方法
*/
@Pointcut(value = "bean(userService)")
public void pointCut() {}
/**
* 匹配spring容器中为指定beanName的对象中的所有方法
* 此处匹配spring容器中名称为Service结尾的对象中的所有方法
*/
@Pointcut(value = "bean(*Service)")
public void pointCut1() {}
3. 案例
使用AOP的例子很多
- 比如spring的事务管理器
- 日志
- 敏感字过滤
- 登录用户参数注入
等等,这儿主要介绍AOP日志和登录用户或参数注入
3.1 AOP日志
项目中一般都会有日志打印,比如方法的入参,出参,有没有异常。通常使用非侵入式的日志打印。aop可以完成非侵入式的日志打印
/**
* @author HLH
* @description Service 日志打印
* @email [email protected]
* @date Created in 2021/8/7 上午11:25
*/
@Component // 注册组件
@Aspect // 开启aop
@Slf4j(topic = "service") // 修改logger name
public class ServiceLogAspect {
/**
* 拦截所有service中的所有方法
*/
@Pointcut(value = "execution(* xyz.hlh.boot3..service..*(..))")
public void pointCut() {}
/**
* 前置打印
*/
@Before(value = "pointCut()")
public void before(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取方法名
String methodName = signature.getName();
// 获取类全限定类名
String declaringTypeName = signature.getDeclaringTypeName();
// 获取类名
String className = declaringTypeName.substring(declaringTypeName.lastIndexOf(".") + 1);
// 记录日志
Object[] args = joinPoint.getArgs();
log.info("{}.{} is called, args list is {}", className, methodName, JSONUtil.toJsonStr(args));
}
/**
* 后置打印
*/
@AfterReturning(value = "pointCut()", returning = "returnObj")
public void afterReturning(JoinPoint joinPoint, Object returnObj) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取方法名
String methodName = signature.getName();
// 获取类全限定类名
String declaringTypeName = signature.getDeclaringTypeName();
// 获取类名
String className = declaringTypeName.substring(declaringTypeName.lastIndexOf(".") + 1);
// 记录日志
log.info("{}.{} is call to complete, return value is {}", className, methodName, JSONUtil.toJsonStr(returnObj));
}
/**
* 异常打印
*/
@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取方法名
String methodName = signature.getName();
// 获取类全限定类名
String declaringTypeName = signature.getDeclaringTypeName();
// 获取类名
String className = declaringTypeName.substring(declaringTypeName.lastIndexOf(".") + 1);
// 记录日志
log.error("{}.{} throws exception '{}' because of '{}'"
, className, methodName, e.getClass(), e.getMessage(), e);
}
}
3.2 登录用户参数注入
登录成功之后,很多方法中需要使用登录用户信息,但是需要写代码从redis中根据token获取登录用户信息。这是一个通用的操作,可以使用AOP+自定义注解+springMVC的参数解析器实现mvc中的方法中,自动注入一个登录用户
具体实现请查看另外一篇文章:https://blog.csdn.net/HLH_2021/article/details/119491890