一、SpringAOP基础
主要是用于把通用、横切多个业务方法的公共逻辑,从业务代码剥离出来统一管理
AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),可简单理解为就是面向特定方法编程。
场景:案例中部分业务方法运行较慢 ,定位执行耗时较长的接口,此时需要统计每一个业务方法的执行耗时。
优势:
-
减少重复代码
-
代码无侵入
-
提高开发效率
-
维护方便
提示: AOP是一种思想,而在Spring框架中对这种思想进行的实现,那我们要学习的就是SpringAOP。
1、入门程序
新项目包!!!
将项目包加到project02目录下,并在maven中引入其pom.xml文件
pom.xml中引入AOP的起步依赖,因为当前项目是基于Spring官方骨架创建的项目,该依赖继承了父工程,已经统一管理了依赖的版本
XML
<!-- AOP起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
主包下创建aop包,创建RecordTimeAspect.java文件,编写AOP的程序(公共的逻辑代码)
java
@Slf4j
@Aspect //标识当前是一个AOP类
@Component
public class RecordTimeAspect {
@Around("execution(* com.itheima.service.impl.*.*(..))")//当前的切入点表达式,对service层的所有方法生效
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//传入参数为原始的方法
//1. 记录方法运行的开始时间
long begin = System.currentTimeMillis();
//2. 执行原始的方法
Object result = pjp.proceed();
//3. 记录方法运行的结束时间, 记录耗时
long end = System.currentTimeMillis();
log.info("方法 {} 执行耗时: {}ms", pjp.getSignature() ,end-begin);
return result;
}
}
SpringAOP的应用场景?
-
记录系统的操作日志
-
事务管理
-
权限控制
-
...
2、核心概念
连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用,实际被AOP控制的方法
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
目标对象:Target,通知所应用的对象


控制层AutoWired的业务层对象实际上是一个代理对象,执行通知方法的逻辑,通知方法再执行原始的方法逻辑

二、SpringAOP进阶
1、通知类型
根据通知方法执行时机的不同,将通知类型分为以下常见的五类:
1.@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行(重点)
2.@Before:前置通知,此注解标注的通知方法在目标方法前被执行
3.@After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
4.@AfterReturning: 返回后通知,此注解标注的通知方法在目标方法后被执行 ,有异常不会执行
5.@AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行

