轻松上手Spring AOP,掌握切面编程的核心技巧

文章目录

  • AOP 是什么
  • AspectJ
  • Spring AOP
  • 切点表达式
    • execution()
      • 语法结构
      • 示例
      • 注意事项
  • 通知(Advice)类型
  • 如何使用
    • 准备阶段
    • 定义切面
    • 定义并使用切点
    • 测试
  • 总结

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。

Spring框架是我们使用比较多的一个框架,而AOP又是Spring的核心特性之一,本篇文章将介绍一下AOP的切点表达式、通知等特性及如何使用Spring AOP。

AOP 是什么

AOP(Aspect-Oriented Programming,面向切面编程) 是一种编程范式,旨在将横切关注点与核心业务逻辑相分离,以提高代码的模块化性、可维护性和复用性。

在传统的面向对象编程中,程序的功能被模块化为类和方法,但某些功能可能会跨越多个类和方法,如日志记录、事务管理、安全控制等,这些功能不属于核心业务逻辑,但又必须在多个地方重复使用,导致代码重复和耦合性增加。

AOP提供了一种机制,可以将这些横切关注点单独定义,并在需要的地方插入到应用程序中,而不必修改核心业务逻辑。

AspectJ

AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP(面向切面编程) 语法,并拥有一个专门的编译器,用于生成遵守Java字节编码规范的Class文件

AspectJ可以单独使用,也可以整合到其他框架中。当单独使用AspectJ时,需要使用专门的编译器ajcAspectJ属于静态织入,通过修改代码来实现,包括编译期织入等多种织入时机。

Spring集成AspectJ,可以在Spring中方便的使用AOP。

Spring AOP

Spring AOP核心概念主要包括以下几个方面:

  1. 切面(Aspect):切面是模块化横切关注点的机制,由切入点和通知组成。在Spring AOP中,一个切面可以定义在什么时候、什么地方以及如何应用某种特定的行为到目标对象上。

  2. 连接点(Joinpoint):连接点是程序执行过程中的一个点,例如方法的调用、字段的访问等。在Spring AOP中,一个连接点总是代表一个方法的执行。连接点是AOP框架可以在其上 “织入” 切面的点。

  3. 通知(Advice):通知定义了在切入点执行时要执行的代码,它是增强应用到连接点上的行为。通知有多种类型,包括前置通知(Before Advice)后置通知(After Advice)环绕通知(Around Advice)异常通知(After Throwing Advice)返回通知(After Returning Advice) 。这些通知类型决定了增强在连接点上的执行顺序和方式。

  4. 切点(Pointcut):切点用于定义通知应该应用到哪些连接点上。它是一组连接点的集合,这些连接点共享相同的特性。切点表达式用于匹配连接点,从而确定哪些连接点应该接收通知。

  5. 目标对象(Target Object) :被一个或多个切面所通知的对象。也被称为被通知(advised)对象。由于Spring AOP是通过代理模式实现的,因此在运行时,目标对象总是被代理对象所包裹。

  6. 织入(Weaving):织入是将切面应用到目标对象并创建代理对象的过程。这是AOP框架在运行时或编译时完成的核心任务。

  7. AOP代理(AOP Proxy):AOP框架创建的对象,用于实现切面编程。在Spring中,AOP代理可以是JDK动态代理CGLIB代理

  8. 引入(Introduction):用于向现有的类添加新的接口和实现,而不需要修改原始类的代码。Introduction允许在不修改现有类结构的情况下,向类引入新的功能和行为。在 AspectJ 社区中,引入称为类型间声明(inter-type declaration)

这些核心概念共同构成了AOP的基础,使得我们能够模块化地处理横切关注点,从而提高代码的可维护性和可重用性。

切点表达式

Pointcut 表达式 是用来定义切入点的规则,它决定了哪些连接点(方法调用或方法执行)将会被通知所影响。在 Spring AOP 中,Pointcut 表达式通常由以下几种规则和通配符组成:

  1. execution(): 用于匹配方法执行的连接点,它是最常用的切点指示器。它基于方法签名进行匹配,可以指定方法的返回类型、包名、类名、方法名以及参数列表等。比如: @Pointcut("execution(* com.example.myapp.service.*.*(..))") 表示匹配com.example.myapp.service包下所有类的所有方法执行。

  2. within(): 匹配指定类型内的方法执行连接点。它通常用于匹配特定包或类中的所有方法。示例:@Pointcut("within(com.example.myapp.service.*)") 表示表示匹配com.example.myapp.service包下所有类的所有方法的执行。

  3. this(): 匹配当前代理对象为指定类型的连接点。这用于限制切点只匹配特定类型的代理对象。示例:@Pointcut("this(com.example.myapp.service.MyService)") 表示匹配当前代理对象类型为com.example.myapp.service.MyService的所有方法的执行。

  4. target(): 匹配目标对象为制定类型的连接点。与this()不同,target()是基于目标对象类型,而不是代理类型。示例:@Pointcut("target(com.example.myapp.service.MyServiceImpl)") 表示匹配目标对象类型为com.example.myapp.service.MyServiceImpl的所有方法的执行。

  5. args(): 匹配方法执行时参数为特定类型的连接点。示例:@Pointcut("args(java.io.Serializable)") 表示匹配方法执行时至少有一个参数是java.io.Serializable类型的连接点。

  6. @annotation(): 匹配执行的方法上带有指定注解的连接点。示例:@Pointcut("@annotation(com.example.myapp.annotation.MyAnnotation)") 表示匹配执行的方法上带有com.example.myapp.annotation.MyAnnotation注解的连接点。

  7. @target:用于匹配所有带有特定注解的类或接口。 这个指示器通常与execution表达式结合使用,以进一步细化匹配条件。示例:@Pointcut("@target(com.example.annotation.MyAnnotation)") 表示匹配目标对象类型上带有com.example.myapp.annotation.MyAnnotation注解的方法执行。

  8. @within:匹配指定类型带有指定注解的连接点。与within()类似,但它是基于注解而不是包或类。示例: @Pointcut("@within(com.example.myapp.annotation.MyAnnotation)") 表示匹配带有MyAnnotation注解的类的方法执行。

  9. bean():匹配Spring容器中特定名称的bean的方法的执行。示例: @Pointcut("bean(myServiceImpl)") 表示匹配Spring容器中名称为myServiceImplbean的方法的执行。

  10. @args():用于限制匹配的方法的参数必须有指定的注解

带有 @ 符的切点表达式都是需要指定注解的连接点。

这些规则可以通过逻辑运算符(如 &&、||、! )进行组合,以实现更复杂的 Pointcut 匹配规则。我们可以根据自己的需求,灵活地使用这些规则来定义切入点表达式,实现对目标方法的精确匹配和监控。

execution()

execution() 表达式使用的比较多,最复杂的一个表达式,这里重点介绍一下。

语法结构

execution() 表达式的语法结构如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

其中,各部分的含义如下:

  • modifiers-pattern: 方法的访问修饰符,如 publicprotected 等,可以省略。
  • ret-type-pattern: 方法的返回类型,如 voidint 等。
  • declaring-type-pattern: 方法所属的类的类型模式,可以使用通配符 * 匹配任意字符。
  • name-pattern: 方法的名称模式,可以使用通配符 * 匹配任意字符。
  • param-pattern: 方法的参数模式,包括参数类型和个数。
  • throws-pattern: 方法抛出的异常类型。

示例

  • 所有公共方法的执行
execution(public * *(..))
  • 名称以 set 开头的所有方法的执行
execution(* set*(..))
  • AccountService 接口定义的任何方法的执行
execution(* com.xyz.service.AccountService.*(..))
  • service 包中定义的任何方法的执行
execution(* com.xyz.service.*.*(..))
  • service 包或其子包之一中定义的任何方法的执行
execution(* com.xyz.service..*.*(..))
  • 执行指定类型参数的方法
execution(* com.example.service.MyService.myMethod(String, int))

注意事项

  • execution() 表达式中,通配符 * 可以用来匹配任意字符或任意个数的字符。
  • 使用 execution() 表达式时,需要注意合理地组织表达式,以确保精准地匹配目标方法。
  • 可以通过组合多个条件来更加灵活地定义切点,例如同时匹配方法的访问修饰符、返回类型、类名、方法名等。

总的来说,execution() 方法提供了一种灵活且强大的方式来定义切点表达式,从而精确定位需要添加通知的目标方法。

通知(Advice)类型

在 Spring AOP 中,通知(Advice)是在切入点(Pointcut)上执行的代码。Spring 提供了几种类型的通知,每种类型都对应着在连接点执行前、执行后或抛出异常时执行的不同代码逻辑。这些通知对应着不同的注解,常用的通知注解包括:

  1. @Before: 在方法执行之前执行的通知。它有以下属性:

    • value:要绑定的切点或者切点表达式。
    • argNames: 用于指定连接点表达式中方法参数的名称,以便在通知方法中通过参数名来获取方法参数的值。这样可以在前置通知中访问和处理方法参数的具体数值。该属性即使不指定也能获取参数。
  2. @AfterReturning: 在方法执行成功返回结果后执行的通知。它比 @Before注解多了2个属性:

    • pointcut:作用和value属性一样,当指定pointcut时,会覆盖value属性的值。
    • returning:方法返回的结果将被绑定到此参数名,可以在通知中访问方法的返回值。
  3. @AfterThrowing: 在方法抛出异常后执行的通知。它的属性前3个和 @AfterReturning注解一样,多了1个属性:

    • throwing:指定方法抛出的异常将被绑定到此参数名,可以在通知中访问方法抛出的异常。
  4. @After: 在方法执行后(无论成功或失败)执行的通知。属性同 @Before 注解。

  5. @Around: 环绕通知,能够在方法执行前后都可以进行操作,具有最大的灵活性。属性同 @Before 注解。

通知的执行顺序为:@Around -> @Before -> @AfterReturning(不抛异常情况) 或者 @AfterThrowing(抛异常情况) -> @After

这些通知注解可以与 Pointcut 表达式结合使用,实现对目标方法的拦截和处理。通过选择合适的通知类型,开发者可以根据需求在不同的时间点插入自定义的逻辑,实现对方法调用的控制和增强。

如何使用

讲了那么多概念性的东西,下面来看怎么使用Spring AOP。

在Spring 中使用AOP也很简单,主要分3步:

  1. 定义切面
  2. 定义切点
  3. 在具体通知上使用切点

准备阶段

我这里使用的是Springboot 3.1.5jdk 17,如果是Springboot低版本的可能需要引入 spring-boot-starter-aop 依赖,高版本的AOP已经包含在spring-boot-starter-web依赖中了:

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

Spring官网中介绍,使用Spring AOP要在启动类或者配置类中加上 @EnableAspectJAutoProxy 注解开启 AspectJ 注解的支持,在我使用的这个版本中并不需要,如果你的项目中切面未生效可以尝试使用该注解。

定义一个接口,下面用于对这个接口及其实现类进行拦截:

public interface AopService {

    /**
     * 两数除法
     * @param a
     * @param b
     * @return
     */
    BigDecimal divide(BigDecimal a, BigDecimal b);

    /**
     * 两数加法
     * @param a
     * @param b
     * @return
     */
    BigDecimal add(BigDecimal a, BigDecimal b);
}
@Service
public class MyAopServiceImpl implements AopService{

    /**
     * 两数除法
     *
     * @param a
     * @param b
     * @return
     */
    @Override
    public BigDecimal divide(BigDecimal a, BigDecimal b) {
        return a.divide(b , RoundingMode.UP);
    }

