011-Spring AOP入门

本文深入解析了AOP的核心概念,如切面、链接点、切入点、通知和目标对象,展示了Spring AOP如何通过JDK/CGLIB动态代理实现织入。通过实例演示了切面类的编写和在Spring中的应用,以及不同通知类型的使用场景。
摘要由CSDN通过智能技术生成

AOP相关术语

  • 切面(Aspect):切面是对象操作过程中的截面。实际上"切面"就是一段代码,这段代码将被"植入"到程序流程中。
  • 链接点(Join Point):连接点是对象操作过程中的某个阶段点(代码的某个位置,如某个方法执行前或执行后)。它实际上是对象的一个操作,例如:对象调用某个方法,读写对象的实例,或者某个方法抛出了异常等等。
  • 切入点(Pointcut):切入点是连接点的集合,它是"切面"注入到程序中的位置,换句话说,"切面"是通过切入点被"注入"的。程序中可以有很多个切入点。
  • 通知(Advice):通知是某个切入点被横切后,所采取的处理逻辑。在"切入点"处拦截程序后,通过通知来执行切面。
  • 目标对象(Target):所有被通知的对象(也可以理解为被代理的对象)都是目标对象。目标对象被AOP所关注。AOP会注意目标对象的变动,随时准备向目标对象"注入切面"。
  • 织入(Weaving):织入是将切面功能应用到目标对象的过程。AOP织入的方式有3种:编译时(Compile time)织入、类加载时期(Classload time)织入、执行期(Runtime)织入。Spring AOP一般多见于执行期织入。

AOP原理

Spring AOP默认使用JDK动态代理。如果被代理的类没有实现接口,则使用CGLIB动态代理。

JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。

入门示例

pom引入aspectjweaver依赖

<dependency>
   	<groupId>org.aspectj</groupId>
   	<artifactId>aspectjweaver</artifactId>
</dependency>

注意:我们的示例是使用@Aspect注解,需要AspectJ 的aspectjweaver.jar库。此示例是在系列文章的基础上继续的,所以其他依赖项请自行补足。

编写需要织入切面的bean

package com.yyoo.boot.aop.beans;

import org.springframework.stereotype.Component;

@Component
public class TestBean1 {

    public void test1(String str){
        System.out.println("参数str:"+str);
    }

}

编写切面类

package com.yyoo.boot.aop.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 // 需要把@Aspect(切面)类也交由Spring容器管理
public class AspectBean1 {

    // 切入点表达式(此处表示com.yyoo.boot.aop.beans包下的所有方法)
    @Pointcut("execution(* com.yyoo.boot.aop.beans..*.*(..))")
    public void pointCut(){}

    // 前置通知
    @Before("com.yyoo.boot.aop.aspect.AspectBean1.pointCut()")
    public void around(){
        System.out.println("切面执行程序");
    }

}

编写AppConfig

package com.yyoo.boot.aop;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;


// 配置类中没有定义@bean所以我们没有添加@Configuration注解
@ComponentScan("com.yyoo.boot.aop")
@EnableAspectJAutoProxy // 使用@EnableAspectJAutoProxy注解启用@AspectJ支持
public class AppConfig {



}

示例程序

package com.yyoo.boot.aop;


import com.yyoo.boot.aop.beans.TestBean1;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Demo1 {

    @Test
    public void test(){

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        TestBean1 bean1 = context.getBean(TestBean1.class);
        System.out.println(bean1);
        bean1.test1("当前参数");

    }

}

程序结构图

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

执行结果

com.yyoo.boot.aop.beans.TestBean1@76b1e9b8
切面执行程序
参数str:当前参数

我们示例中的切面类定义的切点(@Pointcut)注解了一个空方法,实际上我们的切面类可以简化为如下使用

package com.yyoo.boot.aop.aspect;

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

@Aspect
@Component // 需要把@Aspect(切面)类也交由Spring容器管理
public class AspectBean1 {

