⭐表示重要。
第一章:案例引入
1.1 需求:实现计算器的加减乘除功能
- 接口:
package com.github.fairy.era.aop;
/**
* 计算器
*
* @author 许大仙
* @version 1.0
* @since 2021-11-06 09:53
*/
public interface Calculator {
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
}
- 实现:
package com.github.fairy.era.log.impl;
import com.github.fairy.era.log.Calculator;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-06 09:57
*/
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int sub(int a, int b) {
return a - b;
}
@Override
public int mul(int a, int b) {
return a * b;
}
@Override
public int div(int a, int b) {
return a / b;
}
}
1.2 功能扩展:在计算结果的前后打印日志
- 在每个方法计算结果的前后打印日志:
package com.github.fairy.era.log.impl;
import com.github.fairy.era.log.Calculator;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-06 10:07
*/
public class CalculatorImpl2 implements Calculator {
@Override
public int add(int a, int b) {
System.out.println("[日志] add 方法开始了,参数是:" + a + "," + b);
int result = a + b;
System.out.println("[日志] add 方法结束了,结果是:" + result);
return result;
}
@Override
public int sub(int a, int b) {
System.out.println("[日志] sub 方法开始了,参数是:" + a + "," + b);
int result = a - b;
System.out.println("[日志] sub 方法结束了,结果是:" + result);
return result;
}
@Override
public int mul(int a, int b) {
System.out.println("[日志] mul 方法开始了,参数是:" + a + "," + b);
int result = a * b;
System.out.println("[日志] mul 方法结束了,结果是:" + result);
return result;
}
@Override
public int div(int a, int b) {
System.out.println("[日志] div 方法开始了,参数是:" + a + "," + b);
int result = a / b;
System.out.println("[日志] div 方法结束了,结果是:" + result);
return result;
}
}
- 问题:
- ① 代码混乱:越来越多的非业务需求(如:日志等)加入后,原有的业务方法急剧膨胀,每个方法在处理核心逻辑的同时还必须兼顾其他的多个关注点。
② 代码分散:只是为了满足这个单一的需求,就不得不在多个模块(方法)里面多次编写重复相同的日志代码。如果日志需求发生改变,必须修改所有的模块。
解决思路:解决这两个问题的核心就是
解耦
,我们需要将附加功能(比如:日志等)从业务功能代码中抽取出来。困难:要抽取的代码在方法的内部,靠以前将子类中的重复代码抽取到父类的方式不能解决此类问题,所以必须引入新的技术—
代理
。
1.3 代理
1.3.1 概述
- 代理模式是 23 种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类
间接
调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出现,以实现解耦
的目的。 - 调用目标方法的时候需要先调用代理对象的方法,减少对目标方法的调用和打扰,同时也让附加功能能够集中在一起,有利于统一维护。
1.3.2 生活中的代理
广告商找大明星拍广告需要经过经纪人。
合作伙伴找大老板谈合作约见面时间需要经过秘书。
- 房产中介是买卖双方的代理。
- ……
1.3.3 相关术语
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象和方法。
- 目标:被代理
"套用"
了非核心逻辑代码的类、对象和方法。
1.3.4 静态代理
- 要求:需要代理对象和目标对象实现一样的接口。
- 静态代理的优点:可以在不修改目标对象的前提下扩展目标对象的功能。
静态代理的缺点:
- ① 冗余:由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
- ② 不易维护:一旦接口增加方法,目标对象与代理对象都要进行修改。
示例:
package com.github.fairy.era.log;
/**
* 计算器
*
* @author 许大仙
* @version 1.0
* @since 2021-11-06 09:53
*/
public interface Calculator {
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
}
package com.github.fairy.era.log.impl;
import com.github.fairy.era.log.Calculator;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-06 09:57
*/
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int sub(int a, int b) {
return a - b;
}
@Override
public int mul(int a, int b) {
return a * b;
}
@Override
public int div(int a, int b) {
return a / b;
}
}
package com.github.fairy.era.log.proxy;
import com.github.fairy.era.log.Calculator;
/**
* 静态代理
*
* @author 许大仙
* @version 1.0
* @since 2021-11-06 19:47
*/
public class CalculatorProxy implements Calculator {
// 将被代理的目标对象声明为成员变量
private Calculator target;
public CalculatorProxy(Calculator calculator) {
this.target = calculator;
}
@Override
public int add(int a, int b) {
System.out.println("[日志] add 方法开始了,参数是:" + a + "," + b);
int result = target.add(a, b);
System.out.println("[日志] add 方法结束了,结果是:" + result);
return result;
}
@Override
public int sub(int a, int b) {
System.out.println("[日志] sub 方法开始了,参数是:" + a + "," + b);
int result = target.sub(a, b);
System.out.println("[日志] sub 方法结束了,结果是:" + result);
return result;
}
@Override
public int mul(int a, int b) {
System.out.println("[日志] mul 方法开始了,参数是:" + a + "," + b);
int result = target.mul(a, b);
System.out.println("[日志] mul 方法结束了,结果是:" + result);
return result;
}
@Override
public int div(int a, int b) {
System.out.println("[日志] div 方法开始了,参数是:" + a + "," + b);
int result = target.div(a, b);
System.out.println("[日志] div 方法结束了,结果是:" + result);
return result;
}
}
package com.github.fairy.era.log;
import com.github.fairy.era.log.impl.CalculatorImpl;
import com.github.fairy.era.log.proxy.CalculatorProxy;
import org.junit.Test;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-06 19:50
*/
public class CalculatorTest {
@Test
public void test() {
// 创建被代理对象
Calculator calculator = new CalculatorImpl();
// 创建代理对象
Calculator proxy = new CalculatorProxy(calculator);
// 调用代理对象的方法间接实现调用目标对象的方法
int add = proxy.add(2, 3);
System.out.println("add = " + add);
int sub = proxy.sub(2, 3);
System.out.println("sub = " + sub);
int mul = proxy.mul(2, 3);
System.out.println("mul = " + mul);
int div = proxy.div(2, 3);
System.out.println("div = " + div);
}
}
静态代理的确实现了解耦,但是由于代码都写死了,不具备灵活性,就拿上面的日志功能来说,如果其他地方也需要添加日志,那么还需要声明更多的静态代理类,就会产生大量的重复代码(代码会膨胀),日志功能还是分散的,没有统一管理。
我们需要的是将日志功能集中到一个代理类中,将来有任何日志需求,都通过这个代理类来实现,这就需要动态代理来实现。
1.3.5 动态代理
- 动态代理的方式:
- ① 基于接口实现动态代理:JDK 动态代理。
- ② 基于继承实现动态代理:Cglib 、Javassist 动态代理。
- JDK 动态代理的核心类:
- JDK 动态代理的核心方法:
- JDK 动态代理的处理器接口:
- 示例:
package com.github.fairy.era.log;
/**
* 计算器
*
* @author 许大仙
* @version 1.0
* @since 2021-11-06 09:53
*/
public interface Calculator {
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
}
package com.github.fairy.era.log.impl;
import com.github.fairy.era.log.Calculator;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-06 09:57
*/
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int sub(int a, int b) {
return a - b;
}
@Override
public int mul(int a, int b) {
return a * b;
}
@Override
public int div(int a, int b) {
return a / b;
}
}
package com.github.fairy.era.log.proxy;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-06 22:02
*/
public class LogDynamicProxy<T> {
// 将被代理的目标对象声明为成员变量
private T target;
public LogDynamicProxy(T target) {
this.target = target;
}
/**
* 获取代理对象
*
* @return 代理对象
*/
public T getProxy() {
// 获取代理对象的类加载器
ClassLoader classLoader = target.getClass().getClassLoader();
// 获取代理对象实现的接口组成的数组
Class<?>[] interfaces = target.getClass().getInterfaces();
// 返回代理对象
return (T) Proxy.newProxyInstance(classLoader, interfaces, (proxy, method, args) -> {
// 声明一个局部变量,用来存储目标方法的返回值
Object result = null;
// 通过method对象获取方法名
String methodName = method.getName();
// 参数数组
List<Object> argsList = Arrays.asList(args);
try {
// 在目标方法执行前:打印方法开始的日志
System.out.println("[动态代理][日志] " + methodName + " 方法开始了,参数是:" + argsList);
// 调用目标方法:需要传入两个参数
// 参数1:调用目标方法的目标对象
// 参数2:外部调用目标方法时传入的实际参数
// 调用后会返回目标方法的返回值
result = method.invoke(target, args);
// 在目标方法成功后:打印方法成功结束的日志【寿终正寝】
System.out.println("[动态代理][日志] " + methodName + " 方法结束了,返回值是:" + result);
} catch (Exception e) {
// 通过e对象获取异常类型的全类名
String exceptionName = e.getClass().getName();
// 通过e对象获取异常消息
String message = e.getMessage();
// 在目标方法失败后:打印方法抛出异常的日志【死于非命】
System.out.println("[动态代理][日志] " + methodName + " 方法抛异常了,异常信息是:" + exceptionName + "," + message);
} finally {
// 在目标方法最终结束后:打印方法最终结束的日志【盖棺定论】
System.out.println("[动态代理][日志] " + methodName + " 方法最终结束了");
}
// 这里必须将目标方法的返回值返回给外界,如果没有返回,外界将无法拿到目标方法的返回值
return result;
});
}
}
package com.github.fairy.era.log;
import com.github.fairy.era.log.impl.CalculatorImpl;
import com.github.fairy.era.log.proxy.LogDynamicProxy;
import org.junit.Test;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-06 19:50
*/
public class CalculatorTest {
@Test
public void test() {
// 创建被代理对象
Calculator calculator = new CalculatorImpl();
// 创建代理对象
Calculator proxy = new LogDynamicProxy<>(calculator).getProxy();
// 调用代理对象的方法间接实现调用目标对象的方法
int add = proxy.add(2, 3);
System.out.println("add = " + add);
int sub = proxy.sub(2, 3);
System.out.println("sub = " + sub);
int mul = proxy.mul(2, 3);
System.out.println("mul = " + mul);
int div = proxy.div(2, 3);
System.out.println("div = " + div);
}
}
第二章:AOP 概述
2.1 概念(⭐)
- AOP(
A
spectO
rientedP
rogramming):面向切面编程,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
2.2 AOP 的核心套路(⭐)
自古深情留不住,唯有套路得人心。一入腐门深似海,从此节操是路人。
2.3 AOP 的作用(⭐)
- ① 简化代码:将方法中固定位置的重复的代码
抽取
出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。 - ② 代码增强:将特定的功能封装到切面类中,看哪里需要,就增强哪里,被织入了切面逻辑的方法就被切面给
增强
了。
2.4 AOP 的术语
2.4.1 横切关注点
- 从每个方法中抽取出来的同一类的非核心业务,在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
注意:横切关注点不是语法层面天然存在的,而是根据附加功能的逻辑上的需要,有 10 个附加功能,就有 10 个横切关注点。
2.4.2 通知(⭐)
- 每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫做通知方法。
前置通知
:在被代理的目标方法前
执行。返回通知
:在被代理的目标方法成功结束
后执行(寿终正寝
)。异常通知
:在被代理的目标方法异常结束
后执行(死于非命
)。后置通知
:在被代理的目标方法最终结束
后执行(盖棺定论
)。环绕通知
:使用try…catch…finally结构围绕整个
被代理的目标方法,包括上面四种通知对应的所有位置。
2.4.3 切面(⭐)
- 封装通知方法的类。
2.4.4 目标
- 被代理的目标对象。
2.4.5 代理
- 向目标对象应用通知之后创建的代理对象。
2.4.6 连接点
- 这也是一个纯逻辑概念,不是语法定义的。
- 把方法排成一排,每一个横切位置看成 x 轴方向,把方法从上到下执行的顺序看成 y 轴,x 轴和 y 轴的交叉点就是连接点。
2.4.7 切入点(⭐)
- 定位连接点的方式。
- 每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
- 如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
- Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
- 切入点通过
org.springframework.aop.Pointcut
接口进行描述,它使用类和方法作为连接点的查询条件。