前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Spring之AOP切面编程

Spring之AOP切面编程

作者头像
冬天vs不冷
发布2025-01-21 09:46:21
发布2025-01-21 09:46:21
9100
代码可运行
举报
文章被收录于专栏:springbootspringboot
运行总次数:0
代码可运行

一、代理模式

  • 二十三种设计模式中的一种,属于结构型模式
  • 它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用
  • 让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦
  • 调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护
  • 之前做的设计模式系列这里排上用场了:设计模式(六):结构型之代理模式

二、AOP概念及相关术语

1、概述

  • AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程
  • 它是面向对象编程的一种补充和完善
    • 它以通过预编译方式和运行期动态代理方式实现
    • 在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术

2、相关术语

2.1、横切关注点
  • 分散在每个各个模块中解决同一样的问题,如用户验证、日志管理、事务处理、数据缓存都属于横切关注点
  • 从每个方法中抽取出来的同一类非核心业务
  • 在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强
  • 这个概念不是语法层面的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点
2.2、通知(增强)
  • 增强,通俗说,就是你想要增强的功能,比如 安全,事务,日志等
  • 每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法
  • 前置通知:在被代理的目标方法执行
  • 返回通知:在被代理的目标方法成功结束后执行(代码异常则不执行)
  • 异常通知:在被代理的目标方法异常结束后执行
  • 后置通知:在被代理的目标方法最终结束后执行(肯定会执行,类似finally)
  • 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
2.3、切面

封装通知方法的类

2.4、目标

被代理的目标对象

2.5、代理

向目标对象应用通知之后创建的代理对象

2.6、连接点
  • 这也是一个纯逻辑概念,不是语法定义的
  • 把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点
  • 通俗说,就是spring允许你使用通知的地方
2.7、切入点
  • 定位连接点的方式
  • 每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)
  • 如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句
  • Spring 的 AOP 技术可以通过切入点定位到特定的连接点。通俗说,要实际去增强的方法
  • 切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件

三、基于注解的AOP

1、切入点表达式语法

1.1、execute表达式(常用)
  • *号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
  • 在包名的部分,一个*号只能代表包的层次结构中的一层,表示这一层是任意的
    • 例如:*.Hello匹配com.Hello,不匹配com.xc.Hello
  • 在包名的部分,使用“*..”表示包名任意、包的层次深度任意
  • 在类名的部分,类名部分整体用*号代替,表示类名任意
  • 在类名的部分,可以使用*号代替类名的一部分
    • 例如:*Service匹配所有名称以Service结尾的类或接口
  • 在方法名部分,可以使用*号表示方法名任意
  • 在方法名部分,可以使用*号代替方法名的一部分
    • 例如:*Operation匹配所有方法名以Operation结尾的方法
  • 在方法参数列表部分,使用(..)表示参数列表任意
  • 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
  • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
    • 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
    • 例如:execution(public int *..*Service.*(.., int)) 正确
    • 例如:execution(* int *..*Service.*(.., int)) 错误
1.2、within表达式
  • 给定class的所有方法
  • 表达式格式:包名.* 或者 包名..*
    • within(com.xc.service.*):拦截包中任意方法,不包含子包中的方法
    • within(com.xc.service..*):拦截包或者子包中定义的方法
  • within与execution相比,粒度更大,仅能实现到接口级别
  • 而execution可以精确到方法的返回值,参数个数、修饰符、参数类型等
1.3、this表达式
  • 代理对象为指定的类型会被拦截
  • this(com.xc.service.AccountServiceImpl):AccountService接口、AccountServiceImpl实现类
    • 如果使用jdk动态代理生成的对象,不是AccountServiceImpl类型不会拦截
    • 如果使用cglib代理生成的是AccountServiceImpl的子类,会被拦截
1.4、target表达式
  • 目标对象为指定的类型会被拦截
  • target(com.xc.service.AccountService)
  • this作用于代理对象,target作用于目标对象
