⭐表示重要。

第一章:案例引入

1.1 需求:实现计算器的加减乘除功能

  • 接口:
  1. package com.github.fairy.era.aop;
  2. /**
  3. * 计算器
  4. *
  5. * @author 许大仙
  6. * @version 1.0
  7. * @since 2021-11-06 09:53
  8. */
  9. public interface Calculator {
  10. int add(int a, int b);
  11. int sub(int a, int b);
  12. int mul(int a, int b);
  13. int div(int a, int b);
  14. }
  • 实现:
  1. package com.github.fairy.era.log.impl;
  2. import com.github.fairy.era.log.Calculator;
  3. /**
  4. * @author 许大仙
  5. * @version 1.0
  6. * @since 2021-11-06 09:57
  7. */
  8. public class CalculatorImpl implements Calculator {
  9. @Override
  10. public int add(int a, int b) {
  11. return a + b;
  12. }
  13. @Override
  14. public int sub(int a, int b) {
  15. return a - b;
  16. }
  17. @Override
  18. public int mul(int a, int b) {
  19. return a * b;
  20. }
  21. @Override
  22. public int div(int a, int b) {
  23. return a / b;
  24. }
  25. }

1.2 功能扩展:在计算结果的前后打印日志

  • 在每个方法计算结果的前后打印日志:
  1. package com.github.fairy.era.log.impl;
  2. import com.github.fairy.era.log.Calculator;
  3. /**
  4. * @author 许大仙
  5. * @version 1.0
  6. * @since 2021-11-06 10:07
  7. */
  8. public class CalculatorImpl2 implements Calculator {
  9. @Override
  10. public int add(int a, int b) {
  11. System.out.println("[日志] add 方法开始了,参数是:" + a + "," + b);
  12. int result = a + b;
  13. System.out.println("[日志] add 方法结束了,结果是:" + result);
  14. return result;
  15. }
  16. @Override
  17. public int sub(int a, int b) {
  18. System.out.println("[日志] sub 方法开始了,参数是:" + a + "," + b);
  19. int result = a - b;
  20. System.out.println("[日志] sub 方法结束了,结果是:" + result);
  21. return result;
  22. }
  23. @Override
  24. public int mul(int a, int b) {
  25. System.out.println("[日志] mul 方法开始了,参数是:" + a + "," + b);
  26. int result = a * b;
  27. System.out.println("[日志] mul 方法结束了,结果是:" + result);
  28. return result;
  29. }
  30. @Override
  31. public int div(int a, int b) {
  32. System.out.println("[日志] div 方法开始了,参数是:" + a + "," + b);
  33. int result = a / b;
  34. System.out.println("[日志] div 方法结束了,结果是:" + result);
  35. return result;
  36. }
  37. }
  • 问题:
  • ① 代码混乱:越来越多的非业务需求(如:日志等)加入后,原有的业务方法急剧膨胀,每个方法在处理核心逻辑的同时还必须兼顾其他的多个关注点。
  • ② 代码分散:只是为了满足这个单一的需求,就不得不在多个模块(方法)里面多次编写重复相同的日志代码。如果日志需求发生改变,必须修改所有的模块。

  • 解决思路:解决这两个问题的核心就是解耦,我们需要将附加功能(比如:日志等)从业务功能代码中抽取出来。

  • 困难:要抽取的代码在方法的内部,靠以前将子类中的重复代码抽取到父类的方式不能解决此类问题,所以必须引入新的技术—代理

1.3 代理

1.3.1 概述

  • 代理模式是 23 种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类 间接 调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出现,以实现 解耦 的目的。
  • 调用目标方法的时候需要先调用代理对象的方法,减少对目标方法的调用和打扰,同时也让附加功能能够集中在一起,有利于统一维护。

代理.png

1.3.2 生活中的代理

  • 广告商找大明星拍广告需要经过经纪人。

  • 合作伙伴找大老板谈合作约见面时间需要经过秘书。

  • 房产中介是买卖双方的代理。
  • ……

