造轮计划 AOP
预备知识
AOP 实现
AOP 的理念和实现
AOP(面向切面编程)是一种编程范式,它的理念是将程序的业务逻辑和系统级服务分离开来,从而提高代码的可重用性和可维护性。AOP 的实现方式是通过在程序执行过程中动态地将额外的代码(称为“切面”)织入到原有代码中,从而实现对原有代码的增强。
动态机制在 AOP 中发挥的作用
- 动态代理:通过代理实现插入机制
代理的本质是间接执行 By 拦截,相当于实现了执行过程的扩展 + 细分,进而使得插入机制成为可能 - 反射:实现动态代码(Advice)
- 注解:提供定位
插入谁?(Joinpoint)
插入哪?(Before、After、Around)
Spring AOP 机制
- AspectConfig:配置扫描
Configuration - Aspect:切面逻辑(系统级服务),包含 jointpoint 和 advice 的说明
作为 Bean 进行统一管理 - Entity:业务逻辑
作为 Bean 进行统一管理
从 Spring AOP 的机制方面看,使用者只需要做好配置,拦截部分交由框架自动完成,而要造轮子的话,拦截还得想办法自己写
IOC 轮子实现的过程中有一点遗憾的是没有用注解实现配置,尝试用注解实现配置,直接代码级注释就起不到训练体会注解好处的作用了
尝试仿照 Spring AOP 的注解格式,此处自定义注解应该包括
- @Bean:标识当前类为 Bean,表明 IOC 应该接管这个类
- @Aspect:标识当前类为 Aspect,可以通过该类获取 Joinpoint 和 相对位置
- @Pointcut:指定 Joinpoint
- @Before:指定 相对位置
- @After:指定 相对位置
- @Configuration:标识当前类为配置类,可以通过该类获取待扫描的包,进而通过扫描包获取类的配置信息
- @ComponentScan:指定扫描的包
手头上有的动态机制(相当于工具)
- 反射:有类名就有一切
- 注解:提供运行时辅助信息
- 拦截器:提供拦截功能
- JDK
- Cglib
需要扫描机制 参考文档:扫描包内所有类
Aspect 处理
动态代理与动态绑定
动态代理的实现依赖于 Java 的动态绑定机制
动态绑定是指在运行时确定方法调用的实际实现。在Java中,动态绑定是通过虚拟方法调用机制实现的。当一个对象调用一个方法时,Java虚拟机会根据对象的实际类型确定要调用的方法实现。这种机制使得Java具有多态性,可以在运行时动态地替换对象的行为。
在动态代理中,动态绑定被用来实现代理对象对目标对象方法的调用。当代理对象调用一个方法时,它会将方法调用转发给InvocationHandler接口的实现类。该实现类可以在方法调用前后执行一些操作,例如记录日志、检查权限等。然后,实现类使用反射机制调用目标对象的方法,并返回结果给代理对象。
需要注意的是,动态代理的实现还涉及到其他一些机制,例如反射、字节码生成等。但是,动态绑定是其中一个重要的机制,它使得代理对象可以在运行时动态地调用目标对象的方法。
JDK 实现动态代理
初见解决思路思路
Aspect 信息读取 → InvocationHandler 拦截 → Proxy 获取 By 复用 BeanFactory
此路不通,因为 JDK 的动态代理机制依赖于接口,而动态机制的引入就意味着我们所用到的对象有必要保证通用性,故均视为 Object,不带接口
JDK动态代理生成代理类对象的前提:目标对象必须实现接口。那这时一个没有接口的目标对象能不能产生代理类对象呢? → Cglib
Cglib 实现动态代理
代理实现机制复习
- JDK:依赖于接口
- CgLib:无需接口
特点 | JDK 实现代理 | cglib 实现代理 |
---|---|---|
实现方式 | 通过接口实现 | 通过继承实现 |
适用范围 | 只能用于实现了接口的类 | 可用于任何类 |
性能 | 生成代理类的速度较快 | 生成代理类的速度较慢 |
代理类类型 | 基于接口的 | 基于类的 |
综上所述,JDK 实现代理和 cglib 实现代理各有优缺点。如果您的类实现了接口,则应该使用 JDK 实现代理。如果您的类没有实现接口,则应该使用 cglib 实现代理。如果您需要更高的性能,则应该使用 JDK 实现代理。如果您需要访问代理类的方法或属性,则应该使用 cglib 实现代理。
由于引入了动态机制,用的是 Object,很显然就不能用云计算课上给的 JDK 实现方法,而是要用 cglib
Aspect 信息读取 → Cglib 拦截 → Proxy 获取 By 复用 BeanFactory
知识补充 By Cursor
新 Human in the loop → Learn From AI 是大趋势,积极拥抱新技术,将大语义模型融入个人学习中
请解释 Cglib 实现动态代理的基本原理
Cglib 是一个流行的 Java 库,提供了创建动态代理的支持。与传统代理不同,动态代理可以在运行时创建,无需实现接口。
Cglib 实现动态代理的基本原理是在运行时生成目标类的子类。这个子类将覆盖目标类的方法并提供必要的代理行为。
为了实现这一点,Cglib 使用了一种称为字节码生成的技术。它在运行时为子类生成字节码,然后将其加载到 JVM 中并用作代理。这种字节码生成是使用 ASM 库完成的,该库提供了一个用于生成 Java 字节码的低级 API。
总之,Cglib 通过使用字节码生成在运行时生成目标类的子类来实现动态代理。这个子类覆盖了目标类的方法并提供了必要的代理行为。
什么是 ASM 库
ASM 是一个流行的 Java 字节码操作库,它提供了一种低级别的 API,用于生成、转换和分析 Java 字节码。它可以用于许多用途,包括代码生成、代码转换、代码分析和代码操作。Cglib 使用 ASM 库来生成动态代理的字节码。因此,ASM 库是 Cglib 实现动态代理的关键组件之一。
除了 Cglib 以外,还有哪些实现动态代理的方法?
除了 Cglib,还有其他几种实现动态代理的方法。其中最常见的是使用 Java 自带的 java.lang.reflect.Proxy 类。这个类可以用来创建动态代理,但是它只能代理接口,不能代理类。另外,还有一个叫做 Javassist 的库,它也可以用来生成字节码并创建动态代理。与 Cglib 不同的是,Javassist 生成的字节码是基于源代码的,而不是基于目标类的子类。这使得它在某些情况下比 Cglib 更快,但也可能会导致一些限制。总之,除了 Cglib,还有其他几种实现动态代理的方法,每种方法都有其优缺点,具体使用取决于具体的需求。
请用表格总结一下上述实现动态代理的原理的区别
实现方法 | 原理 | 优点 | 缺点 |
---|---|---|---|
java.lang.reflect.Proxy | 创建接口的代理类 | 简单易用 | 只能代理接口 |
Javassist | 基于源代码生成字节码 | 生成速度快 | 生成的字节码有限制 |
Cglib | 在运行时生成目标类的子类 | 可以代理类和接口 | 需要使用 ASM 库生成字节码 |
总结以上三种实现动态代理的方法的原理和优缺点,可以用上述表格进行概括。其中,java.lang.reflect.Proxy 只能代理接口,但是使用简单易用;Javassist 生成速度快,但是生成的字节码有限制;Cglib 可以代理类和接口,但是需要使用 ASM 库生成字节码。具体使用取决于具体的需求。
什么是字节码?
字节码是一种中间代码,它是 Java 源代码编译后的结果。字节码可以在 Java 虚拟机(JVM)上运行,因此它是 Java 跨平台的关键。字节码是一种类似于汇编语言的低级代码,它包含了一系列指令,用于执行 Java 程序。在运行时,JVM 将字节码解释成机器码,并执行相应的操作。字节码可以通过反编译工具转换回 Java 源代码,但是这个过程不是完美的,因为一些信息可能会丢失。总之,字节码是 Java 程序的中间表示形式,它是 Java 跨平台的关键,也是许多高级技术的基础,如动态代理、AOP 和 ORM。
生成字节码和创建代理类的区别是什么?
生成字节码和创建代理类的区别在于,生成字节码是一种技术手段,用于在运行时生成类的字节码,而创建代理类是一种代理模式,用于在运行时创建代理对象。生成字节码通常用于实现动态代理,其中代理类是在运行时生成的子类,而创建代理类通常用于实现静态代理,其中代理类是在编译时生成的。在实现动态代理时,生成字节码比创建代理类更灵活,因为它可以在运行时动态生成代理类,而不需要在编译时指定代理类。此外,生成字节码还可以用于实现其他高级技术,如 AOP 和 ORM。
生成字节码的原理是什么?与注解有什么关系?
生成字节码的原理是在运行时生成类的字节码。这种技术通常用于实现动态代理,其中代理类是在运行时生成的子类。与注解的关系是,注解可以用于指定生成字节码的方式和参数。例如,可以使用注解来指定要生成的类的名称、父类、接口、字段、方法等。此外,还可以使用注解来指定要生成的字节码的版本、访问级别、注释等。总之,生成字节码是一种强大的技术手段,可以用于实现许多高级技术,包括动态代理、AOP 和 ORM。注解可以用于指定生成字节码的方式和参数,从而更加灵活和方便。
带参数 AOP 实现
首先理解带参数的含义:被代理对象方法(Method)带参数还是代理对象增强方法(Advice)带参数
Method 带参数
此处首先要解决的是被代理对象方法(Method)带参数的问题
其实很好解决,Method.invoke(Object obj,Objects[] args)
中将 Object 替换成 被代理对象(不一定是 intercept 方法参数表中的 obj)参数照样用参数表中的 args 就能解决被代理对象方法带参数的问题。
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable
参数 | 意义 |
---|---|
obj | 被代理对象 |
method | 被调用的方法 |
args | 被调用方法的参数 |
proxy | 方法的代理对象 |
Advice 带参数
然后是代理对象增强方法(Advice)带参数的问题
@Before 和 @After 的传参与切点表达式相关,而此处的简易轮子不实现切点表达式相关功能暂时不管。
按照目前使用 Spring AOP 的经验来看,貌似只能用被代理对象的参数,而不能额外指定参数。个人认为这也合理,如果引入其他参数,动态代理过程没有其他合适的时机用于给新引入的参数赋值,最合适的情况是让被代理对象在被代理对象方法(Method)中带上代理可能要用的值,将赋值过程统一到代理对象调用过程
- 直接调用被代理对象的 Method:不带 Advice
- 调用代理对象的 Method:带 Advice
Advice 通过 Method 获得参数
Advice 通过 Method 获得参数获得的感受
使用动态机制,相当于只能在一个封闭系统内部做操作,不能再从外部传值,相当于只能做第一序改变
实践经验总结
按方法增强
不是所有的方法都需要增强,先前的实现方法相当于只做一次代理,直接返回,结果就是要么 Bean 中所有方法都被加上代理,要么只有 Aspect 中指定的第一套 @Before 和 @After 生效
如果需要多次代理,则需要新增临时变量,且不能直接返回,而是要通过方法名称个数(去重 By Set)进行多次循环代理
造轮有感
先简单规定此处语境下的系统级别程序开发和应用级别程序开发:系统级别程序开发指的是使用语言的高级性质,面向业务无关的通用场景下的程序开发流程(包括各类轮子,如框架、中间件等基础设施的开发);应用级别程序开发指的是使用语言的初级性质,面向业务相关的场景下的程序开发流程。(包括各类管理平台、管理系统等具体应用的开发)
深刻体会到系统级别程序开发相较于应用级别程序开发的区别,二者不仅仅是复杂程度的区别,更多的是由于抽象层次不同导致的视角的区别
使用动态机制,相当于只能在一个封闭系统内部做操作,不能再从外部传值,相当于只能做第一序改变
动态机制知识点回顾
反射
反射是指在运行时动态地获取一个对象的信息以及操作对象的能力
反射作为一种静态语言动态化的关键特性,提供了获取运行时信息的机制。其理念是一种等效替换的思想,这种等效替换具体来说就是利用描述实现多对一的抽象,利用抽象描述(配置)替代具体信息(硬编码)以获取通用性。这种通用性的具体实现手段是配置,配置机制引入则使得解耦成为可能。
注解
注解提供了一种获取辅助信息的机制,其理念是定点增强,这种定点增强具体来说就是指定辅助信息的生命周期和标注位置来存放辅助信息
本次实践的注解主要可以分为以下两种用途(按权重模糊划分,每个注解都由注解名和属性组成,前者分类用,后者提供详细信息)
- 标记型注解:分类用,此类注解一般只需要一个 value 属性,作为类内标识,注解名本身就能实现类间区别
- 功能型注解:提供详细信息,此类注解一般需要引入多种属性,用于存放实现特定功能所需的信息
标记型注解的使用
标记型注解的使用难点不在其身,而在其外。注解本质上是一种辅助信息,是一种定点增强。定点增强的配置和使用都不难,但是全局管理就需要引入一套扫描机制
功能型注解的使用
功能型注解的使用很简单,就理解为一个 HashMap,用好现成的封装方法就能获取想要的信息
动态代理实现思路总结
动态代理的实现思路就两种
- 代理模式 → JDK(Java Development Kit)
创建与待增强类接口一致的代理类,在创建的代理类中织入代码来实现增强效果(间接增强) - 字节码生成 → CGlib(Code Generation Library)
运行时拦截并织入代码以获得代理(直接增强)
JDK 实现中,工厂类叫 Proxy(代理),关键类叫 InvocationHandler(调用处理器 → 中介)
CGlib 实现中,工厂类叫 Enhancer(增强器),关键类叫 MethodInterceptor(方法拦截器 → 拦截器)
从命名上也能直观感受到两种实现思路的区别
JDK 实现
工厂类 Proxy
方法 | 功能 |
---|---|
newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) | 创建一个实现了指定接口的代理类的实例 |
isProxyClass(Class<?> cl) | 判断指定的类是否是代理类 |
getProxyClass(ClassLoader loader, Class<?>... interfaces) | 返回一个实现了指定接口的代理类 |
getInvocationHandler(Object proxy) | 返回指定代理实例的 InvocationHandler 对象 |
最重要的方法是 newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h),其他三项只是辅助,newProxyInstance 方法的参数包括
- ClassLoader: 类加载器
- Class[]:接口数组
- InvocationHandler:代理类与目标类之间的中介者
中介者 InvocationHandler
InvocationHandler是代理类与目标类之间的中介者,它实现了在代理对象调用目标对象方法时所需完成的额外逻辑,比如记录日志、授权验证、异常处理等,从而使得代理类可以实现自己的职责,同时不影响目标类原有的功能。我们可以将InvocationHandler看作代理类的“工具箱”,包含了多种方法,可以根据需要自由搭配使用。
方法名 | 说明 |
---|---|
invoke(Object proxy, Method method, Object[] args) | 当代理对象进行方法调用时,将调用此方法 |
proxy | 代理对象 |
method | 被调用的方法对象 |
args | 将调用方法时所传入的参数列表 |
CGlib 实现
工厂类 Enhancer
简介
名称 | Enhancer类 |
---|---|
定义 | Enhancer类是CGLIB库中的一个重要类,用于动态生成Java类和实例,支持对已有类进行增强操作 |
用途 | Enhancer类可以用于创建代理对象,继承已有类并增强其功能 |
优点 | 1. 不需要事先定义接口 2. 可以继承已有类 3. 支持方法拦截和属性拦截 |
缺点 | 1. 不能代理final类或方法 2. 不能代理private方法或私有属性 3. 代理创建过程相对于JDK动态代理略微繁琐 |
使用方法 | 1. 创建Enhancer类对象 2. 设置代理类的超类或实现的接口 3. 设置回调函数 4. 创建代理对象 |
注:回调函数指的是在代理对象方法调用前后需要执行的逻辑函数
附:回调函数和高阶函数的区别
- 高阶函数:接受一个或多个函数作为参数,并/或将函数作为返回值返回的函数。高阶函数是函数式编程的基础。
- 回调函数:作为参数传递给另一个函数,并在该函数执行完毕后被调用的函数。回调函数是一种实现异步编程的方式。
常用方法
方法名 | 描述 |
---|---|
create() | 创建一个 Enhancer 对象,并且设置 Enhancer 的类加载器以及回调 |
setSuperclass(Class<?> clazz) | 设置要增强的类 |
setCallback(Callback interceptor) | 设置回调对象,用于在调用被增强类的方法时进行拦截 |
setCallbacks(Callback[] interceptors) | 设置回调数组,用于在调用被增强类的方法时进行拦截 |
setClassLoader(ClassLoader loader) | 设置类加载器 |
create(Class[] argumentTypes, Object[] arguments, Callback[] callbacks) | 创建一个增强的实例,可以传入参数和回调函数数组 |
Callback 类
Callback(回调)是一种常见的编程模式,即在任务的执行过程中,遇到特定的事件时调用预先定义的函数,传入参数并执行该函数。在 Java 中,Callback 可以使用接口回调,lambda 表达式或方法引用等方式实现。
Callback 可以用于异步编程、事件驱动编程、GUI 编程等情境下。通过将 Callback 传递给需要调用它的方法,实现方法的较少责任,提高代码的灵活性和复用性。
常见的 Callback 接口有:
- Runnable:没有参数和返回值的 Callback 接口。
- Callable:有返回值的 Callback 接口。
- ActionListener:用于 GUI 应用程序中的按钮点击等事件响应。
- CompletionHandler:用于异步 I/O 操作中的数据读写完成后的回调。
- MethodInterceptor: 用于在代理对象的方法调用时拦截并执行指定逻辑
- MethodInterceptor 能够拦截所有方法的调用,并指定实现逻辑
- MethodInterceptor 可以在代理对象方法执行前后执行预处理逻辑
- MethodInterceptor 可以获取当前代理的方法和参数信息
拦截器 MethodInterceptor
下面是 MethodInterceptor 接口的方法以及描述:
方法 | 描述 |
---|---|
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) | 在代理对象的方法调用前后,添加自己的操作逻辑 |
其中,参数解释如下:
- obj:代理对象
- method:代理对象调用的方法
- args:代理对象方法的参数
- proxy:MethodInterceptor 接口实现类创建的代理对象
层流分析
层
项目结构
-
annotation
- 功能型注解
- After
- 通过注解名给出相对位置
- 通过属性给出 全限定类名 和 待增强的方法名
- 通过注解间接获取 advice(提供增强功能的方法名)
- Before
- 通过注解名给出相对位置
- 通过属性给出 全限定类名 和 待增强的方法名
- 通过注解间接获取 advice(提供增强功能的方法名)
- ComponentScan
- 通过属性给出扫描的包路径
- After
- 标记型注解
- Aspect:标识当前类为 Aspect
- Bean:标识当前类为 Bean
- Configuration:标识当前类为 Configuration
- 功能型注解
-
aspect
- Aspect:切面配置(此处只设置了一个,可以为多个)
说明插入谁(待增强对象)和插在哪(BEFORE or AFTER),二者统称 Jointpoint,并给出对应的增强方法(advice) - AspectInfo:结构化的切面信息
- 源对象信息
- bean:源对象全限定类名
- method:源对象待增强方法名
- 增强信息
- aspect:切面名称
- advice:辅助功能方法名
- pos:位置(BEFORE or AFTER)
- 源对象信息
- AspectProc:切面信息处理器(只能有一个)
用于扫描并收集所有的切面信息
- Aspect:切面配置(此处只设置了一个,可以为多个)
-
config
- Config:Bean 配置器
用于指定扫描的包
- Config:Bean 配置器
-
constant
- Constant:常量
-
entity
-
User
普通的实体类 -
Work
普通的实体类
-
-
factory
- BeanFactory
用于扫描并收集所有的 Bean 和 Configuration
用于通过注解生成 Bean(IOC)
- BeanFactory
-
utils
- CglibProxyUtils:动态代理处理器
提供“输入待增强对象和增强信息,输出增强后的对象”的功能 - GetClassesFromPackage:类名扫描器
提供根据包名获取包下面所有的类名的功能
- CglibProxyUtils:动态代理处理器
流
IOC 机制执行流程
- 配置:通过注解获取 Bean 信息
- 获取带 @Configuration 注解的类
- 在带 @Configuration 注解的类中获取扫描的包
- 在待扫描包中获取 Bean,统一收集到 HashMap<String, String> beans 中
- 获取:通过 Bean 名称(若不指定,则在 BeanFactory 中处理时以类名作名称)获取 Bean 对象
AOP 机制执行流程
- 配置:通过注解获取 AspectInfo 信息
- 获取带 @Aspect 注解的类
- 获取 @Before 注解中的信息,将其结构化整理为 AspectInfo,统一收集到 List<AspectInfo> aspectInfos 中
- 获取 @After 注解中的信息,将其结构化整理为 AspectInfo,统一收集到 List<AspectInfo> aspectInfos 中
- 拦截:通过 AspectInfo 信息,对待增强对象进行增强
- 获取 List<AspectInfo> aspectInfos 中涉及到的方法的方法名称 Set<String> methodNames(引入 Set,以实现去重)
- 引入临时变量 proxy,方便实现多次代理
- 引入循环实现多次代理
- 动态获取 Before 方法并调用
- 调用 target 原有方法
- 动态获取 After 方法并调用