    /**
     * 两数加法
     *
     * @param a
     * @param b
     * @return
     */
    @Override
    public BigDecimal add(BigDecimal a, BigDecimal b) {
        return a.add(b);
    }
}

定义切面

新建一个类,在类上加上@Aspect 注解,标记该类为切面。

@Component
@Aspect
public class AspectComponent {
}

定义并使用切点

在切面中使用@Pointcut注解定义切点表达式,然后在通知注解中使用定义好的切点。在该示例中主要对AopService#divide()方法进行拦截。

@Component
@Aspect
public class AspectComponent {

	/**
     * 匹配AopService接口的divide方法
     */
    @Pointcut("execution(* site.suncodernote.aop.AopService.divide(..))")
    void dividePointCut(){
    }

	/**
     * 匹配AopService接口的divide方法
     */
    @Pointcut("within(site.suncodernote.aop.AopService+)")
    void withinPointCut(){
    }

	/**
     * 匹配AopService接口的add方法 或者 divide方法
     */
    @Pointcut("execution(* site.suncodernote.aop.AopService.add(..)) || execution(* site.suncodernote.aop.AopService.divide(..))")
    void addOrDividePointCut(){
    }


	@Before("dividePointCut()")
    public void beforeDivide(JoinPoint joinPoint){
        System.out.println("---------------------@Before----------------");
        printJoinPoint(joinPoint);
    }

    @After("dividePointCut()")
    public void afterDivide(JoinPoint joinPoint){
        System.out.println("---------------------@After----------------");
        printJoinPoint(joinPoint);
    }

    @AfterReturning(pointcut = "dividePointCut()" , returning = "result")
    public void afterReturningDivide(JoinPoint joinPoint , BigDecimal result){
        System.out.println("---------------------@AfterReturning----------------");
        System.out.println("返回结果="+result);
        printJoinPoint(joinPoint);
    }

    @AfterThrowing(pointcut = "dividePointCut()" , throwing = "e")
    public void afterThrowingDivide(JoinPoint joinPoint ,Exception e){
        System.out.println("---------------------@AfterThrowing----------------");
        System.out.println("异常:"+e.getMessage());
        printJoinPoint(joinPoint);
    }

    @Around("dividePointCut()")
    public Object aroundDivide(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("---------------------@Around----------------");
        printJoinPoint(joinPoint);
        Object[] args = joinPoint.getArgs();
        Object result = null;
        try {
            //执行方法
            result = joinPoint.proceed(args);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("返回值:"+result);
        return result;
    }

    private void printJoinPoint(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        Signature signature = joinPoint.getSignature();
        System.out.println("方法名:"+signature.getName());
        System.out.println("方法参数:"+ Arrays.toString(args));
        System.out.println();
    }
}

测试

写个简单的单元测试,调用AopService#divide()方法,然后看一下输出结果。

@SpringBootTest
public class AOPTest {

    @Resource
    private AopService aopService;

