第9章 后端Web进阶(AOP)
发布时间:2026/10/7 9:52:23 锦皓数字建站
`)
一、SpringAOP基础主要是用于把通用、横切多个业务方法的公共逻辑从业务代码剥离出来统一管理AOPAspect Oriented Programming面向切面编程、面向方面编程可简单理解为就是面向特定方法编程。场景案例中部分业务方法运行较慢定位执行耗时较长的接口此时需要统计每一个业务方法的执行耗时。优势:减少重复代码代码无侵入提高开发效率维护方便提示: AOP是一种思想而在Spring框架中对这种思想进行的实现那我们要学习的就是SpringAOP。1、入门程序新项目包将项目包加到project02目录下并在maven中引入其pom.xml文件pom.xml中引入AOP的起步依赖因为当前项目是基于Spring官方骨架创建的项目该依赖继承了父工程已经统一管理了依赖的版本!-- AOP起步依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-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.javaPointCut注解的作用是什么抽取公共的切点表达式提高复用性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(...根据注解匹配1executionBefore(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、protected2包名.类名可省略强烈不建议省略会增加查找负担3throws异常可省略注意是方法上声明抛出的异常不是实际抛出的异常可以使用通配符描述切入点 1*单个独立的任意符号可以通配任意返回值、包名、类名、方法名、任意类型的一个参数也可以通配包、类、方法名的一部分execution(* com.*.service.*.update*(*))2..多个连续的任意符号可以通配任意层级的包或任意类型、任意个数的参数execution(* com.itheima..DeptService.*(..))注意1:根据业务需要可以使用 与、或Il、非来组合比较复杂的切入点表达式。书写建议a.所有业务方法名在命名时尽量规范方便切入点表达式快速匹配。如findXxxupdateXxx。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(..)))2annotation主包下创建anno包创建注解接口LogOperation.javaTarget(ElementType.METHOD)//用于指定该注解只能用于方法 Retention(RetentionPolicy.RUNTIME)//用于指定该注解在运行时生效 public interface LogOperation { }切入点表达式这样写Before(annotation(com.itheima.anno.LogOperation)) public void before(){ log.info(MyAspect5 - before ...); }在需要匹配的方法上加上该注解来匹配LogOperation Override public ListDept list() { // int i 1/0; ListDept 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呢?ThreadLocal2ThreadLocalThreadLocal并不是一个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 ThreadLocalString 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 ThreadLocalInteger 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、什么是ThreadLocalThreadLocal其实是线程的局部变量为每个线程提供单独一份存储空间具有线程隔离的效果 不同的线程之间不会相互干扰2、ThreadLocal的应用场景在同一个线程/同一个请求中进行数据共享。四、注解总结//SpringAOP //标识当前是一个AOP类 Aspect//SpringAOP //标识当前是一个AOP类 Aspect //环绕通知此注解标注的通知方法在目标方法前、后都被执行重点 Around //前置通知此注解标注的通知方法在目标方法前被执行 Before //后置通知此注解标注的通知方法在目标方法后被执行无论是否有异常都会执行 After //返回后通知此注解标注的通知方法在目标方法后被执行有异常不会执行 AterReturning //异常后通知此注解标注的通知方法发生异常后执行 AterThrowing //抽取公共的切点表达式提高复用性 PointCut //加在切面类上来控制通知顺序 Order(1) //匹配方法的注解execution匹配方法的签名 annotation
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。