一、概述
1. 什么是AOP
AOP(Aspect Oriented Programming):面向切面编程,指在程序运行期间动态的将某段代码切入到指定方法指定位置进行运行的操作。如:性能监控、日志记录、权限控制等,通过AOP解决代码耦合问题,让职责更加单一。
AOP技术它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为”Aspect”,即切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性
二、案例实现推导
为了更好的理解aop的原理,我们通过案例来一步步,首先,我们先用spring框架,使用配置类注解方式注入bean,UserService作为业务代码:
- UserService业务代码:
@Service
public class UserService {
public void queryAll(){
System.out.println("业务代码:查询所有数据");
}
}
- 配置类注入bean
@Configuration
@ComponentScan("com.star")
public class AppConfig {
}
- 通过反射获取字节码创建对象
@Test
public void AOPTest(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
UserService bean = ac.getBean(UserService.class);
bean.queryAll();
}
我们运行 AOPTest 方法,可以看到控制台打印出:
咱们就在以上代码的基础上对功能进行增强。
1. 直接增强
现在我们需要给业务代码执行前后加上打印日志,没有aop的时候,咱们可以直接在 service 业务中增加相关方法进行增强:
@Service
public class UserService {
public void queryAll(){
System.out.println("before----业务代码执行前打印日志.....");
System.out.println("业务代码:查询所有数据");
System.out.println("after----业务代码执行前打印日志.....");
}
}
这样一来,就把增强代码和业务代码放到了一起,这是很不合理的,并且增加了耦合,不利于代码的拓展。
2. 动态代理增强
所谓的动态代理,需要一个代理类,这个代理类是动态生成的,字节码要用的时候就创建,要用的时候就加载,在不修改源码的基础上对方法进行增强。有两种代理机制,一种是基于JDK的动态代理,另一种是基于CGLib的动态代理,bean没有接口时使用 CGLib 代理,bean有接口则使用 JDK 代理。由于上面的案例中没有使用接口,所以这里用CGLib代理。
有关动态代理可以参考之前的博客:(0)
@Test
public void AOPTest1(){
final UserService bean = new UserService();
UserService cglibProducer = (UserService) Enhancer.create(bean.getClass(), new MethodInterceptor(){
/**
* 作用:执行被代理对象的任何借口方法都会经过该方法
* @param proxy:代理对象的引用
* @param method:当前执行的方法
* @param args:当前执行方法所需的参数
* @return:和被代理对象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
System.out.println("记录日志");
Object result = method.invoke(bean, args);
return result;
}
});
cglibProducer.queryAll();
}
执行后打印如下:
可以看到,经过CGLib代理后,不修改业务代码的基础上,对方法进行了增强,而在spring aop 的底层,也是使用的动态代理,不过要远远复杂于上面的代码,如果要深究,需要查看spring的源码,这里只讲基本原理,源码有点太费头发。
3. AOP切面增强
最后,咱们来看看spring是如何增强的,AOP是一个标准规范,而为了实现这个标准规范,有几种方式:
- 基于代理的AOP
- @AspectJ注解驱动的切面
- 纯POJO切面
- 注入式AspectJ切面
这四种方式都是实现aop的方法,这里讲一下通过AspectJ提供的注解实现AOP,但在spring官网中,有AspectJ 的概念,主要是因为在spring2.x的时候,spring aop的语法过于复杂,spring想进行改进,而改进的时候就借助了AspectJ 的语法、编程风格来完场aop的配置功能,这里使用AspectJ 注解方式来实现。
- @EnableAspectJAutoProxy注解
在配置类中添加@EnableAspectJAutoProxy注解,开启切面编程功能,添加后如下:
@Configuration
@ComponentScan("com.star")
@EnableAspectJAutoProxy
public class AppConfig {
}
- 增加切面类
使用@Aspect注解声明一个切面,并使用@Before、@After等注解表明连接点
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.star.service..*.*(..))")
public void pointCut(){};
@Before("pointCut()")
public void logStart(){
System.out.println("查询之前打印日志....");
}
@After("pointCut()")
public void logEnd(){
System.out.println("查询之后打印日志....");
}
@AfterReturning("pointCut()")
public void logReturn(){
System.out.println("查询之后正常返回....");
}
@AfterThrowing("pointCut()")
public void logException(){
System.out.println("查询之后返回异常....");
}
}
- 测试运行类不变
@Test
public void AOPTest(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
UserService bean = ac.getBean(UserService.class);
bean.queryAll();
}
直接运行测试类,可以看到对方法进行了增强
直接获取一个代理对象 ,首先产生一个目标对象,然后对目标对象进行代理,返回代理对象,把目标对象放到了map中
在spring初始化的时候就已经完成了代理,也就是执行下面代码的时候就完成了代理
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
三、AOP原理理解
1. AOP术语
术语的理解参考:https://yq.aliyun.com/articles/638791
在上面,我们已经通过实例实现了通过AOP对方法进行增强,现在我们来理解一下,首先,我们必须要了解AOP的术语,这些术语在上面的AOP切面增强案例中都有体现,这里结合案例来理解一下。
- 连接点(Joinpoint):连接点的最小单位称之为方法,每一个方法称之为连接点,如类开始初始化前、类初始化后、类某个方法调用前、调用后、方法抛出异常后。一个类或一段程序代码拥有一些具有边界性质的特定点,这些点中的特定点就称为“连接点”。在上面的案例中,@Before、@After等注解所在的方法都称之为连接点。
- 切点(Pointcut):切点是连接点的集合,每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事物。AOP通过“切点”定位特定的连接点。连接点相当于数据库中的记录,而切点相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。在上面案例中,@Pointcut("execution(* com.star.service...(..))")就是一个切点。
连接点是一个比较空泛的概念,就是定义了哪一些地方是可以切入的,也就是所有允许你通知的地方。
切点就是定义了通知被应用的位置 (配合通知的方位信息,可以确定具体连接点)
- 通知(Advice):切入连接点的时机和切入连接点的内容称为通知,Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning):在目标方法成功执行之后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
通知就定义了,需要做什么,以及在某个连接点的什么时候做。 上面的切点定义了在哪里做。
- 目标对象(Target):指的是被增强的对象,也就是被通知的对象,也就是真正的业务逻辑,在上面案例中,UserService就是目标对象
- 引介(Introduction):允许我们向现有的类添加新方法属性。通过引介,我们可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
- 织入(Weaving):织入是将通知添加到目标类具体连接点上的过程。AOP像一台织布机,将目标类、通知或引介通过AOP这台织布机天衣无缝地编织到一起。根据不同的实现技术,AOP有三种织入的方式:
- 编译期织入,这要求使用特殊的Java编译器。
- 类装载期织入,这要求使用特殊的类装载器。
- 动态代理织入,在运行期为目标类添加通知生成子类的方式。
把切面应用到目标对象来创建新的代理对象的过程,Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
- 代理对象(Proxy):一个类被AOP织入通知后,就产出了一个结果类,这个类就是代理对象,它是融合了原类和通知逻辑的代理类。
- 切面(Aspect):连接点和切点以及通知所在的那个类称为切面,它既包括了横切逻辑的定义,也包括了连接点的定义,Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。
切点的通知的结合,切面知道所有它需要做的事:何时/何处/做什么
2. AOP实现原理
【1】AOP的设计
- 在Spring的底层,如果我们配置了代理模式,Spring会为每一个Bean创建一个对应的ProxyFactoryBean的FactoryBean来创建某个对象的代理对象。
- 每个 Bean 都会被 JDK 或者 Cglib 代理。取决于是否有接口。
- 每个 Bean 会有多个“方法拦截器”。注意:拦截器分为两层,外层由 Spring 内核控制流程,内层拦截器是用户设置,也就是 AOP。
- 当代理方法被调用时,先经过外层拦截器,外层拦截器根据方法的各种信息判断该方法应该执行哪些“内层拦截器”。内层拦截器的设计就是职责连的设计。
【2】代理的创建
- 首先,需要创建代理工厂,代理工厂需要 3 个重要的信息:拦截器数组,目标对象接口数组,目标对象。
- 创建代理工厂时,默认会在拦截器数组尾部再增加一个默认拦截器 —— 用于最终的调用目标方法。
- 当调用 getProxy 方法的时候,会根据接口数量大余 0 条件返回一个代理对象(JDK or Cglib)。
注意:创建代理对象时,同时会创建一个外层拦截器,这个拦截器就是 Spring 内核的拦截器。用于控制整个 AOP 的流程。
【3】代理的调用
- 当对代理对象进行调用时,就会触发外层拦截器。
- 外层拦截器根据代理配置信息,创建内层拦截器链。创建的过程中,会根据表达式判断当前拦截是否匹配这个拦截器。而这个拦截器链设计模式就是职责链模式。
- 当整个链条执行到最后时,就会触发创建代理时那个尾部的默认拦截器,从而调用目标方法。最后返回。
如图:
调用过程: