Spring AOP详解之面向切面编程

一、AOP—另一种编程思想

AOP(Aspect-Oriented Program,即面向切面编程)和OOP(Object-Oriented Programming, 面向对象编程)是Java中两种不同的抽象设计架构。AOP是通过预编译方式,在运行期通过代理的方式向目标类织入增强代码实现程序功能的统一维护的一种技术。Spring 中 AOP 代理由 Spring 的 IoC 容器负责生成和管理,其依赖关系也由 IoC 容器负责管理,因此两者相辅相成。

1. AOP与OOP

AOP与OOP是面向不同领域的两种设计思想。

  • OOP(面向对象编程)针对业务处理过程的实体及其属性行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。OOP的基本单位是类(class)
  • AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果AOP的基本单位是Aspect(切面)

OOP实际上是对对象的属性和行为的封装,而是处理某个步骤和阶段的,从中进行切面的提取。也就是说,如果几个或更多个逻辑过程中,有重复的操作行为,AOP就可以提取出来,运用动态代理,达到程序功能统一维护的效果。

这么说来可能太含蓄,如果说到权限判断,日志记录等,可能就明白了。如下图所示:

Spring AOP详解之面向切面编程_第1张图片

2. 为什么需要AOP

上面我们提到了AOP与OOP,但是如果我们单纯使用OOP,那么权限判断怎么办?在每个操作前都加入权限判断?日志记录怎么办?在每个方法里的开始、结束、异常的地方手动添加日志?所以,如果使用AOP就可以借助代理完成这些重复的操作,就能够在逻辑过程中,降低各部分之间的耦合了。二者扬长补短,互相结合最好。

首先,在面向切面编程的思想里面,把功能分为核心业务功能和周边功能

  • 所谓的核心业务,比如登陆,增加数据,删除数据都叫核心业务
  • 所谓的周边功能,比如性能统计,日志,事务管理等等

周边功能(一般为重复的功能)在 Spring 的面向切面编程AOP思想里,即被定义为切面。

在面向切面编程AOP的思想里面,核心业务功能和切面功能分别独立进行开发,然后把切面功能和核心业务功能 "编织" 在一起,这就叫AOP。

AOP主要应用于日志记录,性能统计,安全控制,事务处理以及缓存等方面,它是为程序员解耦而生,并且对核心业务代码零侵入

因此我们知道了为什么要使用AOP以及AOP做的事情了:

  • 为什么使用AOP:切面将系统中重复的事情(包括日志记录,性能统计,安全控制,事务处理)解耦出来,将功能分为核心和周边功能,从而达到解耦系统的重复代码和工作的效果;
  • AOP做的事情:因为切面是为解耦重复的任务而生的,因此AOP只对与业务无关的周边功能进行切面。不用操作业务部分代码(也可以说不影响到正常业务代码的使用,但是如果切面有影响,那么影响也是全局的)。

3. AOP的相关术语

下面详细了解一些AOP的概念:

  • 切面(Aspect) :实现通用问题的类,例如日志,事务管理,定义了切入点和通知的类,通知和切入点共同组成了切面:时间、地点、做什么。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的Advisor或拦截器实现。
  • 连接点(Joinpoint) :程序能够使用通知(Advice)的一个时机,即程序执行过程中明确的点,一般是方法的调用,或异常被抛出。
  • 通知(Advice) :类似Spring拦截器或者Servlet过滤器,是方法,定义切面要做什么,何时使用,有Before、After、Around等。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链(递归拦截器)。
  • 切点(Pointcut) :定义切面发生在哪里,带了通知的连接点,例如某个类或方法的名称,在程序中主要体现为切入点表达式。AOP框架必须允许开发者指定切入点,例如,使用正则表达式。
  • 引入(Introduction) :添加方法或字段到被通知的类。Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现IsModified接口,来简化缓存。
  • 目标对象(Target Object) :包含连接点的对象,也被称作被通知或被代理对象。
  • AOP代理(AOP Proxy) :AOP框架创建的对象,包含通知。在Spring中,AOP代理可以是JDK或CGLIB动态代理
  • 编织(Weaving) :把切面应用到目标对象时,创建新的代理对象的过程。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

AOP指示器参数

通知方法注解

