当前位置: 首页 > news >正文

[spring] spring AOP - 面向切面编程の学习

[spring] spring AOP - 面向切面编程の学习

几年前开始还在被 spring 的八股文时,AOP 就是一个比较热也比较大的点,为了面试确实背过不少,不过 AOP 实现本身做的不多,一方面也是因为 AOP 一旦配置好了基本上就不需要改什么,我那个时候也是 entry 岗,还没进阶到碰 AOP 的程度

这次正好趁着重学 spring 的机会,补一下 AOP 相关的概念

从结构来说,AOP 可以很好的解决两个问题:

  1. 代码纠缠

    指的是业务逻辑纠缠在一起

  2. 代码分散

    更多的是代码散落的到处都是

以常见的输出日志为例,在以不用第三方库为前提的条件下,controller、service 这两个常见模块都会有 try-catch 的逻辑。那也就代表:

  1. logger 的逻辑和 service/controller 紧紧地纠缠在了一起
  2. logger 的代码散落在 service/controller 的代码中

使用 aspect 就可以很好的解决这个问题,它可以

  • 将这些横切关注点的逻辑进行抽离封装

    这样可以解决了上面 代码纠缠 和 代码分散 的问题,并且让代码修改和维护变得简单不少

    另外,修改 aspect 代码并不需要接触到主要的 java 项目,因此主项目并不需要被重新编译——这需要 aspect 实现遵从规范,保持松散的耦合度;不修改公共类的返回类型;以及进行模块化打包,让 aspcet 部分代码打包在不同的 jar 中

    这样也可以清理业务逻辑,更好的运行 single responsibility principle

  • 可以用在所有地方

    这个后面会提到,它可以使用 wildcard match,所以只要 pattern 写的对,那么就可以用在任何想用的地方

  • 同样的实现(class/aspect)基于配置实现

    这也是上面提到的配置问题

它的具体业务逻辑可以包括:应用代理设计模式、日志输出、安全检查、交易、审计日志、处理异常、管理 API 等

当然,spring aop 也不是没有缺点的:

  • 如果 AOP 太多,那么会导致 aop 的逻辑难以理解和追踪

  • aop 是在 runtime 时完成的 weaving,基于动态代理完成

    因此,太多的 aop 会导致运行速度显著下降

    另一个可以完成效能提升的时间方法是,在做 pointcut 的 matching 时,只包含当前项目的代码

术语

  • 切面(Aspect)- 对关注的逻辑/业务所进行的抽离/抽象

    比较常见的就是 logging 的业务,这里的抽象指的就是对 logger 功能的抽象

    当然,它其实是包含了 具体要执行的业务逻辑 和 需要匹配的业务路径

  • 连接点(Join Point)

    即可以被 aspect 切入的点,以 spring aop 来说,最小且唯一的 Join Point 是方法(method)

  • 切点(Pointcut)

    以 sping aop 来说,因为可以使用 wildcard 进行 match,所以它上可以 match 到

    • 方法名, 如 Service._(..)

    • 包名 / 类名, 如 com.example.service..*

    • 参数类型, 如 (..)(String, ..)

    • 返回类型, 如 void*

    并且可以使用 AND/OR 进行操作关联

    可以理解成,Pointcut 的实现是寻找到对应的 join point;即对 join point 的匹配,也就是 aspect 中需要匹配的业务路径——where

  • 通知(Advice):在匹配到 join point 所要执行的增强逻辑

    也就是 aspect 中提到的 具体要执行的业务逻辑——what

  • 目标对象 (Target):代理的目标对象

    即原始的业务逻辑,也就是未被增强的部分

    依旧以 logging 为例,原本需要打 log 的方法中的业务逻辑——大多数情况下是 CRUD 操作,就是 aspect 的 target

  • 引介(introduction):一种特殊的增强

    大体找了下,意思是:

    允许你给原本没有实现某接口的类,动态添加新的方法或属性

    但是这个方法使用方法比较 edge,本篇笔记中不会提到

  • 织入(Weaving):织入是将 advice 添加到 target 的 Join Point 的过程

    spring aop 实现 weaving 是在 runtime 中通过 proxy 实现的动态插入

AOP 执行过程
匹配
增强逻辑
调用实际方法
连接点 Join Point
切点 Pointcut
通知 Advice
目标对象 Target
切面 Aspect

补充一下,目前比较主流的 AOP 框架有 spring aop 和 AspectJ,spring aop 用了一点 AspectJ 的命名风(语法),但是具体的实现是独立的

