目录
1. 通知类型
2. 通知顺序
3. 切入点表达式
execution()
annotation()
4. 连接点(JoinPoint)
5. 案例:将CRUD接口的相关操作记录到数据库中
AOP: Aspect Oriented Programming (面向切面编程、面向方面编程),其实就是面向特定方法编程
优势:代码无侵入、减少重复代码、提高开发效率、维护方便
环境准备
以下代码是从tlias案例中复制过来的
场景:
1. 案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方 法的执行耗时
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义Aop函数:
package pearl.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
@Aspect
public class TimeAspect {
@Around("execution(* pearl.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;
}
}
完成!
核心概念:
连接点: JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
切入点: PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
目标对象: Target,通知所应用的对象
1. 通知类型
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- 需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
- 方法的返回值必须指定为object,来接收原始方法的返回值。
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing :异常后通知,此注解标注的通知方法发生异常后执行
补充:@Pointcut
该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。
2. 通知顺序
3. 切入点表达式
概念:描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式:
1.execution(..…)︰根据方法的签名来匹配 2.@annotation(...) ︰根据注解匹配
execution()
annotation()
1. 自定义注解:
package pearl.annotation;
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 {
}
2. 在方法前加上自定义的注解
3. 根据注解匹配切入点表达式
4. 连接点(JoinPoint)
5. 案例:将CRUD接口的相关操作记录到数据库中
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长。
步骤:
- 引入AOP依赖(该文上方有,自行查看)
- 准备数据库表,并引入实体类
- 自定义注解@Log
- 定义切面类,完成记录操作日志的逻辑
数据库表:
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_user int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(1000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
实体类OperateLog:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer id; //ID
private Integer operateUser; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
OperateLogMapper接口:
@Mapper
public interface OperateLogMapper {
//插入日志数据
@Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insert(OperateLog log);
}
定义注解类:
首先创建一个包anno
然后在包里新建Annotation(注解类)Log
@Retention(RetentionPolicy.RUNTIME) //指定运行时生效
@Target(ElementType.METHOD) //指定该注解作用到方法上
public @interface Log {
}
定义切面类 LogAspect
package pearl.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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import pearl.mapper.OperateLogMapper;
import pearl.pojo.OperateLog;
import pearl.utils.JwtUtils;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;
@Slf4j
@Component //标注一个类为Spring容器的Bean,(把普通pojo实例化到spring容器中,相当于配置文件中的<bean id="" class=""/>)
@Aspect //切面类
public class LogAspect {
@Autowired
private OperateLogMapper operateLogMapper; //要调用该类中的insert方法
@Autowired
private HttpServletRequest request;
@Around("@annotation(pearl.anno.Log)")//@annotation表示匹配注解类,括号中是要匹配注解的全类名,表示使用该注解的方法都会匹配
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取记录到日志中的信息
// 操作人ID
// 获取请求头中的jwt令牌,解析令牌
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer operateUserId = (Integer) claims.get("id");
// 操作时间
LocalDateTime operateTime = LocalDateTime.now();
// 操作类名
String className = joinPoint.getTarget().getClass().getName();
// 操作方法名
String methodName = joinPoint.getSignature().getName();
// 操作方法参数
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args);
long begin = System.currentTimeMillis();
//调用原始方法运行
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
// 操作方法返回值
String returnValue = JSONObject.toJSONString(result);
// 操作耗时
Long costTime = end - begin;
//记录日志操作
OperateLog operateLog = new OperateLog(null,operateUserId,operateTime,className,methodName,methodParams,returnValue,costTime);
operateLogMapper.insert(operateLog);
return result;
}
}
在需要的controller接口方法前设置@Log注解即可。(切记不要在登录方法前加)