Spring Boot -- Spring AOP原理及简单实现
目录
一、AOP基本概念
什么是AOP,AOP英语全名就是Aspect oriented programming,字面意思就是面向切面编程。面向切面的编程是对面向对象编程的补充,面向对象的编程核心模块是类,然而在AOP中核心模块是切面。切面实现了多种类型和对象的模块化管理,比如事物的管理。
上面的解释可以你还是看不懂,那么我们举个例子来说明AOP是来解决什么样的问题。我们都知道传统的OOP是自上而下的逻辑开发:
上面这张图形象生动了描述了我们通过浏览器访问一个接口的函数调用过程,我们发送的http请求首先会根据url匹配到对应的controller,然后controller会去调用对应的的service,service再去调用dao、然后将处理的结果返回给浏览器。
1.1、Filter、Interceptor、AOP
那么现在我们有一个需求,想记录发送http请求的客户端IP以及请求接口信息,最简单的方法就是我们在每一个controller方法中调用一个打印相关信息的函数,这样会存在一个问题,有多少接口,我们就会调用多少次日志打印函数。那么有没有一种简单的方法可以实现日志的记录呢,有当然有,我们可以通过Filter或者Interceptor实现请求拦截功能,记录日志信息。那么该有人问什么是Filter、Interceptor,那它们和我们将要说的AOP有什么区别?
AOP使用的主要是动态代理 , 过滤器使用的主要是函数回调;拦截器使用是反射机制 。一个请求过来,先进行过滤器处理,看程序是否受理该请求 。 过滤器放过后 , 程序中的拦截器进行处理 ,处理完后进入 被 AOP动态代理重新编译过的主要业务类进行处理 。
- Filter:和框架无关,过滤器拦截的是URL,可以控制最初的http请求,但是更细一点的类和方法控制不了。
- Interceptor:拦截器拦截的也是URL,拦截器有三个方法,相对于过滤器更加细致,有被拦截逻辑执行前、后等。
- AOP: 面向切面拦截的是类的元数据(包、类、方法名、参数等) 相对于拦截器更加细致,而且非常灵活,拦截器只能针对URL做拦截,而AOP针对具体的代码,能够实现更加复杂的业务逻辑。
三者功能类似,但各有优势,从过滤器 》拦截器 》切面,拦截规则越来越细致,执行顺序依次是过滤器、拦截器、切面。一般情况下数据被过滤的时机越早对服务的性能影响越小,因此我们在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是AOP。比如权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;针对日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。
1.2、AOP中的一些概念
上面说了那么多题外话,你应该对AOP有了一个初步的理解,下面我们将更深入的介绍AOP,AOP是一种面向切面的编程思想。这些横切性问题,把它们抽象为一个切面,关注点在切面的编程。
AOP主要应用在日志记录,权限验证,事务管理中。我们首先来看一下Spring官方提供的一些有关AOP的基本概念:
1).通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理;通知类型,主要有以下几种:
- Before :前置通知,在连接点方法前调用;对应Spring中@Before注解;
- After :后置通知,在连接点方法后调用;对应Spring中的@After注解;
- AfterReturning:返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常;对应Spring中的@AfterReturning注解;
- AfterThrowing:异常通知,当连接点方法异常时调用;对应Spring中的@AfterThrowing注解;
- Around:环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法;对应Spring中的@Around注解;
2).连接点(Join Point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用,可以说目标对象中的方法就是一个连接点;
3).切点(Pointcut): 就是连接点的集合;对应Spring中的@Pointcut注解;
4).切面(Aspect): 切面是通知和切点的结合;对应Spring中的注解@Aspect修饰的一个类;
5).目标对象(Target object):即被代理的对象;
6).代理对象(AOP proxy):包含了目标对象的代码和增强后的代码的那个对象;
我们利用上面这一张图来说一下目标对象和代理对象的关系,代理对象可以看作是目标对象的加强版,它是对目标对象中方法功能的一个扩充。代理对象的实现主要有两种,一种是基于jdk动态代理的,这要求目标对象必须是接口的实现;而另一种实现是基于cglib,即代理对象是继承自目标对象。
1.3、切点匹配表达式
切面是如何拦截到指定的类的元数据(包、类、方法名、参数等) 的呢,这是通过切点匹配表达式实现的。目前Spring支持的切点匹配表达式主要有以下几种:
- execution:可以定义到的最小粒度是方法,修饰符,包名,类名,方法名,Spring AOP主要也是使用这个匹配表达式;
- within:只能定义到类;例如@Pointcut(within(com.jnu.example.*))
- this:当前生成的代理对象的类型匹配;
- target:目标对象类型匹配;
- args:只针对参数;
- annotation:针对注解;
例如: execution (* com.sample.service..*. *(..))
整个表达式可以分为五个部分:
- 1、execution()::表达式主体;
- 2、第一个*号:表示返回类型, *号表示所有的类型;
- 3、包名:表示需要拦截的包名,包名后面的..,表明com.sample.service包、及其子包;
- 4、第二个*号:表示类名,*号表示所有的类;
- 5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个点表示任何参数;
- 关于Spring AOP注解的使用,我这里就不介绍了,网上有大量的博客介绍如何使用。我们接下来就带大家来实现一个类似within切点匹配表达式的效果。
二、AOP简单实现
在开始之前,我们先引入一个概念,Spring扩展点和后置处理器,我们知道Spring IOC可以对应用程序中的java bean做一个集中化的管理,从而使我们从繁琐的new Object()中解锁出来。
其核心就是先创建一个bean工厂,也就是我们常说的beanFactory,通过beanFactory来生产出我们应用程序中所需要的java ben,这些java bean大多都是被Spring代理之后的对象。
2.1、后置处理器
今天呢,我跟大家介绍的后置处理器呢,有三个,它们在Spring启动过程中均会被执行,具体详情可以阅读Spring启动过程源码分析:
BeanFactoryPostProcessor:可以插手beanFactory的生命周期,是针对整个工厂生产出来的BeanDefinition作出修改或者注册,作用于BeanDefinition时期;
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.beans.factory.config; import org.springframework.beans.BeansException; @FunctionalInterface public interface BeanFactoryPostProcessor { void postProcessBeanFactory(ConfigurableListableBeanFactory var1) throws BeansException; }
BeanPostProcessor:可以插手bean的生命周期,该接口定义了两个方法,分别在bean初始化前后执行(注意这里的初始化是指对象创建之后,属性赋值之前),方法的返回值为一个object,这个object呢就是我们存在于容器的对象了(所以这个位置我们是不是可以对我们的bean做一个动态的修改,替换等等操作,所以这也是我们Spring的扩展点之一,后面结合我么自己手写AOP来详细讲解这个扩展点的应用);
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.beans.factory.config; import org.springframework.beans.BeansException; import org.springframework.lang.Nullable; public interface BeanPostProcessor { @Nullable default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Nullable default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } }
ImportSelector:借助@Import注解,可以动态实现将一个类是否交由Spring管理,常用作开关操作;
在Spring处理我们的java类的时候,会分成四种情况去处理 :
- 普通类:就是@Component,@Service,@Repository等注解的类
- Import进来的类:这里呢,又分为三种情况:
a)Import一个普通类:@Import(A.class)
b)Import一个Registrar:比如我们的aop @Import(AspectJAutoProxyRegistrar.class)
c)Import一个ImportSelector:比如:@import(ImportSelector.class) ImportSelector 接口有一个实现方法,返回一个字符串类型的数组,里面可以放类名,在@import(ImportSelector.class)的时候,Spring会把我们返回方法里面的类全部注册到BeanDefinitionMap中,继而将对象注册到Spring容器中;
至于Spring在什么时候处理的呢,我大致叙述一下,有兴趣的可以自己去研究下spring源码: 对于普通类,Spring在扫描的时候,就将扫描出来的java类转换成我们的BeanDefinition,然后放入一个BeanDefinitionMap中去;
对于@import的三种情况,处理就在ConfigurationClassPostProcessor(该类是BeanDefinitionRegistryPostProcessor后置处理器的一个实现,同时这也是我们spring内部自己维护的唯一实现类)类中,具体处理Import的核心代码如下,if-else 很容易可以看出spring对于我们Import三种类型的处理:
/** * 处理我们的@Import注解,注意我们的@Import注解传入的参数,可能有三种类型 * 1,传入一个class类,直接解析 * 2,传入一个registrar,需要解析这个registrar * 3,传入一个ImporterSelector,这时候会去解析ImporterSelector的实现方法中返回的数组的class * */ private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates, boolean checkForCircularImports) { if (importCandidates.isEmpty()) { return; } if (checkForCircularImports && isChainedImportOnStack(configClass)) { this.problemReporter.error(new CircularImportProblem(configClass, this.importStack)); } else { this.importStack.push(configClass); try { for (SourceClass candidate : importCandidates) { //处理我们的ImportSelector if (candidate.isAssignable(ImportSelector.class)) { // Candidate class is an ImportSelector -> delegate to it to determine imports Class<?> candidateClass = candidate.loadClass(); ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class); ParserStrategyUtils.invokeAwareMethods( selector, this.environment, this.resourceLoader, this.registry); if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) { this.deferredImportSelectors.add( new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector)); } else { String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata()); Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames); //注意可能我们ImportSelector传入的类上还有可能会Import,所以这里,spring采用了 //一个递归调用,解析所有的import processImports(configClass, currentSourceClass, importSourceClasses, false); } } //处理我们的Registrar else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // Candidate class is an ImportBeanDefinitionRegistrar -> // delegate to it to register additional bean definitions Class<?> candidateClass = candidate.loadClass(); ImportBeanDefinitionRegistrar registrar = BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class); ParserStrategyUtils.invokeAwareMethods( registrar, this.environment, this.resourceLoader, this.registry); //添加的一个和Importselector方式不同的map中,sprig对两种方式传入的类注册方式不同 configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata()); } else { // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> // process it as an @Configuration class //最后如果是普通类,传入importStack后交由processConfigurationClass进行注册处理 this.importStack.registerImport( currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); processConfigurationClass(candidate.asConfigClass(configClass)); } } } catch (BeanDefinitionStoreException ex) { throw ex; } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to process import candidates for configuration class [" + configClass.getMetadata().getClassName() + "]", ex); } finally { this.importStack.pop(); } } }
2.2、AOP实现思路
同样根据如上原理,下面我们便可以来模拟我们的AOP,如果有点基础的可能应该会知道,Spring是基于我们的动态代理实现的(先不考虑是cglib还是jdk动态代理),结合我们AOP使用,那么我们就需要解决如下几个问题:
- 我们知道开启和关闭aop需要注解@EnableAspectJAutoProxy,如何实现,结合上文,我们可以使用@import(ImportSelector.class)来实现该功能;
- 如何确定代理关系,即哪些是我们需要代理的目标对象和其中的目标方法,以及哪些方法是要增强到目标对象的目标方法上去的?
- 如何实现目标对象的替换,就是我们在getBean的时候,如何根据目标对象来获取到我们增强后的代理对象?
2.3、AOP实现代码
我们首先需要构建一个项目,创建如下包:
annotation:存放我们所有自定义的注解;
holder:存放代理类信息;
processor:存放后置处理器的实现类;
selector:存放ImportSelector的实现类;
1、首先创建AOP中使用到的注解,这些注解类名称和Spring自带的一致:
After.java:
package com.zy.blog.common.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 后置通知注解类 * * @author zy * @since 2020/6/20 17:32 */ @Retention(RetentionPolicy.RUNTIME) public @interface After { String value() default ""; }
Around.java:
package com.zy.blog.common.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 环绕通知注解类 * * @author zy * @since 2020/6/20 17:33 */ @Retention(RetentionPolicy.RUNTIME) public @interface Around { String value() default ""; }
Before.java:
package com.zy.blog.common.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 前置通知注解类 * * @author zy * @since 2020/6/20 17:31 */ @Retention(RetentionPolicy.RUNTIME) public @interface Before { String value() default ""; }
Aspect.java:
package com.zy.blog.common.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface Aspect { }
EnableAspectAutoProxy.java:
package com.zy.blog.common.annotation; import com.zy.blog.common.selector.CustomizedImportSelector; import org.springframework.context.annotation.Import; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * aop开关注解 * 如果在App启动类上加入该注解,将会将我们的后置处理器的实现交给spring管理,spring才能去扫描得到这个类,才能去执行我们的自定义的后置处理器里面的方法,才能实现我们的aop的代理 * * @author zy * @since 2020/6/20 17:22 */ @Retention(RetentionPolicy.RUNTIME) @Import(CustomizedImportSelector.class) public @interface EnableAspectAutoProxy { }
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了