aop实现原理_从宏观的实现原理和设计本质入手,带你理解 AOP 框架的原理

点击上方“Java知音”,选择“置顶公众号”

技术文章第一时间送达!

作者:FeelsChaotic

juejin.im/post/5c57b2d5e51d457ffd56ffbb

前言

本文将从另一个角度讲解 AOP,从宏观的实现原理和设计本质入手。大部分讲 AOP 的博文都是一上来就罗列语法,然后敲个应用 demo就完了 。但学习不能知其然,不知其所以然。

对 AOP 我提出了几点思考:

  • AspectJ 为什么会大热?

  • AspectJ 是怎样工作的?

  • 和 Spring AOP 有什么区别?

  • 什么场景下适用我们能不能自己实现一个 AOP 方法?

一、引入

敲一个小 Demo 来引入主题,假设我想不依赖任何 AOP 方法,在特定方法的执行前后加上日志打印。

第一种方式:写死代码

定义一个目标类接口

74831fc2eb1679fff71d9253b18b77a9.png

ce2e55bc45baf84b277496a1876acb92.png

把 before() 和 after() 方法写死在 execute() 方法体中,非常不优雅,我们改进一下。

第二种方式:静态代理

3a49ca581f30c4754f68f0b115da9d48.png

但是存在一个问题,随着打印日志的需求增多,Proxy 类越来越多,我们能不能保持只有一个代理呢?这时候我们就需要用到 JDK 动态代理了。

第三种方式:动态代理

新建动态代理类

8e1cf1126e939b97a1e31eb2f5c07432.png

客户端调用

951d3c3a056d5a6cfd7c57a5dfd72a93.png

这又引出一个问题,日志打印和业务逻辑耦合在一起,我们希望把前置和后置抽离出来,作为单独的增强类。

第四种方式:动态代理 + 分离增强类

新建增强类接口和实现类

13b7b877a77b27bf446f5e2c217188fb.png

用反射代替写死方法,解耦代理和操作者

0b62b5b737e0ec4139282b237bcbd83f.png

客户端调用

8b80d5d711713054c9be8a542279ab2c.png

但是用了反射性能太差了,而且动态代理用起来也不方便,有没有更好的办法?

我们发现 Demo 存在种种问题

  • 静态代理每次都要自己新建个代理类,太繁琐,重用性又差,一个代理不能同时代理多种类;

  • 动态代理可以重用,但性能太差;

  • 代理类耦合进被代理类的调用阶段,万一我需要改下 before、after 的方法名,可能会点燃一个炸弹;

  • 代理拦截了一个类,就会拦截这个类的所有方法,难道我还要在代理类里加个 if-else 判断特定方法过滤拦截?我们可以不可以只拦截特定的方法?

  • 如果我既要打印日志,又要计算方法执行用时,每次都要去改增强类吗?

我们的诉求很简单:1. 性能高;2. 松耦合;3. 步骤方便;4. 灵活性高。

那主流的 AOP 框架是怎么解决这个问题的呢?我们赶紧来看看!

二、AOP 方法

不同的 AOP 方法原理略微有些不同,我们先看下 AOP 实现方式有哪些:

208b323450ba598dfe00a35c2293d2f6.png

所有 AOP 方法本质就是:拦截、代理、反射(动态情况下),实现原理可以看作是代理 / 装饰设计模式的泛化,为什么这么说?我们来详细分析一下。Java:由浅入深揭开 AOP 实现原理

三、静态织入原理,以 AspectJ 为例

静态织入原理就是静态代理,我们以 AspectJ 为例。

1. AspectJ 设计思路

前面说到 Demo 存在的种种问题,AspectJ 是怎么解决的呢?AspectJ 提供了两套强大的机制:

(1)切面语法 | 解决业务和切面的耦合

AspectJ 中的切面,就解决了这个问题。

@Before("execution(* android.view.View.OnClickListener.onClick(..))")

我们可以通过切面,将增强类与拦截匹配条件(切点)组合在一起,从而生成代理。这把是否要使用切面的决定权利还给了切面,我们在写切面时就可以决定哪些类的哪些方法会被代理,从而逻辑上不需要侵入业务代码。

而普通的代理模式并没有做到切面与业务代码的解耦,虽然将切面的逻辑独立进了代理类,但是决定是否使用切面的权利仍然在业务代码中。这才导致了 Demo 中种种的麻烦。

