第9章 后端Web进阶(AOP)

一、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
相关推荐
软萌萌的11 小时前
Python零基础从入门到精通详细教程-数据类型
开发语言·windows·python
东北赵四1 小时前
关于Java泛型的知识点及其相关面试题
java·windows·python
姓蔡小朋友1 小时前
Java线程并发
java·开发语言·python
执笔画流年呀2 小时前
⾃动化测试常⽤函数
java·dubbo
我星期八休息2 小时前
扩展— TCP 全连接队列与 tcpdump 抓包
linux·服务器·开发语言·前端·网络·tcp/ip·tcpdump
疯狂打码的少年2 小时前
【面向对象】UML概述(基本构造块:事物/关系/图)
java
小小代码狗2 小时前
PHP 常用函数与变量参考文档
开发语言·安全·php
一水2 小时前
TTFT优化:一个方案的5次推倒重来
java·ai·状态模式
编程阿峰3 小时前
Python 环境配置(二)安装jupyter、matplotlib、numpy库
开发语言·python·jupyter·numpy·matplotlib