    // 前置通知(直接月切点表达式一起使用)
    @Before("execution(* com.yyoo.boot.aop.beans..*.*(..))")
    public void around(){
        System.out.println("切面执行程序");
    }

}

切入点表达式

execution:用来匹配连接点的执行方法,这是Spring中使用的最主要的切入点定义方法

  • execution(public * *(..)):匹配任意公共方法。
  • execution(* set*(..)):匹配任何以"set"开头的方法。
  • execution(* com.yyoo.boot.service.*.*(..)):匹配service包中的任意方法。
  • execution(* com.yyoo.boot.service..*.*(..)):匹配service包及其子包中的任意方法。

within:通过类型来匹配连接点的执行方法

  • within(com.yyoo.boot.service.*):匹配service包中的任意方法
  • within(com.yyoo.boot.service…*):匹配service包及其子包中的任意方法。

this:用于匹配特定类型的连接点,但它是代理对象的范围内进行匹配

  • this(com.yyoo.boot.service.AccountService):匹配代理对象中类型为AccountService的Java对象内的连接点。

target:也是用于匹配特定类型的连接点,但它是在目标对象的范围内进行匹配

  • target(cn.hxex.springcore.service.AccountService):匹配目标对象中类型为AccountService的Java对象内的连接点

args:通过参数的类型来匹配特定的连接点

  • args(java.io.Serializable):匹配只有一个参数并且该参数实现了Serializable接口的连接点

通过注解指定连接点

@target:匹配目标对象中的连接点

  • @target(org.springframework.transaction.annotation.Transactional):匹配使用了@Transactional注解的目标对象中的连接点

@args:用于匹配参数实现特定注解的连接点

  • @args(com.yyoo.boot.service.Classified):匹配只有一个参数并且该参数实现了@Classified注解的连接点

@within:匹配有指定注解的目标对象类型中的连接点

  • @within(org.springframework.transaction.annotation.Transactional):匹配与指定了@Transactional注解的对象中类型相同的对象中的连接点

@annotation:用于匹配具有指定类型注解的连接点

  • @annotation(org.springframework.transaction.annotation.Transactional):匹配任何有@Transactional注解的连接点

运算符

切入点表达式也可以使用&&(and)、||(or) 和!(negation) 运算符,如:
execution(* set*(…)) && execution(* com.yyoo.boot.aop.beans….(…))

Spring AOP advice通知类型

  • @Before:前置通知,在切点执行之前执行
  • @AfterReturning:返回后通知,在切点方法返回之后(在return之后)执行
  • @AfterThrowing:异常后通知,在切点方法抛出异常后执行
  • @After:后置通知,在切点方法有结果后(无论是异常结果还是正常结果)返回
  • @Around:环绕通知,我们可以在切点方法执行前后添加代码逻辑。

@Before前置通知

    @Before("execution(* com.yyoo.boot.aop.beans..*.*(..))")
    public void before(){
        System.out.println("切面执行程序");
    }

执行结果

com.yyoo.boot.aop.beans.TestBean1@f78a47e
切面执行程序
参数str:当前参数

@AfterReturning返回后通知

    @AfterReturning("execution(* com.yyoo.boot.aop.beans..*.*(..))")
    public void afterReturning(){
        System.out.println("切面执行程序");
    }

执行结果

com.yyoo.boot.aop.beans.TestBean1@f78a47e
参数str:当前参数
切面执行程序

在切面中获取返回结果

修改TestBean1的方法如下

    public String test1(String str){
        System.out.println("参数str:"+str);
        return "rs-" + str;
    }

给我们的示例方法加入返回值
修改切面代码

    @AfterReturning(value = "execution(* com.yyoo.boot.aop.beans..*.*(..))",returning = "rs")
    public void afterReturning(String rs){
        System.out.println("切面执行程序");
        System.out.println("方法返回结果:"+rs);
    }

注意:returning属性的名称要与方法上对应的参数名称一致

执行结果

com.yyoo.boot.aop.beans.TestBean1@6892b3b6
参数str:当前参数
切面执行程序
方法返回结果:rs-当前参数

@AfterThrowing异常后通知

