【Spring】之 SpringAOP 理论与实践

目录



一、AOP 简介


1、OOP 与 AOP

OOP(Object Oriented Programming面向对象编程,允许开发者定义纵向的关系,但并不适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用

而 AOP(Aspect Oriented Programming面向切面编程,则作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑(比如日志、事务等),抽取并封装为一个可重用的模块,这个模块被命名为切面Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

2、横切关注点

为了深入理解 AOP,我们需要理解一个叫横切关注点cross-cutting concern)的概念,它描述的是我们应用中的功能特点,即假如有一个功能,它在应用程序中很多个地方都用到了,那么我们把这样的功能称之为横切关注点

​日常开发中,我们都会将不同的业务场景抽象出对应的模块进行开发,而不同的模块,除了那些针对特定领域的核心功能外,还有一些相同的辅助功能,比如日志管理、安全管理、事务管理等等。横切关注点这个概念其实就点明了:类似这样的功能就是我们面向切面编程需要关注的地方。这也是面向切面编程的意义所在:它帮助我们实现横切关注点和他们所影响的对象之间的解耦

面向切面编程的实质,就是将横切关注·点模块化成被称为切面的特殊的类


二、SpringAOP 简介


1、SpringAOP 与 AspectJ

AOP 实现的关键在于代理模式,AOP 代理主要分为静态代理动态代理。静态代理的代表为 AspectJ,动态代理则以 Spring AOP 为代表:

  • AspectJ 是静态代理的增强:所谓静态代理,指的是 AOP 框架会在编译阶段生成 AOP 代理类,因此也称为编译时增强,它会在编译阶段将 AspectJ(切面)织入到 Java 字节码中,运行的时候就是增强之后的 AOP 对象。
  • Spring AOP 使用的是动态代理:所谓的动态代理,指的是 AOP 框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个 AOP 对象,这个 AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

2、SpringAOP 动态代理的种类

Spring AOP 中的动态代理主要有两种方式:JDK 动态代理和 CGLIB 动态代理:

  • JDK 动态代理只提供接口的代理,不支持类的代理。核心 InvocationHandler 接口和 Proxy 类,InvocationHandler 通过 invoke() 方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy 利用 InvocationHandler 动态创建一个符合某一接口的的实例,生成目标类的代理对象。
  • 如果代理类没有实现 InvocationHandler 接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现 AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代理的。

静态代理与动态代理区别在于生成 AOP 代理对象的时机不同,相对来说 AspectJ 的静态代理方式具有更好的性能,但是 AspectJ 需要特定的编译器进行处理,而 Spring AOP 则无需特定的编译器处理。


三、SpringAOP 相关术语


1、切面(Aspect

切面就是被抽取的公共模块。可能会横切多个对象。 在 Spring AOP 中,切面可以使用通用类(基于模式的风格) 或者在普通类中以 @AspectJ 注解来实现。

2、连接点(Join point

连接点就是程序中我们需要应用通知的地方。这个点可以是我们调用方法时、抛出异常时或甚至是修改某一个字段的时候。切面代码可以通过这些点插入到应用的正常流程中,使原本的功能增添新的行为。

在 Spring AOP 中,一个连接点总是代表一个方法

3、通知(Advice

切面的工作被称为通知。也就是定义了切面的要做什么,以及什么时候做的问题。Spring 切面有5种类型的通知,相应的在 SpringBoot 中有对应的五个注解:

  • 前置通知Before Advice):在某连接点之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常);对应 SpringBoot 的注解为 @Before
  • 后置通知After Advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出);对应 SpringBoot 的注解为@After
  • 返回通知After-returning Advice):在某连接点正常完成后执行的通知,例如,一个方法没有抛出任何异常,正常返回;对应 SpringBoot 的注解为 @AfterReturning
  • 异常通知After-throwing Advice):目标方法抛出异常退出时执行的通知;对应 SpringBoot 的注解为 @AfterThrowing
  • 环绕通知Around Advice):包围一个连接点的通知。这是最强大的一种通知类型。 环绕通知可以在目标方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。 环绕通知是最常用的一种通知类型。大部分基于拦截的 AOP 框架,例如 NanningJBoss4,都只提供环绕通知;对应 SpringBoot 的注解为 @Around