spring aop 比 AspectJ 更轻量级,相对来说 AspectJ 的功能比 spring aop 更强大

目前来说,比较常见的业务使用 spring aop 就够了

advice 类型

下面是几个常见的 advice 类型:

  • @Before

    在方法执行前调用

  • @AfterReturning

    在方法成功执行后调用

  • @AfterThrowing

    在方法失败抛出异常后调用

  • @After

    在方法执行后调用

    @AfterReturning@AfterThrowing之 之间的关系有点像 try-catch-after

  • @Around

    在代码执行前后都会调用

下面的案例代码会在项目里面运行一下做个介绍,加入 spring aop 的方法可以在 pom 文件里面添加下面这个 dependency:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

⚠️:现在的新 spring boot 的项目,aop 都是自动开启的。但是如果跑的是老项目,导入 dependency 还是没有开启,需要手动添加 @EnableAspectJAutoProxy,让 spring 开启对 aop 的识别

@Before

代码还是比较简单的,pointcut 的部分下面会提到,这里具体就不讲了:

package com.example.aopdemo.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyDemoLoggingAspect {
    @Before("execution(public void addAccount())")
    public void beforeAddAccountAdvice() {
        System.out.println("\n=====>> Executing @Before advice on AddAccount()");
    }
}

main 代码中依旧使用 commandLineRunner 去执行:

package com.example.aopdemo;

import com.example.aopdemo.dao.AccountDAO;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class AopdemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(AopdemoApplication.class, args);
	}

	@Bean
	public CommandLineRunner commandLineRunner(AccountDAO accountDAO) {
		return runner -> {
			demoTheBeforeAdvice(accountDAO);
		};
	}

	private void demoTheBeforeAdvice(AccountDAO accountDAO) {
		accountDAO.addAccount();
	}

}

DAOImpl 只是保证有这个方法调用,并不是真的负责执行 CRUD 操作,执行结果如下:

在这里插入图片描述

👀:这里的部分内容是在 pointcut 之后写的,所以会涉及一些暂时还没提到的语法。不过对 match 稍微理解一点的应该能猜到,@Before 里的写法做的就是方法的 matching

@Before 通过 JoinPoint 获取参数

回顾一下:

即可以被 aspect 切入的点,以 spring aop 来说,最小且唯一的 Join Point 是方法(method)

这里的 JoinPoint 就是当前 advice 中,连接到的方法

@Before("com.example.aopdemo.aspect.AopExpressions.forDaoPackageNoGetterSetter()")
    public void beforeAddAccountAdvice(JoinPoint joinPoint) {
        System.out.println("=====>> Executing @Before advice on executionforDaoPackage() && !(getter() || setter())");

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        System.out.println("Method: " + methodSignature);
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            System.out.println(arg);

        }
    }

效果如下:

在这里插入图片描述

@AfterReturning

像上面提到过的, @AfterReturning 只有在方法成功运行没有抛出异常的情况下,才会执行。因此它大概率是需要能够接触到返回结果的,因此可以用 returning 去获取返回结果

⚠️:returning 中的名字,需要与 parameter 中的名字一致

@AfterReturning(pointcut = "execution(* com.example.aopdemo.dao.AccountDAO.findAccounts(..))", returning = "result")
    public void afterReturningAddAccountAdvice(JoinPoint joinPoint, List<Account> result) {
        String method = joinPoint.getSignature().toShortString();
        System.out.println("=====>> Executing @AfterReturning advice on method: " + method + " with result:");
        System.out.println("=====>> Result is: " + result + "\n");

    }

    @AfterReturning(pointcut = "com.example.aopdemo.aspect.AopExpressions.forDaoPackageNoGetterSetter()")
    public void afterReturningAddAccountAdvice(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().toShortString();
        System.out.println("=====>> Executing @AfterReturning advice on method: " + method);
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        System.out.println("Method: " + methodSignature);
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            System.out.println(arg);
        }
    }

运行结果如下:

在这里插入图片描述

修改返回结果

⚠️:这个操作,在大部分的实现里面是属于不太好的操作。正常需要对返回结果实现修改的操作,都应该放在 controller/service 中,而不是 advice 中

这里只是表示 aspect 中可以这么做,而不是推荐这么做

修改的代码为:

