后端Web进阶(AOP)

一.AOP基础

AOP的优势主要体现在以下四个方面:

  • 减少重复代码:不需要在业务方法中定义大量的重复性的代码,只需要将重复性的代码抽取到AOP程序中即可。

  • 代码无侵入:在基于AOP实现这些业务功能时,对原有的业务代码是没有任何侵入的,不需要修改任何的业务代码。

  • 提高开发效率

  • 维护方便

1.AOP入门

(1)导入依赖:在 pom.xml 文件中导入 AOP 的依赖

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

(2)编写AOP程序:针对于特定方法根据业务需要进行编程

如果想知道一系列操作中具体哪个方法的耗时:


AOP常见的应用场景如下:

  • 记录系统的操作日志

  • 权限控制

  • 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务

2.AOP核心概念

连接点不一定是切入点,切入点一定是连接点


AOP的执行流程:------>AOP的底层就是动态代理技术

二.AOP进阶

1.通知类型

程序没有发生异常的情况下,@AfterThrowing标识的通知方法不会执行。

修改DeptServiceImpl业务实现类中的代码, 添加异常

程序发生异常的情况下:

  • @AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了

  • @Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了 (因为原始方法调用已经出异常了)


我们发现每一个注解里面都指定了切入点表达式,而且这些切入点表达式都一模一样。

怎么来解决这个切入点表达式重复的问题? 答案就是:抽取

++@PointCut++

在引用的时候,具体的语法为:

java 复制代码
@Slf4j
@Component
@Aspect
public class MyAspect2 {
    //引用MyAspect1切面类中的切入点表达式
    @Before("com.itheima.aspect.MyAspect1.pt()")
    public void before(){
        log.info("MyAspect2 -> before ...");
    }
}

2.通知顺序(可通过@Order来控制)


控制通知的执行顺序--@Order

3.切入点表达式

(1)execution

组成部分 说明 通配符 / 示例
访问修饰符 可选,如 public/private/protected,省略则匹配所有修饰符(private 实际不生效) publicprivate(省略则匹配所有)
返回值类型 必选,匹配方法返回值类型,* 表示任意返回值 voidString*(任意返回值)
包名 必选 ,匹配类所在包,* 匹配单层包,.. 匹配多层包 com.example.servicecom.example..(com.example 下所有子包)
类名 必选,匹配目标类,* 匹配任意类 EmpService、++*Service(以 Service 结尾的类)++ 、*(任意类)
方法名 必选,匹配目标方法,* 匹配任意方法 login、++get*(以 get 开头的方法)++ 、*(任意方法)
参数类型 必选,() 无参,(..) 任意参数,(String) 匹配单个 String 参数 ()(..)(String, Integer)
异常类型 可选,匹配方法抛出的异常,如throws Exception 极少使用,一般省略

切入点表达式示例

  • 省略方法的修饰符号

execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))

  • 使用*代替返回值类型

execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))

  • 使用*代替包名(一层包使用一个*

execution(* com.itheima.*.*.DeptServiceImpl.delete(java.lang.Integer))

  • 使用..省略包名

execution(* com..DeptServiceImpl.delete(java.lang.Integer))

  • 使用*代替类名

execution(* com..*.delete(java.lang.Integer))

  • 使用*代替方法名

execution(* com..*.*(java.lang.Integer))

  • 使用 * 代替参数

execution(* com.itheima.service.impl.DeptServiceImpl.delete(*))

  • 使用..省略参数

execution(* com..*.*(..))


可以使用逻辑运算符:

切入点表达式书写建议:

  • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:findXxx,updateXxx。

  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。

  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名尽量不使用..,使用 * 匹配单个包。

(2)@annotation(@LogOPeration(业务方法),@Target @Retention(切面类))

(1)建包,anno包下,专门存放注解


  • execution切入点表达式

    • 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式

    • 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐

  • annotation 切入点表达式

    • 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

4.连接点(JoinPoint)

三.AOP案例

1.分析

  • 问题1:项目当中增删改相关的方法是不是有很多?

    • 很多
  • 问题2:我们需要针对每一个功能接口方法进行修改,在每一个功能接口当中都来记录这些操作日志吗?

    • 这种做法比较繁琐

以上两个问题的解决方案:可以使用AOP解决(每一个增删改功能接口中要实现的记录操作日志的逻辑代码是相同)。

可以把这部分记录操作日志的通用的、重复性的逻辑代码抽取出来定义在一个通知方法当中,我们通过AOP面向切面编程的方式,在不改动原始功能的基础上来对原始的功能进行增强。目前我们所增强的功能就是来记录操作日志,所以也可以使用AOP的技术来实现。使用AOP的技术来实现也是最为简单,最为方便的。

  • 问题3:既然要基于AOP面向切面编程的方式来完成的功能,那么我们要使用 AOP五种通知类型当中的哪种通知类型?

    • 答案:环绕通知 @Around。因为所记录的操作日志当中包括:操作人、操作时间,访问的是哪个类、哪个方法、方法运行时参数、方法的返回值、方法的运行时长。方法返回值,是在原始方法执行后才能获取到的。方法的运行时长,需要原始方法运行之前记录开始时间,原始方法运行之后记录结束时间。通过计算获得方法的执行耗时。基于以上的分析我们确定要使用Around环绕通知。
  • 问题4:最后一个问题,切入点表达式我们该怎么写?

    • 答案:使用**@annotation** 来描述切入点表达式。要匹配业务接口当中所有的增删改的方法,而增删改方法在命名上没有共同的前缀或后缀。此时如果使用execution切入点表达式也可以,但是会比较繁琐。 当遇到增删改的方法名没有规律时,就可以使用 @annotation切入点表达式

2.代码实现

实体类:

Mapper接口:

自定义注解 @LogOperation:

定义AOP记录日志的切面类:

3.获取当前登录员工

(1)ThreadLocal

使用完ThreadLocal后,必须调用remove()清理变量副本 (尤其是线程池场景,线程复用会导致变量残留)---->避免内存泄漏

java 复制代码
public class ThreadLocalDemo {
    // 定义ThreadLocal变量(泛型指定变量类型)
    private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();

    public static void main(String[] args) {
        // 线程1:设置并获取自己的副本
        new Thread(() -> {
            USER_ID.set("user-1001");
            System.out.println("线程1的userID:" + USER_ID.get()); // 输出user-1001
            USER_ID.remove(); // 用完及时清理
        }).start();

        // 线程2:设置并获取自己的副本
        new Thread(() -> {
            USER_ID.set("user-1002");
            System.out.println("线程2的userID:" + USER_ID.get()); // 输出user-1002
            USER_ID.remove();
        }).start();
    }
}
java 复制代码
public class ThreadLocalNestedDemo {
    private static final ThreadLocal<String> DATA = new ThreadLocal<>();

    public static void main(String[] args) {
        // 父线程设置变量
        DATA.set("父线程数据");
        
        // 子线程尝试获取
        new Thread(() -> {
            System.out.println("子线程获取:" + DATA.get()); // 输出null
        }).start();
    }
}

如果想让子线程继承父线程的ThreadLocal变量副本

java 复制代码
public class InheritableThreadLocalDemo {
    // 改用InheritableThreadLocal
    private static final InheritableThreadLocal<String> DATA = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        DATA.set("父线程数据");
        
        new Thread(() -> {
            System.out.println("子线程获取:" + DATA.get()); // 输出"父线程数据"
        }).start();
    }
}

(2)记录当前登录员工

前端发起请求,请求到达服务器后,服务器需要接收这次请求,接收到这次请求之后,就会从++线程池中获取一个线程++ 来处理这个请求直到请求处理完毕,这就意味着++一次请求对应着一个线程++,每一次请求都会有一个独立的线程来处理这次请求

(CurrentHolder中的setCurrentIdremove静态方法,可以直接用类名调用CurrentHolder的方法

相关推荐
特拉熊1 小时前
23种设计模式之桥接模式
java·架构
麻辣烫不加辣1 小时前
跑批调额系统说明文档
java·后端
raoxiaoya1 小时前
ADK-Go:Golang开发AI Agent
开发语言·人工智能·golang
一只乔哇噻1 小时前
java后端工程师+AI大模型开发进修ing(研一版‖day61)
java·开发语言·学习·算法·语言模型
拾忆,想起1 小时前
Dubbo服务降级全攻略:构建韧性微服务系统的守护盾
java·前端·网络·微服务·架构·dubbo
我爱学习_zwj1 小时前
Node.js模块管理:CommonJS vs ESModules
开发语言·前端·javascript
zlpzlpzyd1 小时前
jetbrains系工具idea和webstorm默认编辑器设置
java·intellij-idea·webstorm
@YDWLCloud1 小时前
谷歌云 Compute Engine 实操手册:虚拟机配置与负载均衡全流程
java·运维·服务器·云计算·负载均衡·googlecloud
ldmd2841 小时前
Go语言实战:入门篇-6:锁、测试、反射和低级编程
开发语言·后端·golang