总结

  • 切入点(Pointcut) 在哪些类,哪些方法上切入(where
  • 通知(Advice) 在方法执行的什么实际(when: 方法前/方法后/方法前后)做什么(what: 增强的功能)
  • 切面(Aspect) 切面 = 切入点 + 通知,通俗点就是:在什么时机,什么地方,做什么增强!
  • 织入(Weaving) 把切面加入到对象,并创建出代理对象的过程。(由 Spring 来完成)

4. AOP的实现方式

AOP 要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。

AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码,其实就是代理模式的典型应用。 按照 AOP 框架修改源代码的时机,可以将其分为四类:

  • 静态 AOP : 在编译阶段对程序源代码进行修改,生成了静态的 AOP 代理类(生成的 *.class 文件已经被改掉了,需要使用特定的编译器),比如 AspectJ。
  • 动态 AOP : 在运行阶段对动态生成代理对象(在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类),如 SpringAOP。
  • 动态代码字节生成:在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中。
  • 自定义类加载器:在运行前,目标加载前,将切面逻辑加到目标字节码中。

Spring AOP详解之面向切面编程_第2张图片

因此Spring AOP是基于JDK和CGLib动态代理实现的,AspectJ是基于ASM与自定义ClassLoader实现的。

5. JDK和CGLib动态代理的区别

Spring AOP的实现是JDK和CGLib动态代理,这两种代理方式的使用了策略模式。

  1. 既然Spring AOP的实现是动态代理,那么JDK和CGLIB动态代理有什么区别呢?
  • JDK动态代理是面向接口的。JDK动态代理只能对实现了接口的类生成代理,而不能针对类。底层原理是:利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,再调用具体方法前调用InvokeHandler来处理。
  • CGLIB动态代理是面向类的。CGLIB对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。主要是通过继承父类然后重写父类的方法实现,所以该类或方法最好不要声明成final。底层原理是:利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
  1. 何时使用JDK和CGLib?
  • 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP;
  • 如果目标对象实现了接口,可以强制使用CGLIB实现AOP;
  • 如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLib之间转换。

因此,Spring AOP默认是使用JDK动态代理,如果代理的类没有接口则会使用CGLib代理

  1. 那么我们开发时,JDK代理和CGLib代理该用哪个呢?

在《精通Spring4.x 企业应用开发实战》一书中给出了建议:

  • 如果是单例的我们最好使用CGLib代理,如果是多例的我们最好使用JDK代理。因此ASM框架一般是构建诸多开源框架的底层实现,但是随着JDK版本的提升,这个也有所改善。

原因:JDK在创建代理对象时的性能要高于CGLib代理,而生成代理对象的运行性能却比CGLib的低。

总结:

类型 机制 回调方式 适用场景 效率
JDK动态代理 委托机制,代理类和目标类都实现了同样的接口,InvocationHandler持有目标类,代理类委托InvocationHandler去调用目标类的原始方法 反射 目标类是接口类 效率瓶颈在反射调用稍慢
CGLIB动态代理 继承机制,代理类继承了目标类并重写了目标方法,通过回调函数MethodInterceptor调用父类方法执行原始逻辑 通过FastClass方法索引调用 非接口类,非final类,非final方法 第一次调用因为要生成多个Class对象较JDK方式慢,多次调用因为有方法索引较反射方式快,如果方法过多switch case过多其效率还需测试

二、技术选型

AOP的实现有Spring AOP、AspectJ、JBoss AOP等。

本篇文章只在一定维度上讨论目前主流的AOP框架:Spring AOP 和 AspectJ

1.Spring AOP和AspectJ

AspectJ是语言级别的AOP实现,扩展了Java语言,定义了AOP语法,能够在编译期提供横切代码的织入,所以它有专门的编译器用来生成遵守Java字节码规范的Class文件。

而Spring借鉴了AspectJ很多非常有用的做法,融合了AspectJ实现AOP的功能,可以说Spring AOP是新世纪的AspectJ。但Spring AOP本质上底层还是动态代理,所以Spring AOP是不需要有专门的编辑器配合(为了编织代码的各个方面, 它引入了称为 AspectJ 编译器 (ajc) 的编译器, 通过它编译我们的程序, 然后通过提供一个小型 (100K) 运行时库来运行它。)。

Spring aop 旨在提供一个跨 Spring IoC 的简单的 aop 实现, 以解决程序员面临的最常见问题。它不打算作为一个完整的 AOP 解决方案 —— 它只能应用于由 Spring 容器管理的 bean。

另一方面, AspectJ 是原始的 aop 技术, 目的是提供完整的 aop 解决方案。它更健壮, 但也比 Spring AOP 复杂得多。还值得注意的是, AspectJ 可以在所有域对象中应用。

对比总结

Spring AOP AspectJ
在纯 Java 中实现 使用 Java 编程语言的扩展实现
不需要单独的编译过程 除非设置 LTW,否则需要 AspectJ 编译器 (ajc)
只能使用运行时织入 运行时织入不可用。支持编译时、编译后和加载时织入
功能不强-仅支持方法级编织 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等
只能在由 Spring 容器管理的 bean 上实现 可以在所有域对象上实现
仅支持方法执行切入点 支持所有切入点
代理是由目标对象创建的, 并且切面应用在这些代理上 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入
比 AspectJ 慢多了 更好的性能,基准测试AspectJ 速度比 Spring AOP 快8到35倍
易于学习和应用 相对于 Spring AOP 来说更复杂

2. 应用场景

Spring AOP只是针对Spring IOC容器进行设计的aop实现,因此适用于Spring进行管理的项目;

AspectJ更复杂,因为它引入了 AspectJ Java 工具 (包括编译器 (ajc)、调试器 (ajdb)、文档生成器 (ajdoc)、程序结构浏览器 (ajbrowser)), 我们需要将它们与我们的 IDE 或生成工具。要使用 AspectJ,我们需要引入 AspectJ 编译器 (ajc) 并重新打包所有的库 (除非我们切换到编译后或加载时间的织入)。

Spring AOP更加简单、开箱即用,AspectJ性能更高但更复杂,但需要特定编译器配合。

具体应用场景有:日志记录,性能统计,安全控制,事务处理以及缓存等。

3. 结论

Spring AOP项目用于Spring项目,AspectJ应用于Spring生态外的项目。

三、Spring AOP实践

AOP 的思想:减少重复的工作,让重复代码与业务代码分离!

Spring AOP详解之面向切面编程_第3张图片

在上图中,用户购物的核心业务就是下单和付款。而浏览商品、咨询商品以及售后都是重复且边缘的工作,每一个人在进行交易之前以及交易之后都需要重复进行这些工作,因此可以交给边缘业务其他人代理完成。因此,AOP的实现就是:围绕核心业务,以核心业务的时间先后为切入点来做核心业务的切面。

1. Maven依赖

开发环境为SpringBoot FrameWork,故使用AOP只需添加下述依赖即可:

pom.xml



    org.springframework.boot
    spring-boot-starter-aop
    2.6.4

2. 创建目标接口和接口实现类

目标类就是我们的核心,以方法执行先后时间为切入点,下单前、下单和下单后等地方都是插入切面通知的地方。

这些地方的集合就是连接点,而某一个具体的位置就是切点,连接点就是切点的集合。

OrderService.java

package com.deepinsea.springbootaop.springaop.service;
​
/**
 * @author deepinsea
 * @date 2022/4/12
 */
public interface OrderService {
​
    /**
     * 目标方法(订单)
     */
    void order();
}

OrderServiceImpl.java

package com.deepinsea.springbootaop.springaop.service.impl;
​
import com.deepinsea.springbootaop.springaop.service.OrderService;
import org.springframework.stereotype.Service;
​
/**
 * @author deepinsea
 * @date 2022/4/12
 */
@Service
public class OrderServiceImpl implements OrderService {
​
    @Override
    public void order() {
        // 仅仅是实现了核心的业务功能
        System.out.println("下单");
        System.out.println("付款");
    }
}

3. 创建切面类

创建一个类,将这个类变为切面。

  • @Aspect 将该类变为一个切面
  • @Component -- 将该类注册到Spring的容器里面
  • 切入点表达式:execution(* 包名.类名.方法名(..))

spring aop中的五种通知方式:

  • @Before:前置通知,在目标方法被调用之前调用通知功能
  • @After:后置通知,在目标方法执行结束后,无论执行结果如何都执行通知定义的任务
  • @After-returning:后置通知,在目标方法执行结束后,如果执行成功,则执行通知定义的任务
  • @After-throwing:异常通知,如果目标方法执行过程中抛出异常,则执行通知定义的任务
  • @Around:环绕通知,在目标方法执行前和执行后,都需要执行通知定义的任务。

前置(@Before)与最终通知(@After)

OrderAspect.java

package com.deepinsea.springbootaop.springaop.common.aspect;
​
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
​
/**
 * @author deepinsea
 * @date 2022/4/11
 * 切面类
 */
@Component
@Aspect
public class OrderAspect {
​
    @Before("execution(* com.deepinsea.springbootaop.springaop.service.impl.OrderServiceImpl.order())")
    public void beforeOrder() {
        System.out.println("下单之前--浏览商品");
        System.out.println("下单之前--咨询商品");
    }
​
    @After("execution(* com.deepinsea.springbootaop.springaop.service.impl.OrderServiceImpl.order())")
    public void afterOrder(){
        System.out.println("下单之后--售后");
    }
}

在定义完切面类后,我们发现所有方法的execution表达式内容完全一样,因此可以提取为一个公共的。这个也叫做切点(pointcut),是Spring AOP的高级抽象。使用@Pointcut实现重复内容的提取。

切点(@Pointcut)

OrderAspectPointcut.java

package com.deepinsea.springbootaop.springaop.common.aspect;
​
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
​
/**
 * @author deepinsea
 * @date 2022/4/12
 * 切面类
 */
@Component
@Aspect
public class OrderAspectPointcut {
​
    /**
     * 通过注解@Pointcut定义切点,iOrder()只是一个切点标识,无所谓是什么,
     * 方法中内容本身也是空的,使用该切点的地方直接通过切点方法iOrder引用切点表达式。
     */
    @Pointcut("execution(* com.deepinsea.springbootaop.springaop.service.impl.OrderServiceImpl.order())")
    public void iOrder(){}
​
    @Before("iOrder()")
    public void beforeOrder() {
        System.out.println("下单之前--浏览商品");
        System.out.println("下单之前--咨询商品");
    }
​
    @After("iOrder()")
    public void afterOrder(){
        System.out.println("下单之后--售后");
    }
}

环绕通知(@Around)

环绕通知,顾名思义,其会在目标方法被调用前后均会执行切面代码的一种通知,其可将前置通知、后置通知的代码放在一个通知方法当中,使得代码逻辑更加清晰。

这是Spring AOP中最强大的通知类型,因为它集成了前面所有通知的功能,同时保留了连接点原有的方法的功能,因此一个可以干掉上面4个注解。

注意

  • 只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint。
  • 环绕通知需要返回返回值(对于有返回值的切点方法,本身为void的切面核心方法不用),否则真正调用者将拿不到返回值,只能得到一个null。
  • 环绕通知有控制目标方法是否执行、控制是否返回值和改变返回值的能力。

OrderAspectAround.java

package com.deepinsea.springbootaop.springaop.common.aspect;
​
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
​
/**
 * @author deepinsea
 * @date 2022/4/12
 * 切面类
 */
@Component
@Aspect
public class OrderAspectAround {
​
    // 环绕通知同样可以使用切点来简化
    @Pointcut("execution(* com.deepinsea.springbootaop.springaop.service.impl.OrderServiceImpl.order())")
    public void pointcutOrder(){}
​
    /**
     * 使用 @Around 注解来同时完成前置和最终通知
     * Spring AOP提供的ProceedingJoinPoint接口的proceed方法可以执行切点方法(并划分切点前后执行界限)
     * 以joinPoint.proceed()为界限,划分出前置通知、后置通知和最终通知以及异常通知
     */
    @Around("pointcutOrder()")
    public void aroundOrder(ProceedingJoinPoint joinPoint){
        System.out.println("下单之前--浏览商品");
        System.out.println("下单之前--咨询商品");
​
        try {
            joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
​
        System.out.println("下单之后--售后");
    }
​
    /**
     * 在有返回值的核心业务(切点)方法中,使用环绕通知需要给返回值,否则核心方法返回值为null;
     * 上面切点方法本身返回值为void,则不需要给返回值
     */
//    @Around("pointcutOrder()")
//    public Object aroundOrder(ProceedingJoinPoint joinPoint){
//        Object returnValue = null;
//        // 获取切点方法入参(形参)
//        Object[] args = joinPoint.getArgs();
//        // 执行前置方法
//        System.out.println("前置通知方法");
//        try {
//            returnValue = joinPoint.proceed(args);
//            System.out.println("后置通知方法");
//        } catch (Throwable e) {
//            e.printStackTrace();
//            System.out.println("异常通知方法");
//        }finally {
//            System.out.println("最终通知方法");
//        }
//
//        return returnValue;
//    }
}

上面三种方法都可以(使用其中一个,另外两个注释即可),只是一个更符合常规逻辑,一个进行了常规逻辑上的公共提取,另一个进行两者功能上的融合而已。推荐使用环绕通知进行aop配置。

扩展:Spring框架的设计非常优美,对多种设计模式进行了实现。在多切面执行时,就采用了责任链设计模式

4. 启动AOP代理功能

切面类,目标方法都创建完了,但是切面类在启动时还不能自动代理目标方法。因为现在切面类和目标方法只是注入到Spring IOC容器的Bean,没有开启 @EnableAspectJAutoProxy注解启动AspectJ自动代理,这个开关默认是关闭的。说到底,Spring AOP是一种除了Spring本身以外,另一种由用户代理注册到Spring的Bean的一种方式。

注意:SpringBoot中无需显式指定@EnableAspectJAutoProxy注解,默认为开启;但Spring FrameWork需要手动开启

(1)注解的方式

我们可以以配置类的方式,通过@EnableAspectJAutoProxy注解启动AspectJ自动代理:

AspectConfig.java

package com.deepinsea.springbootaop.springaop.common.config;
​
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
​
/**
 * @author deepinsea
 * @date 2022/4/12
 * 切面配置类
 *
 * Jdk代理:基于接口的代理,一定是基于接口,会生成目标对象的接口的子对象。
 * Cglib代理:基于类的代理,不需要基于接口,会生成目标对象的子对象。
 *
 * 1. 注解@EnableAspectJAutoProxy开启代理;
 * 2. 如果属性proxyTargetClass默认为false, 表示使用jdk动态代理织入增强;
 * 3. 如果属性proxyTargetClass设置为true,表示使用Cglib动态代理技术织入增强;
 * 4. 如果属性proxyTargetClass设置为false,但是目标类没有声明接口,spring aop还是会使用Cglib动态代理,也就是说非接口的类要生成代理都用Cglib。
 */
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
//@ComponentScan("com.deepinsea.springbootaop.springaop")
public class AspectConfig {
}

(2)配置文件的方式

application.yml

spring:
  # AOP 配置
  aop:
    auto: true #spring aop开关, 相当于标注了@EnableAspectJAutoProxy
    proxy-target-class: true #开启cglib代理

注意:默认EnableAspectJAutoProxy是关闭的,并且需要proxy-target-class选项一定需要开启,否则会报错。

如果proxy-target-class属值被设置为false或者这个属性被省略,那么标准的JDK 基于接口的代理将起作用。即使你未声明 proxy-target-class="true" ,但运行类没有继承接口,spring也会自动使用CGLIB代理(高版本spring自动根据运行类选择 JDK 或 CGLIB 代理)

默认情况下,我们会在接口实现类加上@service注解以及进行事务注解的声明(全局声明或方法上声明都可),这种做法提供接口类服务供rpc调用是没问题的,客户端只需引入相应的jar包,spring中配置bean,使用时@Resource或@Autowired注解注入都可以。

5. 测试类进行测试

SpringbootAopApplicationTests.java

package com.deepinsea.springbootaop;
​
import com.deepinsea.springbootaop.springaop.service.impl.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
@SpringBootTest
class SpringbootAopApplicationTests {
​
    @Autowired
    private OrderServiceImpl orderService;
​
    /**
     * 测试AOP切面功能
     */
    @Test
    void testAop() {
        orderService.order();
    }
​
}

启动后,测试结果:

  .   ____          _            __ _ _
 /\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )___ | '_ | '_| | '_ / _` | \ \ \ \
 \/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |___, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.4)
​
2022-04-12 03:25:04.600  INFO 57392 --- [           main] c.d.s.SpringbootAopApplicationTests      : Starting SpringbootAopApplicationTests using Java 1.8.0_311 on aries with PID 57392 (started by deepinsea in E:\IdeaProjects\MyProjects\springboot-in-action\springboot-aop)
2022-04-12 03:25:04.609  INFO 57392 --- [           main] c.d.s.SpringbootAopApplicationTests      : No active profile set, falling back to 1 default profile: "default"
2022-04-12 03:25:05.079  INFO 57392 --- [           main] c.d.s.SpringbootAopApplicationTests      : Started SpringbootAopApplicationTests in 0.661 seconds (JVM running for 1.44)
下单之前--浏览商品
下单之前--咨询商品
下单
付款
下单之后--售后
​
Process finished with exit code 0


原文链接:https://juejin.cn/post/7129100142749155336

 

你可能感兴趣的:(spring,java,jvm)