1 简介
- AOP:面向切面编程,指面向特定方法编程
- 实现:
- 动态代理
- SpringAOP旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
- 场景:记录操作日志、权限控制、事务管理…
- 优势:代码无侵入、减少重复代码、提高开发效率、维护方便
引入依赖:
<!-- AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
案例:记录方法运行时间:
TimeAspect.java:
package org.example.ssmpratice1.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Component//注意,同样需要交给IOC容器管理
@Aspect //声明为一个aop类
public class TimeAspect {
@Around("execution(* org.example.ssmpratice1.service.*.*(..))")//切入点表达式
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
//1.记录开始时间
long begin=System.currentTimeMillis();
//2.调用原始方法
Object result= joinPoint.proceed();
//3.记录结束时间,得到运行耗时
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature()+"方法执行耗时:{}ms",end-begin);
return result;
}
}
2 核心概念:
- 连接点: JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
- 通知: Advice,指方法中重复的逻辑、共性功能,最终体现为一个方法(如上文 recordTime)
- 切入点: pointCut,匹配连接点的条件通知只会在切入点方法执行时被应用。(实际被AOP控制的方法,由切入点表达式execution(XXX )决定)
- @Point()可以用于抽取切入点表达式,使用时在通知参数内调用所修饰方法
- 切面:通知+切入点=切面,即@Aspect这个类,描述通知和切入点的关系
- 目标对象:通知应用的对象
- AOP执行流程中:运行的不再是原始的目标对象,而是基于原始对象生成且增强的代理对象
3 通知类型
- @Around:环绕通知,这种通知类型会在目标方法执行前后都执行。它允许你完全控制目标方法的执行,包括在执行前后添加自定义的行为。
- 方法中需要:
- 声明参数: ProceedingJoinPoint joinPoint
- 调用目标对象的原始方法执行:joinPoint.proceed();
- 注意还要返回原始方法的结果(注意将返回类型设为 Object)
- 方法中需要:
- @Before:前置通知,这种通知类型会在目标方法执行之前执行。它用于在目标方法执行前添加一些前置处理逻辑。
- @After:后置通知,这种通知类型会在目标方法执行之后执行,无论目标方法是否抛出异常。它用于在目标方法执行后添加一些后置处理逻辑。
- @AfterReturning:返回后通知,这种通知类型会在目标方法正常返回后执行。它用于在目标方法成功执行并返回结果后添加一些处理逻辑。
- @AfterThrowing:异常后通知,这种通知类型会在目标方法抛出异常后执行。它用于处理目标方法执行过程中发生的异常情况。
4 通知顺序:
默认类名字典序
可以通过@Order(x)来手动指定顺序
5 切入点表达式:
- 描述切入点方法的一种表达式
- 主要用来指定要加入通知的方法
- 常见形式:
- execution(….):根据方法的签名来匹配
- @annotation(…):根据注解匹配
5.1 execution
execution([访问修饰符] 返回值 [包名.类名.]方法名(方法参数) [throws 异常])
[]表示可省略
- * :单个独立的通配符
- ..:多个连续的通配符
可以使用 && || ! 来组合复杂的表达式
5.2 @annotation
- @annotation(特定注解/自定义注解 的全类名):切入点表达式,用于匹配标识有特定注解的方法
6 连接点:
在spring中用JoinPoint抽象了连接点,用它可获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
- @Around 通知,只能使用 ProceedingJoinPoint
- 其他四种通知, 只能使用 JoinPoint
例
String className = joinPoint.getTarget().getClass().getName(); // 获取目标类名
Signature signature = joinPoint.getSignature(); // 获取目标方法签名
String methodName = joinPoint.getSignature().getName(); // 获取目标方法名
Object[] args = joinPoint.getArgs(); // 获取目标方法运行参数
Object res = joinPoint.proceed(); // 执行原始方法,获取返回值(环绕通知)
综合案例
将操作日志记录到数据库表
信息:操作人,操作时间,执行方法全类名,方法名,运行时参数,返回值,执行时长
自定义注解:
package org.example.ssmpratice1.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
//元注解
@Retention(RetentionPolicy.RUNTIME)//指定注解在运行时生效
@Target(ElementType.METHOD)//指定注解修饰的是方法
public @interface Log {
}
AOP类:重点:获取当前操作人的id的操作!!!
package org.example.ssmpratice1.aop;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.example.ssmpratice1.mapper.OperateLogMapper;
import org.example.ssmpratice1.pojo.OperateLog;
import org.example.ssmpratice1.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;
@Slf4j
@Component
@Aspect
public class LogAspect {
@Autowired//Spring 能自主注入 HttpServletRequest,而无需手动托管至IOC,依赖 Web 请求绑定机制,需在 Servlet 容器且 Spring Web 配置完备。
HttpServletRequest request;
@Autowired
OperateLogMapper operateLogMapper;
@Around("@annotation(org.example.ssmpratice1.anno.Log)")
public Object recordLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//获取操作人id
String jwt = request.getHeader("token");
Map maps = JwtUtils.parseJWT(jwt);
Integer operateUser= (Integer) maps.get("id");
//获取操作时间
LocalDateTime localDateTime=LocalDateTime.now();
//获取操作类名
String className= proceedingJoinPoint.getTarget().getClass().getName();
//获取操作方法名
String methodName=proceedingJoinPoint.getSignature().getName();
//获取操作方法参数
Object args[] = proceedingJoinPoint.getArgs();
String methodParams = Arrays.toString(args);
long begin=System.currentTimeMillis();
//获取操作方法返回值
Object result = proceedingJoinPoint.proceed();
String returnValue = JSONObject.toJSONString(result);
//获取操作耗时
long costTime=System.currentTimeMillis()-begin;
//写入日志
OperateLog operateLog= new OperateLog(null,operateUser,localDateTime,className,methodName,methodParams,returnValue,costTime);
operateLogMapper.insert(operateLog);
log.info("AOP记录日志:{}",operateLog);
return result;
}
}