AspectJ 提供了两套对切面的描述方法:

1.我们常用的基于 java 注解切面描述的方法,写起来十分方便,兼容 Java 语法;

@Aspect
public class AnnoAspect {
    @Pointcut("execution(...)")
    public void jointPoint() {
    }

    @Before("jointPoint()")
    public void before() {
        //...
    }

    @After("jointPoint()")
    public void after() {
        //...
    }
}

2.基于 aspect 文件的切面描述方法,这种语法不兼容 Java 语法。

public aspect AnnoAspect {

    pointcut XX():execution(...);
    before(): XX() {
        //...
    }
    after(): XX() {
        //...
    }
}    

(2)织入工具 | 解决代理手动调用的繁琐

那么切面语法让切面从逻辑上与业务代码解耦,但是我要怎么找到特定的业务代码织入切面呢?

两种解决思路:一种就是提供注册机制,通过额外的配置文件指明哪些类受到切面的影响,不过这还是需要干涉对象创建的过程;另外一种解决思路就是在编译期或类加载期先扫描切面,并将切面代码通过某种形式插入到业务代码中。

那 AspectJ 织入方式有两种:一种是 ajc 编译,可以在编译期将切面织入到业务代码中。另一种就是 aspectjweaver.jar 的 agent 代理,提供了一个 Java agent 用于在类加载期间织入切面。

2. 通过 class 反推 AspectJ 实现机制

(1)@Before 机制

国际惯例写个 Demo

1.自定义 AutoLog 注解

04a756a12c85d5f21a655cc1ce64f5de.png

2.编写 LogAspect 切面

402a471fc1bcc96bfcf58e03e1d03809.png

3.在切入点中加上注解

03dde4f5d45fc265bc6b75b1bb989379.png

反编译后(请点开大图查看)

a29db7be2654bfc48e3e859c71a5c66f.png

发现 AspectJ 会把调用切面的方法插入到切入点中,且封装了切入点所在的方法名、所在类、入参名、入参值、返回值等等信息,传递给切面,这样就建立了切面和业务代码的关联。

我们跟进 LogAspect.aspectOf().aroundJoinPoint(localJoinPoint); 一探究竟。

e778add93506fa3fbc79d9aeb4302dbe.png

我们发现了什么?其实 Before 和 After 的插入就是在匹配到的 JoinPoint 调用前后插入 Advise 方法,以此来达到拦截目标 JoinPoint 的作用。如下图所示:

23592a0b68acb5d335f30b7267c5db1f.png

(2)@Around 机制

1.自定义 SingleClick 注解

9745d5a33bfbe54dc2569e36cbd93c3c.png

2.编写 SingleClickAspect 切面

12f97312a08552f5046f5f836e9c04f0.png

3.业务方加上注解

94b494becfdaea1f919d309ff13d4461.png

打开编译后的 class 文件(请点开大图查看)

bb99dc993f55b191dd7caceff85cc11d.png

我们发现和 Before、After 织入不一样了!前者的织入只是在匹配的 JoinPoint 前后插入 Advise 方法,仅仅是插入。而 Around 拆分了业务代码和 Advise 方法,把业务代码迁移到新函数中,通过一个单独的闭包拆分来执行,相当于对目标 JoinPoint 进行了一个代理,所以 Around 情况下我们除了编写切面逻辑,还需要手动调用 joinPoint.proceed() 来调用闭包执行原方法。

我们看下 proceed() 都做了些什么

ff235dccf0213cc001d769fe3c392897.png

那这个 arc 是什么?什么时候拿到的呢?

bef3ac9db3e612c3a07d9d23c4277fb8.png

继续回溯

0e5dc6b1943b0045c811573ef79c9405.png

在 AroundClosure 闭包中,会把运行时对象和当前连接点 joinPoint 对象传入,调用 linkClosureAndJoinPoint() 绑定两端,这样在 Around 中就可以通过 ProceedingJoinPoint.proceed() 调用 AroundClosure,进而调用到目标方法了。

那么一图总结 Around 机制:

a2fde8e256317b1722966d5b1fb9690b.png

我们从 AspectJ 编译后的 class 文件可以明显看出执行的逻辑,proceed 方法就是回调执行被代理类中的方法。