注意: 这里有两个重要的区别需要留意:

  • 后置通知和返回通知的区别:后置通知应用时机在返回通知之后,任何情况下都会应用,而返回通知只有方法正常执行,正常返回后执行。
  • 环绕通知与其它通知的区别:不同于其它的通知,环绕通知有目标方法的执行权,能够控制目标方法是否执行。而其它的通知更多是对目标方法的一个增强,无法影响目标方法的执行。

同一个切面 Aspect,不同通知 Advice 的执行顺序:

  • 没有发生异常的情况下的执行顺序:
    • around before advice
    • before advice
    • target method(目标方法或连接点)
    • after returning advice
    • after advice
    • around after advice
  • 发生异常的情况下的执行顺序:
    • around before advice
    • before advice
    • target method(目标方法或连接点)
    • after throwing
    • after advice

4、切点(Pointcut

我们的应用程序可能会有很多个连接点需要我们应用通知,所以我们有必要把连接点进行分类汇总,抽象出相同的特点,好让正确的切面切入到正确的地方去,各司其职,而不是切入所有的连接点。切点定义了一个切面需要在哪里进行切入,是一堆具有特定切面切入需求的连接点的共性抽象

我们通常通过明确类和方法名、或者匹配正则表达式的方式来指定切点。

5、目标对象(Target Object

被一个或者多个切面(Aspect)所通知(Advise)的对象。也可以把它叫做被通知对象。 既然 Spring AOP 是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied) 对象。

6、引入(Introduction

引入也被称为内部类型声明inter-type declaration),声明额外的方法或者某个类型的字段。Spring 允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使 bean 实现 IsModified 接口,以便简化缓存机制。

引入能够让我们在不修改原有类的代码的基础上,添加新的方法或属性

7、织入(Weaving

织入是指把切面应用到目标对象并创建新的代理对象的过程。


四、SpringAOP 实战


1、实战场景

我们以这样一个场景为例:打印服务的开始和结束信息,并输出服务处理的时长。

我们以 SpringBoot 为例,SpringBoot 引入 AOP 依赖后,spring.aop.auto 属性默认是开启的,也就是说只要引入了 AOP 依赖后,默认已经增加了 @EnableAspectJAutoProxy 注解。

2、实战步骤

1)引入依赖

首先在项目中引入 SpringAOP 的依赖:

<!--引入AOP依赖-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2)创建注解

创建一个自定义注解 @MyLog,用 boolean 类型的字段来决定是否开启日志输入功能,代码如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
    boolean isOn() default true;
}

3)定义切面

@Aspect
@Component
public class MyAspect {

    @Pointcut("execution(* com.example.demo.controller.AspectController.*(..))")
    private void onePointcut() { }

    @Around("onePointcut()")
    private Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        // 获取目标方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 获取方法上的注解,根据参数判断是否开启AOP
        MyLog myLog = method.getAnnotation(MyLog.class);
        if (myLog == null || !myLog.isOn()) {
            System.out.println("MyLog is off, stop working aop.");
            return new Object();
        }
        System.out.println("MyLog is on.");

        // 获取目标方法参数信息
        Object[] args = joinPoint.getArgs();
        String serviceName = (String)args[0];
        System.out.println("--- " + serviceName + " start ---");
        // 计算方法花费时长
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        System.out.println("--- " + serviceName + " end,耗时: " + (end - start) + "ms ---");

        return result;
    }
}

代码中,我们设置了一个切点:

@Pointcut("execution(* com.example.demo.controller.AspectController.*(..))")
private void onePointcut() { }

@Pointcut 注解表示声明一个切点,里面需要配置切点指示符,这里我们使用 execution 指示器,它是一个用来匹配连接点为方法的切点指示器,匹配规则解析如下:

  • 第一个 * 号表示不关心方法的返回值类型;
  • 中间的 com.example.demo.controller.AspectController.* 为目标方法的特点;
  • 第二个 * 号表示匹配 AspectController 类下的任意方法;
  • (..) 表示不关心参数个数和类型。

