AOP 概述:
AOP:Aspect Oriented Programming (面向切面编程、面向方法编程),是一种思想
优点:减少重复代码,代码无侵入,提高开发效率,维护方便
AOP 入门:
需求:统计部门管理各个业务层方法执行耗时
在 pom.xml 文件中导入 AOP 的依赖:
XML
<!-- AOP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
编写 AOP 程序:针对于特定方法根据业务需要进行编程
java
@Component
@Aspect//当前类为切面类
@Slf4j
public class RecordTimeAspect{
@Around("execution(* org.example.service.impl.DeptServiceImpl.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable{
//记录方法执行开始时间
long begin = System.currentTimeMillis();
//执行原始方法
Object result = pjp.proceed();
//记录方法执行结束时间
long end = System.currentTimeMillis();
//计算方法执行耗时
log.info("方法执行耗时: {}毫秒",end-begin);
return result;
}
}
AOP 详解:
连接点:JoinPoint
指的是可以被 AOP 控制的方法
在 SpringAOP 提供的 JoinPoint 当中,封装了连接点方法在执行时的相关信息
通知:Advice
指的是重复的逻辑,也就是共性的功能
切入点:PointCut
匹配连接点的条件,通知仅会在切入点方法被执行时被应用
切面:Aspect
描述通知与切入点的对应关系
切面所在的类,为切面类(被 @Aspect 注解标识的类)
目标对象:Target
指的是通知所应用的对象
通知类型:
|-----------------|--------------------------------------|
| Spring AOP 通知类型 ||
| @Around | 环绕通知,此注解标注的通知方法在目标方法前、后都被执行 |
| @Before | 前置通知,此注解标注的通知方法在目标方法前被执行 |
| @After | 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 |
| @AfterReturning | 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 |
| @AfterThrowing | 异常后通知,此注解标注的通知方法发生异常后执行 |
代码测试:
java
@Slf4j
@Component
@Aspect
public class MyAspect1{
//前置通知
@Before("execution(* org.example.service.*.*(..))")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("execution(* org.example.service.*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法如果执行时有异常,环绕通知中的后置代码将不会执行
log.info("around after ...");
return result;
}
//后置通知
@After("execution(* org.example.service.*.*(..))")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* org.example.service.*.*(..))")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* org.example.service.*.*(..))")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
程序发生异常的情况下:
@AfterReturning 标识的通知方法不会执行,@AfterThrowing 标识的通知方法执行
@Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑将不会执行(原始方法调用已经出现异常)
在使用通知时的注意事项:
@Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
@Around 环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,将获取不到返回值
抽取:
Spring 提供了 @Pointcut 注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可
java
@Slf4j
@Component
@Aspect
public class MyAspect1{
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* org.example.service.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void before(JoinPoint joinPoint){
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(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
注意:当切入点方法使用 private 修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把 private 改为 public,而在引用的时候,具体的语法为:
java
@Slf4j
@Component
@Aspect
public class MyAspect2{
//引用 MyAspect1 切面类中的切入点表达式
@Before("org.example.aop.MyAspect1.pt()")
public void before(){
log.info("MyAspect2 -> before ...");
}
}
通知顺序:
当在项目开发当中定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法,此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行
在不同切面类中,默认按照切面类的类名字母排序:
-
目标方法前的通知方法:字母排名靠前的先执行
-
目标方法后的通知方法:字母排名靠前的后执行
控制通知的执行顺序:修改切面类的类名 或 使用 Spring 提供的 @Order 注解(推荐)
切入点表达式:
切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中哪些方法需要加入通知
常见形式:
execution(...):根据方法的签名来匹配
@annotation(...):根据注解匹配
execution:
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
java
execution([访问修饰符] 返回值 [包名.类名.]方法名(方法参数) [throws 异常])
使用通配符描述切入点:
* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
切入点表达式的语法规则:
方法的访问修饰符可以省略
返回值可以使用 * 号代替(任意返回值类型)
包名可以使用 * 号代替,代表任意包(一层包使用一个 * )
使用 ..配置包名,标识此包以及此包下的所有子包
类名可以使用 * 号代替,标识任意类
方法名可以使用 * 号代替,表示任意方法
可以使用 * 配置参数,一个任意类型的参数
可以使用 .. 配置参数,任意个任意类型的参数
根据业务需要,可以使用 且 (&&) 、或 (||) 、非 (!) 来组合比较复杂的切入点表达式:
java
execution(* org.example.service.DeptService.findAll(..)) || execution(* org.example.service.DeptService.deleteById(..))
切入点表达式书写建议:
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配(如:findXxx,updateXxx)
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
在满足业务需要的前提下,尽量缩小切入点的匹配范围(如:包名尽量不使用 .. ,使用
*匹配单个包)
@annotation:
基于注解的方式来匹配切入点方法
自定义注解:LogOperation
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}
DetpServiceImpl:
java
@Service
public class DeptServiceImpl implements DeptService{
@Autowired
private DeptMapper deptMapper;
@Override
@LogOperation//自定义注解:表示当前方法属于目标方法
public List<Dept> findAll(){
return deptMapper.findAll();
}
@Override
@LogOperation
public void deleteById(Integer id){
deptMapper.deleteById(id);
}
/*
...
*/
}
切面类:
java
@Slf4j
@Component
@Aspect
public class MyAspect{
//前置通知
@Before("@annotation(org.example.anno.LogOperation)")
public void before(){
log.info("MyAspect -> before ...");
}
//后置通知
@After("@annotation(org.example.anno.LogOperation)")
public void after(){
log.info("MyAspect -> after ...");
}
}
AOP 案例:
需求:
将案例中增、删、改相关接口的操作日志记录到数据库表中
操作日志信息包含:
操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
代码实现:
准备工作:
在 pom.xml 中引入 AOP 的依赖:
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建数据库表结构:
sql
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_emp_id 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 '返回值, 存储json格式',
cost_time int comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
创建实体类 OperateLog:
java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog{
private Integer id; //ID
private Integer operateEmpId; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
创建日志操作 Mapper 接口:
java
@Mapper
public interface OperateLogMapper{
//插入日志数据
@Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
void insert(OperateLog log);
}
自定义注解 @LogOperation:
java
//自定义注解,用于标识哪些方法需要记录日志
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}
定义 AOP 记录日志的切面类:
java
@Aspect
@Component
public class OperationLogAspect{
@Autowired
private OperateLogMapper operateLogMapper;
//环绕通知
@Around("@annotation(log)")
public Object around(ProceedingJoinPoint joinPoint, LogOperation log) throws Throwable{
//记录开始时间
long startTime = System.currentTimeMillis();
//执行方法
Object result = joinPoint.proceed();
//当前时间
long endTime = System.currentTimeMillis();
//耗时
long costTime = endTime - startTime;
//构建日志对象
OperateLog operateLog = new OperateLog();
operateLog.setOperateEmpId(getCurrentUserId());//需要实现 getCurrentUserId 方法
operateLog.setOperateTime(LocalDateTime.now());
operateLog.setClassName(joinPoint.getTarget().getClass().getName());
operateLog.setMethodName(joinPoint.getSignature().getName());
operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
operateLog.setReturnValue(result.toString());
operateLog.setCostTime(costTime);
//插入日志
operateLogMapper.insert(operateLog);
return result;
}
//获取当前用户ID
private int getCurrentUserId(){
return 1;
}
}
在 Controller 层需要记录日志的方法上加上注解 @LogOperation:
java
@Slf4j
@RequestMapping("/depts")
@RestController
public class DeptController{
@Autowired
private DeptService deptService;
//查询部门列表
@GetMapping
public Result list(){
log.info("查询部门列表");
List<Dept> deptList = deptService.findAll();
return Result.success(deptList);
}
//根据ID删除部门
@LogOperation
@DeleteMapping
public Result delete(Integer id){
log.info("根据Id删除部门, id: {}" , id);
deptService.deleteById(id);
return Result.success();
}
//新增部门
@LogOperation
@PostMapping
public Result add(@RequestBody Dept dept){
log.info("新增部门, dept: {}" , dept);
deptService.add(dept);
return Result.success();
}
//根据ID查询部门
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id){
log.info("根据ID查询部门, id: {}" , id);
Dept dept = deptService.getById(id);
return Result.success(dept);
}
//修改部门
@LogOperation
@PutMapping
public Result update(@RequestBody Dept dept){
log.info("修改部门, dept: {}" , dept);
deptService.update(dept);
return Result.success();
}
}
java
@Slf4j//在类中自动生成日志对象
@RequestMapping("/clazzs")
@RestController//标识 RESTful 风格的控制器类
public class ClazzController{
@Autowired
private ClazzService clazzService;
//条件分页查询班级
@GetMapping//绑定 HTTP GET 请求
public Result page(String name,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,//请求参数绑定
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end,
@RequestParam(defaultValue = "1") Integer page,//接收前端传递的 URL 查询参数
@RequestParam(defaultValue = "10") Integer pageSize){
PageResult pageResult = clazzService.page(name,begin,end,page,pageSize);
return Result.success(pageResult);
}
//查询全部班级
@GetMapping("/list")
public Result findAll(){
List<Clazz> clazzList = clazzService.findAll();
return Result.success(clazzList);
}
//新增班级
@LogOperation
@PostMapping
public Result add(@RequestBody Clazz clazz){//将请求体中的json数据自动转换为指定的对象
clazzService.add(clazz);
return Result.success();
}
//根据ID查询班级
@GetMapping("/{id}")
public Result getInfo(@PathVariable Integer id){
Clazz clazz = clazzService.getInfo(id);
return Result.success(clazz);
}
//修改班级
@LogOperation
@PutMapping
public Result update(@RequestBody Clazz clazz){
clazzService.update(clazz);
return Result.success();
}
//删除班级
@LogOperation
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id){
clazzService.deleteById(id);
return Result.success();
}
}
测试操作日志记录功能:

