JavaWeb 课堂笔记 —— 24 AOP 面向切面编程
本文介绍了Spring AOP的基本概念与实现方式。
AOP(面向切面编程)通过动态代理机制对特定方法进行编程,解决代码重复性问题。文章以统计方法执行耗时为例,演示了Spring AOP的快速入门步骤:导入依赖、编写切面类、定义切入点表达式。核心概念包括连接点、通知、切入点等,并详细讲解了AOP的执行流程和五种通知类型。此外,还介绍了通知顺序规则、切入点表达式语法(@execution和@annotation)及其通配符使用技巧,提出了书写切入点表达式的优化建议。最后展示了如何通过自定义注解实现更灵活的AOP编程。
01 概述
AOP
全称Aspect Oriented Programming
(面向切面编程、面向方面编程),其实就是面向特定方法编程。
场景:我们的案例部分功能运行较慢,定位耗时较长,需要统计每一个业务方法的执行耗时,旧有的方法通过时间差计算,但太过于繁琐古板。
实现:我们采用动态代理对旧有的方法进行升级,动态代理是面向切面编程最主流的实现,而SpringAOP
是Spring
框架的高级技术,旨在管理bean
对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
优势:
02 Spring AOP
快速入门:统计各个业务层方法执行耗时
① 导入依赖,在pom.xml
中导入AOP
的依赖
<!--AOP依赖-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
② 编写AOP
程序,针对特定方法根据业务需要进行编程
package com.itheima.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 //AOP类
public class TimeAspect {@Around("execution(* com.itheima.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;}
}
03 AOP
核心概念
连接点:JoinPoint
,是可以被AOP
控制的众多方法
通知:Advice
,可重复逻辑,也就是共性功能(最终体现为一个方法)
切入点表达式:PointCut Expression
,描述匹配条件
切入点:PointCut
,符合匹配条件
切面:Aspect
,通知 + 切入点表达式
目标对象:Target
,字面意思
04 AOP
执行流程
05 通知类型
MyAspect1.java
package com.itheima.aop;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Slf4j
@Component
//@Aspect
public class MyAspect1 {//抽取切入点表达式哈哈哈哈哈@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")public void pt(){}@Before("pt()") //前置通知public void before(){log.info("before ...");}@Around("pt()") //环绕通知public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {log.info("around before ...");//调用目标对象的原始方法执行Object result = proceedingJoinPoint.proceed();log.info("around after ...");return result;}@After("pt()") //后置通知public void after(){log.info("after ...");}@AfterReturning("pt()") //返回后通知public void afterReturning(){log.info("afterReturning ...");}@AfterThrowing("pt()") //异常后通知public void afterThrowing(){log.info("afterThrowing ...");}
}
@PointCut
的作用是抽取公共的冗余的切入点表达式,引入pt()
即可
06 通知顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行,执行顺序依照类名首字母,目标方法前的通知方法,字母排名靠前的先执行,目标方法后的通知方法,字母排名靠前的后执行,另外,注解@Order
也可以决定执行顺序,目标方法前的通知方法,数字小的先执行,目标方法后的通知方法,数字小的后热行。
07 切入点表达式
切入点表达式用来决定项目中到底哪些目标方法需要应用我们所定义的通知,常见形式如下;
@execution()
:根据方法的签名来匹配@annotation()
:根据注解匹配
① @execution()
@execution()
主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符❓返回值 包名.类名. ❓方法名(方法参数)throws 异常❓)
其中带❓的表示可以省略的部分
访问修饰符
:可省略(比如:public
、protected
)包名.类名
:可省略throws异常
:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
通配符
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
execution(* com.*.service.*.update*())
..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
execution(*com.itheima..Deptservice.*(..))
书写建议
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是
find
开头,更新类方法都是update
开头 - 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
- 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用,使用
*
匹配单个包
② @annotation()
@annotation()
用于匹配标识有特定注解的方法
@annotation(com.itheima.anno.Log)
MyLog.java
自定义注解(标识)
package com.itheima.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 {
}
08 连接点
Spring
当中采用JoinPoint
抽象了连接点,用它可以获得方法执行时的相关信息,比如目标类名、方法名、方法参数等。注意,对于@Around
通知,获取连接点信息只能用ProceedingJoinPoint
,对于其他四种通知,取连接点信息只能用JoinPoint
,其是ProceedingJoinPoint
的父类型。
package com.itheima.aop;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.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;import java.util.Arrays;//切面类
@Slf4j
@Aspect
@Component
public class MyAspect8 {@Pointcut("execution(* com.itheima.service.DeptService.*(..))")private void pt(){}@Before("pt()")public void before(JoinPoint joinPoint){log.info("MyAspect8 ... before ...");}@Around("pt()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("MyAspect8 around before ...");//1. 获取 目标对象的类名 String className = joinPoint.getTarget().getClass().getName(); //目标→类→名字log.info("目标对象的类名:{}", className);//2. 获取 目标方法的方法名 String methodName = joinPoint.getSignature().getName(); //方法签名→名字log.info("目标方法的方法名: {}", methodName);//3. 获取 目标方法运行时传入的参数 Object[] args = joinPoint.getArgs(); //参数log.info("目标方法运行时传入的参数: {}", Arrays.toString(args));//4. 放行 目标方法执行 Object result = joinPoint.proceed();//5. 获取 目标方法运行的返回值 log.info("目标方法运行的返回值: {}", result);log.info("MyAspect8 around after ...");return result;}
}
09 案例:将案例中增、删、改相关接口的操作日志记录到数据库表中
日志信息包含操作人、操作时间、实行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长等,此时,需要对所有业务类中的增、删、改方法添加统一功能,使用AOP
技术最为划算。
准备工作:
① 引入AOP
依赖包
<!--AOP-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
② 导入准备好的数据库表结构,引入对应实体类
-- 操作日志表
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 '操作日志表';
编码工作:
① 自定义注解@Log
package com.itheima.anno;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 Log {
}
② 定义切面类、完成记录操作日志的逻辑,获取request
对象,从请求头中获取jwt
令牌,解析令牌当前用户的id
package com.itheima.aop;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.itheima.mapper.OperateLogMapper;
import com.itheima.pojo.OperateLog;
import com.itheima.utils.JwtUtils;
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 javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;@Slf4j //输出日志
@Component
@Aspect //切面类
public class LogAspect {@Autowiredprivate HttpServletRequest request;//注入Mapper接口@Autowiredprivate OperateLogMapper operateLogMapper;@Around("@annotation(com.itheima.anno.Log)")public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable{//1.操作员工ID(借助jwt令牌)String jwt = request.getHeader("token");Claims claims = JwtUtils.parseJWT(jwt);Integer operateUser = (Integer) claims.get("id");//2.操作时间LocalDateTime operateTime = LocalDateTime.now();//3.操作类名String className = joinPoint.getTarget().getClass().getName();//4.操作方法名String methodName = joinPoint.getSignature().getName();//5.操作方法参数Object[] args = joinPoint.getArgs();String methodParams = Arrays.toString(args);long begin = System.currentTimeMillis();Object result = joinPoint.proceed();long end = System.currentTimeMillis();//6.操作方法返回值String returnValue = JSONObject.toJSONString(result);//7.操作时间Long costTime = end - begin;//记录操作日志OperateLog operateLog = new OperateLog(null, operateUser, operateTime, className, methodName, methodParams, returnValue, costTime);operateLogMapper.insert(operateLog);log.info("AOP记录操作日志,{}", operateLog);return result;}
}