所以 AspectJ 做的事情如下:

  • 首先从文件列表里取出所有的文件名,读取文件,进行分析;

  • 扫描含有 aspect 的切面文件;

  • 根据切面中定义规则,拦截匹配的 JoinPoint ;

  • 继续读取切面定义的规则,根据 around 或 before ,采用不同策略织入切面。

(3)@Before @After 机制与 @Around 机制区别

  • Before、After 仅仅是织入了 Advise 方法

  • Around 使用了代理 + 闭包的方式进行替换

3. AspectJ 底层技术总结

分析完 class 你会发现,AspectJ 实际上就是用一种特定语言编写切面,通过自己的语法编译工具 ajc 编译器来编译,生成一个新的代理类,该代理类增强了业务类。

AspectJ 就是一个代码生成工具;

编写一段通用的代码,然后根据 AspectJ 语法定义一套代码生成规则,AspectJ 就会帮你把这段代码插入到对应的位置去。Java知音扩展:代码神器:拒绝重复编码,这款IDEA插件了解一下.....

AspectJ 语法就是用来定义代码生成规则的语法。

扩展编译器,引入特定的语法来创建 Advise,从而在编译期间就织入了Advise 的代码。

如果使用过 Java Compiler Compiler (JavaCC),你会发现两者的代码生成规则的理念惊人相似。JavaCC 允许你在语法定义规则文件中,加入你自己的 Java 代码,用来处理读入的各种语法元素。

四、动态织入原理,以 Spring AOP 为例

动态织入原理就是动态代理。

1. Spring AOP 执行原理

Spring AOP 利用截取的方式,对被代理类进行装饰,以取代原有对象行为的执行,不会生成新类。

2. Spring AOP VS AspectJ

可能有的小伙伴会困惑了,Spring AOP 使用了 AspectJ,怎么是动态代理呢?

那是因为 Spring 只是使用了与 AspectJ 一样的注解,没有使用 AspectJ 的编译器,转向采用动态代理技术的实现原理来构建 Spring AOP 的内部机制(动态织入),这是与 AspectJ(静态织入)最根本的区别。

Spring 底层的动态代理分为两种 JDK 动态代理和 CGLib:

JDK 动态代理用于对接口的代理,动态产生一个实现指定接口的类,注意动态代理有个约束:目标对象一定是要有接口的,没有接口就不能实现动态代理,只能为接口创建动态代理实例,而不能对类创建动态代理。

CGLIB 用于对类的代理,把被代理对象类的 class 文件加载进来,修改其字节码生成一个继承了被代理类的子类。使用 cglib 就是为了弥补动态代理的不足。

3. JDK 动态代理的原理

我们前面的 Demo 第三种方式使用了动态代理,我们不禁有了疑问,动态代理类及其对象实例是如何生成的?调用动态代理对象方法为什么可以调用到目标对象方法?

e62bcbf854528f3df13bc642b1eb077e.png

我们通过 Proxy.newProxyInstance 可以动态生成指定接口的代理类的实例。我们来看下newProxyInstance内部实现机制。

fb9c902155a28c6cd81b78828caa93be.png

代理对象会实现接口的所有方法,实现的方法交由我们自定义的 handler 来处理。

56d2a7c5a207699d031198bdf5d1e75b.png

我们看下 getProxyClass0 方法,只凭一个类加载器、一个接口,是怎么创建代理类的?

d2a61ca90671ecdf386a92dcb9476a5e.png

注意一下:Android 中动态代理类是直接生成,而 Java 是生成代理类的字节码,再根据字节码生成代理类。

那么客户端就可以 getProxy() 拿到生成的代理类 com.sun.proxy.$Proxy0

2c2580a175139fb156c3d64080baa674.png

这个代理类继承自 Proxy 并实现了我们被代理类的所有接口,在各个接口方法的内部,通过反射调用了 InvocationHandlerImpl 的 invoke 方法。

总结下步骤:

  • 获得被代理类的接口信息,生成一个实现了代理接口的动态代理类;

  • 通过反射获得代理类的构造函数;

  • 利用构造函数生成动态代理类的实例对象,在调用具体方法前调用 invokeHandler 方法来处理。

36adfc7e6e06027e77f3b619ec564acb.png

