在软件开发中,散布于应用中多处的功能被称为横切关注点(crosscutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。
AOP 概述
AOP,Aspect Oriented Programming,面向切面编程
- AOP 是一种新的方法论,是对传统 OOP 的补充。
- AOP 操作的主要对象是切面(aspect),切面是模块化后的横切关注点。
- 在应用 AOP 编程时,仍然需要定义公共功能,但可以明确地定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。
- AOP 的好处:
- 关注点集中,代码不分散,便于维护和升级
- 业务模块更简洁,只包含核心业务代码,降低耦合
- 常见应用场景:日志、声明式事务、安全和缓存等
AOP 术语
通知 Advice
切面的工作称为通知,它定义了切面是什么(what)以及何时(when)使用。
Spring AOP 中的五种通知:
- 前置通知(Before):在目标方法被调用之前调用通知。
- 后置通知(After):在目标方法完成之后调用通知。无论是正常返回还是抛出异常,后置通知都会执行。
- 返回通知(After-Returning):在目标方法成功执行之后调用通知。
- 异常通知(After-Throwing):在目标方法抛出异常后调用通知。
- 环绕通知(Around):在目标方法调用之前和调用之后执行自定义的行为。
连接点 Join point
可以应用切面的点称为连接点,可以是调用某个方法时、某个方法异常时等等。
例如在上面的图片中,所有圆点都是连接点。
切点 Pointcut
真正应用切面的点称为切点,切点定义了在何处(where)应用切面。
并不需要在每个连接点都应用切面,只需要根据实际需求选择连接点进行切入,这些连接点就是切点。
切面 Aspect
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。
织入 Weaving
把切面应用到目标对象并创建新的代理对象的过程称为织入。
可以在目标对象生命周期的不同阶段进行织入:
- 编译期:切面在目标类编译时被织入。
- 类加载期:切面在目标类加载到 JVM 时被织入。
- 运行期:切面在应用运行的某个时刻被织入,AOP 容器为目标容器动态地创建一个代理对象。Spring AOP 采用的就是这种方式。
Spring AOP 运行的大致流程
底层原理
Spring AOP 构建在动态代理基础之上,因此 Spring 对 AOP 的支持局限于方法拦截。
Spring AOP 底层使用动态代理。有两种情况:
有接口,使用 JDK 动态代理
创建接口实现类代理对象,增强类的方法
没有接口,使用 CGLIB 动态代理
创建子类的代理对象,增强类的方法
JDK 动态代理
使用 JDK 动态代理,使用 java.lang.reflect.Proxy
类里面的方法创建代理对象
代码演示:
创建接口,定义方法
1
2
3
4
5public interface UserDao {
int add(int a, int b);
String update(String id);
}创建接口实现类,实现方法
1
2
3
4
5
6
7
8
9
10
11
12
13public class UserDaoImpl implements UserDao {
public int add(int a, int b) {
System.out.println("add 方法执行");
return a + b;
}
public String update(String id) {
System.out.println("update 方法执行");
return id;
}
}使用
java.lang.reflect.Proxy
类创建接口代理对象1
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
34public class JdkProxy {
public static void main(String[] args) {
// 创建接口实现类代理对象
Class[] interfaces = {UserDao.class};
UserDao userDao = new UserDaoImpl(); // 被代理对象
UserDao proxyInstance =
(UserDao) Proxy.newProxyInstance(JdkProxy.class.getClassLoader(),
interfaces, new UserDaoProxy(userDao));
System.out.println(proxyInstance.add(2, 3));
System.out.println(proxyInstance.update("abc"));
}
}
// 创建代理对象代码
class UserDaoProxy implements InvocationHandler {
private Object obj;
// 把被代理对象传递过来
public UserDaoProxy(Object obj) {
this.obj = obj;
}
// 增强的逻辑
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 方法之前
System.out.println(method.getName() + " 方法之前执行,参数:" + Arrays.toString(args));
// 被增强的方法执行
Object returnValue = method.invoke(obj, args);
// 方法之后
System.out.println(method.getName() + " 方法之后执行");
return returnValue;
}
}输出:
1
2
3
4
5
6
7
8add 方法之前执行,参数:[2, 3]
add 方法执行
add 方法之后执行
5
update 方法之前执行,参数:[abc]
update 方法执行
update 方法之后执行
abc
上面代码中的 java.lang.reflect.InvocationHandler
接口及其 invoke
方法:
大致流程
通过在代理类中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。如上图所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标 bean。当代理拦截到方法调用时,在调用目标 bean 方法之前,会执行切面逻辑。
参考资料:
Craig Walls - Spring 实战(第 4 版)