连接点:
在 Spring 中用 JoinPoint 抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint 类型
java
@Around("execution(* org.example.service.DeptService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
String className = joinPoint.getTarget().getClass().getName();//获取目标类名
Signature signature = joinPoint.getSignature();//获取目标方法签名
String methodName = joinPoint.getSignature().getName();//获取目标方法名
Object[] args = joinPoint.getArgs();//获取目标方法运行参数
Object res = joinPoint.proceed();//执行原始方法,获取返回值(环绕通知)
return res;
}
对于其他四种通知,获取连接点信息只能使用 JoinPoint,是 ProceedingJoinPoint 的父类型
java
@Before("execution(* org.example.service.DeptService.*(..))")
public void before(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getName();//获取目标类名
Signature signature = joinPoint.getSignature();//获取目标方法签名
String methodName = joinPoint.getSignature().getName();//获取目标方法名
Object[] args = joinPoint.getArgs();//获取目标方法运行参数
}
获取当前登录员工:
ThreadLocal:
ThreadLocal并不是一个 Thread,而是 Thread 的局部变量
ThreadLocal 为每个线程提供一份单独的存储空间,具有线程隔离的效果,不同的线程之间不会相互干扰

常见方法:
public void set(T value)`` 设置当前线程的线程局部变量的值
public T get()`` 返回当前线程所对应的线程局部变量的值
public void remove()`` 移除当前线程的线程局部变量
记录当前登录员工:

