# 手写 AOP
作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)
相关源码参考: 手写Spring代码仓库 (opens new window)
在 Spring 中,AOP(面向切面编程)是非常核心的特性之一。它通过动态代理在不修改业务代码的前提下,实现了日志、事务、权限校验等横切逻辑的统一管理。
下面我会一步步展示如何用最小的代码,手写一个 简化版的 AOP 框架,从配置解析、代理实现到与 IoC 容器的衔接,最终实现类似 Spring 的效果。
# 一、增加切面配置
首先,我们需要一个配置文件,用来描述切点、切面类、以及通知方法。这相当于告诉框架:哪些方法需要被拦截?拦截后执行哪些增强逻辑?
#多切面配置可以在key 前面加前缀
#例如 aspect.logAspect.
#切面表达式, expression#
pointCut=public .* com.yym.demo.service..*Service..*(.*)
#切面类#
aspectClass=com.yym.demo.aspect.LogAspect
#切面前置通知#
aspectBefore=before
#切面后置通知#
aspectAfter=after
#切面异常通知#
aspectAfterThrow=afterThrowing
#切面异常类型#
aspectAfterThrowingName=java.lang.Exception
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 二、代理顶层接口
为了统一不同代理方式(JDK 动态代理、CGLIB),先定义一个顶层接口,所有代理类都要实现它。
/**
* 代理顶层接口定义
*/
public interface YymAopProxy {
Object getProxy();
Object getProxy(ClassLoader classLoader);
}
2
3
4
5
6
7
8
9
# 三、AOP配置类
用来承载配置文件中的内容,相当于一个数据载体。
/**
* AOP 配置类
*/
@Data
public class YymAopConfig {
private String pointCut;
private String aspectClass;
private String aspectBefore;
private String aspectAfter;
private String aspectAfterThrow;
private String aspectAfterThrowingName;
}
2
3
4
5
6
7
8
9
10
11
12
13
# 四、核心 AOP 支持类
这一部分是最关键的,它负责 解析配置、匹配切点、缓存方法与通知的对应关系。作用类似于 Spring 的 AdvisedSupport。
/**
* 核心 AOP 支持类:封装切点、目标对象、通知方法等配置,并完成配置的解析。
* 作用类似于 Spring 中的 AdvisedSupport,主要职责:
* - 解析配置中的 pointCut 表达式
* - 生成目标类与通知方法(Advice)的映射关系
* - 判断目标类是否匹配切点规则
* - 获取目标类中方法对应的通知集合
*/
public class YymAdvisedSupport {
// AOP 配置类,包含切点表达式、切面类、通知方法名等
private YymAopConfig config;
// 目标对象实例
private Object target;
// 目标对象的 Class 类型
private Class targetClass;
// 用于匹配类名的切点正则表达式(从配置中解析得到)
private Pattern pointCutClassPattern;
// 方法缓存:目标类中的每个方法对应的通知集合
private Map<Method, Map<String, YymAdvice>> methodCache;
// 构造方法,注入配置类
public YymAdvisedSupport(YymAopConfig config) {
this.config = config;
}
/**
* pointCut=public .* com.yym.demo.service..*Service..*(.*)
* 解析配置:将配置的切点表达式转换为 Java 正则表达式,
* 并构建目标类方法与通知方法之间的对应关系缓存。
*/
private void parse() {
// 将 pointCut 表达式转换为正则表达式
String pointCut = config.getPointCut()
.replaceAll("\\.", "\\\\.") // 点转义
.replaceAll("\\\\.\\*", ".*") // .* 保留
.replaceAll("\\(", "\\\\(") // 括号转义
.replaceAll("\\)", "\\\\)");
// 提取用于匹配类的正则 (com.yym.demo.service..*Service) 提取用于校验业务代码中的切点
String pointCutForClassRegex = pointCut.substring(0, pointCut.lastIndexOf("\\(") - 4);
// 通过该正则对具体切点进行校验
pointCutClassPattern = Pattern.compile(
"class " + pointCutForClassRegex.substring(pointCutForClassRegex.lastIndexOf(" ") + 1)
);
// 初始化方法缓存
methodCache = new HashMap<>();
// 编译方法匹配的正则表达式
Pattern pointCutPattern = Pattern.compile(pointCut);
try {
// 加载切面类
Class aspectClass = Class.forName(this.config.getAspectClass());
// 存储切面类中方法名 -> Method 的映射
Map<String, Method> aspectMethods = new HashMap<>();
for (Method method : aspectClass.getMethods()) {
aspectMethods.put(method.getName(), method);
}
// 遍历目标类的方法,根据正则匹配符合切点的方法
for (Method method : this.targetClass.getMethods()) {
String methodString = method.toString();
// 去掉 throws 关键字后的部分
if (methodString.contains("throws")) {
methodString = methodString.substring(0, methodString.lastIndexOf("throws")).trim();
}
// 匹配切点表达式
Matcher matcher = pointCutPattern.matcher(methodString);
if (matcher.matches()) {
// 初始化该方法的通知集合
Map<String, YymAdvice> advices = new HashMap<>();
// 添加前置通知
if (config.getAspectBefore() != null && !"".equals(config.getAspectBefore())) {
advices.put("before", new YymAdvice(
aspectClass.newInstance(),
aspectMethods.get(config.getAspectBefore())
));
}
// 添加后置通知
if (config.getAspectAfter() != null && !"".equals(config.getAspectAfter())) {
advices.put("after", new YymAdvice(
aspectClass.newInstance(),
aspectMethods.get(config.getAspectAfter())
));
}
// 添加异常通知
if (config.getAspectAfterThrow() != null && !"".equals(config.getAspectAfterThrow())) {
YymAdvice advice = new YymAdvice(
aspectClass.newInstance(),
aspectMethods.get(config.getAspectAfterThrow())
);
advice.setThrowName(config.getAspectAfterThrowingName());
advices.put("afterThrow", advice);
}
// 方法与通知的映射关系保存进缓存
methodCache.put(method, advices);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取目标类中某个方法对应的通知集合。
* 使用缓存提升性能,支持根据签名再查找一次。
*/
public Map<String, YymAdvice> getAdvices(Method method, Object o) throws Exception {
Map<String, YymAdvice> cache = methodCache.get(method);
if (cache == null) {
// 可能是由于 method 不是同一实例,通过反射重新查找
Method m = targetClass.getMethod(method.getName(), method.getParameterTypes());
cache = methodCache.get(m);
this.methodCache.put(m, cache);
}
return cache;
}
/**
* 判断目标类是否匹配切点规则,即是否需要创建代理。
* 在 IoC 初始化时调用,用于判断是否应用 AOP。
* 这边需要区分: pointCutClassPattern 的正则规则是由aop配置文件生成的
* this.targetClass 是当前实例
* 这2个对比才能判断是否符合aop
*/
public boolean pointCutMath() {
return pointCutClassPattern.matcher(this.targetClass.toString()).matches();
}
// setter 和 getter 方法
public void setTargetClass(Class<?> targetClass) {
this.targetClass = targetClass;
parse(); // 设置目标类后立即解析配置
}
public void setTarget(Object target) {
this.target = target;
}
public Class getTargetClass() {
return targetClass;
}
public Object getTarget() {
return target;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
这里的核心逻辑是:
- 把
pointCut表达式转为正则表达式; - 找出目标类中符合切点的方法;
- 把这些方法和切面中的增强方法绑定起来,存入缓存。
# 五、基于 JDK 动态代理实现
当有方法被调用时,代理类会先执行对应的 增强逻辑(Advice),再执行目标方法。
/**
* YymJdkDynamicAopProxy 是一个基于 JDK 动态代理的 AOP 实现类。
* 它实现了 InvocationHandler 接口,用于在代理方法执行前后织入横切逻辑(Advice)。
*/
public class YymJdkDynamicAopProxy implements InvocationHandler {
// 封装了切面配置、目标对象等 AOP 所需元数据
private YymAdvisedSupport config;
// 构造方法,传入配置
public YymJdkDynamicAopProxy(YymAdvisedSupport config) {
this.config = config;
}
/**
* JDK 动态代理的核心方法:拦截代理对象的方法调用
*
* @param proxy 代理对象
* @param method 被代理的方法
* @param args 方法参数
* @return 方法执行结果
* @throws Throwable 异常
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取当前方法对应的增强配置(before、after、afterThrow)
Map<String, YymAdvice> advices = config.getAdvices(method, null);
Object returnValue;
try {
// 执行前置通知(如果有)
invokeAdivce(advices.get("before"));
// 反射调用目标类的方法
returnValue = method.invoke(this.config.getTarget(), args);
// 执行后置通知(如果有)
invokeAdivce(advices.get("after"));
} catch (Exception e) {
// 异常通知(如果配置了)
invokeAdivce(advices.get("afterThrow"));
throw e; // 继续抛出异常
}
return returnValue;
}
/**
* 执行增强方法(Advice)
*
* @param advice 增强封装对象,包含切面类与方法
*/
private void invokeAdivce(YymAdvice advice) {
if (advice == null) {
return; // 没有配置则跳过
}
try {
// 执行切面类中配置的方法
advice.getAdviceMethod().invoke(advice.getAspect());
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace(); // 打印异常堆栈
}
}
/**
* 创建并返回代理对象
*
* @return 基于 JDK 的代理对象
*/
public Object getProxy() {
// 用当前类作为 InvocationHandler,生成代理对象
return Proxy.newProxyInstance(
this.getClass().getClassLoader(),
this.config.getTargetClass().getInterfaces(), // 只能代理接口
this
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
核心流程是:
- 取出该方法对应的通知集合;
- 先执行
before通知; - 调用目标方法;
- 执行
after通知; - 如果发生异常,则执行
afterThrow通知。
# 六、接入 getBean()方法与IOC 容器衔接
在 IoC 容器创建对象时,加入一个判断逻辑:如果某个类匹配了切点规则,就生成代理对象返回;否则返回原始对象。
/**
* 创建真正的实例对象
*/
private Object instantiateBean(String beanName, YymBeanDefinition beanDefinition) {
String className = beanDefinition.getBeanClassName();
Object instance = null;
try {
if (this.factoryBeanObjectCache.containsKey(beanName)) {
instance = this.factoryBeanObjectCache.get(beanName);
} else {
Class<?> clazz = Class.forName(className);
//2、默认的类名首字母小写
instance = clazz.newInstance();
//==================AOP开始=========================
//如果满足条件,就直接返回Proxy对象
//1、加载AOP的配置文件
YymAdvisedSupport config = instantionAopConfig(beanDefinition);
config.setTargetClass(clazz);
config.setTarget(instance);
//判断规则,要不要生成代理类,如果要就覆盖原生对象
//如果不要就不做任何处理,返回原生对象
if (config.pointCutMath()) {
instance = new YymJdkDynamicAopProxy(config).getProxy();
}
//===================AOP结束========================
this.factoryBeanObjectCache.put(beanName, instance);
}
} catch (Exception e) {
e.printStackTrace();
}
return instance;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
通过该方法判断当前实例是否满足aop, 其中pointCutClassPattern是配置文件aop生成的规则, this.targetClass是当前实例
public boolean pointCutMath() {
return pointCutClassPattern.matcher(this.targetClass.toString()).matches();
}
2
3
这样,每个 getBean() 出来的对象,就自动具备了 AOP 的能力。
# 七、总结
至此,一个简化版的 手写 AOP 框架 就完成了,核心流程是:
定义切面配置(切点 + 通知方法);
解析配置,找到匹配的目标方法;
通过 JDK 动态代理拦截方法调用;
在 IoC 容器实例化 bean 时,决定是否包装为代理对象;
业务层无需感知,AOP 就能自动生效。
换句话说,我们已经在一个小型 Web 框架里实现了 Spring 的 AOP 核心思想。
← 手写MVC 不同IOC的流程解析 →