@AfterReturning(pointcut = "execution(* com.example.aopdemo.dao.AccountDAO.findAccounts(..))", returning = "result")
    public void afterReturningAddAccountAdvice(JoinPoint joinPoint, List<Account> result) {
        String method = joinPoint.getSignature().toShortString();
        System.out.println("=====>> Executing @AfterReturning advice on method: " + method + " with result:");
        System.out.println("=====>> Result is: " + result + "\n");

        convertAccountNamesToUpperCase(result);
        System.out.println("=====>> Result is: " + result + "\n");
    }

    private void convertAccountNamesToUpperCase(List<Account> result) {
        for (Account account : result) {
            account.setName(account.getName().toUpperCase());
        }
    }

效果其实已经贴在上面了

这种情况下,如果要修改 aspect,就需要对原本的 main app 也进行 recompile

@AfterThrowing

这个算是用的比较多的了,以我们现在的系统为例,任何的 exception 发生后都会触发各种各样的 email notification……

它的规则和 @AfterReturning 类似,只不过获取的不是返回结果,而是异常,代码实现如下:

    @AfterThrowing(pointcut = "execution(* com.example.aopdemo.dao.AccountDAO.findAccounts(..))", throwing = "ex")
    public void afterThrowingAddAccountAdvice(JoinPoint joinPoint, Throwable ex) {
        String method = joinPoint.getSignature().toShortString();
        System.out.println("=====>> Executing @AfterThrowing advice on method: " + method + " with exception:");
        System.out.println("=====>> Exception is: " + ex + "\n");
    }

一个 catch 并 swallow,另一个 catch 并且抛出的结果对比:

rethrowswallow
在这里插入图片描述在这里插入图片描述

@After

@After 是获取不到返回的数据或者是抛出的异常的,实现大体如下:

    @After("execution(* com.example.aopdemo.dao.AccountDAO.findAccounts(..))")
    public void afterFinallyAddAccountAdvice(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().toShortString();
        System.out.println("=====>> Executing @After (finally) advice on method: " + method);
    }

在这里插入图片描述

这里比较适合处理一些清理上下文、释放资源——手动分配的资源,如数据库这种 spring 自行管理的,还是让 spring 自己操作比较好、tracing 日志之类的操作,因为这些操作,不管业务逻辑成功还是失败,都是要执行的

@Around

@Around 在方法调用前后都会被触发,所以是最强大的 advice,因为这个特性,它可以执行很多的操作,如:

  • 全局统一处理异常——非消化异常,而是将其包装成统一的格式进行返回
  • 性能监控可以在调用前后进行计时,统计代码效率。当耗时太久时便可触发警报
  • logging 中参数对比管理
  • 用户校验
  • 熔断/限流

实现代码如下:

@Around("execution(* com.example.aopdemo.service.*.getTraffic(..))")
    public Object getTraffic(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("=====>> Around: Executing @Around advice on getTraffic()");

        long begin = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        long end = System.currentTimeMillis();
        long duration = end - begin;
        System.out.println("=====>> Around: Duration: " + duration / 1000.0 + " seconds");
        return result;
    }

效果如下:

在这里插入图片描述

@Around 处理异常

上面提到过 @Around 也可以处理异常,下面是实现方法:

    @Around("execution(* com.example.aopdemo.service.*.getTraffic*(..))")
    public Object getTraffic(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("=====>> Around: Executing @Around advice on getTraffic()");

        Object result = null;
        long begin = System.nanoTime();
        try {
            result = proceedingJoinPoint.proceed();
        } catch (Exception ex) {
            System.out.println("=====>> Around: Exception: " + ex);
            result = "Major Accident! But no worries, your private AOP helicopter is on the way!";
        }

        long end = System.nanoTime();
        long duration = end - begin;
        System.out.println("=====>> Around: Duration: " + duration + " nanoseconds.");

        return result;
    }


效果如下:

在这里插入图片描述

其实这里比较推荐的是将异常统一包装后,再用 throw ex; 进行抛出。具体的业务逻辑还是在具体的地方实现比较好。如果统一在 advice 中处理,那么很有可能在某一个时间段,就会发生有些异常被 advice 消化了,而没有正确的在业务逻辑中被处理,那对于 debug 来说,也是非常大的挑战

最后补充一下 sequence diagram:

调用者 代理对象 (AOP) 目标方法 调用方法() @Around(前置逻辑) proceed() @Before(方法执行前) 执行业务逻辑 返回结果 @AfterReturning(方法成功后) @Around(后置逻辑) @After(最终通知) 返回结果 @Before(方法执行前) 抛出异常 @AfterThrowing(捕获异常) @Around(后置逻辑) @After(最终通知) 抛出异常 alt [如果方法成功返回] [如果方法抛出异常] 调用者 代理对象 (AOP) 目标方法