我们再次修改TestBean1的方法

    public String test1(String str){
        System.out.println("参数str:"+str);
        int a = 3/0;
        return "rs-" + str;
    }

就添加了一个除数为0的代码

    @AfterThrowing(value = "execution(* com.yyoo.boot.aop.beans..*.*(..))",throwing = "ex")
    public void afterThrowing(Exception ex){
        System.out.println("切面执行程序");
        System.out.println("方法返回结果:"+ex);
    }

执行结果

com.yyoo.boot.aop.beans.TestBean1@2a265ea9
参数str:当前参数
切面执行程序
方法返回结果:java.lang.ArithmeticException: / by zero
异常打印....

@After后置通知

    @After("execution(* com.yyoo.boot.aop.beans..*.*(..))")
    public void after(){
        System.out.println("切面执行程序");
    }

执行结果


com.yyoo.boot.aop.beans.TestBean1@740cae06
参数str:当前参数
切面执行程序

异常打印....

@Around环绕通知

我们先将TestBean1的除数为0异常去掉

    public String test1(String str){
        System.out.println("参数str:"+str);
        return "rs-" + str;
    }

编写切面方法

    @Around("execution(* com.yyoo.boot.aop.beans..*.*(..))")
    public void around(ProceedingJoinPoint pjp){
        System.out.println("切面前置逻辑代码");
        try {
            pjp.proceed(); // 执行原方法
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("切面后置逻辑代码");
    }

执行结果

com.yyoo.boot.aop.beans.TestBean1@f78a47e
切面前置逻辑代码
参数str:当前参数
切面后置逻辑代码

pjp.proceed();这里还有个重载的方法pjp.proceed(Object[]);如果我们要在切面类中覆盖原来的参数,可以使用该方法

    @Around("execution(* com.yyoo.boot.aop.beans..*.*(..))")
    public void around(ProceedingJoinPoint pjp){
        System.out.println("切面前置逻辑代码");
        try {
            pjp.proceed(new Object[]{"更改后的参数"}); // 执行原方法
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("切面后置逻辑代码");
    }

执行结果

com.yyoo.boot.aop.beans.TestBean1@f78a47e
切面前置逻辑代码
参数str:更改后的参数
切面后置逻辑代码

@AfterReturning、@AfterThrowing、@After的区别

@AfterReturning:只在切点方法正确执行返回之后才执行。
@AfterThrowing:只在切点方法抛出异常后才执行。
@After:无论切点方法是否正确返回均会执行(类似于异常的finally块)

切面方法的参数JoinPoint

所有类型的通知,其通知方法的第一个参数为JoinPoint(如果要使用JoinPoint,那么它必须是第一个参数)。只有环绕通知@Around第一个参数是ProceedingJoinPoint,ProceedingJoinPoint是JoinPoint的子类。

JoinPoint常用方法

  • getArgs():返回方法参数。
  • getThis(): 返回代理对象。
  • getTarget(): 返回目标对象。
  • getSignature():返回所建议的方法的描述。

示例

    @Around("execution(* com.yyoo.boot.aop.beans..*.*(..))")
    public void afterReturning(ProceedingJoinPoint pjp){
        System.out.println("切面前置逻辑代码");
        try {
            pjp.proceed(new Object[]{"更改后的参数"}); // 执行原方法

            Object[] args = pjp.getArgs();
            if(args != null){
                for(int i = 0; i< args.length;i++){
                    System.out.print("\t参数["+i+"]:"+args[i]);
                }
            }else {
                System.out.print("\t无参数");
            }
            System.out.println();

            System.out.println(pjp.getSignature().getName());
            System.out.println(pjp.getTarget());
            System.out.println(pjp.getThis());

        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("切面后置逻辑代码");
    }

本章只介绍了注解方式实现的Spring AOP,目前使用的更多的方式。Spring也提供了xml配置的方式,有兴趣可以自行到官网查看https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-schema

上一篇:010-Spring 资源Resource接口
下一篇:012-Spring DAO 数据访问对象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值