后记

1. 设计模式不能脱离业务场景

不知不觉我们复习了一下代理模式,设计模式必须依赖大量的业务场景,脱离业务去看设计模式是没有意义的。

因为脱离了应用场景,即使理解了模式的内容和结构,也学不会在合适的时候应用。

设计模式推荐:设计模式内容聚合

2. 敢于追求优雅的代码

首先你要敢于追求优雅的代码,就像我们开头的打印日志的需求,不断提出问题,不断追求更好的解决方案,在新的方案上挖掘新的问题……如果你完全不追求设计,那自然是不会想到去研究设计模式的。

END

Java面试题专栏

【41期】盘点那些必问的数据结构算法题之链表【42期】盘点那些必问的数据结构算法题之二叉堆【43期】盘点那些必问的数据结构算法题之二叉树基础【44期】盘点那些必问的数据结构算法题之二分查找算法【45期】盘点那些必问的数据结构算法题之基础排序【46期】盘点那些必问的数据结构算法题之快速排序【47期】六大类二叉树面试题汇总解答【48期】盘点Netty面试常问考点:什么是 Netty 的零拷贝?【49期】面试官:SpringMVC的控制器是单例的吗?【50期】基础考察:ClassNotFoundException 和 NoClassDefFoundError 有什么区别

7485db5a60498fcd3c767ed68dfd2c63.png

我知道你 “在看72db43c5c75d995b86bd34d40137eff9.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/276697.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

xxl-job源码分析

xxl-job源码分析 xxl-job 系统说明 安装 安装部署参考文档:分布式任务调度平台xxl-job 功能 定时调度、服务解耦、灵活控制跑批时间(停止、开启、重新设定时间、手动触发) XXL-JOB是一个轻量级分布式任务调度平台,其核心设计目标是…

定制jQuery File Upload为微博式单文件上传

原文链接:http://avnpc.com/pages/single-file-upload-component-by-jquery-file-upload jQuery File Upload是一个非常优秀的上传组件,主要使用了XHR作为上传方式,并且利用了相当多的现代浏览器功能,所以可以实现诸如批量上传、超…

vb趣味编程弹球小游戏_最好玩的微信小游戏集合,总有一款是你没玩过的

大家好,这里是小雅龙生活趣味时间,自从17年微信推出小游戏程序以来,微信小游戏行业可谓是炙手可热,知道2019年不断有许许多多的微信小游戏如雨后春笋般的生根发芽。下面就由我带大家来看看今年最好玩,最受欢迎的微信小…

Golang——垃圾回收GC(2)

1 垃圾回收中的重要概念 1.1 定义 In computer science, garbage collection (GC) is a form of automatic memory management. The garbage collector, or just collector, attempts to reclaim garbage, or memory occupied by objects that are no longer in use by the pro…

java gui框架_推荐!程序员整理的Java资源大全

构建这里搜集了用来构建应用程序的工具。Apache Maven:Maven使用声明进行构建并进行依赖管理,偏向于使用约定而不是配置进行构建。Maven优于Apache Ant。后者采用了一种过程化的方式进行配置,所以维护起来相当困难。Gradle:Gradle…

帆软报表(finereport)控件背景色更改

setTimeout(function() {$(.fr-trigger-btn-up).css({"background-color": "#003399" });}, 100); 转载于:https://www.cnblogs.com/Williamls/p/11571586.html

mybatis 大于_酸爽!IDEA 中这么玩 MyBatis,让编码速度飞起!

作者:Orsoncnblogs.com/java-class/p/6237564.html1. 搭建 MyBatis Generator 插件环境a. 添加插件依赖 pom.xmlb. 配置文件 generatorConfig.xmlc. 数据库配置文件 jdbc.propertiesd. 配置插件启动项2.项目实战a. 比如在一个项目 我们要删除某个小组下某个用户的信…

scatter函数_matplotlib.pyplot常用函数scatter讲解大全(三)

前言这篇文章再来总结一个常用画图函数scatter-散点图。参数常用参数示例import matplotlib.pyplot as plt import numpy as np#导入需要的包 datanp.random.multivariate_normal([0,1],[[1,0],[0,1]],200)#准备数据,二维正态分布plt.rcParams["axes.unicode_m…

c++万能头文件_初学Python,与C对比

