Spring框架中的面向切面编程,详细解析Spring AOP的基本原理,代理选择和实现模式

833 阅读14分钟

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

AOP基本概念

  • AOP: Aspect-oriented programming. 面向切面的程序设计
    • 将横切关注点进一步分离,以提高代码的模块化程度
    • 在现有的项目代码的基础上增加额外的通知Advice机制,能够对声明为切点Pointcut的逻辑功能代码块进行统一的管理与装饰
      • 横切关注点: 在项目的多个模块中都有出现的逻辑功能代码
    • 面向切面的程序设计可以将与代码核心业务逻辑联系较弱的功能添加到程序中,同时保证业务代码的可读性
  • AOP核心思想:
    • 从核心关注点分离出横切关注点是面向切面程序设计的核心
      • 分离关注点使得解决特定领域问题的代码从业务逻辑中独立出来
      • 业务逻辑中不再含有针对特定领域问题代码的调用
      • 业务逻辑和特定领域问题的关系通过切面来进行封装和维护
  • AOP和OOP的比较:
    • OOP: 面向对象的程序设计
      • OOP引入了封装,继承和多态来建立一个对象层次结构来模拟公共行为的集合.但是OOP无法为分散的对象引入公共行为
      • OOP适用于定义对象自上而下的关系,不适合定义对象从左到右的关系
    • AOP: 面向切面的程序设计
      • 使用横切的技术,分解封装对象的内部,将影响多个类的公共行为封装到一个可重用的模块称作切面
      • AOP通过横切的关系将与业务无关,但是被业务模块共同调用的逻辑和责任封装起来,减少项目中的重复代码,降低模块之间的耦合度,提升项目的操作性和维护性
  • AOP中的基本概念:
    • 切面: AspectJ
      • 模块化的横切关注点.也就是横向分散在多个功能模块中的特定逻辑功能
    • 连接点: Join point
      • 在项目执行过程中能够在项目中插入切面功能的项目中的某一个点
      • Spring AOPJoinpoint指代的是所有方法的执行点
      • 这个点可以是项目中某个方法调用前,调用后,方法抛出异常后等等
      • 切面利用这些连接点切入到正常的项目流程中,添加某些行为
        • AOP中的Joinpoint可以有多种类型:
          • 构造方法调用
          • 字段的设置和获取
          • 方法的调用
          • 方法的执行
          • 异常处理的执行
          • 类的初始化
          • 注意: Spring中只支持执行方法类型的joinpoint
    • 通知: Advice
      • AOP框架在特定的连接点处执行的动作
      • Spring AOP中提供5种类型的通知Advice:
        • 前置通知: Before. 在目标方法调用之前执行通知功能
        • 后置通知: After. 在目标方法调用完成之后执行通知功能.无论目标方法是否发生异常,后置通知都会执行
        • 后置返回通知: After-returning. 在目标方法返回值之后执行通知功能
        • 后置异常通知: After-throwing. 在目标方法抛出异常之后执行通知功能
        • 环绕通知: Around. 在目标方法调用之前和调用之后执行通知功能
    • 切点: Pointcut
      • 执行通知的一系列连接点的集合
      • Pointcut是一个描述信息,用于修饰Joinpoint, 通过Pointcut,AOP就可以确定哪些Joinpoint可以编织进入Advice
      • AOP通过切点定位到特定的连接点
      • 一个切点匹配多个连接点
    • 引入: Introduction
      • 添加方法或者字段到被通知的类
      • Spring支持引入接口到任何被通知的对象中
      • Spring中使用Introduction, 可以通过DelegatingIntroductionInterceptor来实现通知,通过DefaultIntroductionAdvisor来配置Advice和代理类要实现的接口
    • 目标对象: Target Object
      • 包含连接点的对象.也就是被通知或者被代理的对象
    • AOP代理: AOP Proxy
      • AOP框架创建的对象,包括通知
      • Spring中的AOP代理包括JDK动态代理和CGLIB代理
    • 织入: Weaving
      • 织入描述的是将切面应用到目标对象来创建新的代理对象的过程
        • Spring AOP使用了动态代理技术,在项目运行时织入切面
        • Spring AOP支持使用两种方式JDK动态代理和CGLIB代理生成代理对象.默认策略是如果目标类为接口,则使用JDK动态代理,如果目标类是一个类对象则使用CGLIB代理
  • AOP实现的两种方式:
    • 动态代理方式: 利用拦截方法,对拦截方法进行装饰,取代原有方法的执行
    • 静态织入方式: 引入特定的语法创建切面,使得编译器在编译期间织入切面的代码
  • AOP使用的四种方式:
    • 基于代理模式的AOP使用
    • @AspectJ注解驱动的切面
    • POJO切面,通过 <aop:config /> 配置切面
    • 注入式AspectJ切面,通过 <aop:aspectj-autoproxy /> 配置切面
  • Spring AOP的使用方法有以下三个步骤:
    • 定义增强方法,并在方法里使用AspectJ的注解标注Spring在何处调用方法
    • 在横切方法上使用注解 @Component@Aspect
    • @Configuration配置类上使用注解 @EnableAspectJAutoProxy
  • Spring AOP中XML配置使用示例
  • SpringBoot AOP中注解式使用示例
  • AOP中的切入点表达式: 切入点表达式用来指定切入点的目标
    execution([modifier-pattern]? [return-type-pattern] [declaring-type-pattern]? [name-pattern]([param-pattern])[throws-pattern]?)
    
    • modifier-pattern: 修饰符匹配
    • return-type-pattern: 返回值匹配
    • declaring-type-pattern: 类路径匹配
    • name-pattern: 方法名匹配
    • param-pattern: 参数匹配
    • throws-pattern: 异常类型匹配
    • ? : 表示该匹配项是可选项
      • 示例:
        • 匹配所有方法:
        execution(* *(..))
        
        • 匹配com.oxford.service包下面的所有公共方法:
        execution(public * com.oxford.service.*(..))
        
        • 匹配com.oxford.service包以及子包下面的所有方法:
        execution(* com.oxford.service..*.*(..))
        
    • 使用 @Pointcut定义切点时,可以使用 &&, ||! 逻辑运算符

