《SpringBoot 3:入门与应用实战》第 7 章 AOP 思想与实现 阅读笔记 14

《SpringBoot 3:入门与应用实战》第 7 章 AOP 思想与实现 阅读笔记 14

7.2 AOP 的基础---动态代理

通过 7.1 节的内容,从最原始的推演过程中,读者可以深刻理解 AOP 思想的底层实现基础就是动态代理。JDK 1.3 原生提供了动态代理机制,而Cglib 也有一种动态代理。本节会简单回顾动态代理的内容。

7.2.1 JDK 动态代理的使用

JDK 动态代理要求被代理的对象所属类必须实现一个及以上的接口,创建代理对象时使用 Proxy.newProxyInstance 方法,该方法中有3个参数。

  • ClassLoader loader:被代理的对象所属类的类加载器。
  • Class<?>\[\] interfaces:被代理的对象所属类实现的接口。
  • InvocationHandler h:代理的具体代码实现。

在这三个参数中,前面两个都容易理解,最后一个 InvocationHandler 是一个接口,它的核心方法 invoke 中也有 3 个参数。

  • Object proxy:代理对象的引用(代理后的)。
  • Method method:代理对象执行的方法。
  • Object\[\] args:代理对象执行方法的参数列表。

具体的代理逻辑在 InvocationHandler 的 invoke 方法中编写,可根据方法名称等因素过滤需要增强的代码逻辑。

7.2.2 Cglib 动态代理的使用

虽然基于 JDK 动态代理的场景不需要额外依赖,但是接口实现的限制使得 JDK 动态代理的使用场景受到一定限制,好在 Cglib 可以直接使用字节码增强技术同样实现动态代理。要想使用 Cglib,必须先引入 Cglib 的 jar 包。使用 Cglib 时有两个小前提:被代理的类不能是 final 的(Cglib动态代理会创建子类,final 类型的 Class 无法继承)​;被代理的类必须有无参构造方法(底层反射创建对象时不能获取构造方法参数)​。

xml 复制代码
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.1</version>
        </dependency>

Cglib 动态代理的内容相对较少,它只需传入两个元素。

  • Class type:被代理的对象所属类的类型。
  • Callback callback:增强的代码实现。

由于一般情况下是对类中的方法增强,因此在传入 Callback 时通常选择这个接口的子接口 MethodInterceptor。另外 MethodInterceptor 中intercept 方法的参数列表与 InvocationHandler 中 invoke 方法的参数列表类似,唯独多了一个 MethodProxy,它是对参数列表中的 Method 又做了一层封装,利用它可以直接执行被代理对象的方法。

代码展示了一个简单的 Cglib 动态代理使用方式,使用 Enhancer 的 create 方法可以创建代理对象。

java 复制代码
public static Partner getPartner(int money) {
    Partner partner = partners.remove(0);
    return (Partner) Enhancer.create(partner.getClass(), new MethodInterceptor() {
        private int budget = money;
        private boolean status = false;
 
        @Override
        public Object intercept(Object proxy, Method method, Object[] args, 
                MethodProxy methodProxy) throws Throwable {
            // 如果在付钱时没给够钱,则标记budget为异常值
            if (method.getName().equals("receiveMoney")) {
                int money = (int) args[0];
                this.status = money >= budget;
            }
            if (status) {
                return method.invoke(partner, args);
            }
            return null;
        }
    });
}

7.3 AOP 概述与术语

了解了 AOP 思想的由来及动态代理后,下面开始了解 AOP 的相关概念和对应的术语,理解相关的概念有助于进行后续的实操。

7.3.1 AOP 概述

在 Spring Framework 的官方文档中有专门的一个章节介绍 Spring 的 AOP。简单抽取文档中的关键要素,大致可以得到以下 3 点。

