目录
一、什么是AOP
二、AOP使用场景
三、使用AOP的好处
四、先举个例子理解AOP面向切面编程
五、Spring5.X的AOP切入点表达式有这些种写法
六、实战基于Spring的AOP快速实现通用日志打印
七、实战基于Spring的AOP快速实现统计接口耗时
本篇重点介绍了AOP面向切面编程,同时在实际项目中使用自定义注解后置通知和环绕通知方式分别用来实现日志统一收集和接口耗时统计功能。
Aspect Oriented Program 面向切面编程,再通俗点说就是在不改变原有逻辑上增加额外的功能,比如解决系统层面的问题,或者增加新的功能
权限控制、缓存、日志处理、事务控制、接口统计耗时
AOP思想把功能分两个部分,分离系统中的各种关注点
核心关注点
业务的主要功能。就比如订单模块中的下单操作,属于业务的主要功能
横切关注点
非核心、额外增加的功能。就比如订单模块中下单操作之前的权限校验以及事务控制或者防重提交校验等
减少代码侵入,解耦
可以统一处理横切逻辑
方便添加和删除横切逻辑
用户下单逻辑中
核心关注点:创建订单
横切关注点:记录日志、控制事务、权限校验
VideoOrderService{
//新增订单
addOrder(){ }
//查询订单
findOrderById(){}
//删除订单
delOrder(){}
//更新订单
updateOrder(){}
}
JointPoint连接点:addOrder/findOrderById/delOrder/updateOrder
Pointcut切入点:过滤出那些JointPoint哪些目标函数进行切入,比如说记录日志,通常增删改
才需要记录日志,查询的日志可以忽略
Advice通知:在切入点中的函数上执行的动作,比如记录日志,权限校验的话,就在对应的方法上加上注解。
Aspect切面:由切入点和通知组合而成,定义通知应用到哪些切入点
Weaving织入:把切面的代码,应用到目标函数的过程
核心概念:
通知 Advice
在特定的切入点上执行的增强处理,有5种通知
@Before前置通知
在执行目标方法之前运行
@After后置通知
在目标方法运行结束之后
@AfterReturning返回通知
在目标方法正常返回值后运行
@AfterThrowing异常通知
在目标方法出现异常后运行
@Around环绕通知
在目标方法完成前、后做增强处理 ,环绕通知是最重要的通知类型 ,像事务,日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint,需要手动执行 joinPoint.procced()
做啥? 比如你需要记录日志,控制事务 ,提前编写好通用的模块,需要的地方直接调用
连接点 JointPoint
要用通知的地方,业务流程在运行过程中需要插入切面的具体位置,
一般是方法的调用前后,全部方法都可以是连接点
只是概念,没啥特殊
切入点 Pointcut
不能全部方法都是连接点,通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法
在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点
过滤出相应的 Advice 将要发生的joinpoint地方
切面 Aspect
通常是一个类,里面定义 切入点+通知 , 定义在什么地方; 什么时间点、做什么事情
通知 advice指明了时间和做的事情(前置、后置等)
切入点 pointcut 指定在什么地方干这个事情
web接口设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面,对象和对象,方法和方法之间都是一个个切面
目标 target
目标类,真正的业务逻辑,可以在目标类不知情的条件下,增加新的功能到目标类的链路上
织入 Weaving
把切面(某个类)应用到目标函数的过程称为织入
AOP代理
AOP框架创建的对象,代理就是目标对象的加强
Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理
下单权限校验/日志业务流程伪代码例子:
//目标类 VideoOrderService; 里面每个方法都是连接点,;切入点是CUD类型的方法,R读取的不作为切入点
//CRDU全称:增加(Create)、读取查询(Retrieve)、更新(Update)和删除(Delete)
VideoOrderService{
//新增订单
addOrder(){ }
//查询订单
findOrderById(){}
//删除订单
delOrder(){}
//更新订单
updateOrder(){}
}
//权限切面类 = 切入点+通知
PermissionAspects{
//切入点 定义了什么地方
@Pointcut("execution(public int net.wnn.permission.service.VideoOrderService.*(..))")
public void pointCut(){}
//before 通知 表示在目标方法执行前切入, 并指定在哪个方法前切入
//什么时候,做什么事情
@Before("pointCut()")
public void permissionCheck(){
System.out.println("在 xxx 之前执行权限校验");
}
....
}
//日志切面类 = 切入点+通知
LogAspect{
//切入点 定义了什么地方
@Pointcut("execution(public int net.wnn.permission.service.VideoOrderService.*(..))")
public void pointCut(){}
//after 通知 表示在目标方法执行后切入, 并指定在哪个方法前切入
//什么时候,做什么事情
@After("pointCut()")
public void logStart(){
System.out.println("在 xxx 之后记录日志");
}
....
}
切入点表示式,除了返回类型、方法名和参数外,其它项都是可选的 (修饰符基本都是省略不写)
访问修饰符 返回值类型(必填) 包和类 方法(必填)
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
比如上面的:
@Pointcut("execution(public int net.wnn.permission.service.VideoOrderService.*(..))")
其中:*:匹配任何数量字符 单个VideoOrderService.*表示VideoOrderService下任意方法
..: () 匹配一个不接受任何参数的方法
(..) 匹配一个接受任意数量参数的方法
(*) 匹配了一个接受一个任何类型的参数的方法
(*,Integer) 匹配了一个接受两个参数的方法,其中第一个参数是任意类型,第二个参数必须是Integer类型
常见的例子:
任意公共方法
execution(public * *(..))任何一个名字以“save”开始的方法
execution(* save*(..))VideoService接口定义的任意方法(识别)
execution(* net.wnn.service.VideoService.*(..))在service包中定义的任意方法(识别)
execution(* net.wnn.service.*.*(..))匹配 service 包,子孙包下所有类的所有方法(识别)
execution(* net.wnn.service..*.*(..))
第一步开启SpringAOP注解配置
@Configuration
@ComponentScan("net.wnn")
@EnableAspectJAutoProxy //开启了spring对aspect的支持
public class AnnotationAppConfig {
}
第二步
配置切入点和通知。自定义注解类型配置
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OpLog {
/**
*
* 操作模块
*/
String opModule();
/**
*
* 操作类型
*/
String operType();
/**
*
* 操作描述
*/
String operDesc();
}
import lombok.extern.slf4j.Slf4j;
import net.wnn.model.OpLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
//让spring进行扫描 一定要加
@Component
//告诉spring,这个一个切面类,里面可以定义切入点和通知
@Aspect
@Slf4j
public class LogAdvice {
/**
* 首先定义一个切点
*/
@Pointcut("@annotation(net.wnn.model.OpLog)")
public void printLog() {
}
@After("printLog()")
public void after(JoinPoint joinPoint){
try {
//获取方法名称
String methodName = joinPoint.getSignature().getName();
//获取类名称
String className = joinPoint.getSignature().getDeclaringTypeName();
System.out.println("类名:"+className+" 方法名:"+methodName);
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature =(MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method =signature.getMethod();
// 获取操作
OpLog opLog =method.getAnnotation(OpLog.class);
log.info("模块名称:[{}],类型:[{}],描述信息:[{}]",opLog.opModule(),opLog.operType(),opLog.operDesc());
//执行连接点的方法
try {
((ProceedingJoinPoint)joinPoint).proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
第三步在请求方法上增加注解
/**
* 用户登录
* @param request
* @return
*/
@PostMapping("login")
@OpLog(opModule = "用户模块",operType = "登录操作",operDesc = "此方法用户用户登录")
public JsonData login(@RequestBody AccountLoginRequest request){
JsonData jsonData = accountService.login(request);
return jsonData;
}
postman请求接口验证:
日常开发工作中,可以根据实际日志存储需求,将统一收集到的需求进行入库等操作。
第一步开启SpringAOP注解配置
@Configuration
@ComponentScan("net.wnn")
@EnableAspectJAutoProxy //开启了spring对aspect的支持
public class AnnotationAppConfig {
}
第二步配置切入点
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
//让spring进行扫描 一定要加
@Component
//告诉spring,这个一个切面类,里面可以定义切入点和通知
@Aspect
@Slf4j
public class LogAdvice {
//切入点表达式,也可以直接在通知上编写切入点表达式
@Pointcut("execution(* net.wnn.service.impl.AccountServiceImpl.*(..))")
public void aspect(){
}
@Around("aspect()")
public void around(JoinPoint joinPoint){
Object target = joinPoint.getTarget().getClass().getName();
//通过joinPoint获取参数
Object [] args = joinPoint.getArgs();
log.info("调用者:{} 调用方法:{} 调用参数:{}",target,joinPoint.getSignature(),args[0]);
long start = System.currentTimeMillis();
log.info("===========环绕通知 环绕前========");
//执行连接点的方法
try {
((ProceedingJoinPoint)joinPoint).proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
long end = System.currentTimeMillis();
log.info("===========环绕通知 环绕后========");
log.info("调用方法总耗时 time = " + (end - start) +" ms");
}
}
控制台输出:
实际开发工作中,可根据接收到的方法 以及耗时进行入库统计展示。