手写Spring框架---AOP实现

目录

容器是OOP的高级工具

系统需求

关注点分离Concern Separation

原有实现

AOP的成员

Advice的种类

单个Aspect的执行顺序

多个Aspect的执行顺序

Introduction-引入型Advice

代理模式

JDK动态代理

Spring AOP的实现原理之JDK动态代理

Spring AOP的实现原理之CGLIB动态代理

JDK动态代理和CGLIB

自研框架AOP机制1.0

自研框架的AOP 1.0待改进的地方

AspectJ框架

自研框架AOP2.0


  • 容器是OOP的高级工具

  • 以低耦合低侵入的方式打通从上到下的开发通道
    • 按部就班填充代码逻辑实现业务功能,每层逻辑都可无缝替换
    • OOP将业务程序分解成各个层次的对象,通过对象联动完成业务
    • 无法很好地处理分散在各业务里的通用系统需求
  • 系统需求

  • 码农才去关心的需求
    • 添加日志信息:为每个方法添加统计时间
    • 添加系统权限校验:针对某些方法进行限制
    • OOP下必须得为每个方法都添加通用的逻辑工作,增加维护成本
  • 关注点分离Concern Separation

  • 不同的问题交给不同的部分去解决,每部分专注解决自己的问题
    • Aspect Oriented Programming就是其中一种关注点分离的技术
    • 通用化功能的代码实现即切面Aspect
    • Aspect之于AOP,就相当于Class之于OOP,Bean之于Spring
  • 原有实现

  • 在不改变业务代码的基础上,增加系统需求
  • @Aspect:告诉Spring这是一个需要被织入的通用逻辑,它修饰的类也需要被Spring作为bean管理起来所以可以使用@Component
  • @Pointcut:告诉Spring往哪里织入Aspect里面的逻辑
  • JoinPoint: 切面可以织入逻辑的地方;表示调用的方法;不能直接调用proceed方法,可以先强转为ProceedingJointPoint再使用
  • @Around:在方法执行的时候进行织入
  • 将被代理方法的结果返回
  • @AfterReturning:在方法返回结果时调用相关逻辑

  • 注意:
  • Spring是默认不开启支持aop注解的逻辑的,需要告诉Spring去开启aop注解的逻辑;即添加@EnableAspectJAutoProxy 注解

  • AOP的成员

  • 如果将Aspect比作类的话,Advice就是里面的方法
    • 切面Aspect:将横切关注点逻辑进行模块化封装的实体对象
    • 通知Advice:好比是Class里面的方法,还定义了织入逻辑的时机
    • 连接点Joinpoint,允许使用Advice的地方
    • SpringAOP默认只支持方法级别的Joinpoint
    • 切入点Pointcut:定义一系列规则对Joinpoint进行筛选
    • 目标对象Target:符合Pointcut条件,要被织入横切逻辑的对象
  • 织入:将Aspect模块化的横切关注点集成到OOP里
  • 织入器:完成织入过程的执行者,如ajc
  • Spring AOP则会使用一组类来作为织入器以完成最终的织入操作
  • Advice的种类

  • BeforeAdvice:在JoinPoint前被执行的Advice
  • AfterAdvice:好比try…catch...finally里面的finally(不管是否抛出异常都会被执行)
  • AfterReturningAdvice:在Joinpoint执行流程正常返回后被执行(若期间抛出了异常就不会被执行)
  • AfterThrowingAdvice:Joinpoint执行过程中抛出异常才会触发
  • AroundAdvice:在Joinpoint前和后都执行,最常用的Advice
  • 单个Aspect的执行顺序

  • 先去执行@Around 上半身的逻辑,即调用proceed方法之前的,再这就是@Before里面的逻辑,之后就是执行目标对象joinpoint的方法
  • 再就是执行@Around 方法的下半身逻辑(proceed之后的),之后就是@After
  • 如果正常返回就调用@AfterReturing,否则调用@AfterThrowing(前提是Around里面没有catch相关的异常,仅仅只是抛出也可以否则不会走到这步)
  • 多个Aspect的执行顺序

  • order值越小,优先级越高
  • 优先级高的入操作会被优先执行,优先级低的出操作会被优先执行
  • Introduction-引入型Advice

  • 为目标类引入新接口,而不需要目标类做任何实现
  • 使得目标类在使用的过程中转型成新接口对象,调用新接口的方法
  • @DeclareParent 可实现
  • 代理模式

  • 代理角色:代理类
  • 被代理角色:被代理类
  • 抽象主题:抽象类或者接口
  • 代理类和被代理类都需要继承或者实现抽象主题,而实例化的时候要使用代理类,用户只需要面向接口或者抽象类编程
  • Spring AOP就是实现一个代理类来替换掉实现类来对外提供服务
  • 例子:
    • 用户用支付宝支付,用户不需要关心支付宝怎么从银行卡里提取并且转帐给店家
    • 从银行卡里提取并且转帐给店家会委托给支付宝去整
    • 支付宝作为代理类

  • 用AlipayToC作为ToCPaymentImpl的代理
  • 主函数测试:

  • 以上是静态代理:代理对象在编译时就实现了
  • 他不能满足业务需要,因为针对的目标对象不同的话我们都需要单独实现一个代理对象

  • 针对不同的目标对象都需要去单独实现一个代理对象,而代理对象的逻辑都是一样的,都要创建一一对应的实现类,这也是OOP的局限
  • JDK动态代理

  • 类是通过类加载器加载进来的,该过程主要做3件事
  • 1---通过带有包名的类来获取对应class文件的二进制字节流
  • 2---根据读取的字节流,将代表的静态存储结构转化为运行时数据结构(有了数据结构才能供外界进行数据访问)
  • 3---生成一个代表该类的Class对象,作为方法区该类的数据访问入口
  • 字节流能定义类的行为,根据一定规则去改动或者生成新的字节流
  • 将切面逻辑织入其中,使其动态生成织入切面逻辑的类,该机制就为动态代理机制
  • 根据接口或者目标类,计算出代理类的字节码并加载到JVM中去
  • JDK动态代理小结:
  • java反射包中的Proxy类可以调用newProxyInstance创建一个代理类对象
  • 传入三个参数:
    • 一个是代理类的类加载器
    • 一个是所有的实现接口数组
    • 一个是调用处理器的实现类
  • 创建好了代理对象,代理对象就可以执行被代理类实现的接口的方法
  • 在执行方法时,会先去执行调用处理器实现类中的invoke方法
  • invoke方法就可以对被代理类进行功能增强
  • Spring AOP的实现原理之JDK动态代理

  • 静态代理:在编译前进行实现,完成后代理类是一个实际的class文件
  • 动态代理是动态生成的,编译完成后并没有实际的class文件,而是在运行时动态生成类的字节码,并加载到JVM中
    • 要求被代理的类必须实现接口
    • 并不要求代理对象去实现接口,所以可以复用代理对象的逻辑
  • InvocationHandler
  • 只是用来统一管理横切逻辑的Aspect
  • 只有实现了InvocationHandler接口的类才具有代理的功能

    • proxy:真实的代理对象
    • method:所要调用的目标对象的方法实例(对应上面例子就是ToBPaymentImpl 的pay()方法实例)
    • args:方法里面需要用到的参数
  • 静态代理需要显式地调用代理类的方法,例如上面toCProxy.pay()
  • 而对于动态代理来讲,显式调用的是目标对象的方法,而不是代理类的方法
  • 动态代理就是直接调用ToCPaymentImpl的方法,静态代理就是调用代理类的方法
  • Proxy
  • 主要用来创建动态代理类的

    • ClassLoader loader:类加载器
    • Class[] interfaces:给代理类提供什么样的接口数组,代理类会最终实现这些接口
    • InvocationHandler h:当前动态代理的方法被调用的时候会关联到哪一个InvocationHandler实现类实例上
  • 用来封装通用的横切逻辑相当于Aspect
  • 用来保存被代理的对象也就是目标类
  • 执行被代理的方法,invoke需要传入被代理的实例以及参数

  • 工具类里面去调用proxy.newProxyInstance去针对AlipayInvocationHandler创建出对应的动态代理
  • 生成代理类的实例

  • 主函数调用

  • JDK动态代理机制是通过让动态代理类来实现和被代理类一样的接口从而在运行中去替代被代理类来工作的
  • Spring AOP的实现原理之CGLIB动态代理

  • 使用CGLib实现动态代理,完全不受代理类必须实现接口的限制
  • 而且CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,比使用Java反射效率要高
  • 唯一需要注意的是,CGLib不能对声明为final的方法进行代理,因为CGLib原理是动态生成被代理类的子类
  • 代码生成库: Code Generation Library
    • 不要求被代理类实现接口
    • 内部主要封装了ASM Java字节码操控框架
    • 对于代理没有生成接口的类,CGLIB很适用
    • 动态生成子类以覆盖非final的方法,绑定钩子回调自定义拦截器(从本质上说对于需要CGLIB代理的类,CGLIB只是生成子类以覆盖非final的方法,绑定钩子回调自定义拦截器)
  • MethodInterceptor
  • 是AOP项目中的拦截器,它拦截的目标是方法,而不是请求
  • 作用:定义横切逻辑Aspect
  • 好比JDK动态代理的InvocationHandler,实现MethodInterceptor就可以定义出Aspect

  • 分别对应参数:动态代理对象、被代理的方法对象实例、被代理对象方法需要的参数数组、动态代理生成的method对象实例
  • Enhancer
  • CGLIB库的Enhancer在Spring AOP中作为一种生成代理的方式被广泛使用
  • 它是一个字节码增强器,可以用来为无接口的类创建代理
  • 它的功能与java自带的Proxy类挺相似的
  • 它会根据某个给定的类创建子类,并且所有非final的方法都带有回调钩子
  • 在Spring中经常可以看到他的身影比如@Configuration 注解的类就会被Enhancer代理
  • 代理时底层用了个字节码处理ASM
  • 创建代理类例子:

    • 切面Aspect逻辑
    • 创建代理类
    • 支持不实现接口的类的动态代理
    • 没实现接口的类
    • 主函数测试
  • CGLIB支持对实现接口的类的织入、无实现接口的类的织入
  • JDK动态代理和CGLIB

  • 实现机制:
    • JDK动态代理:基于反射机制实现,要求业务类必须实现接口
    • CGLIB:基于ASM机制实现,生成业务类的子类作为代理类
  • JDK动态代理的优势:
    • JDK原生,在JVM里运行较为可靠(不需要额外的jar包依赖)
    • 平滑支持JDK版本的升级(而cglib需要升级支持在新的jdk上的使用)
  • CGLIB的优势:
    • 被代理对象无需实现接口,能实现代理类的无侵入
    • 使用字节码技术生成代理类,比使用Java反射效率要高
  • Spring AOP的底层机制:
    • CGLIB和JDK动态代理共存
    • 默认策略:Bean实现了接口则用JDK,否则使用CGLIB
  • 为什么CGLIB即支持实现接口的代理也支持没有实现接口的代理,还要用JDK代理呢?
    • 因为JDK动态代理不依赖其他的package,并且兼容性好,性能也不差
  • 自研框架AOP机制1.0

  • 使用CGLIB来实现:不需要业务类实现接口, 相对灵活
  • 思路:
    • 解决标记的问题(识别Aspect以及Advice),定义横切逻辑的骨架
    • 定义Aspect横切逻辑以及被代理方法的执行顺序
    • 将横切逻辑织入到被代理的对象以生成动态代理对象
  • 解决横切逻辑的标记问题以及定义Aspect骨架
  • 步骤:
    • 定义与横切逻辑相关的注解
    • 定义供外部使用的横切逻辑骨架
  • 定义注解:

  • value表示当前被@Aspect 标记的横切逻辑是会织入到被属性值注解标签标记的那些类里
  • 例如当value为@Controller,则表示会将Aspect逻辑织入到所有被@Controller 标记的类里
  • 指定@Aspect 的执行顺序

  • 定义框架支持几种Advice
  • 作为钩子方法,就是空实现,让用户选择是否实现

  • 之所以用钩子方法而不是用注解的形式,主要为了实现的简单
  • 用户只需要对DefaultAspect进行继承选择实现里面的钩子方法,就能实现对目标类方法的事前和事后的织入,满足绝大多数的需求
  • 实现Aspect横切逻辑以及被代理方法的定序执行
    • 创建MethodInterceptor的实现类
    • 定义必要的成员变量---被代理的类以及Aspect列表
    • 按照Order对Aspect进行排序
    • 实现对横切逻辑以及被代理对象方法的定序执行
  • 如果一个AlipayMethodInterceptor去对应一个Aspect会有问题,因为每个Aspect都要去实现Interceptor方法,但是它里面会调用invokeSuper方法
  • 调用被代理类的方法,如果存在多个Aspect,invokeSuper方法会被多次调用
  • 同时我们无法像Spring一样先升序执行所有Aspect的beforeAdvice之后再一路执行afterAdvice,所以一对一的关系不可行
  • 能否实现一个MethodInterceptor对应多个Aspect? 可以的
  • 可以在MethodInterceptor里面保存一个有序的Aspect列表用来保存排序好的Aspect集合
  • 之后遍历Aspect集合先顺序执行Aspect集合里面的before逻辑,之后只需要执行一次invokeSuper方法,逆序执行Aspect里面的after逻辑
  • 向被代理类对象的方法里面添加横切逻辑,因为是CGLIB,所以要继承MethodInterceptor
  • 定义两个成员变量
  • 接收与实现类对应的order值
  • AspectInfo用来封装@Order和DefaultAspect的属性值

  • 将其排序好之后再赋值给对应的成员变量
  • 排序以及定义成员变量的逻辑
  • 既然已经接受了order的属性值,为什么没有aspect的属性值?
  • 到给构造函数传参的时候,我们已经确定了那些aspect是为targetClass织入逻辑的了,毕竟这里就是绑定两者的关系
  • 因此aspect标签的值在创建该类的实例之前已经用来确定两者的关系了,就不再需要传递
  • 测试:
  • 导包:

  • 实现:

  • 实现了MethodInterceptor是远远不够的,还需要Handler来创建动态代理对象

  • 将横切逻辑织入到被代理的对象以生成动态代理对象
  • 从容器当中筛选出两种bean,一种是被Aspect标记的bean,一种是被代理的bean
  • 在构造器里获取容器单例实例赋值给成员变量
  • 不要忘记将被Aspect标签标记的类放到Spring容器里面管理

  • 被Aspect标记的类里面的属性值可能是不同的(即织入的目标类集合是不同的,需要按照Aspect的属性值进行归类)
  • 针对key(被Controller、Service标记的类进行织入操作)织入的是各自的Aspect列表
  • 获取织入前的目标类集合,比如说你传入Class,就可以获得被Class标签标记的所有的Class对象实例(传入Controller,就可以获得被Controller标签标记的所有对象实例)
  • 完成了对容器里所有需要加入Aspect横切逻辑的bean实例的织入操作

  • 依赖注入和AOP织入谁先执行?
  • 我们希望bean都被织入了横切逻辑之后才供外界去使用,这其中也包含了被@Autowired 标记的属性对应的bean
  • 所以AOP的注入操作要先于依赖注入执行的
  • 自研框架的AOP 1.0待改进的地方

  • Aspect只支持对被某个标签标记的类进行横切逻辑的织入(没法对被Controller标签标记的某些部分类进行织入)
  • 需要披上AspectJ的外衣
  • AspectJ框架

  • 提供了完整的AOP解决方案,是AOP的Java实现版本
  • 定义切面语法以及切面语法的解析机制
  • 提供了强大的织入工具
  • Spring AOP只支持方法级别的织入,但是AspectJ几乎支持所有连接点的织入,称为AOP的一套完整的解决方案

  • 既然AspectJ那么强大,为什么Spring不直接复用AspectJ呢?
  • 因为方法级别的织入已经能满足绝大多数的需求了,并且Aspect的学习成本比Spring AOP高很多,还没有Spring AOP用起来简单;花20%的代价完成80%的需求
  • AspectJ框架的织入时机:静态织入和LTW
    • 编译时织入:利用ajc,将切面逻辑织入到类里生成class文件
    • 编译后织入:利用ajc,修改javac编译出来的class文件
    • 类加载期织入:利用java agent,在类加载的时候织入切面逻辑
  • 前两种为静态织入,因为在class文件编译好之后切面逻辑已经织入好了(写死进了代码里面)
  • CGLIB和JDK动态代理都是依靠继承关系来生成类对应的代理对象,即最终会多出一个动态代理类
  • 自研框架AOP2.0

  • 引入jar包

  • 折衷方案改进框架里的AOP
  • 使用最小的改造成本,换取尽可能大的收益---理清核心诉求
  • 需求:我们为了让织入目标的选择(Pointcut)更加灵活
  • 即只需要引入AspectJ的切面表达式和相关的定位解析机制
  • 创建一个解析类解析AspectJ即可
  • 不再使用固定的注解标签去筛选被代理的类,所以将Aspect标签下的属性名从value改为pointcut,类型也改为String

  • PointcutParser实例是需要被创建出来并且装配上相关的语法树,才能识别注解Aspect属性里面的pointcut表达式
  • PointcutExpression:是PointcutParser根据表达式解析出来的产物,用来判断某个类或者方法是否匹配pointcut表达式
  • 针对类级别的判断

  • 精细筛选:需要对被代理类里的方法将他传入到pointcutExpression方法里去做精准的匹配
  • 完全匹配:alwaysMatches

  • 之前按照aspect标签的属性值进行分类没有意义,因为不同的pointcut表达式可能获取同样的被代理的目标
  • 例如不同的表达式均可获得一样的目标
  • 加上我们为每一个被Aspect标签标记的类创建一个专门的PointcutLoader,去按照Aspect里面对应的expression也就是pointcut表达式匹配目标类以及目标方法,因此就不需要进行Aspect的分类了

  • 将符合条件的AspectInfo给筛选出来
  • 使用foreach的方式去遍历某个集合,但在遍历的时候会对集合元素进行移除操作,此时会报错,为什么?
  • 因为foreach里面用到的iterator对象是工作在一个独立的线程中,并且会拥有一个mutex的锁,iterator在创建之后会建立一个指向原来对象的单列索引表,当原来的数量发生变化时(即集合发生变化时),索引表的内容不会同步地去改变,所以当索引指针向后移动时,就找不到要迭代的对象了,就会抛出异常
  • 因此iterator在工作时不允许被迭代的对象动态的变化
  • 也就是不支持调用集合的remove方法在遍历的过程中动态地移除元素
  • 但是我们可以使用iterator本身的remove方法来删除对象
  • 因为它会在删除当前迭代对象的同时去维护之前说的索引表的一致性
  • 对初筛列表的精筛

  • pointcut表达式解析及定位,粗筛精筛

  • 进行aop织入

  • 通知(Advice)1 & 切面(Aspect)

  • 通知(Advice)2 & 切面(Aspect)

  • 目标对象(Target)

  • 测试成功:

猜你喜欢

转载自blog.csdn.net/weixin_59624686/article/details/131276305