(1) AOP 是 OOP 的补充

  • 7.1 节中通过介绍一些基于 OOP 的设计模式,发现这些设计模式并不能完全有效地解决分散的相同逻辑所造成的重复代码问题。
  • 而 AOP 可以将这些重复代码逻辑抽取为一个切面,在运行时通过动态代理织入原有的对象,依然能实现预期的效果。

(2) AOP 关注的是核心切面

  • 7.1.6 节中可以看到,切面可以简单地理解为分散在不同类中的相同逻辑。
  • AOP 在对指定类中指定方法进行增强时,需要直接编写这些增强的逻辑,并切入原有的代码中。

(3) AOP 也是 Spring IOC 的补充

  • 如果没有 AOP,IOC 本身也是 Spring 非常强大的特性。
  • 不过,AOP 可以对 IOC 容器中需要的 Bean 增强原有的功能(比如给普通 Service 层代码实现事务控制)。

根据上述内容进一步总结,AOP 要完成的核心工作依然是程序的解耦。借助 AOP 思想将分散在各个类中方法的重复逻辑抽取为一个切面,并在运行时生成代理对象,将这些重复逻辑组合进原有对象,实际上就完成了原有业务与扩展逻辑之间的解耦。通过这种解耦,最大的好处显而易见:业务逻辑只需要关注业务逻辑本身,扩展逻辑只关心扩展逻辑以及切入业务逻辑的位置即可。

最后,本节尝试总结一个尽可能表述完整的 AOP 概述。

面向切面编程 (Aspect Oriented Programming,AOP) 是 OOP 的补充。OOP 关注的核心是对象,AOP 的核心是切面 (Aspect)。AOP 可以在不修改功能代码本身的前提下,使用动态代理技术对已有代码逻辑进行增强。

AOP 面向切面编程,是 OOP 面向对象编程的补充。

AOP 可以在不修改功能代码本身的前提下,使用动态代理技术对已有代码逻辑进行增强。

AOP 的好处是程序的解耦。将分散在各个类中方法的重复逻辑抽取为一个切面,实现原有业务与扩展逻辑之间的解耦。业务逻辑只需要关注业务逻辑本身,扩展逻辑只关心扩展逻辑以及切入业务逻辑的位置。

7.3.2 AOP 的演变历史

AOP 的发展历程相当有年头,在 Spring Framework 还没有出现之前,开源界也有一些其他的 AOP 解决方案。

1.AOP 理论的提出

早在 1990 年,有一个名为 Xerox Palo Alto Research Lab (PARC) 的组织就对 OOP 的设计思想进行了分析,当时这个组织就已经分析出 OOP在抽取重复逻辑时的局限性,于是这个组织的成员费尽心思研究出了一套理论,使用它就可以将通用的重复逻辑抽取出来,并在合适的时机把这些逻辑再组合进原始的业务类中,这就是 AOP 的早期设计思想。随着研究的不断深入,AOP 理论和思想也慢慢完善,逐渐形成了一套完整的设计思想。

2.第一代 AOP 的诞生

当 Xerox Palo Alto Research Lab 这个组织在研究 AOP 理论的同时,美国东北大学的一个博士生和他的团队也在研究如何解决 OOP 重复逻辑的抽取问题,后来这个团队研究并制定出一套 AOP 框架,它就是 AspectJ。随后到了 2002 年,AspectJ 转移到 Eclipse 开源基金会组织,并逐渐成为 Java 开源社区中非常流行的 AOP 框架。

AspectJ 被称为第一代 AOP 的代表,它采用静态字节码编译的方式,使用特殊的编译器,将事先写好的通知逻辑织入目标类中,这样产生的 .class 字节码文件就已经是带有增强通知的代理类。这种静态 AOP 的最大好处是快,因为 .class 文件本身就已经是被增强过的,接下来的动作跟普通的字节码没有任何区别;至于缺点也很明显,每次修改任何增强逻辑时都要重新编译所有要被增强的业务类,并重新打包工程,这个过程相对麻烦。

3.第二代 AOP 的诞生

