掌握SpringAOP

Posted 你这家伙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了掌握SpringAOP相关的知识,希望对你有一定的参考价值。

我们在了解SpringMVC的时候,我们使用@ControllerAdvice来完成统一异常处理或响应的统一数据格式封装,这其实就是我们的AOP思想,AOP是面向切面编程的一种编程语言,但是和语言无关,那么今天就让你对AOP思想不在陌生

背景介绍

AOP(Aspect-Oriented Programming):面向切面编程。
AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、
继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。如传统的后端三层架构分
层设计模式为:Controller、Service、DAO三层,每一层对应不同的作用

  • Conytroller 抽象了业务中,接受HTTP请求,解析/校验请求数据等操作,是前端控制器
  • Service 抽象了业务中,比较复杂的业务逻辑,是业务处理类
  • DAO 抽象了业务中,对数据的处理,是数据访问类
    其调用次序为:Controller方法调用Service方法,Service方法调用DAO方法

假如有以下三个功能:付款、转账及贷款功能,要实现统一的业务,如下图
(单个业务纵向执行:Controller -> Service -> DAO)

要在以上所有功能中,都加入记录HTTP请求执行时间的功能,需要在以上三个Controller方法
中,以方法执行完毕的时间点,减去方法执行前的时间点,得到方法执行时间。这样的代码往往横向地
散布在所有对象层次中,而与它对应的对象的核心功能毫无关系。这样的编码设计会产生大量的重复代
码,且对原有代码的侵入性非常大,也就是我们说的代码耦合性很高。

这种散布在各处的无关的代码被称为横切(Cross Cutting),在OOP设计中,它导致了大量代码的重 复,而不利于各个模块的重用。

什么是AOP

AOP(Aspect Oriented Programming)称为面向切面编程,在程序开发中主要用来解决一些系统层面
上的问题(如日志,事务,权限等),利用一种称为"横切"的技术,将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面,使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点

AOP的使用场景及作用

对于统一业务来说,常见切面业务如:

  1. 响应统一的数据格式
  2. 统一异常处理
  3. 统一日志记录,如跟踪用户访问信息
  4. 用户统一会话管理、权限管理
  5. 方法执行时间计算
  6. 事务管理
    ……

AOP技术解决了对横切业务的统一管理

  • 横切代码的高度复用,和业务代码互相独立,满足代码设计上的高内聚低耦合
  • 系统更好的扩展性,可维护性

静态代理

例如现在有以下代码

public class AliPayService implements PayService 
    
    @Override
    public void pay() 
        //1.安全检查
        System.out.println("安全检查");
        //2.记录日志
        System.out.println("记录日志");
        //3.时间统计开始
        System.out.println("记录开始时间");   
        //支付业务逻辑
        System.out.println("支付宝支付...");
        //4.时间记录结束
        
       

存在问题1
比如现在有支付宝和微信两种支付手段,然后每种支付方式都有很多一样的逻辑,此时就会出现

  • 每个实现类都有大量的重复代码
  • 对原有代码的侵入性,必须修改原有代码

解决办法
那肯定很多人都会把公共部分提取出模板或者公共的父类方法。然后让调用的类来使用公共模板或者继承公共的父类

存在问题二
虽然问题一解决了代码的重复问题,但是是必须要求实现类继承统一的父类,会对实现类的代码造成侵入性。那么在不改动原有代码的基础如何实现呢?

解决办法
使用代理设计模式,代理模式又分为静态代理和动态代理两种模式,关键在于:

  • 被代理类:原始类不进行任何修改,但创建和使用时,不再使用原始的被代理类,而是设计一个原始类的代理类
  • 代理类:基于被代理类,构造一个代理类,在使用时,也是使用代理类

静态代理

从代码设计上解决,可以使用静态代理设计模式。该设计模式可以采取两种手段:继承的方式,或聚合+接口的方式。

继承的方式

import org.example.demo.service.AliPayService;