✎背景学了一学年的C的基础,下学年开课Python,现在正在自学中...C也不是不学了,而是之前买了一本《CPrimer》在学校里,就准备先学一下Python,下学期利用自由时间接着学习C。这里分析了一下二者的优缺点,供大…

本地无法启动MySQL服务,报的错误:1067,进程意外终止---解决

原文链接:http://blog.csdn.net/shenhonglei1234/article/details/5928873 在本地计算机无法启动MYSQL服务错误1067进程意外终止 这种情况一般是my.ini文件配置出错了 首先找到这个文件: 默认安装路径 C:/Program Files/MySQL/MySQL Server 5.1/my.ini …

团队升级

2019独角兽企业重金招聘Python工程师标准>>> 转载于:https://my.oschina.net/yulongblog/blog/2988702

Css3: gradient背景渐变

Css3: gradient背景渐变 原文链接&#xff1a;http://kk073000.blog.163.com/blog/static/34826942012123111322691/ css3实现了背景渐变。 <gradient> [ <linear-gradient> | <radial-gradient> | <repeating-linear-gradient> | <repeating-r…

聚类 python_python中实现k-means聚类算法详解

算法优缺点&#xff1a; 优点&#xff1a;容易实现 缺点&#xff1a;可能收敛到局部最小值&#xff0c;在大规模数据集上收敛较慢 使用数据类型&#xff1a;数值型数据 算法思想 k-means算法实际上就是通过计算不同样本间的距离来判断他们的相近关系的&#xff0c;相近的就会放…

python笔试常见题

1、冒泡排序&#xff1a; 冒泡排序算是最基本的python算法了。也算python面试遇到问的最多的了。 如果是封装成函数。代码如下&#xff1a; 如果初始就一个字典。那么代码为&#xff1a; 冒泡排序的本质就是两两比较。根据结果调换位置。最终达到一个排序的效果。 注&#xff1…

centos 关闭防火墙_CentOS7操作系统下如何关闭防火墙

centos系统如果不关闭防火墙在使用中会遇到不少问题&#xff0c;而且centos7和centos6关闭防火墙的方式不一样。centos6:1.永久性生效&#xff0c;重启后不会复原开启&#xff1a; chkconfig iptables on关闭&#xff1a; chkconfig iptables off2.即时生效&#xff0c;重启后复…

Apache的认证、授权、访问控制

原文链接&#xff1a; http://man.chinaunix.net/newsoft/Apache2.2_chinese_manual/howto/auth.html Apache认证、授权、访问控制 认证(Authentication)是指任何识别用户身份的过程。授权(Authorization)是允许特定用户访问特定区域或信息的过程。 相关模块和指令 认证和授权…

dim private public static_PHP中const,static,public,private,protected的区别

const: 定义常量&#xff0c;一般定义后不可改变static: 静态&#xff0c;类名可以访问public: 表示全局&#xff0c;类内部外部子类都可以访问&#xff1b;private: 表示私有的&#xff0c;只有本类内部可以使用&#xff1b;protected: 表示受保护的&#xff0c;只有本类或子类…

C#图解教程 第六章 深入理解类

深入理解类 类成员 前两章阐述了9种类成员中的两种&#xff1a;字段和方法。本章将会介绍除事件(第14章)和运算符外的其他类成员&#xff0c;并讨论其特征。 成员修饰符的顺序 字段和方法的声明可以包括许多如public、private这样的修饰符。本章还会讨论许多其他修饰符。多个修…

Apache用户身份验证

原文链接&#xff1a;http://www.yylog.org/?p4830 Apache用户身份验证 在apache应用过程中&#xff0c;管理员经常需要对apache下的目录做一些限制&#xff0c;不希望所有用户都能访问该目录下的文件&#xff0c;只对指定用户访问&#xff0c;此时我们就要用到apache用户身…

c# 获取word表格中的内容_Java 获取、删除Word文本框中的表格

本文介绍如何来获取Word文本框中包含的表格&#xff0c;以及删除表格。程序测试环境包括&#xff1a;IDEAJDK 1.8.0Spire.Doc.jar注&#xff1a;jar导入&#xff0c;可通过创建Maven程序项目&#xff0c;并在pom.xml中配置Maven仓库路径&#xff0c;并指定Free Spire.Doc for J…