一.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 实际不生效) | public、private(省略则匹配所有) |
| 返回值类型 | 必选,匹配方法返回值类型,* 表示任意返回值 |
void、String、*(任意返回值) |
| 包名 | 必选 ,匹配类所在包,* 匹配单层包,.. 匹配多层包 |
com.example.service、com.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中的setCurrentId和remove是静态方法,可以直接用类名调用CurrentHolder的方法)