//通过继承的方式实现静态代理类
class AliPayServiceStaticProxyByExtends extends AliPayService 
    @Override
    public void pay() 
        //1.安全检查
        System.out.println("安全检查");
        //2.记录日志
        System.out.println("记录日志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        //支付业务逻辑:实际还是调用父类的业务
        super.pay();
        //4.时间统计结束
        System.out.println("记录结束时间");
   

 public static void main(String[] args) throws InterruptedException 
        //使用时,不再直接使用原有对象,而是使用代理对象,所以是new代理类
        AliPayService aliPayService = new AliPayServiceStaticProxyByExtends();
        //使用:发起支付业务
        aliPayService.pay();
   


接口的方式

import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;

public class StaticProxyByImplements implements PayService 
    private PayService payService;
    //构造方法中传入被代理的对象
    public StaticProxyByImplements(PayService payService) 
        this.payService = payService;
   
    @Override
    public void pay() 
        //1.安全检查
        System.out.println("安全检查");
        //2.记录日志
        System.out.println("记录日志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        //支付业务逻辑:其实还是调用传入的被代理类的方法
        payService.pay();
        //4.时间统计结束
        System.out.println("记录结束时间");
   
   public static void main(String[] args) 
        //使用时,同样是使用代理类:通过被代理类创建代理类
        PayService aliPayService = new StaticProxyByImplements(new
AliPayService());
        //使用代理类完成支付业务
        aliPayService.pay();
   

静态织入技术

从 AOP 的技术实现上看,可以通过 java 代码编译期及类加载期动态的织入字节码来解决。
java程序的开发、执行流程为:

使用一些静态织入的字节码技术,如 AspectJ,可以完成在真正运行前就织入字节码的内容。

  • 编译期织入:在Java编译期,采用特殊的编译器,将切面的字节码织入到生成的 class 字节码文件中。可以在编译 java 文件为 class 文件时织入,也可以在已生成的 class 文件再次织入。
  • 加载期织入:通过特殊的类加载器,在类被加载进虚拟机之前织入。

动态代理

上面的织入技术这种设计模式成为动态代理模式,都是在class代码运行期,动态的织入字节码

动态代理的实现方式:JDKCGLIB 的方式,这两种方式的代理目标都是被代理类中的方法,在运行期,动态的织入字节码生成代理类

  • CGLIB是Java中的动态代理框架,主要作用就是根据目标类和方法,动态生成代理类。
  • Java中的动态代理框架,几乎都是依赖字节码框架(如 ASM,Javassist 等)实现的。
  • 字节码框架是直接操作class字节码的框架。可以加载已有的class字节码文件信息,修改部分信息,或动态生成一个class

JDK实现

JDK实现时,先通过实现 InvocationHandler 接口创建方法调用处理器,再通过 Proxy 来创建代理
类。

//动态代理:使用JDK提供的api(InvocationHandler、Proxy实现),此种方式实现,要求被代理类必
须实现接口
public class PayServiceJDKInvocationHandler implements InvocationHandler 
    
    //目标对象即就是被代理对象
    private Object target;
    
    public PayServiceJDKInvocationHandler( Object target) 
        this.target = target;
   
    
    //proxy代理对象
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable 
        //1.安全检查
        System.out.println("安全检查");
        //2.记录日志
        System.out.println("记录日志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        //通过反射调用被代理类的方法
        Object retVal = method.invoke(target, args);
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
   
    public static void main(String[] args) 
        PayService target=  new AliPayService();
        //方法调用处理器
        InvocationHandler handler =
            new PayServiceJDKInvocationHandler(target);
        //创建一个代理类:通过被代理类、被代理实现的接口、方法调用处理器来创建
        PayService proxy = (PayService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]PayService.class,
                handler);
        proxy.pay();
   

CGLIB实现

public class PayServiceCGLIBInterceptor implements MethodInterceptor 
    //被代理对象
    private Object target;
    
    public PayServiceCGLIBInterceptor(Object target)
        this.target = target;
   
    
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy
methodProxy) throws Throwable 
        //1.安全检查
        System.out.println("安全检查");
        //2.记录日志
        System.out.println("记录日志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        //通过cglib的代理方法调用
        Object retVal = methodProxy.invoke(target, args);
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
   
    
    public static void main(String[] args) 
        PayService target=  new AliPayService();
        PayService proxy= (PayService) Enhancer.create(target.getClass(),new
PayServiceCGLIBInterceptor(target));
        proxy.pay();
   

JDK和GCLIB实现的区别

  1. JDK 实现,要求被代理类必须实现接口,之后是通过 InvocationHandler 及 Proxy ,在运行时
    动态的在内存中生成了代理类对象,该代理对象是通过实现同样的接口实现(类似静态代理接口实
    现的方式),只是该代理类是在运行期时,动态的织入统一的业务逻辑字节码来完成。
  2. CGLIB 实现,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类对象。(因此不能被final修饰,因为被final修饰就不能够实现继承)

SpringAOP的实现

Spring AOP由 spring-aop 、 spring-aspects 和 spring-instrument 3 个模块组成。
SpringAOP构建在动态代理基础上,因此Spring对AOP的支持局限于方法拦截。
SpringAOP支持 JDK 和 CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使用 AOP 会基于JDK 生成代理类,没有实现接口的类,会基于 CGLIB 生成代理类

注意:虽然Spring AOP由 spring-aop 、 spring-aspects 和 spring-instrument 3 个模块组成,但是SpringAOP值提供了AspectJ的注解语法支持,并没有真的实现AspectJ的编辑器,也就是说,加入spring-aspects依赖包,只是可以使用 AspectJ 的语法,运行时还是基于spring-aop 依赖包的动态代理实现

SpringAOP示例

AOP已经形成了自己的术语。描述切面的常用术语有通知(advice),切点(pointcut)和连接点(joinpoint) ![在这里插入图片描述](https://img-blog.csdnimg.cn/9aca0dc630de45ffbd7cb126ccef0cbc.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3draDE4ODkxODQzMTY1,size_16,color_FFFFFF,t_70)

切面(Aspect)

切面(Aspect)由 切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。 切面由容器中的Bean使用@Aspect注解实现,如:
@Aspect
@Component
public class PojoAspect 
    

连接点(Join Point)

  • 应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切
    面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

  • 在 Spring AOP 中,Join Point 总是方法的执行点, 即只有方法连接点。所以使用SpringAOP,无需使用
    连接点的代码

切点(Pointcut)

在 Spring 中, 所有的方法都可以认为是 Join Point,但是我们并不希望在所有的方法上都添加 Advice, 而 Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述)来匹配Join Point,给满足规则的 Join Point 添加 Advice。

如:
在切面类中的定义一个 public void 的普通方法,并使用切点注解@Pointcut

@Aspect
@Component
public class PojoAspect 
    
    /**
     * 切点方法:查找org.example.demo.controller包下,以Test开头,Controller结尾的类,
     * 在找到的类中,再查找以pojo开头的方法。
     */
    @Pointcut("execution(* org.example.demo.controller.Test*Controller.pojo*
(..))")
    public void pojoPointcut() 
   

  • :匹配任意字符,只匹配一个元素(包,类,或方法,方法参数)
  • … :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用。
  • :表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.cad.Car+ ,表示继承该类的所有
    子类包括本身

通知(Advice)

切面也是有目标的 ——它必须完成的工作。在AOP术语中,切面的工作被称之为通知。 通知:定义了切面是什么,何时使用,其描述了切面要完成的工作,还解决何时执行这个工作的问题。Spring切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
  • 前置通知
    使用@Before:通知方法会在目标方法调用之前执行。
    后置通知
  • 使用@After:通知方法会在目标方法返回或者抛出异常后调用。
  • 返回之后通知
    使用@AfterReturning:通知方法会在目标方法返回后调用。
  • 抛异常后通知
    使用@AfterThrowing:通知方法会在目标方法抛出异常后调用。
  • 环绕通知
    使用@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行
    为。

织入

织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
  • 编译期
    切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载器
    切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入(load-time weaving. LTW)就支持以这种方式织入切面。
  • 运行期
    切面在应用运行的某一时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。SpringAOP就是以这种方式织入切面的。

常见面试题:

  1. AOP的概念、使用场景、优点
  2. springAOP的实现方式(JDK和CGLIB),以及两种方式的区别
  3. springAOP的注解(如:@Aspect(切面)、@Pointcut(切点)、以及通知的几种注解)

以上是关于掌握SpringAOP的主要内容,如果未能解决你的问题,请参考以下文章

SpringAOP的简单使用

SpringAOP 面向切面编程-环绕通知(掌握)

SpringAOP理解

二:SpringAOP

一文带你掌握Spring AOP的底层实现

一文带你掌握Spring AOP的底层实现