目录
[1 AOP基础](#1 AOP基础)
[1.1 AOP入门程序](#1.1 AOP入门程序)
[1.2 AOP常见场景](#1.2 AOP常见场景)
[1.3 AOP核心概念](#1.3 AOP核心概念)
[2 AOP进阶](#2 AOP进阶)
[2.1 通知类型](#2.1 通知类型)
[2.2 通知顺序](#2.2 通知顺序)
[2.3 切入点表达式](#2.3 切入点表达式)
[2.3.1 execution(主流方法)](#2.3.1 execution(主流方法))
[2.3.2 @annotation(简短)](#2.3.2 @annotation(简短))
[2.4 连接点](#2.4 连接点)
[3 AOP案例](#3 AOP案例)
[3.1 需求分析](#3.1 需求分析)
[3.2 步骤](#3.2 步骤)
[3.3 ThreadLocal](#3.3 ThreadLocal)
[3.4 记录当前登陆员工](#3.4 记录当前登陆员工)
[4 作业--日志信息统计](#4 作业--日志信息统计)
[4.1 完善实体类OperateLog](#4.1 完善实体类OperateLog)
[4.2 SQL语句](#4.2 SQL语句)
[4.3 OperateLogMapper](#4.3 OperateLogMapper)
[4.4 OperateLogController](#4.4 OperateLogController)
[4.5 OperateLogService](#4.5 OperateLogService)
[4.6 OperateLogServiceImpl](#4.6 OperateLogServiceImpl)
[4.7 测试](#4.7 测试)
什么是AOP?
AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。
场景:统计当前这个项目当中每一个业务方法的执行耗时。
AOP的优势主要体现在以下四个方面:
减少重复代码:不需要在业务方法中定义大量的重复性的代码,只需要将重复性的代码抽取到AOP程序中即可。
代码无侵入:在基于AOP实现这些业务功能时,对原有的业务代码是没有任何侵入的,不需要修改任何的业务代码。
提高开发效率
维护方便
AOP是一种思想,在Spring框架中对这种思想进行了实现,我们要学习Spring AOP。
1 AOP基础
1.1 AOP入门程序
需求:统计部门管理各个业务层方法执行耗时。
**原始方式:**在原始的实现方式中,我们需要在业务层的也一个方法执行执行,获取方法运行的开始时间; 然后运行原始的方法逻辑; 最后在每一个方法运行结束时,获取方法运行结束时间,计算执行耗时。
SpringAOP实现步骤:
- 导入依赖:在 pom.xml 文件中导入 AOP 的依赖。
XML
<!-- AOP起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 编写AOP程序:针对于特定方法根据业务需要进行编程。
java
@Slf4j
@Aspect //标识当前是一个切面类AOP
@Component
public class RecordTimeAspect {
/**
* 定义切点
*
* @param pjp
* @return
* @throws Throwable
*/
@Around("execution(* com.itheima.service.impl.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
// 1.记录方法运行的开始时间
long begin = System.currentTimeMillis();
// 2. 执行原始方法
Object result = pjp.proceed();
// 3. 记录方法运行结束的时间,计算方法运行耗时
long end = System.currentTimeMillis();
log.info("方法 {} 运行耗时:{} 毫秒", pjp.getSignature(), (end - begin));
return result;
}
}

1.2 AOP常见场景
AOP常见的应用场景如下:
-
记录系统的操作日志。
-
权限控制。
-
事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务。
1.3 AOP核心概念
-
连接点:JoinPoint ,可以被AOP控制的方法(暗含方法执行时的相关信息)

-
通知:Advice ,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

- 切入点: PointCut ,匹配连接点的条件,通知仅会在切入点方法执行时被应用。

- 切面:Aspect ,描述通知与切入点的对应关系(通知+切入点)

- 目标对象:Target ,通知所应用的对象

Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。

2 AOP进阶
2.1 通知类型
|-----------------|--------------------------------------|
| Spring AOP 通知类型 ||
| @Around ❤ | 环绕通知,此注解标注的通知方法在目标方法前、后都被执行 |
| @Before | 前置通知,此注解标注的通知方法在目标方法前被执行 |
| @After | 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 |
| @AfterReturning | 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 |
| @AfterThrowing | 异常后通知,此注解标注的通知方法发生异常后执行 |
在使用@Around环绕通知时的注意事项:
-
@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
-
@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
java
@Slf4j
@Aspect
@Component
public class MyAspect1 {
/**
* 切入点方法
*/
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
public void pt() {
}
/**
* 前置通知 - 目标方法执行之前执行
*/
@Before("pt()")
public void before() {
log.info("before ...");
}
/**
* 环绕通知 - 目标方法执行之前和之后执行
*/
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("around before ...");
Object result = pjp.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 ...");
}
}
Spring提供了**@PointCut注解:**抽取公共的切入点表达式,提高复用性。
2.2 通知顺序
通过以上程序运行可以看出在不同切面类中,默认按照切面类的类名字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行

控制通知的执行顺序:@Order
**@Order(2) :**切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
2.3 切入点表达式
切入点表达式:描述切入点方法的一种表达式
-
作用:主要用来决定项目中的哪些方法需要加入通知。
-
常见形式:
-
execution(......):根据方法的签名来匹配。
-
@annotation(......) :根据注解匹配。
-
2.3.1 execution(主流方法)
java
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带 ?的表示可以省略的部分
-
访问修饰符:可省略(比如: public、protected)
-
包名.类名: 可省略(不建议省略)
-
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
通配符描述切入点
-
*:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分。 -
..:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数。
完整版:
java
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
通配符+省略:
java
@Before("execution(* com.itheima.service.impl.*.*(..))")
匹配DeptServiceImpl类中以find开头的方法
java
execution(* com.itheima.service.impl.DeptServiceImpl.find*(..))
因此,建议所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:findXxx,updateXxx。
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名尽量不使用..,使用 * 匹配单个包,提高效率。
2.3.2 @annotation(简短)
自定义注解:LogOperation(com.itheima.anno)
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}
在需要的地方加上自定义注解
java
@LogOperation //自定义注解(表示:当前方法属于目标方法)
切面类
java
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知
//前置通知
@Before("@annotation(com.itheima.anno.LogOperation)")
public void before(){
log.info("MyAspect6 -> before ...");
}
//后置通知
@After("@annotation(com.itheima.anno.LogOperation)")
public void after(){
log.info("MyAspect6 -> after ...");
}
}
execution切入点表达式
- 根据我们所指定的方法的描述信息来匹配切入点方法,也是最为常用的一种方式。
- 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐。
annotation 切入点表达式
- 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了。
2.4 连接点
连接点可以简单理解为:可以被AOP控制的方法。
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
- 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型。

- 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型。

3 AOP案例
3.1 需求分析
将案例(Tlias智能学习辅助系统)中增、删、改相关接口的操作日志记录到数据库表中。
操作日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长。
问题一:采用哪种通知类型?计算耗时 -> @Around环绕通知。
问题二:切入点表达式怎么写?方法运行时参数、返回值 -> 匹配controller层的方法。匹配增save、删delete、改update,使用execution切入点表达式也可以,但是会比较繁琐,需要 " || " 三个。 此处可以使用 @annotation切入点表达式。
3.2 步骤
自定义注解

java
@Target(ElementType.METHOD) // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface Log {
}
实现代码

java
@Slf4j
@Aspect
@Component
public class OperationLogAspect {
// 注入Mapper对象
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.itheima.anno.Log)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 开始记录日志
System.out.println("开始记录日志...");
long startTime = System.currentTimeMillis();
// 执行原始方法
Object result = pjp.proceed();
// 记录日志耗时
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 构建日志对象
OperateLog olog = new OperateLog();
olog.setOperateEmpId(1);
olog.setOperateTime(LocalDateTime.now());
olog.setClassName(pjp.getTarget().getClass().getName());
olog.setMethodName(pjp.getSignature().getName());
olog.setMethodParams(Arrays.toString(pjp.getArgs()));
olog.setReturnValue(result == null ? "void" : result.toString());
olog.setCostTime(costTime);
// 保存日志
log.info("记录操作日志: {}", olog);
// 调用Mapper对象保存日志
operateLogMapper.insert(olog);
// 返回结果
System.out.println("结束记录日志...");
return result;
}
}

测试

之前员工id是写死的,此处如何实现【获取操作员工id】呢?
获取请求头中传递的jwt 令牌,解析需要的参数。
TokenFilter 中已经解析了令牌的信息,如何传递给AOP程序、Controller、Service呢?ThreadLocal 。
3.3 ThreadLocal
-
ThreadLocal并不是一个Thread,而是Thread的局部变量。
-
ThreadLocal为每个线程提供一份单独的存储空间,具有线程隔离的效果,不同的线程之间不会相互干扰。
-
常见方法:
public void set(T value) 设置当前线程的线程局部变量的值。
public T get() 返回当前线程所对应的线程局部变量的值。
public void remove() 移除当前线程的线程局部变量。
在同一个线程/同一个请求中,进行数据共享就可以使用 ThreadLocal。
3.4 记录当前登陆员工

定义ThreadLocal操作的工具类,用于操作当前登录员工ID。
在 com.itheima.utils 引入工具类 CurrentHolder
java
package com.itheima.util;
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
//5.如果token存在,则解析token,如果校验失败,则返回错误信息401
try {
Claims claims = JwtUtils.parseToken(token);
Integer empId = (Integer) claims.get("id");
CurrentHolder.setCurrentId(empId); //存入, 用完后要删除
log.info("当前用户id: {},将其存入ThreadLocal", empId);
} catch (Exception e) {
log.info("令牌解析失败,响应401");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
//6.校验通过,则放行
log.info("令牌解析成功,放行");
filterChain.doFilter(request,response);
//7.用完后删除ThreadLocal中的数据
CurrentHolder.remove();
在AOP程序中,从ThreadLocal中获取当前登录员工的ID。
java
private Integer getCurrentUserId() {
return CurrentHolder.getCurrentId();
}
测试

4 作业--日志信息统计
4.1 完善实体类OperateLog
java
@Data
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; //操作耗时
//封装非数据库字段:操作人姓名(关联emp表)
private String operateEmpName;
}
4.2 SQL语句
sql
select ol.id, operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time, e.name
from operate_log ol
left join emp e on ol.operate_emp_id = e.id
4.3 OperateLogMapper
java
@Mapper
public interface OperateLogMapper {
//查询所有日志数据
@Select("select ol.id, operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time, e.name operate_emp_name " +
"from operate_log ol left join emp e on ol.operate_emp_id = e.id")
List<OperateLog> page(OperateLogQueryParam operateLogQueryParam);
}
4.4 OperateLogController
java
@Slf4j
@RequestMapping("/log/page")
@RestController
public class OperateLogController {
@Autowired
private OperateLogService operateLogService;
@GetMapping
public Result page(OperateLogQueryParam operateLogQueryParam) {
log.info("分页查询:{}", operateLogQueryParam);
PageResult<OperateLog> operateLogList = operateLogService.page(operateLogQueryParam);
return Result.success(operateLogList);
}
}
4.5 OperateLogService
java
public interface OperateLogService {
/**
* 分页查询操作日志
*
* @param operateLogQueryParam
* @return
*/
PageResult<OperateLog> page(OperateLogQueryParam operateLogQueryParam);
}
4.6 OperateLogServiceImpl
java
@Service
public class OperateLogServiceImpl implements OperateLogService {
@Autowired
private OperateLogMapper operateLogMapper;
@Override
public PageResult<OperateLog> page(OperateLogQueryParam operateLogQueryParam) {
// 1. 设置分页参数
PageHelper.startPage(operateLogQueryParam.getPage(), operateLogQueryParam.getPageSize());
// 2. 执行查询
List<OperateLog> operateLogList = operateLogMapper.page(operateLogQueryParam);
// 3. 解析查询结果并封装
Page<OperateLog> p = (Page<OperateLog>) operateLogList;
return new PageResult<OperateLog>(p.getTotal(), p.getResult());
}
}
4.7 测试
/log/page?page=1&pageSize=10