在 AspectJ 成型之后的一段时间,开源界又出现了一个动态 AOP(第二代 AOP)框架,称为 AspectWerkz,它最初的设计就与 AspectJ 不一样,它的出发点就是动态 AOP,所以后来在 2005 年 AspectJ 跟 AspectWerkz 达成协议,将 AspectWerkz 的内容合并至 AspectJ 中。从那之后 AspectJ 成为独有的、同时支持静态 AOP 和动态 AOP 的强大 AOP 框架。此外,JBoss 也推出了它的AOP框架,但由于传统的重量级框架和组件已经被时代抛弃,我们已经很难接触到它们,仅供了解。

动态 AOP 的优点是不再需要频繁地编译业务类,切面修改完毕后只需要单独重新编译切面类,其余部分不太需要变化;缺点也就随之而来,由于不是在字节码的编译环节完成的通知织入,因此需要在类加载时/运行时动态织入增强逻辑,这会在一定程度上对程序的运行性能有所影响。

4.Spring Framework 与 AspectJ

AspectJ 已经非常强大,Spring Framework 对其产生了兴趣,所以后来的 Spring Framework 2.0 版本中直接声明了对 AspectJ 的支持,可以使用 AspectJ 的方式定义切面类、声明通知方法等。不过话又说回来,如果 Spring Framework 直接把 AspectJ 的核心全部纳入体系内似乎不太现实(因为当时的 AspectJ 在开源界已有一席之地)​,Spring Framework 的创始人在权衡之后做出决定:整合 AspectJ 的方式只是使用了它的声明和定义方式,具体的底层实现还是采用 Spring Framework 原生底层支持,这样做既兼容了主流技术,又不会影响 Spring Framework 本身的底层架构设计,可谓一举两得。

随着后续 Spring Framework 的地位逐渐强大,其他 AOP 框架慢慢销声匿迹,最终留下的就是 Spring Framework + AspectJ 的组合方式。

7.3.3 AOP 的基本术语

AOP 的重要基础知识就是 AOP 术语。在 Spring Framework 的官方文档中,紧接着 AOP 的描述之后就是 Spring Framework 定义的 AOP 相关术语。

考虑到部分读者是刚接触 AOP,或者以前没有彻底理解 AOP 的这些基本术语,所以本节会使用一种不同的方式,用尽可能通俗易懂的现实场景举例,帮助读者理解 AOP 的基本术语。

预设场景如图所示,在预设场景中,左边的主管视为 "原始对象"​,主管提供账户充值、账号解封等业务,表示一个 Class 中定义的几个方法;

中间的业务经理视为 "中间的代理层"​,他平时招揽客人,并且将客人的需求传达给主管;

右边的客人视为 "客户端"​,发生业务办理的动作都是由他发起。

1.Target:目标对象

目标对象是第一个非常容易理解的概念,即被代理的对象。很明显,在上面的预设场景中,左边的主管就是目标对象。回到前面的动态代理的例子,这个 partner 就可以被称为目标对象。

java 复制代码
public static Partner getPartner(int money) {
    // partner即目标对象
    Partner partner = partners.remove(0);
    return (Partner) Proxy.newProxyInstance(......);
}

2.Proxy:代理对象

代理对象也很容易理解,就是 Proxy.newProxyInstance 返回的结果。在上面的预设场景中,中间的业务经理 + 左边的主管组合起来形成一个代理对象(请读者注意一点,代理对象中还包含原始对象本身)​。

3.JoinPoint:连接点

所谓连接点,可以简单地理解为目标对象的所属类中定义的所有方法。在上面的预设场景中,很明显主管提供的几项业务(账户充值、账号解封)就属于连接点。回到动态代理的例子,Partner 接口中声明的两个方法就是连接点。

java 复制代码
public interface Partner {
    void receiveMoney(int money);
    void playWith(Player player);
}

4.Pointcut:切入点

切入点的含义是那些被拦截 / 被增强的连接点。可以这样理解:代理层会选择目标对象的一部分连接点作为切入点,在执行目标对象的方法前/后做出额外的动作。由这个解释不难得出,切入点与连接点的关系应该是包含关系。