面向切面的基础

  • 面向切面程序设计的核心概念就是从核心关注点中分离出横切关注点:
    • 分离关注点使得解决特定领域问题的逻辑功能代码从业务逻辑代码中独立出来
    • 业务逻辑代码中不再含有对特定领域问题的逻辑功能代码的调用
    • 业务逻辑代码同对特定领域问题的逻辑功能代码的关系通过切面来进行封装和维护
  • 关注点: Concern
    • 对软件工程有意义的,小的,可管理的,可描述的软件组成部分
    • 一个关注点通常只和一个特定概念或者目标相关联
  • 主关注点: Core Concern
    • 一个软件中的最主要的关注点
  • 关注点分离: Separation of concerns, SOC
    • 标识,封装和操作关注点的能力
  • 方法: Method
    • 用来描述,设计和实现一个给定关注点的软件构造单位
  • 横切: Crosscut
    • 如果实现两个关注点方法存在交集,那么这两个关注点相互横切
  • 支配性分解: Dominant decomposition
    • 将软件分解成模块的主要方式
      • 传统的程序设计语言以一种线性的文本来描述软件,只采用一种方式,比如类来将软件分解成模块
      • 这样使得某些关注点比较好捕捉,容易进一步组合,扩展
      • 但是仍然会存在一些关注点没有被捕捉,弥撒在整个软件内部
    • 支配性分解通常是按照主关注点进行模块分解的
  • 横切关注点: Crosscuting concerns
    • 在传统的程序设计语言中,除了主关注点可以被支配性分解的方式捕捉外,仍然有许多没有被支配性分解方式捕捉到的关注点
    • 这些关注点弥散在整个软件内部,这些关注点同主关注点是横切的
  • 切面: Aspect
    • 支配性分解的基础上提供的一种辅助的模块化机制
    • 切面的模块化机制可以捕捉横切关注点

AOP原理剖析

在这里插入图片描述

  • 后置处理器根据Bean是否配置Advisor判断是否需要进行动态代理

  • 如果需要进行动态代理,那么就构造创建ProxyFactory对象

  • 通过ProxyFactory对象的父类中的getAopProxyFactory() 方法获取AopProxyFactory对象,默认获取的是DefaultAopProxyFactory

  • 通过AopProxyFactory工厂createAopProxy() 方法创建实际的代理对象

  • ProxyBeanFactory:

在这里插入图片描述

  • Spring使用工厂模式创建每一个Proxy. 每一个Class类型,都会有一个相对应的ProxyFactoryBean
    • 对于生成Proxy的工厂而言,只需要知道对应的Advice信息即可
    • Advice信息维护在Advised,Advised可以根据特定的类名和方法名返回对应的AdviceChain, 来表示需要执行的Advice

在这里插入图片描述

  • JDK动态代理:
    • JDK动态代理是面向接口的JdkDynamicAopProxy生成代理对象
      • 溯源到JDK动态代理的字节码文件.在反编译的代码文件中可以发现代理类已经是继承了Proxy类,因为Java是单继承多实现的,所以不能再继承其余的类,但是代理类与目标类之间必须建立一种关系来保证代理对象可以被引用到,这样就只能实现接口
      • 目标类实现了至少一个接口时,那么代理类就可以通过实现与目标类相同的接口,使用接口类型的变量接收代理类的实例
    • JdkDynamicAopProxy类实现了AopProxy, 返回Proxy, 并且自身也实现了InvocationHandler
    • 也就是说,当我们使用proxy时,对proxy对象调用的方法,都会转到这个类的invoke() 方法中
  • Cglib代理:
    • 基于Cglib子类继承方式CglibAopProxy生成代理对象
Spring AOP代理选择代理描述实现机制优点
JDK动态代理1. 通过反射接受被代理的类,并且要求被代理的类必须实现一个接口
2. JDK动态代理的核心是InvocationHandler接口和Proxy类
通过Java的内部反射机制实现反射机制在生成类的过程中非常高效
CGLIB代理1. 通过继承的方式动态生成目标类的代理
2. 通过修改字节码的方式来实现代理,可以在运行时动态的生成某个类的子类
3. 如果某个类使用final修饰,那么无法使用CGLIB做动态代理
4. CGLIB代理的核心是Enhancer类
使用ASM框架实现. ASM框架是一种可以操作字节码的框架ASM在类生成之后的执行过程中非常高效

