AOP(Aspect Orient Programming),即面向切面编程。AOP 是一种编程思想,是 OOP(Object Oriented Programming)面向对象编程的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。
以代码举例:
public interface Azh3ngInterface { void save(); }
public class Azh3ngService implements Azh3ngInterface {
@Override
public void doSave() {
// do save...
}
}
定义了一个接口和一个实现类,其中有一个方法 doSave()
目的是持久化一些数据。
现在期望在 doSave()
方法之前和之后打印日志,可以直接在代码里添加:
public class Azh3ngService implements Azh3ngInterface {
@Override
public void doSave() {
System.out.println("doSave method starts in: " + new Date());
// do save...
System.out.println("doSave method ended in: " + new Date());
}
}
当新增了一个 doUpdate()
方法,也需要在方法开始之前和之后记录日志,如果再在代码中添加类似的两行打印代码,明显代码就变得非常分散和冗余;
假设有多个方法都已经添加了这类代码,当需要修改打印格式时,需要到所有地方都修改。
由此可以看出,对于一些较为通用的操作,如果直接在代码中添加,会使代码耦合度变高,可维护性和可扩展性变低。
尝试进一步思考,对于打印日志、事务管理、安全检查、权限校验等通用操作,需要作用在一段业务代码上时,如果只需要关注在哪里和做什么,代码执行时会自动在业务逻辑代码之前和之后执行通用操作,会给开发和维护带来极大的便利。
比如,希望在 Azh3ngService
的 doSave()
和 doUpdate()
方法之前和之后打印日志和开启/提交事务,此时:
- 在哪里:
Azh3ngService
的doSave()
和doUpdate()
方法之前和之后 - 做什么:打印日志
如果只需要在某个地方指定【Azh3ngService
的 doSave()
和 doUpdate()
方法】,然后编写【方法之前和之后打印日志】的代码,当被指定的方法调用时,会自动打印日志的话,便可以算作面向切面编程了,例如:
@Aspect
@Component
public class Azh3ngAspect {
// **在哪里**:在执行 Azh3ngService 的 方法名以“do”开头 的所有方法前后执行:
@Around("execution(public * com.azh3ng.service.Azh3ngService.do*(..))")
public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
// **做什么**:打印日志
System.out.println("[Around] start " + pjp.getSignature());
Object retVal = pjp.proceed();
System.out.println("[Around] done " + pjp.getSignature());
return retVal;
}
}
public class TestAspect {
public static void main(String[] args) {
Azh3ngInterface azh3ngServiceProxy = BeanFactory.getBean(Azh3ngService.class);
azh3ngServiceProxy.doSave(); // 自动打印日志
}
}
由于目前两个类没有直接的关系,所以需要一些技术完成这个功能。
在了解技术之前,先了解一下相关术语。
相关概念
参考:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-introduction-defn
Aspect
表示切面,对应示例中的 Azh3ngAspect
类(被 @Aspect
注解的类)就是切面
切面中可以定义 Pointcut、Advice 等等
Join point
表示连接点,比如一个方法的执行、或者一个异常的处理,如 doSave()
和 doUpdate()
方法;
在 Spring AOP 中,一个连接点通常表示被代理的目标方法。
Advice
直译为通知,或者可以理解为增强,表示在一个特定连接点上所采取的动作,对应示例中的 doLogging()
方法,【在方法前后打印日志】就是一种 Advice;
Advice 分为不同的类型, 包括 “around”, “before” 和 “after” 等。
Advice 可以简单理解为:只是一段执行逻辑
由于做什么可以不止做一件事,可能有多个逻辑需要执行,因此在很多 AOP 框架中,包括 Spring,会用拦截器(Interceptor)来实现 Advice,并且在连接点周围维护一个 Interceptor 链,顺序执行定义的多个逻辑
Pointcut
直译为切点,用来匹配一个或多个连接点,对应示例中的 "execution(public * com.azh3ng.service.Azh3ngService.do*(..))"
表达式
Advice 与切点表达式是关联在一起的,Advice 将会执行在和切点表达式所匹配的连接点上
Target object
目标对象,指被切面指定的对象。对应示例中的 Azh3ngService
类
在 Spring AOP 中根据被指定的对象生成代理对象。
AOP proxy
AOP 代理,表示被各类 AOP 框架生成的、用来实现 AOP 的类,对应示例中的 azh3ngServiceProxy
对象
在 Spring 中,使用 JDK 动态代理和 CGLIB 代理技术生成代理类
Weaving
直译为织入,表示创建代理对象的动作,对应示例中的 BeanFactory.getBean(Azh3ngService.class)
- AspectJ 的织入动作发生在编译时期
- Spring AOP 的织入动作发生在运行时
Introduction
给某个类型声明额外的方法或字段。例如在 Spring AOP 中可以使用 @DeclareParents
注解给指定的类添加接口,并指定一个默认实现,实际开发中使用较少。
相关实现技术
在 Java 中场景的实现技术有 AspectJ 和 Spring AOP。
AspectJ
AspectJ 是在编译时对字节码进行修改,直接对类的字节码进行增强,可以理解为在编译时就解析 @Around
等注解,得到代理逻辑后对被代理的类中的字节码进行增强,所以如果想用 AspectJ 技术生成代理对象,需要用单独的 AspectJ 编译器的,实际项目中很少这么用。通常开发者使用 Spring 框架,在 Spring 启动时解析处理 @Around
等注解,然后利用动态代理机制生成代理对象。
AspectJ注解
AspectJ 定义了一些 Pointcut 相关的注解:
- Pointcut
- Around
- Before
- After
- AfterReturning
- AfterThrowing
这些注解在 Spring 中也有使用