1.AOP基础
1.概述
AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实就是面向特定方法编程。
场景:案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时
实现:动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
2.简单实现步骤
1.导入pom依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.编写AOP程序:针对于特定方法根据业务需要进行编程
@Component
@Aspect
public class TimeAspect {
@Around("execution(* com.itheima.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long begin = System.currentTimeMillis();
Object object = proceedingJoinPoint.proceed(); //调用原始方法运行
long end = System.currentTimeMillis();
log.info(proceedingJoinPoint.getSignature()+"执行耗时: {}ms", end - begin);
return object;
}
3.AOP核心概念
2.AOP进阶
1.通知类型
1.@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
2.@Before:前置通知,此注解标注的通知方法在目标方法前被执行
3.@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
4.@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
5.@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
ps:
@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。
@PointCut注解
该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。
2.通知顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。
执行顺序:
1.不同切面类中,默认按照切面类的类名字母排序:
目标方法前的通知方法:字母排名靠前的先执行
目标方法后的通知方法:字母排名靠前的后执行
2.用 @Order(数字) 加在切面类上来控制顺序
目标方法前的通知方法:数字小的先执行
目标方法后的通知方法:数字小的后执行
3.切入点表达式
切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式:
1.切入点表达式-execution
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带 ? 的表示可以省略的部分
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
2.切入点表达式-@annotation
1.定义注解类
代码如下:
@Retention(RetentionPolicy.RUNTIME)//该注解表示该注解运行时有效
@Target(ElementType.METHOD)//该注解表示该注解只能作用方法上
public @interface mylog {
}
此代码是一个注解类,起标识作用
2.定义aop切面类
代码如下:
@Component
@Aspect
@Slf4j
public class aop {
@Pointcut("@annotation(com.sky.annotation.mylog)")
public void pt(){
}
@Before("pt()")
public void before(){
log.info("before执行了");
}
}
@Pointcut中是注解类的路径。此代码表示在方法上加入了@mylog注解的都会执行这个before前置类型的方法。
4.连接点
在通知方法上的形参就是连接点,除了around通知是ProceedingJoinPoint参数,其他四种是JoinPoint参数。
around通知中ProceedingJoinPoint参数调用代码如下:
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取目标类名
String classname = joinPoint.getTarget().getClass().getName();
//获取目标方法名
String functionname = joinPoint.getSignature().getName();
//获取目标方法的参数
Object[] args = joinPoint.getArgs();
//放行方法
Object reslut = joinPoint.proceed();
//返回返回值
return reslut;
}
其他四种通知JoinPoint参数调用代码如下:(以before为例)
@Before("pt()")
public void before(JoinPoint joinPoint){
//获取目标类名
String classname = joinPoint.getTarget().getClass().getName();
//获取目标方法名
String functionname = joinPoint.getSignature().getName();
//获取目标方法的参数
Object[] args = joinPoint.getArgs();
}
3.AOP记录操作日志
1.导入对应的表结构和表对应的实体类
表结构代码如下:
-- 操作日志表
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 '操作日志表';
实体类对应如下:
@Data//get,set方法
@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; //操作耗时
}
操作表的mapper类代码如下:
@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);
}
2.进行aop操作
注解类代码如下:(起标识作用)
@Retention(RetentionPolicy.RUNTIME)//该注解表示该注解运行时有效
@Target(ElementType.METHOD)//该注解表示该注解只能作用方法上
public @interface mylog {
}
记录日志适合用around通知。
代码如下:
@Component
@Aspect
@Slf4j
public class log {
@Autowired
private HttpServletRequest httpServletRequest;
@Around("@annotation(com.sky.annotation.mylog)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取当前登陆人的id
String token = httpServletRequest.getHeader("token");
Claims claims = JwtUtil.parseJWT("Hs256", token);
Integer id = (Integer) claims.get("id");
//操作时间
LocalDateTime now = LocalDateTime.now();
//操作类名
String classname = joinPoint.getTarget().getClass().getName();
//操作方法名
String functionname = joinPoint.getSignature().getName();
//获取方法参数
Object[] args = joinPoint.getArgs();
String methodparams = Arrays.toString(args);
//获取开始时间
long begin = System.currentTimeMillis();
//放行
Object reslut = joinPoint.proceed();
//获取结束时间
long end = System.currentTimeMillis();
//执行耗时
Long costTime = end - begin;
//获取方法返回值
String reslutvalue = JSONObject.toJSONString(reslut);
//记录操作日志
OperateLog operateLog = new OperateLog(null, id, now, classname, functionname, methodparams, reslutvalue, costTime);
OperateLogMapper.insert(operateLog);
return reslut;
}
}
此代码获取信息填充到实体类中,再执行mapper方法操作数据库实现记录日志的功能。
最终在要记录日志的方法上加上@mylog注解即可。