1.3.3 相关术语

  • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象和方法。
  • 目标:被代理 "套用" 了非核心逻辑代码的类、对象和方法。

1.3.4 静态代理

  • 要求:需要代理对象和目标对象实现一样的接口。
  • 静态代理的优点:可以在不修改目标对象的前提下扩展目标对象的功能。
  • 静态代理的缺点:

    • ① 冗余:由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
    • ② 不易维护:一旦接口增加方法,目标对象与代理对象都要进行修改。
  • 示例:

  1. package com.github.fairy.era.log;
  2. /**
  3. * 计算器
  4. *
  5. * @author 许大仙
  6. * @version 1.0
  7. * @since 2021-11-06 09:53
  8. */
  9. public interface Calculator {
  10. int add(int a, int b);
  11. int sub(int a, int b);
  12. int mul(int a, int b);
  13. int div(int a, int b);
  14. }
  1. package com.github.fairy.era.log.impl;
  2. import com.github.fairy.era.log.Calculator;
  3. /**
  4. * @author 许大仙
  5. * @version 1.0
  6. * @since 2021-11-06 09:57
  7. */
  8. public class CalculatorImpl implements Calculator {
  9. @Override
  10. public int add(int a, int b) {
  11. return a + b;
  12. }
  13. @Override
  14. public int sub(int a, int b) {
  15. return a - b;
  16. }
  17. @Override
  18. public int mul(int a, int b) {
  19. return a * b;
  20. }
  21. @Override
  22. public int div(int a, int b) {
  23. return a / b;
  24. }
  25. }
  1. package com.github.fairy.era.log.proxy;
  2. import com.github.fairy.era.log.Calculator;
  3. /**
  4. * 静态代理
  5. *
  6. * @author 许大仙
  7. * @version 1.0
  8. * @since 2021-11-06 19:47
  9. */
  10. public class CalculatorProxy implements Calculator {
  11. // 将被代理的目标对象声明为成员变量
  12. private Calculator target;
  13. public CalculatorProxy(Calculator calculator) {
  14. this.target = calculator;
  15. }
  16. @Override
  17. public int add(int a, int b) {
  18. System.out.println("[日志] add 方法开始了,参数是:" + a + "," + b);
  19. int result = target.add(a, b);
  20. System.out.println("[日志] add 方法结束了,结果是:" + result);
  21. return result;
  22. }
  23. @Override
  24. public int sub(int a, int b) {
  25. System.out.println("[日志] sub 方法开始了,参数是:" + a + "," + b);
  26. int result = target.sub(a, b);
  27. System.out.println("[日志] sub 方法结束了,结果是:" + result);
  28. return result;
  29. }
  30. @Override
  31. public int mul(int a, int b) {
  32. System.out.println("[日志] mul 方法开始了,参数是:" + a + "," + b);
  33. int result = target.mul(a, b);
  34. System.out.println("[日志] mul 方法结束了,结果是:" + result);
  35. return result;
  36. }
  37. @Override
  38. public int div(int a, int b) {
  39. System.out.println("[日志] div 方法开始了,参数是:" + a + "," + b);
  40. int result = target.div(a, b);
  41. System.out.println("[日志] div 方法结束了,结果是:" + result);
  42. return result;
  43. }
  44. }
  1. package com.github.fairy.era.log;
  2. import com.github.fairy.era.log.impl.CalculatorImpl;
  3. import com.github.fairy.era.log.proxy.CalculatorProxy;
  4. import org.junit.Test;
  5. /**
  6. * @author 许大仙
  7. * @version 1.0
  8. * @since 2021-11-06 19:50
  9. */
  10. public class CalculatorTest {
  11. @Test
  12. public void test() {
  13. // 创建被代理对象
  14. Calculator calculator = new CalculatorImpl();
  15. // 创建代理对象
  16. Calculator proxy = new CalculatorProxy(calculator);
  17. // 调用代理对象的方法间接实现调用目标对象的方法
  18. int add = proxy.add(2, 3);
  19. System.out.println("add = " + add);
  20. int sub = proxy.sub(2, 3);
  21. System.out.println("sub = " + sub);
  22. int mul = proxy.mul(2, 3);
  23. System.out.println("mul = " + mul);
  24. int div = proxy.div(2, 3);
  25. System.out.println("div = " + div);
  26. }
  27. }
  • 静态代理的确实现了解耦,但是由于代码都写死了,不具备灵活性,就拿上面的日志功能来说,如果其他地方也需要添加日志,那么还需要声明更多的静态代理类,就会产生大量的重复代码(代码会膨胀),日志功能还是分散的,没有统一管理。

  • 我们需要的是将日志功能集中到一个代理类中,将来有任何日志需求,都通过这个代理类来实现,这就需要动态代理来实现。