    @Test
    public void testAOP() {
        BigDecimal a = new BigDecimal(1);
        BigDecimal b = new BigDecimal(2);

//        aopService.add(a, b);
        aopService.divide(a, b);
    }
}

测试结果:

---------------------@Around----------------
方法名:divide
方法参数:[1, 2]

---------------------@Before----------------
方法名:divide
方法参数:[1, 2]

---------------------@AfterReturning----------------
返回结果=1
方法名:divide
方法参数:[1, 2]

---------------------@After----------------
方法名:divide
方法参数:[1, 2]

返回值:1

从测试结果中通知执行的顺序是按照我们上面所说的执行顺序执行的。

总结

本文介绍了Spring AOP的常用的切点表达式、通知注解等,我们可以利用AOP对业务逻辑的各个部分进行隔离,使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率。

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/619739.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

轨迹规划 | 图解纯追踪算法Pure Pursuit(附ROS C++/Python/Matlab仿真)

目录 0 专栏介绍1 纯追踪算法原理推导2 自适应纯追踪算法(APP)3 规范化纯追踪算法(RPP)4 仿真实现4.1 ROS C仿真4.2 Python仿真4.3 Matlab仿真 0 专栏介绍 &#x1f525;附C/Python/Matlab全套代码&#x1f525;课程设计、毕业设计、创新竞赛必备&#xff01;详细介绍全局规划…

Android面试题之Kotlin和Java之间互操作

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 互操作性和可空性 要注意Java中所有类型都是可空的String!表示平台数据类型 public class JavaTest {public String generateName() {return …

瞬息全宇宙——平行宇宙终极教程,手把手教你做出百万点赞视频

最近一种叫“瞬息全宇宙”的视频火了&#xff0c;抖音一期视频百万赞&#xff0c;各个博主视频都在带瞬息全宇宙这个标签&#xff0c;于是就有很多朋友催我出教程了&#xff0c;在琢磨了几天之后&#xff0c;终于整出来了 教程包含了插件的安装&#xff0c;界面的讲解&#xff…

for 双重循环

一.双循环&#xff1a; 可以使用嵌套循环来实现脚本的双层循环&#xff0c;示例代码如下 1.显示外循环是$a 内循环$b encho -e \t 是制表符 2.9 9乘法表 for 循环&#xff1a; echo -n是不换行输出 while循环: 3.输出长度宽度都为9个星的矩形 for循环 while循环 …

开发者出海时都在用哪些组件库?

❝ 哈喽&#x1f44b;&#xff0c;我是树酱。今天我要介绍的是在开发者出海时经常使用的组件库。这些组件库大多采用Tailwind CSS作为基础&#xff0c;它们不仅风格独树一帜&#xff0c;而且外观也非常吸引人&#xff01; 1.Shadcn-ui shadcn的风格跟Notion风格很像&#xff0c…

博客互动革命:如何打造活跃读者社区并提升参与度

CSDN 的朋友你们好&#xff0c;我是未来&#xff0c;今天给大家带来专栏【程序员博主教程&#xff08;完全指南&#xff09;】的第 10 篇文章“与读者互动”。本文揭示了提升技术博客参与度的秘诀。从评论互动到社交媒体策略&#xff0c;本文将指导你如何建立强大的读者社区。掌…

编程技巧:什么是JavaScript递归

什么是递归 程序调用自身的编程技巧称为递归&#xff08;recursion&#xff09; 递归的基本思想是将一个复杂的问题分解成更小、更易于管理的子问题&#xff0c;这些子问题与原始问题相似&#xff0c;但规模更小。 递归的要素 基本情况&#xff08;Base Case&#xff09;&…

【C语言 | 数据结构】栈

文章目录 前言1、栈1.1栈的概念和定义1.1.2栈的基本概念&#xff1a; 1.2栈的方法(接口)1.3栈的实现方法1.4栈的性质1.5栈的应用1.6栈的结构 2、栈的实现2.1 顺序栈2.1.1 顺序栈的结构体2.1.2 顺序栈的初始化2.1.3 顺序栈的销毁2.1.4 顺序栈的入栈2.1.5 顺序栈的出栈2.1.5 顺序…

聚观早报 | 比亚迪海狮07 EV上市;苹果将升级Siri

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 5月13日消息 比亚迪海狮07 EV上市 苹果将升级Siri OpenAI开发全新技术 沃尔沃EX30车型将上市 SpaceX计划新建发…

template——模板进阶(C++)

在之前的文章中&#xff0c;介绍了模板初阶&#xff1a;Cpp_桀桀桀桀桀桀的博客-CSDN博客 在本篇中将会对模板进一步的讲解。本篇中的主要内容为&#xff1a;非类型模板参数、函数模板的特化、类模板的特化&#xff08;其中包含全特化和偏特化&#xff09;&#xff0c;最后讲解…

【计算机网络篇】数据链路层(9)使用集线器的共享式以太网

文章目录 &#x1f6f8;使用同轴电缆的共享总线以太网 &#x1f386;使用集线器的共享式以太网&#x1f95a;集线器的特点 &#x1f354;10BASE-T星型以太网 &#x1f6f8;使用同轴电缆的共享总线以太网 若总线上的某个机械连接点接触不良或断开&#xff0c;则整个网络通信就不…

【前端工程化指南】Git常见操作之忽略文件

默认情况下&#xff0c;Git管理代码版本时会对所有文件进行跟踪&#xff0c;但有些时候我们并不希望项目中的一些文件上传到远程仓库或公共仓库中&#xff0c;例如密钥&#xff0c;个人隐私文件等。因此Git提供了两种忽略跟踪文件的方式.gitignore文本文件与git rm命令&#xf…

亿级流量下通用的高并发架构设计

既然是亿级用户应用&#xff0c;那么高并发必然是其架构设计的核心要素。 本文我们将介绍高并发架构设计的一些通用设计方案。 关键词&#xff1a;读/写分离、数据缓存、缓存更新、CQRS、数据分片、异步写 本文节选自电子工业出版社博文视点刚刚出版的《亿级流量系统架构设计…

Java随笔1

1.编程中组件的概念&#xff1a; 在编程中&#xff0c;组件&#xff08;Component&#xff09;通常指的是一种可重用的、模块化的代码单元&#xff0c;它封装了特定的功能或用户界面元素&#xff0c;并提供了与其他代码进行交互的接口。组件可以看作是对数据和方法的简单封装&…

ADS基础操作篇2

上篇文章《ADS基础介绍篇1》,对ADS界面,常用小工具及自带设计模板进行了介绍。ADS使用非常方便,含大量的控件和仿真模板。这篇文章我们主要讲解ADS的基础操作,包含Workspace、原理图、symbol的创建,仿真结果查看及优化。 1. 新建Workspace 添加名称及路径后,点击create…

共享充电宝语音芯片ic方案支持远程4g无线更新语音

一、简介 共享充电宝语音芯片ic方案支持远程4g无线wifi蓝牙更新语音 共享充电宝已经是遍布在大街小巷的好产品&#xff0c;解决了携带充电宝麻烦的痛点 但是很多的共享充电宝在人机交互方便&#xff0c;还做得不够好&#xff0c;比如&#xff1a;借、还设备没有语音提示&…

基于SSM的计算机课程实验管理系统的设计与实现(源码)

| 博主介绍&#xff1a;✌程序员徐师兄、8年大厂程序员经历。全网粉丝15w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f44…

AI视频教程下载:用ChatGPT制作 YouTube视频的指南

课程大纲&#xff1a; 面向 YouTuber 的 ChatGPT YouTube关键词研究 YouTube标题 YouTube缩略图 YouTube社区帖子 组织您的 YouTube 视频 本课程将通过两个不同领域的YouTube视频&#xff0c;展示如何使用Chat GPT来创建关键词、标题、缩略图、描述和社区帖子。 关键词研…

【Linux网络】Https【下】{CA认证/证书的签发与认证/安全性/总结}

文章目录 1.引入证书【为方案五铺垫】1.1再谈https1.2SSL/TLS1.3CA机构1.4理解数字签名1.4继续铺垫1.5方案五服务端申请证书回顾一二三回顾方案四方案五过程寻找方案五的漏洞客⼾端对证书进⾏认证 2.查看证书2.1查看浏览器的受信任证书发布机构2.2中间⼈有没有可能篡改该证书2.…

Postman工具介绍与安装

一、Postman介绍 Postman 乃是一款对 HTTP 协议予以支持的接口调试及测试工具&#xff0c;其突出特性在于功能强大&#xff0c;并且使用简便、易用性良好。不管是开发人员开展接口调试工作&#xff0c;还是测试人员进行接口测试任务&#xff0c;Postman 均属于首选工具之一。 接…