pointcut

这里主要提一下 pointcut 怎么做 match

pointcut 表达式

方法名 match -> exact match

这是前面出现过的写法:

@Before("execution(public void updateAccount())")

这里的话只会 match 到 public void updateAccount() 这一个方法,而且返回类型类型必须是 void,而且是个无参方法。如果调用了其他的方法,或者是返回类型/参数不一样,那么就都不会触发这个 advice

main 部分如下:

package com.example.aopdemo;

import com.example.aopdemo.dao.AccountDAO;
import com.example.aopdemo.dao.MembershipDAO;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class AopdemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(AopdemoApplication.class, args);
	}

	@Bean
	public CommandLineRunner commandLineRunner(AccountDAO accountDAO, MembershipDAO membershipDAO) {
		return runner -> {
			demoTheBeforeAdvice(accountDAO, membershipDAO);
		};
	}

	private void demoTheBeforeAdvice(AccountDAO accountDAO, MembershipDAO membershipDAO) {
		accountDAO.addAccount();
		membershipDAO.addAccount();
	}

}

效果截图:

在这里插入图片描述

类 match -> exact match

这里会提供一个完整的路径(fully qualified classname)去做一个完整的 match:

    @Before("execution(public void com.example.aopdemo.dao.AccountDAO.addAccount())")
    public void beforeAddAccountAdvice() {
        System.out.println("\n=====>> Executing @Before advice on AddAccount()");
    }

换言之,这个 advice 只会在制定类下的 AccountDAO 调用 addAccount 才会触发。方法名的 match 上面提到了

这里 main 没有修改,效果如下:

在这里插入图片描述

可以看到 MembershipDAOImpl 中的方法没有触发 advice

match 所有 add 开始的方法

这里就可以用 wildcard 了:

@Before("execution(public void add*())")
    public void beforeAddAccountAdvice() {
        System.out.println("\n=====>> Executing @Before advice on Add*()");
    }

效果如下:

在这里插入图片描述

⚠️:这里还是有返回类型和参数的限制

返回类型 match

这里的 advice 还是使用 void,但是 addMembership 的返回类型换成了 boolean,可以看到 addMembership 没有出发 advice:

在这里插入图片描述

如果换成了 wildcard match,那么 addMembership 也能触发了:

在这里插入图片描述

参数 match

这里主要有这么几种:

  • () 无参

  • (SomeClass) exact match 类型

    如:

    @Before("execution(* add*(com.example.adpdemo.Account))")

    这里就需要保证,传进去的参数必须是 com.example.adpdemo.Account

    效果如下:

    在这里插入图片描述

  • (*) 匹配 1 个任意格式的参数

  • (..) 匹配 0-n 个任意格式的参数

    这个写法中,第一个参数的要求依旧是 com.example.adpdemo.Account,只是对于后面的参数没有限制,因此 addMembership 依旧不会被触发:

    在这里插入图片描述

    将第一个参数 com.example.adpdemo.Account 移除后,当前 advice 对 argument 没有任何的匹配,这样所有的方法都能触发 advice 了:

    在这里插入图片描述

package 匹配

这个和之前的写法比较类似:

@Before("execution(* com.example.adpdemo.dao.*.*(..))")

需要注意的是,如果不对方法进行匹配,就需要额外再加一个 *,上面这个匹配具体的理解方法是:任意参数 com.example.adpdemo.dao.包下的方法.任意方法(..)

在这里插入图片描述

❗:如果想匹配所有的 sub package 下的方法,写法为:

@Before("execution(* com.example.adpdemo.dao..*.*(..))")

pointcut 装饰器

之前的写法都在反复的 cv execution 这一段,其实 spring aop 提供了 pointcut 装饰器,用以提供 expression 的复用性,实现如下:

package com.example.aopdemo.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyDemoLoggingAspect {
    @Pointcut("execution(* com.example.aopdemo.dao.*.*(..))")
    private void forDaoPackage() {
    }

    @Before("forDaoPackage()")
    public void beforeAddAccountAdvice() {
        System.out.println("\n=====>> Executing @Before advice on execution(* add*(com.example.aopdemo.Account, ..))");
    }

    @Before("forDaoPackage()")
    public void performApiAnalytics() {
        System.out.println("\n=====>> Performing API analytics");
    }
}

最终效果和反复 cv execution 的结果是一样的:

在这里插入图片描述

组合 pointcut 表达式