1.3.5 动态代理

  • 动态代理的方式:
    • ① 基于接口实现动态代理:JDK 动态代理。
    • ② 基于继承实现动态代理:Cglib 、Javassist 动态代理。
  • JDK 动态代理的核心类:

JDK 动态代理的核心类.png

  • JDK 动态代理的核心方法:

JDK 动态代理的核心方法.png

  • JDK 动态代理的处理器接口:

JDK 动态代理的处理器接口.png

  • 示例:
  1. package com.github.fairy.era.log;
  2. /**
  3. * 计算器
  4. *
  5. * @author 许大仙
  6. * @version 1.0
  7. * @since 2021-11-06 09:53
  8. */
  9. public interface Calculator {
  10. int add(int a, int b);
  11. int sub(int a, int b);
  12. int mul(int a, int b);
  13. int div(int a, int b);
  14. }
  1. package com.github.fairy.era.log.impl;
  2. import com.github.fairy.era.log.Calculator;
  3. /**
  4. * @author 许大仙
  5. * @version 1.0
  6. * @since 2021-11-06 09:57
  7. */
  8. public class CalculatorImpl implements Calculator {
  9. @Override
  10. public int add(int a, int b) {
  11. return a + b;
  12. }
  13. @Override
  14. public int sub(int a, int b) {
  15. return a - b;
  16. }
  17. @Override
  18. public int mul(int a, int b) {
  19. return a * b;
  20. }
  21. @Override
  22. public int div(int a, int b) {
  23. return a / b;
  24. }
  25. }
  1. package com.github.fairy.era.log.proxy;
  2. import java.lang.reflect.Proxy;
  3. import java.util.Arrays;
  4. import java.util.List;
  5. /**
  6. * @author 许大仙
  7. * @version 1.0
  8. * @since 2021-11-06 22:02
  9. */
  10. public class LogDynamicProxy<T> {
  11. // 将被代理的目标对象声明为成员变量
  12. private T target;
  13. public LogDynamicProxy(T target) {
  14. this.target = target;
  15. }
  16. /**
  17. * 获取代理对象
  18. *
  19. * @return 代理对象
  20. */
  21. public T getProxy() {
  22. // 获取代理对象的类加载器
  23. ClassLoader classLoader = target.getClass().getClassLoader();
  24. // 获取代理对象实现的接口组成的数组
  25. Class<?>[] interfaces = target.getClass().getInterfaces();
  26. // 返回代理对象
  27. return (T) Proxy.newProxyInstance(classLoader, interfaces, (proxy, method, args) -> {
  28. // 声明一个局部变量,用来存储目标方法的返回值
  29. Object result = null;
  30. // 通过method对象获取方法名
  31. String methodName = method.getName();
  32. // 参数数组
  33. List<Object> argsList = Arrays.asList(args);
  34. try {
  35. // 在目标方法执行前:打印方法开始的日志
  36. System.out.println("[动态代理][日志] " + methodName + " 方法开始了,参数是:" + argsList);
  37. // 调用目标方法:需要传入两个参数
  38. // 参数1:调用目标方法的目标对象
  39. // 参数2:外部调用目标方法时传入的实际参数
  40. // 调用后会返回目标方法的返回值
  41. result = method.invoke(target, args);
  42. // 在目标方法成功后:打印方法成功结束的日志【寿终正寝】
  43. System.out.println("[动态代理][日志] " + methodName + " 方法结束了,返回值是:" + result);
  44. } catch (Exception e) {
  45. // 通过e对象获取异常类型的全类名
  46. String exceptionName = e.getClass().getName();
  47. // 通过e对象获取异常消息
  48. String message = e.getMessage();
  49. // 在目标方法失败后:打印方法抛出异常的日志【死于非命】
  50. System.out.println("[动态代理][日志] " + methodName + " 方法抛异常了,异常信息是:" + exceptionName + "," + message);
  51. } finally {
  52. // 在目标方法最终结束后:打印方法最终结束的日志【盖棺定论】
  53. System.out.println("[动态代理][日志] " + methodName + " 方法最终结束了");
  54. }
  55. // 这里必须将目标方法的返回值返回给外界,如果没有返回,外界将无法拿到目标方法的返回值
  56. return result;
  57. });
  58. }
  59. }
  1. package com.github.fairy.era.log;
  2. import com.github.fairy.era.log.impl.CalculatorImpl;
  3. import com.github.fairy.era.log.proxy.LogDynamicProxy;
  4. import org.junit.Test;
  5. /**
  6. * @author 许大仙
  7. * @version 1.0
  8. * @since 2021-11-06 19:50
  9. */
  10. public class CalculatorTest {
  11. @Test
  12. public void test() {
  13. // 创建被代理对象
  14. Calculator calculator = new CalculatorImpl();
  15. // 创建代理对象
  16. Calculator proxy = new LogDynamicProxy<>(calculator).getProxy();
  17. // 调用代理对象的方法间接实现调用目标对象的方法
  18. int add = proxy.add(2, 3);
  19. System.out.println("add = " + add);
  20. int sub = proxy.sub(2, 3);
  21. System.out.println("sub = " + sub);
  22. int mul = proxy.mul(2, 3);
  23. System.out.println("mul = " + mul);
  24. int div = proxy.div(2, 3);
  25. System.out.println("div = " + div);
  26. }
  27. }