注意1: @Around环绕通知需要**自己调用ProceedingJoinPoint.proceed()**来让原始方法执行,其他通知不需要考虑目标方法执行
注意2: @Around环绕通知方法的返回值,必须指定为object,来接收原始方法的返回值。
aop包下新建MyAspect1.java
@PointCut注解的作用是什么?
抽取公共的切点表达式,提高复用性
java
@Slf4j
@Aspect
@Component
public class MyAspect1 {
//公共的切入点表达式
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private 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 ....");
}
}
2、通知顺序
当有多个切面的切入点 都匹配到了目标方法 ,目标方法运行时,多个通知方法都会被执行。
在aop包下创建MyAspect2、MyAspect3、MyAspect4
执行顺序:
-
不同切面类中,默认按照切面类的类名字母排序:
-
目标方法前的通知方法:字母排名靠前的先执行
-
目标方法后的通知方法:字母排名靠前的后执行
-
-
用@Order(数字)加在切面类上来控制顺序
-
目标方法前的通知方法:数字小的先执行
-
目标方法后的通知方法:数字小的后执行
-
java
@Slf4j
@Order(8)
@Component
@Aspect
public class MyAspect2 {
//前置通知
@Before("execution(* com.itheima.service.impl.*.*(..))")
public void before(){
log.info("MyAspect2 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.impl.*.*(..))")
public void after(){
log.info("MyAspect2 -> after ...");
}
}
3、切入点表达式
介绍:描述切入点方法的一种表达式。
作用:用来决定项目中的哪些方法需要加入通知
常见形式:
1.execution(...):根据方法的签名来匹配
2.@annotation(...):根据注解匹配

(1)execution
java
@Before("execution (public void com.itheima.service.impl.DeptServiceImpl.delete (java.lang.Integer))")
public void before(JoinPoint joinPoint){}
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
java
execution(访问修饰符? 返回值包名.类名.? 方法名(方法参数) throws 异常?)
//省略前
execution (public void com.itheima.service.impl.DeptServiceImpl.delete (java.lang.Integer) throws ...)
//省略后
execution (void delete (java.lang.Integer))
其中带?的表示可以省略的部分
-
1.访问修饰符:可省略(比如:public、protected)
-
2.包名.类名:可省略(强烈不建议省略,会增加查找负担)
-
3.throws异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
可以使用通配符描述切入点 1.*:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数 ,也可以通配包、类、方法名的一部分
java
execution(* com.*.service.*.update*(*))
2...:多个连续的任意符号 ,可以通配任意层级的包 ,或任意类型、任意个数的参数
java
execution(* com.itheima..DeptService.*(..))
注意1:根据业务需要,可以使用 与(&&)、或(Il)、非()来组合比较复杂的切入点表达式。
书写建议
a.所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:findXxx,updateXxx。
b.描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
c.在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名尽量不使用.·,使用*匹配单个 包。
各种切入点表达式的省略形式
java
//前置通知
//@Before("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
//@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
//@Before("execution(void delete(java.lang.Integer))") //包名.类名 强烈不建议省略
//@Before("execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
//@Before("execution(* com.*.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
//@Before("execution(* com.itheima.service.impl.*.delete(java.lang.Integer))")
//@Before("execution(* com.itheima.service.impl.*.*(java.lang.Integer))")
//@Before("execution(* com.itheima.service.impl.*.*(*))")
//@Before("execution(* com.itheima.service.impl.*.del*(*))")
//@Before("execution(* com.itheima.service.impl.*.*e(*))")
//@Before("execution(* com..service.impl.DeptServiceImpl.*(..))")
//@Before("execution(* com.itheima.service.*.*(..))")
//匹配list 与 delete 方法
//@Before("execution(* com.itheima.service.impl.DeptServiceImpl.list(..)) ||" +
// "execution(* com.itheima.service.impl.DeptServiceImpl.delete(..))")
(2)@annotation
主包下创建anno包,创建注解接口LogOperation.java
java
@Target(ElementType.METHOD)//用于指定该注解只能用于方法
@Retention(RetentionPolicy.RUNTIME)//用于指定该注解在运行时生效
public @interface LogOperation {
}
切入点表达式这样写
java
@Before("@annotation(com.itheima.anno.LogOperation)")
public void before(){
log.info("MyAspect5 -> before ...");
}
在需要匹配的方法上加上该注解来匹配
java
@LogOperation
@Override
public List<Dept> list() {
// int i = 1/0;
List<Dept> deptList = deptMapper.list();
return deptList;
}
@LogOperation
@Override
public void delete(Integer id) {
deptMapper.delete(id);
}
4、连接点JoinPoint
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint
对于其它四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
java
@Slf4j
@Aspect
@Component
public class MyAspect6 {
@Before("execution(* com.itheima.service.*.*(..))")
public void before(JoinPoint joinPoint){
log.info("before ....");
//1. 获取目标对象
Object target = joinPoint.getTarget();
log.info("获取目标对象: {}", target);
//2. 获取目标类
String className = joinPoint.getTarget().getClass().getName();
log.info("获取目标类: {}", className);
//3. 获取目标方法名称
String methodName = joinPoint.getSignature().getName();
log.info("获取目标方法: {}", methodName);
//4. 获取目标方法参数
Object[] args = joinPoint.getArgs();
log.info("获取目标方法参数: {}", Arrays.toString(args));
}
//环绕通知,和上面调用方法一样
@Around("execution(* com.itheima.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("around ... before ....");
Object result = pjp.proceed();
log.info("around ... after ....");
return result;
}
}
三、SpringAOP案例
1、记录操作日志
在原来的项目中引入数据库表operate_log,
引入实体类OperateLog,
引入数据访问层OperateLogMapper,
引入AOP起步依赖,
引入anno包LogOperation接口根据注解匹配切入点,
引入aop包OperationLogAspect类
2、获取当前登录员工-思路
(1)问题
1.员工登录成功后,哪里存储的有当前登录员工的信息?
给客户端浏览器下发的jwt令牌中
2.如何从jwt令牌中获取到当前登录的员工信息?
获取请求头 中传递的jwt令牌,并解析
3.TokenFilter中已经解析了令牌的信息,如何将其传递给AOP程序、Controller、Service呢?
ThreadLocal
(2)ThreadLocal
ThreadLocal并不是一个Thread,而是Thread的局部变量。
ThreadLocal为每个线程提供一份单独的存储空间 ,具有线程隔离 的效果,不同的线程之间不会相互干扰。

ThreadLocal常用方法:
-
public void set(T value) 设置 当前线程的线程局部变量的值
-
public T get() 返回 当前线程所对应的线程局部变量的值
-
public void remove() 移除 当前线程的线程局部变量
(3)试一试
在test类中新建ThreadLocalTest方法
java
package com.itheima;
public class ThreadLocalTest {
//泛型表示往ThreadLocal中存的都是String类型的数据
private static ThreadLocal<String> local = new ThreadLocal<>();
public static void main(String[] args) {
local.set("Main Mess");//主线程存的只有主线程能取
//创建线程
new Thread(new Runnable() {
@Override
public void run() {
local.set("Child Mess");//子线程存的只有子线程能取
System.out.println(Thread.currentThread().getName() + " " + local.get());
}
}).start();
System.out.println(Thread.currentThread().getName() + " " + local.get());
local.remove();
}
}
3、获取当前登录员工-实现
每一次前后端请求都是一个线程工作,在过滤器中获取到当前员工的id,可以存放在当前线程的本地存储空间中

具体操作步骤:
1.定义ThreadLocal操作的工具类 ,用于操作当前登录员工ID。
2.在TokenFilter中,解析完 当前登录员工ID,将其存入ThreadLocal (用完之后需将其删除)。
3.在AOP程序中,从ThreadLocal中获取当前登录员工的ID。
(1)定义工具类
在utils包下创建CurrentHolder,对员工id进行存值取值删除值
ThreadLocal在这里使用,用于数据共享
java
package com.itheima.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();
}
}
(2)解析、存入、删除
在TokenFilter包中
解析并存入
java
//5. 如果token存在, 校验令牌, 如果校验失败 -> 返回错误信息(响应401状态码)
try {
Claims claims = JwtUtils.parseToken(token);
Integer empId = Integer.valueOf(claims.get("id").toString());//获取当前登录员工的整形id
CurrentHolder.setCurrentId(empId); //存入
log.info("当前登录员工ID: {}, 将其存入ThreadLocal", empId);
} catch (Exception e) {
log.info("令牌非法, 响应401");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
删除
java
//7. 删除ThreadLocal中的数据
CurrentHolder.remove();
(3)获取ID
在OperationLogAspect文件中
java
private Integer getCurrentUserId() {
return CurrentHolder.getCurrentId();
}
(4)小结
1、什么是ThreadLocal
ThreadLocal其实是线程的局部变量 ,为每个线程提供单独一份存储空间 ,具有线程隔离的效果 , 不同的线程之间不会相互干扰
2、ThreadLocal的应用场景
在同一个线程/同一个请求中 ,进行数据共享。
四、注解总结
//SpringAOP //标识当前是一个AOP类 @Aspect
java
//SpringAOP
//标识当前是一个AOP类
@Aspect
//环绕通知,此注解标注的通知方法在目标方法前、后都被执行(重点)
@Around
//前置通知,此注解标注的通知方法在目标方法前被执行
@Before
//后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@After
//返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AterReturning
//异常后通知,此注解标注的通知方法发生异常后执行
@AterThrowing
//抽取公共的切点表达式,提高复用性
@PointCut
//加在切面类上来控制通知顺序
@Order(1)
//匹配方法的注解;execution匹配方法的签名
@annotation