1.5、args 表达式
  • 方法参数为指定的类型会被拦截
  • args(com.xc.UserModel):匹配只有一个参数,且类型为UserModel
  • args(type1,type2,typeN):匹配多个参数
  • args(com.xc.UserModel,..):匹配任意多个参数
1.6、@target表达式
  • 目标对象中包含指定注解,调用该目标对象的任意方法都会被拦截
  • @target(com.xc.MyAnnotation)
1.7、@within表达式
  • @within(com.xc.MyAnnotation)
  • @target 和 @within 的不同点
    • 父类有注解,但子类没有注解的话,@within和@target是不会对子类生效的
    • 子类没有注解的情况下,只有没有被重写的有注解的父类的方法才能被@within匹配到
    • 如果父类无注解,子类有注解的话,@target对父类所有方法生效,@within只对重载过的方法生效
1.8、@annotation表达式
  • 方法上有指定注解会被拦截(注解作用在方法上面)
  • @annotation(com.xc.MyAnnotation)
1.9、@args表达式
  • 方法参数上有指定注解会被拦截
  • @args(com.xc.Annotation1):匹配1个参数,且第1个参数所属的类中有Annotation1注解
  • @args(com.xc.Annotation1,com.xc.Annotation2):匹配多个参数,且多个参数所属的类型上都有指定的注解
  • @args(com.xc.Annotation1,..):匹配多个参数,且第一个参数所属的类中有Annotation1注解

2、准备工作

添加依赖

代码语言:javascript
代码运行次数:0
复制
<!--spring context依赖-->
<!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.0.2</version>
</dependency>

<!--spring aop依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>6.0.2</version>
</dependency>

<!--spring aspects依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.0.2</version>
</dependency>

准备被代理的目标资源(计算器接口-加减乘除)

接口:

代码语言:javascript
代码运行次数:0
复制
public interface Calculator {
    int add(int i, int j);
    
    int sub(int i, int j);
    
    int mul(int i, int j);
    
    int div(int i, int j);
}

实现类:

代码语言:javascript
代码运行次数:0
复制
@Component
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int i, int j) {
        int result = i + j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
        int result = i - j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
        int result = i * j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    
    @Override
    public int div(int i, int j) {
        int result = i / j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
}

3、创建切面类并配置

代码语言:javascript
代码运行次数:0
复制
@Aspect //切面类
@Component //ioc容器
public class LogAspect {

    //设置切入点和通知类型
    //切入点表达式: execution(访问修饰符 增强方法返回类型 增强方法所在类全路径.方法名称(方法参数))
    //通知类型:
    // 前置 @Before(value="切入点表达式配置切入点")
    //@Before(value = "execution(* com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))")
    @Before(value = "execution(public int com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("前置通知,方法名称:" + methodName + ",参数:" + Arrays.toString(args));
    }

    // 后置 @After()
    //@After(value = "com.xc.spring6.aop.annoaop.LogAspect.pointCut()")
    @After(value = "pointCut()")
    public void afterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("后置通知,方法名称:" + methodName);
    }

    // 返回 @AfterReturning
    @AfterReturning(value = "execution(* com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))", returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("返回通知,方法名称:" + methodName + ",返回结果:" + result);
    }

    // 异常 @AfterThrowing 获取到目标方法异常信息
    //目标方法出现异常,这个通知执行
    @AfterThrowing(value = "execution(* com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))", throwing = "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("异常通知,方法名称:" + methodName + ",异常信息:" + ex);
    }

    // 环绕 @Around()
    @Around("execution(* com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        // 方法名、参数名
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        Object result = null;
        try {
            System.out.println("环绕通知 ==> 目标方法之前执行");

            //调用目标方法
            result = joinPoint.proceed();

            System.out.println("环绕通知 ==> 目标方法返回值之后");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("环绕通知 ==> 目标方法出现异常执行");
        } finally {
            System.out.println("环绕通知 ==> 目标方法执行完毕执行");
        }
        return result;
    }

    //重用切入点表达式
    @Pointcut(value = "execution(* com.xc.spring6.aop.annoaop.CalculatorImpl.*(..))")
    public void pointCut() {
    }
}