定义 ThreadLocal 操作的工具类,用于操作当前登录员工 ID:
java
package org.example.utils;
public class CurrentHolder{
private static final ThreadLocal<Integer> CURRENT_lOCAL = new ThreadLocal<>();
public static void setCurrentId(Integer employeeId){
CURRENT_lOCAL.set(employeeId);
}
public static Integer getCurrentId(){
return CURRENT_lOCAL.get();
}
public static void remove(){
CURRENT_lOCAL.remove();
}
}
在 TokenFilter 中,解析完当前登录员工 ID,将其存入 ThreadLocal (用完后需将其删除)
java
//令牌校验过滤器
@Slf4j
//@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter{
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;//转换为HTTP请求对象
HttpServletResponse response = (HttpServletResponse) resp;//转换为HTTP响应对象
//获取请求url
String url = request.getRequestURL().toString();
//判断是否是登录请求
if(url.contains("login")){//登录请求
log.info("登录请求 , 放行");
chain.doFilter(request, response);
return;
}
//获取请求头中的令牌(token)
String jwt = request.getHeader("token");
//判断令牌是否存在
if(!StringUtils.hasLength(jwt)){//jwt为空
log.info("获取到jwt令牌为空, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);//设置响应状态码为401
return;
}
//解析token
try{
Claims claims = JwtUtils.parseJWT(jwt);
Integer empId = Integer.valueOf(claims.get("id").toString());
CurrentHolder.setCurrentId(empId);
}catch(Exception e){//解析失败
e.printStackTrace();
log.info("解析令牌失败, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);//设置响应状态码为401
return;
}
//放行
log.info("令牌合法, 放行");
chain.doFilter(request , response);//允许请求继续访问目标接口
//清空当前线程绑定的ID
CurrentHolder.remove();
}
}
在 AOP 程序中,从 ThreadLocal 中获取当前登录员工的 ID:
java
@Aspect
@Component
public class OperationLogAspect{
@Autowired
private OperateLogMapper operateLogMapper;
//环绕通知
@Around("@annotation(log)")
public Object around(ProceedingJoinPoint joinPoint, LogOperation log) throws Throwable{
//记录开始时间
long startTime = System.currentTimeMillis();
//执行方法
Object result = joinPoint.proceed();
//当前时间
long endTime = System.currentTimeMillis();
//耗时
long costTime = endTime - startTime;
//构建日志对象
OperateLog operateLog = new OperateLog();
operateLog.setOperateEmpId(getCurrentUserId());//需要实现 getCurrentUserId 方法
operateLog.setOperateTime(LocalDateTime.now());
operateLog.setClassName(joinPoint.getTarget().getClass().getName());
operateLog.setMethodName(joinPoint.getSignature().getName());
operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
operateLog.setReturnValue(result.toString());
operateLog.setCostTime(costTime);
//插入日志
operateLogMapper.insert(operateLog);
return result;
}
//获取当前用户ID
private int getCurrentUserId(){
return CurrentHolder.getCurrentId();
}
}
在同一个线程/同一个请求中,进行数据共享就可以使用 ThreadLocal