Aop
Aop
面向切面,面向方面编程,其实就是面向特定的方法编程
优势
- 代码无侵入
- 减少重复代码
- 提高开发效率
- 维护方便
实现
动态代理是面向切面编程最主流的实现,而Spring AOP是spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程
- 导入Aop依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 编写Aop程序:针对特定方法根据业务需要进行编程
package com.springboot.AOP;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
//1.将该类交给IOC容器管理
@Component
//2.加入AOP类注解
@Aspect
public class TimeAspect {
//指定作用范围
@Around("com.springboot.AOP.TextAspect.pt()")//切入点表达式
//记录方法运行时间
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("TimeAspect.checkTime()");
//1. 记录开始时间
long start = System.currentTimeMillis();
//2.调用原始方法运行
Object result = joinPoint.proceed();
//3.记录结束时间
long end = System.currentTimeMillis();
//4.计算总耗时
long time = end - start;
System.out.println(joinPoint.getSignature() + ": " + time + "ms");
//5.返回结果 相当于拦截 因此一定要将结果返回出去
return result;
}
}
通知类型
@Around环绕通知 :此注解标注的通知方法在目标方法前、后都被执行 程序发生异常环绕后通知不会执行 需要返回值@Before前置通知 :此注解标注的通知方法在目标方法前执行@After后置通知 :此注解标注的通知方法在目标方法后执行,无论是否有异常都会执行@AfterReturning返回通知 :此注解标注的通知方法在目标方法正常返回后执行,有异常不会执行@AfterThrowing异常通知 :此注解标注的通知方法在目标方法发生异常后执行
注意
@Around环绕通知需要自己调用Proceeding]oinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,并把结果返回。
package com.springboot.AOP;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
//切入点通知类型测试
@Component
@Aspect
public class TextAspect {
//抽离切入点表达式
@Pointcut("execution(* com.springboot.Server.*.*(..))")
public void pt() {
}
//前置通知
@Before("pt()")
public void before() {
System.out.println("before");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around环绕通知前置");
Object proceed = joinPoint.proceed();
System.out.println("around环绕通知后置");
return proceed;
}
//后置通知
@After("pt()")
public void after() {
System.out.println("after");
}
//返回通知 有异常不会执行
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("afterReturning");
}
//异常通知 有异常才会执行
@AfterThrowing("pt()")
public void afterThrowing() {
System.out.println("afterThrowing");
}
}
通知的执行顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行
执行顺序
不同切面类中,默认按切面类的类名字母排序
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠后的后执行
使用@Order注解指定通知的执行顺序
- 目标方法前的通知方法:数字小的先执行
- 目标方法后的通知方法:数字小的后执行
切入点表达式
- 定义:描述切入点方法的一种表达式
- 作用:主要用来决定项目中的那些方法需要加入通知
常见形式
execution(...):根据方法的签名来匹配@annotation(...):根据方法上的注解来匹配
execution表达式
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数)throws 异常?)
其中?号表示可以省略的部分
- 访问修饰符:
public、protected、private、default - 包名.类名:可省略
throws异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
可以使用通配符来描述切入点
*:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
execution(* com.springboot.Server.*.*(..)) //匹配com.springboot.Server包下的任意类的任意方法
..:多个连续的任意符号,可以通配任意数量的包、类、方法名、参数
execution(* com.springboot.Server..*.*(..)) //匹配com.springboot.Server包及其子包下的任意类的任意方法
完整的切入点表达式
//方法参数需要使用全类名
//基于实现类
execution(public void com.springboot.Server.impl.UserServiceImpl.saveUser(java.lang.String))
//基于接口
execution(public void com.springboot.Server.UserService.saveUser(java.lang.String))
匹配两个切入点表达式
@Pointcut("execution(* com.springboot.Server.*.*(..)) || execution(* com.springboot.Server2.*.*(..))")
书写建议
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update开头
- 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
- 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 .,使用*匹配单个包。
@annotation切入点表达式
用于匹配标识特定注解的方法
- 自定义注解
package com.springboot.AOP;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//自定义注解
//标识注解什么时候生效
@Retention(RetentionPolicy.RUNTIME)
//标识注解作用在什么地方
@Target(ElementType.METHOD)
public @interface MyLog {
}
- 绑定切入点
//1.将该类交给IOC容器管理
@Component
//2.加入AOP类注解
@Aspect
public class TimeAspect {
//指定作用范围
@Around("@annotation(com.springboot.AOP.MyLog)")//切入点表达式
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
---
}
}
- 使用
public class DeptServerImp implements DeptServer {
//作用切入点
@MyLog
@Override
public List<Dept> list() {
return deptMapper.list();
}
}
获取连接点的相关信息
- 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
- 对于 @Around 通知,获取连接点信息只能使用
ProceedingJoinPoint获取 - 对于其他四种通知,获取连接点信息只能使用
JoinPoint,它是 ProceedingJoinPoint 的父类型
package com.springboot.AOP;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
//切入点通知类型测试
@Component
@Aspect
public class TextAspect {
//抽离切入点表达式
@Pointcut("execution(* com.springboot.Server.*.*(..))")
public void pt() {
}
//前置通知
@Before("pt()")
public void before() {
System.out.println("before");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取目标对象类名
String className = joinPoint.getTarget().getClass().getName();
//获取目标方法名
String methodName = joinPoint.getSignature().getName();
//获取目标传入的参数
Object[] args = joinPoint.getArgs();
System.out.println("参数" + Arrays.toString(args));
//放行 目标方法执行
joinPoint.proceed();
//带参数
Object proceed = joinPoint.proceed(args);
return proceed;
}
//后置通知
@After("pt()")
public void after() {
System.out.println("after");
}
//返回通知 有异常不会执行
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("afterReturning");
}
//异常通知 有异常才会执行
@AfterThrowing("pt()")
public void afterThrowing() {
System.out.println("afterThrowing");
}
}
使用切入点记录操作日志
package com.springboot.AOP;
import com.alibaba.fastjson.JSONObject;
import com.springboot.Mapper.EmpMapper;
import com.springboot.Mapper.LogMapper;
import com.springboot.Pojo.Emp;
import com.springboot.Pojo.MyLog;
import com.springboot.Utils.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
//日志切面类
@Slf4j
@Component
//切入点
@Aspect
public class LogAsept {
@Autowired
private LogMapper logMapper;
@Autowired
private HttpServletRequest request;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private EmpMapper empMapper;
//连接点
@Around("@annotation(com.springboot.AOP.MyLog)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//记录开始时间
long start = System.currentTimeMillis();
//获取操作人id-当前登录员工的·id
//获取请求头中的jwt令牌
String authorization = request.getHeader("Authorization");
String token = authorization.replace("Bearer ", "");
//解析jwt令牌
String operateUserName = (String) jwtUtils.parseJWT(token).get("username");
//通过用户名获取人员信息
Emp emp = empMapper.findEmpByUsername(operateUserName);
//获取人员id
Integer operateUser = emp.getId();
//获取操作时间
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String operateTime = LocalDateTime.now().format(dateTimeFormatter);
//获取操作类名
String className = joinPoint.getTarget().getClass().getName();
//获取操作方法名
String methodName = joinPoint.getSignature().getName();
//获取操作参数
String methodParams = Arrays.toString(joinPoint.getArgs());
//获取返回值
Object result = joinPoint.proceed();
String returnValue = JSONObject.toJSONString(result);
//获取操作状态
long end = System.currentTimeMillis();
//获取操作耗时
long time = end - start;
//记录操作日志
MyLog myLog = new MyLog(operateUser, operateUserName, operateTime, className, methodName, methodParams, returnValue, time, (byte) 1, "123");
logMapper.insert(myLog);
return result;
}
}
使用切入点自动插入实体参数
/**
* 自定义切面 来实现公共字段自动填充
*/
@Aspect //加入切面注解
@Component //加入组件注解 将自定义切面加入到IOC容器中
@Slf4j
public class AutoFillAspect {
/**
* 加入切入点
* 说明对哪些类的哪些方法进行拦截 哪些方法需要执行此方法
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
//execution(* com.sky.mapper.*.*(..)):匹配com.sky.mapper包及其子包下的所有方法
//@annotation(com.sky.annotation.AutoFill) :匹配加了AutoFill注解的方法
public void autoFillPointCut() {
}
//定义通知
@Before("autoFillPointCut()")//指定切入点
public void autoFill(JoinPoint joinPoint) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
log.info("开始进行公共字段填充");
//获取当前被拦截的方法上加入@AutoFill注解的value值
//获取方法签名对象 将对象强制转换为方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取注解
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
//获取注解的操作类型
OperationType value = autoFill.value();
//获取到当前被拦截方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) return;
//获取实体对象 约定方法的第一个参数必须为数据库实体对象
Object entity = joinPoint.getArgs()[0];
//为实体对象的公共字段赋值 通过反射
//获取时间
LocalDateTime now = LocalDateTime.now();
//获取操作人员id
Long currentId = BaseContext.getCurrentId();
//通过反射得到setUpdateTime方法
Method setUpdateTime = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
//根据不同的操作类型为对应实体的属性赋值
if (value == OperationType.INSERT) {
Method setCreateTime = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity, now);
setCreateUser.invoke(entity, currentId);
}
}
}