配置类:

代码语言:javascript
代码运行次数:0
复制
@Configuration
@ComponentScan(basePackages = "com.xc")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig {
}

4、测试

代码语言:javascript
代码运行次数:0
复制
public class Client {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
        Calculator calculator = context.getBean(Calculator.class);
        int result = calculator.add(2, 3);
        System.out.println("执行结果:"+result);
    }
}

输出结果:

代码语言:javascript
代码运行次数:0
复制
环绕通知 ==> 目标方法之前执行
前置通知,方法名称:add,参数:[2, 3]
方法内部 result = 5
返回通知,方法名称:add,返回结果:5
后置通知,方法名称:add
环绕通知 ==> 目标方法返回值之后
环绕通知 ==> 目标方法执行完毕执行
执行结果:5

5、切面的优先级

  • 相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序
    • 优先级高的切面:外面
    • 优先级低的切面:里面
  • 使用@Order注解可以控制切面的优先级
    • @Order(较小的数):优先级高
    • @Order(较大的数):优先级低

两个切面的执行顺序

6、 Spring5.2.7.RELEASE之前版本顺序

新版顺序:环绕通知(后)放在最后执行,后置通知(最终通知)在返回通知后面

代码语言:javascript
代码运行次数:0
复制
环绕通知 ==> 目标方法之前执行
前置通知,方法名称:add,参数:[2, 1]
方法内部 result = 3
环绕通知 ==> 目标方法返回值之后
后置通知,方法名称:add
返回通知,方法名称:add,返回结果:3

四、@Aspectj番外篇

  • AspectJ:全称Eclipse AspectJ,可以单独使用,也可以整合到其它框架中
  • 单独使用AspectJ时需要使用专门的编译器ajc
  • spring基于@AspectJ注解的方式,也只是使用一些注解,并没有依赖于 AspectJ 实现具体的功能

Spring AOP与单独使用@AspectJ区别

aspectj的类加载期织入的实现方式

  • aspectj是AOP一种实现,主要原理是用asm做字节码替换来达到AOP的目的
  • aspectj有3个时间点可以做织入
    • 编译期(Compile-time weaving):把aspect类(aop的切面)和目标类(被aop的类)放在一起用ajc编译
    • 编译期后(Post-compile weaving):目标类可能已经被打成了一个jar包,这时候也可以用ajc命令将jar再织入一次
    • 类加载期(Load-time weaving):在jvm加载类的时候,做字节码替换
  • 其中前2个时间点,我们可以理解为静态织入,因为在class文件生成后,就已经织入好了
  • 类加载期织入,可以理解为“动态织入”(注意不同于java动态代理的“动态”),因为这个类替换是在jvm加载类的时候完成的
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-10-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、代理模式
  • 二、AOP概念及相关术语
    • 1、概述
    • 2、相关术语
      • 2.1、横切关注点
      • 2.2、通知(增强)
      • 2.3、切面
      • 2.4、目标
      • 2.5、代理
      • 2.6、连接点
      • 2.7、切入点
  • 三、基于注解的AOP
    • 1、切入点表达式语法
      • 1.1、execute表达式(常用)
      • 1.2、within表达式
      • 1.3、this表达式
      • 1.4、target表达式
      • 1.5、args 表达式
      • 1.6、@target表达式
      • 1.7、@within表达式
      • 1.8、@annotation表达式
      • 1.9、@args表达式
    • 2、准备工作
    • 3、创建切面类并配置
    • 4、测试
    • 5、切面的优先级
    • 6、 Spring5.2.7.RELEASE之前版本顺序
  • 四、@Aspectj番外篇
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档