一、SpringAOP基础
主要是用于把通用、横切多个业务方法的公共逻辑,从业务代码剥离出来统一管理
AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),可简单理解为就是面向特定方法编程。
场景:案例中部分业务方法运行较慢,定位执行耗时较长的接口,此时需要统计每一个业务方法的执行耗时。
优势:
减少重复代码
代码无侵入
提高开发效率
维护方便
提示: AOP是一种思想,而在Spring框架中对这种思想进行的实现,那我们要学习的就是SpringAOP。
1、入门程序
新项目包!!!
将项目包加到project02目录下,并在maven中引入其pom.xml文件
pom.xml中引入AOP的起步依赖,因为当前项目是基于Spring官方骨架创建的项目,该依赖继承了父工程,已经统一管理了依赖的版本
<!-- AOP起步依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>主包下创建aop包,创建RecordTimeAspect.java文件,编写AOP的程序(公共的逻辑代码)
@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注解的作用是什么?
抽取公共的切点表达式,提高复用性
@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(数字)加在切面类上来控制顺序
目标方法前的通知方法:数字小的先执行
目标方法后的通知方法:数字小的后执行
@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
@Before("execution (public void com.itheima.service.impl.DeptServiceImpl.delete (java.lang.Integer))") public void before(JoinPoint joinPoint){}execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
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.*:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
execution(* com.*.service.*.update*(*))2...:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
execution(* com.itheima..DeptService.*(..))注意1:根据业务需要,可以使用 与(&&)、或(Il)、非()来组合比较复杂的切入点表达式。
书写建议
a.所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:findXxx,updateXxx。
b.描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
c.在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名尽量不使用.·,使用*匹配单个 包。
各种切入点表达式的省略形式
//前置通知 //@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
@Target(ElementType.METHOD)//用于指定该注解只能用于方法 @Retention(RetentionPolicy.RUNTIME)//用于指定该注解在运行时生效 public @interface LogOperation { }切入点表达式这样写
@Before("@annotation(com.itheima.anno.LogOperation)") public void before(){ log.info("MyAspect5 -> before ..."); }在需要匹配的方法上加上该注解来匹配
@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的父类型
@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方法
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在这里使用,用于数据共享
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包中
解析并存入
//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; }删除
//7. 删除ThreadLocal中的数据 CurrentHolder.remove();(3)获取ID
在OperationLogAspect文件中
private Integer getCurrentUserId() { return CurrentHolder.getCurrentId(); }(4)小结
1、什么是ThreadLocal
ThreadLocal其实是线程的局部变量,为每个线程提供单独一份存储空间,具有线程隔离的效果, 不同的线程之间不会相互干扰
2、ThreadLocal的应用场景
在同一个线程/同一个请求中,进行数据共享。
四、注解总结
//SpringAOP //标识当前是一个AOP类 @Aspect
//SpringAOP //标识当前是一个AOP类 @Aspect //环绕通知,此注解标注的通知方法在目标方法前、后都被执行(重点) @Around //前置通知,此注解标注的通知方法在目标方法前被执行 @Before //后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 @After //返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 @AterReturning //异常后通知,此注解标注的通知方法发生异常后执行 @AterThrowing //抽取公共的切点表达式,提高复用性 @PointCut //加在切面类上来控制通知顺序 @Order(1) //匹配方法的注解;execution匹配方法的签名 @annotation