即添加 and, or 和 not 的操作:

    @Pointcut("execution(* com.example.aopdemo.dao.*.get*(..))")
    private void getter() {}

    @Pointcut("execution(* com.example.aopdemo.dao.*.set*(..))")
    private void setter() {}

    @Pointcut("forDaoPackage() && !(getter() || setter())")
    private void forDaoPackageNoGetterSetter() {}

结果如下:

在这里插入图片描述

aspect order

在运行过程中,流程可以确定的是从 before -> after returning/throwing -> after。但是在同一个周期内,不同 advice 运行的默认顺序是无法规定的。如果想要控制同一个生命周期内,多个 advice 的运行顺序,那么就可以通过 @Order 去实现

@Order 中接受参数的值从 integer 的最小值到最大值,并不需要连续,spring aop 会优先运行数值更小的 advice。当 advice 的 order 一样时,顺序则随机

实现大体如下:

在这里插入图片描述

规模太小了,所以没办法触发多个随机运行的结果

其他项目的运行结果

从之前 thymeleaf 的项目里重新用 Logger 实现了一下 aspect,一个比较简单的实现如下:

package com.demo.springboot.thymeleafdemo.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.logging.Logger;

@Component
@Aspect
public class DemoLoggingAspect {
    private Logger logger = Logger.getLogger(getClass().getName());

    @Pointcut("execution(* com.demo.springboot.thymeleafdemo.controller.*.*(..))")
    private void controllerMethods() {}

    @Pointcut("execution(* com.demo.springboot.thymeleafdemo.service.*.*(..))")
    private void serviceMethods() {}

    @Pointcut("execution(* com.demo.springboot.thymeleafdemo.dao.*.*(..))")
    private void daoMethods() {}

    @Pointcut("controllerMethods() || daoMethods() || serviceMethods()")
    private void appFlow() {}

    @Before("appFlow()")
    public void logBefore(JoinPoint joinPoint) {
        logger.info("========>> @Before " + joinPoint.getSignature().toShortString());
        logger.info("========>> Arguments: " + Arrays.toString(joinPoint.getArgs()));
    }

    @AfterReturning(pointcut = "appFlow()", returning = "result")
    public void logAfter(JoinPoint joinPoint, Object result) {
        logger.info("========>> @AfterReturning " + joinPoint.getSignature().toShortString());
        logger.info("========>> Result: " + result);
    }
}

效果如下:

在这里插入图片描述

总结

  • ✅ 推荐使用 AOP 处理:日志、权限校验、上下文清理、异常封装、性能监控

  • ✅ 配合 MDC + ThreadLocal 实现 traceId 链路追踪更实用

    这里没提到,有空再研究下

  • ❌ 不推荐用 AOP 处理:复杂业务流程控制、数据库事务强控制、请求参数转换

    这部分的内容大体都会通过框架管理,使用 aspect 可能会 break 框架的管理

  • 🚫 Advice 中避免直接修改返回值、吞掉异常

相关文章:

  • Kubernetes详细教程(一):入门、架构及基本概念
  • 1️⃣ Coze智能体基础入门教学(2025年全新版本)
  • 【学Rust写CAD】31 muldiv255函数(muldiv255.rs,已经取消)
  • 【ElasticSearch】
  • linux | ubuntu安装docker
  • 破局与赋能:信息系统战略规划方法论
  • 【从零实现Json-Rpc框架】- 项目实现 - 服务端registrydiscovery实现
  • MySQL Explain 分析 SQL 执行计划
  • Navicat Premium 17 安装教程
  • P4779 【模板】单源最短路径(标准版)
  • Vue CLI创建项目指南
  • 【leetcode100】买卖股票的最佳时机
  • 小家电等电子设备快充方案,XSP15支持全协议和支持MCU与电脑传输数据
  • 自动化备份全网服务器数据平台
  • 快手Python开发面经及参考答案
  • Android Canvas动画实践:实现小球旋转、扩散、聚合效果
  • VS2022远程调试Linux程序
  • LeetCode 1863. 找出所有子集的异或总和再求和
  • ROS2笔记-2:第一个在Gazebo中能动的例子
  • Linux——冯 • 诺依曼体系结构操作系统初识
  • 专业零基础网站建设教学培训/发稿平台
  • 网站建设哪家比较靠谱/关键词优化排名软件哪家好
  • 地方门户网站运营/seo常用工具包括
  • 西安网站建设资讯/百度官网链接
  • 网站建设的原则有哪些/深圳关键词推广整站优化
  • 政务咨询投诉举报网站建设/关键词优化排名的步骤