想了解更多关于切点指示符的话,可以参考我的另一篇博客:【Spring】之 SpringAOP 指示符详解

4)编写测试接口

@RestController
@RequestMapping("/aop")
public class AspectController {

    @GetMapping("/{name}")
    @MyLog(isOn = true)
    public void testMyLog(@PathVariable("name") String serviceName) throws InterruptedException {
        System.out.println("This is a controller for testing MyLog AOP!");
        // 模拟业务耗时
        Random random = new Random();
        Thread.sleep(random.nextInt(100));
    }

}

测试结果如下:

MyLog is on.
--- 业务1 start ---
This is a controller for testing MyLog AOP!
--- 业务1 end,耗时: 76ms ---

3、各种通知的执行顺序

上面的实战例子简单介绍了如何使用 AOP,接下来我们完善所有通知类型,分析各种通知的执行顺序,包括没有发生异常时执行顺序和发生异常后的执行顺序。

代码如下:

@Aspect
@Component
public class MyAspect {

    @Pointcut("execution(* com.example.demo.controller.AspectController.*(..))")
    private void onePointcut() { }

    @Before("onePointcut()")
    private void before() {
        System.out.println("--- before");
    }

    @After("onePointcut()")
    private void after() {
        System.out.println("--- after");
    }

    @AfterReturning("onePointcut()")
    private void afterReturning() {
        System.out.println("--- afterReturning");
    }

    @AfterThrowing("onePointcut()")
    private void afterThrowing() {
        System.out.println("--- afterThrowing");
    }

    @Around("onePointcut()")
    private Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        // 获取目标方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 获取方法上的注解,根据参数判断是否开启AOP
        MyLog myLog = method.getAnnotation(MyLog.class);
        if (myLog == null || !myLog.isOn()) {
            System.out.println("MyLog is off, stop working aop.");
            return new Object();
        }
        System.out.println("MyLog is on.");

        // 获取目标方法参数信息
        Object[] args = joinPoint.getArgs();
        String serviceName = (String)args[0];
        System.out.println("--- " + serviceName + " start ---");
        // 计算方法花费时长
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        System.out.println("--- " + serviceName + " end,耗时: " + (end - start) + "ms ---");

        return result;
    }

}

然后修改测试接口,增加业务名称的一个长度判断,如果名称太长,抛出异常,来模拟方法执行过程中发生异常:

@RestController
@RequestMapping("/aop")
public class AspectController {

    @GetMapping("/{name}")
    @MyLog(isOn = true)
    public void testMyLog(@PathVariable("name") String serviceName) throws InterruptedException {
        System.out.println("This is a controller for testing MyLog AOP!");
        if (serviceName.length() > 5) {
            throw new RuntimeException("名称过长!");
        }
        Random random = new Random();
        Thread.sleep(random.nextInt(100));
    }

}

1)正常执行的结果如下:

MyLog is on.
--- 业务1 start ---
--- before
This is a controller for testing MyLog AOP!
--- afterReturning
--- after
--- 业务1 end,耗时: 103ms ---

从中可以看到正常执行的结果顺序如下:

  • around before advice
  • before advice
  • target method(目标方法或连接点)
  • after returning advice
  • after advice
  • around after advice

注意:在环绕通知中的 joinPoint.proceed(); 代码表示调用目标方法,在它之前就是 around before advice,之后的就是 around after advice

2)发生异常的结果如下:

MyLog is on.
--- 这时长业务名称 start ---
--- before
This is a controller for testing MyLog AOP!
--- afterThrowing
--- after
java.lang.RuntimeException: 名称过长!

从中可以看到发生异常后的执行顺序如下:

  • around before advice
  • before advice
  • target method(目标方法或连接点)
  • after throwing
  • after advice

并且,当发生异常后,先执行异常通知,不执行返回通知,并且后置通知不管什么情况都会执行。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340