请注意:切入点一定是连接点,连接点不一定是切入点。

被使用的连接点就是切入点。

5.Advice:通知

Advice直译为通知,但这个概念似乎很抽象,所以我们会换一个相对容易理解的词:增强的逻辑,也就是增强的代码。在上面的预设场景中,业务经理发现有客人的时候,并没有直接交给主管,而是先执行了他自己的逻辑:搞清楚客人要办理账户账户充值业务,这就是业务经理针对账户充值这个连接点的增强逻辑。由此可以得出一个这样的结论:代理对象=目标对象+通知。

到这里读者是否突然意识到一个问题:切入点和通知要配合在一起使用。有了切入点之后,需要搭配增强的逻辑,AOP 才能为目标对象创建具有增强功能的代理。

6.Aspect:切面

紧接着是切面,切面比较容易理解,切面 = 切入点 + 通知。

实际上切面不仅仅包含通知,还有一个不常见的部分是引介,下面将会提到。

7.Weaving:织入

从名字上听起来,织入更像一个动作,而且即便不理解概念,也能凭借大胆猜想得到差不多的结论:织入就是将通知应用到目标对象,进而生成代理对象的过程。

代理对象 = 目标对象 + 通知,这个算式中的加号就表示织入动作。目标对象和通知都准备就绪时,需要一个动作将它们绑定到一起。

8.Introduction:引介

引介 / 引入这个概念对标的是通知,通知是针对切入点提供增强的逻辑,而引介是针对 Class 类型,它可以在不修改原始类的代码的前提下,在运行时为原始类动态添加新的属性 / 方法。引介在目前的企业应用、场景中已经很少出现,所以读者对它的重视程度一定要放低,当然感兴趣的读者学习一下也无可厚非。

7.3.4 通知的类型

通知指的是对目标方法加的扩展逻辑。

Spring Framework 的官方文档中介绍了 AOP 术语,紧接着列举了 Spring Framework 中定义的通知的类型。Spring Framework 中支持的通知的类型包含以下 5 种,这些通知的类型是基于 AspectJ 的,在 7.4 节中将要用到。

  • Before(前置通知):目标对象的方法调用之前触发。
  • After(后置通知):目标对象的方法调用之后触发。
  • AfterReturning(返回通知):目标对象的方法调用完成,在返回结果值之后触发。
  • AfterThrowing(异常通知):目标对象的方法运行中抛出/触发异常后触发。
    • 请注意,AfterReturning 与 AfterThrowing 两者是互斥的!如果方法调用成功无异常,就会有返回值;如果方法抛出了异常,就不会有返回值。
  • Around(环绕通知):编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接获取目标对象以及要执行的方法,所以环绕通知可以任意在目标对象的方法调用前/后扩展额外的逻辑,甚至不调用目标对象的方法。
相关推荐
脉动数据行情11 小时前
Java SpringBoot 对接知名台股 TWSE|批量采集上市股票行情实践
java·开发语言·spring boot
她的男孩1 小时前
我把管理系统接给AI,它改条数据都要先问我
java·后端·架构
元界metalite1 小时前
SpringBoot分页接口怎么设计-pageSize不设上限会发生什么
后端
IT爱学堂3 小时前
Go开发疑难杂症终结者通关指南
后端
大勇前进3 小时前
从10分钟到10秒:一个真实慢查询的SQL Server优化全记录
后端
站大爷IP3 小时前
被Python异步坑了三年,才搞懂await和create_task的区别
后端
大白803 小时前
你的数据库密码还在用明文?SQL Server 凭据管理的 3 种现代方案
后端
凤山老林3 小时前
Spring Boot 集成 ShardingSphere-Encrypt 实现字段级实时脱敏
java·spring boot·后端·数据脱敏
vipxieliang3 小时前
ValidX v1.2.0 更新日志
java·后端