第二章:AOP 概述

2.1 概念(⭐)

  • AOP(Aspect Oriented Programming):面向切面编程,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

2.2 AOP 的核心套路(⭐)

自古深情留不住,唯有套路得人心。一入腐门深似海,从此节操是路人。

AOP的核心套路.png

2.3 AOP 的作用(⭐)

  • ① 简化代码:将方法中固定位置的重复的代码 抽取 出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
  • ② 代码增强:将特定的功能封装到切面类中,看哪里需要,就增强哪里,被织入了切面逻辑的方法就被切面给 增强 了。

2.4 AOP 的术语

AOP术语.png

2.4.1 横切关注点

  • 从每个方法中抽取出来的同一类的非核心业务,在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

注意:横切关注点不是语法层面天然存在的,而是根据附加功能的逻辑上的需要,有 10 个附加功能,就有 10 个横切关注点。

横切关注点.png

2.4.2 通知(⭐)

  • 每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫做通知方法。
  • 前置通知:在被代理的目标方法 执行。
  • 返回通知:在被代理的目标方法 成功结束 后执行(寿终正寝)。
  • 异常通知:在被代理的目标方法 异常结束 后执行(死于非命)。
  • 后置通知:在被代理的目标方法 最终结束 后执行(盖棺定论)。
  • 环绕通知:使用try…catch…finally结构围绕 整个 被代理的目标方法,包括上面四种通知对应的所有位置。

通知.png

2.4.3 切面(⭐)

  • 封装通知方法的类。

切面.png

2.4.4 目标

  • 被代理的目标对象。

2.4.5 代理

  • 向目标对象应用通知之后创建的代理对象。

2.4.6 连接点

  • 这也是一个纯逻辑概念,不是语法定义的。
  • 把方法排成一排,每一个横切位置看成 x 轴方向,把方法从上到下执行的顺序看成 y 轴,x 轴和 y 轴的交叉点就是连接点。

连接点.png

2.4.7 切入点(⭐)

  • 定位连接点的方式。
  • 每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
  • 如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
  • Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
  • 切入点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。