Spring AOP的代理选择

  • AOP类型的选择:
    • AspectJ框架
    • Spring AOP框架
      • 两种AOP类型分为基于XML配置文件的风格和基于 @AspectJ注解的风格
    • AspectJ和Spring AOP比较:
      • AspectJ:
        • AspectJ在功能完善度方法更具有优势
        • AspectJ使用静态织入的方式实现AOP
        • 依赖于特定的编译器ajc
      • Spring AOP:
        • Spring AOP中并没有提供完整的AOP功能,更加注重的是和IOC容器的结合来解决横切业务问题
        • Spring AOP使用动态代理构建内部机制的动态织入的方式实现AOP
        • Spring AOP只是使用了和AspectJ相同的注解,但是底层是通过动态代理技术实现AOP
        • 不依赖于AspectJ特定的编译器ajc
    • 基于XML配置文件的风格个基于@AspectJ注解的风格比较:
      • 基于XML配置文件的风格:
        • aop:config
          • proxy-target-class : 是否作为代理的目标类,默认为false
          • expose-proxy : 是否暴露代理的Bean. 如果暴露,就可以通过Spring AopContext获取到代理的Bean. 默认为不暴露
        • aop:pointcut
          • pointcut的解析是生成一个BeanDefinition, 并将配置的id,expression等属性保存到BeanDefinition
          • BeanDefinitionID来自配置的id属性,如果没有配置就自动生成ID
          • BeanDefinitionclass就是AspectJExpressionPointcut
          • BeanDefinitionscopeprototype
        • aop:advisor
          • advice-ref : 必须的属性.这里的advice必须实现org.aopalliance.aopAdvice的子接口
          • pointcut-ref : 构建一个RuntimeBeanReference对象指向员pointcut的引用
        • aop:aspect
          • ref: 必须的属性. 用来指定AOP中的标签是哪一个bean, 类和对象的方法
        • aop:declare-parents
        • 优点:
          • 基于XML配置文件由POJO支持,可以从配置文件中清楚地看出系统存在哪些方面Aspect
        • 缺点:
          • 基于XML配置文件没有完全封装在一个地方,需求点Joinpoint分为支持Bean类的声明和配置文件中的XML
          • 基于XML配置文件只支持单例的实例化切入点Pointcut模型,不能组合XML中声明命名的切入点
      • 基于@AspectJ注解的风格:
        • 基于 @AspectJ的注解支持额外的实例化模型和丰富的切入点组合
        • 基于 @AspectJ的注解可以将所有信息封装在一个模块Aspect中,将Aspect保持为模块化单元的优点
        • 基于 @AspectJ的注解可以很方便地迁移到AspectJ框架中
  • Spring AOP中代理的选择:
    • Spring AOP中的代理机制没有默认使用的代理 : (SpringBoot中默认开启了proxyTargetClass,默认都是使用CGLIB代理)
      • 如果代理的是实现接口的类,则使用JDK动态代理. 只要代理的类实现了至少一个接口,那么目标类型实现的接口都将会使用JDK动态代理
      • 如果代理的是子类,没有实现任何接口,则使用CGLIB代理
        • 也可以对实现接口的类强制使用CGLIB代理,需要满足以下条件:
          • 不能对final修饰的方法添加通知Advice, 因为final修饰的方法无法被覆盖
            • CGLIB代理中为了获取对目标类的引用,代理类必须要继承目标类,并且代理类会覆写所有publicprotected方法,在内部将调用委托给目标类
          • Spring 3.2之后不需要将CGLIB添加到项目类路径中,因为CGLIB已经包含在jarspring-core
          • Spring 4.0之后,因为CGLIB代理实例通过Objenesis创建,所以代理对象的构造函数不会被调用两次 ,CGLIB代理类不会初始化构造代理类自身继承的任何成员变量.想要访问只能通过gettersetter方法. 只有当JVM不允许时,才会有SpringAOP支持的双重调用
        • 使用CGLIB代理时,需要将 <aop:config />proxy-target-class属性设置为true
        • 如果使用 @AspectJ自动代理时需要强制使用CGLIB, 需要将 <aop:aspectj-autoproxy />proxy-target-class属性设置为true

Spring AOP的实现模式

  • 代理模式: 接口 + 真正实现类 + 代理类
    • 真正实现类和代理类都需要实现接口
    • 实例化时使用代理类进行实例化
    • Spring AOP的作用就是生成一个代理类来替换真正的实现类来对外提供服务
  • Spring中代理的实现模式:
    • Spring AOP中使用 @EnableAspectJAutoProxy注解,将BeanPostProcessor的子类AnnotationAwareAspectJAutoProxyCreator.class注入到容器中,然后在Bean生命周期中的后置处理器里判断切面的情况进行生成代理增强器,并且利用动态代理机制生成代理类
    • 真正实现类的逻辑包含在getBean() 方法中
    • getBean() 方法返回一个Proxy代理类实例
      • Proxy代理类实例时Spring通过JDK动态代理或者CGLIB代理动态生成的
  • 因为Spring AOP中要通过getBean() 方法查找和实例化容器中的Bean, 所以Spring AOP只能作用于Spring容器中的Bean, 无法作用于不是使用Spring IOC容器管理的对象
  • 总结:
    • Spring AOP的核心是代理创建器AbstractAutoProxyCreator的子类,本质上是一个Bean的后置处理器
    • Spring根据配置将相应的代理创建器注册到Spring容器中.如果项目中配置 @EnableAspectJAutoProxy注解 ,Spring就会将AnnotationAwareAspectJAutoProxyCreator注册到Spring容器中
    • 因为代理创建器是一个Bean的后置处理器,所以会在Bean的初始化阶段被调用
    • 代理创建器会判断当前的Bean是否需要被代理,如果不需要代理就直接将原Bean实例返回.如果需要代理就使用动态代理技术根据当前Bean作为目标类创建一个代理类,并且将横切代码注入到代理类中,然后生成一个代理类的实例并返回,使用这个实例作为Bean的实例
    • 如果这个Bean是单例的,那么这个代理对象就会放置到Spring容器的单例池中,这样使用getBean() 时,就可以直接从Spring容器中直接获取这个代理对象