天天看点

5、基于注解的AOP配置

一、开启注解支持

1、概述

1、Spring AOP如同IoC一样支持基于XML和基于注解两种配置方式,基于注解所需的依赖和基于XML所需的依赖一致,其中spring-context包含了Spring IoC、Spring AOP等核心依赖,而aspectjweaver则是AspectJ框架的依赖,Spring使用该依赖来解析AspectJ的切入点表达式语法,以及AOP的注解支持。

2、开启AOP注解支持的方式:

  • 使用XML配置启用AOP注解。
  • 使用Java配置启用AOP注解。

2、使用XML方式的配置

1、加入aop命名空间,使用

<aop:aspectj-autoproxy />

标签即可,Spring将会查找被@Aspect注解标注的Bean,这表明它是一个切面Bean,然后就会进行AOP的自动配置。

2、属性说明:

  • proxy-target-class

    是否为被代理类生成CGLIB子类,只为接口生成代理子类(即:是否使用CGlib动态代理);默认为false,使用的是JDK代理

  • expose-proxy

    是否将代理的Bean暴露给用户,如果暴露,就可以通过AopContext类获得,默认不暴露

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/aop
         http://www.springframework.org/schema/aop/spring-aop.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context.xsd">

	<!--开启注解自动配置-->
    <aop:aspectj-autoproxy/>
</beans>
           

3、使用Java方式的配置

1、AOP的注解支持同样可以使用Java配置方式开启,从而彻底舍弃XML配置文件。

2、配置类上添加

@EnableAspectJAutoProxy

,该注解用于开启Spring AOP的注解自动配置支持。

3、属性说明:

  • proxy-target-class

    是否为被代理类生成CGLIB子类,只为接口生成代理子类(即:是否使用CGlib动态代理);默认为false,使用的是JDK代理

  • expose-proxy

    是否将代理的Bean暴露给用户,如果暴露,就可以通过AopContext类获得,默认不暴露

/**
 * @Date: 2023/2/11
 * 开启AOP注解支持配置类
 * @Configuration注解:表示该类为一个配置类
 * @ComponentScan注解:用于扫描包上的注解将组件加载到IoC容器中
 */
@Configuration
@ComponentScan("com.itan.aop.*")
@EnableAspectJAutoProxy
public class AopConfig {
}
           

二、切面相关注解

1、@Aspect注解

1、

@Aspect

注解对应着XML配置中的

<aop:aspect />

标签,被该注解标注的类,会被当做切面类并且用于自动配置Spring AOP。如果类仅仅只标注了该注解,那么是不会被Spring组件扫描工具自动扫描到的,并且不会加入到IoC容器中,

因此还需要搭配组件注册相关的注解一起使用(如:@Component)

2、切面类和普通类一样,可以有自己的方法和字段,还可以包含切入点(pointcut)、通知(advice)、声明(introduction),这些都是通过方法来绑定的。

3、切面类本身是不能成为其他切面通知的目标类,类上面标注了@Aspect注解之后,该类的Bean将从AOP自动配置Bean中排除,因此切面类里面的方法是不能被代理的。

2、@Pointcut注解

1、

@Pointcut

注解对应着XML配置中的

<aop:pointcut />

标签,用来定义一个切入点,在匹配的情况下执行通知,切入点表达式的语法都是一样的,请参考切入点声明规则。

2、

@Pointcut注解标注在一个切面类的方法上,方法名就是该切入点的名字,在通知中通过名字引用该切入点(XXX(),要带上括号),也可以将多个切入点组合成一个新的切入点(可以使用&&、||、!等运算符连接起来),XML配置中不具备该组合方式

3、通知相关注解

1、advice通知同样可以使用注解声明,并且绑定到一个方法上,一共有五种通知注解,分别和XML中的五中通知标签一一对应。

2、五种通知注解:

  • @Before

    :用于定义前置通知,在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。
  • @AfterReturning

    :用于定义后置通知,在使用时可以指定pointcut或value和returning属性;其中pointcut/value这两个属性的作用一样,都用于指定切入点表达式;

    returning属性值是后置通知方法中的参数名,用来将切入点方法的返回值绑定到该参数上

  • @AfterThrowing

    :用于定义异常通知,在前置通知、切入点方法和后置通知中抛出异常之后可能会执行;在使用时可指定pointcut或value和throwing属性,其中pointcut/value用于指定切入点表达式;

    throwing属性值是异常通知方法中的参数名,用来将前置通知、切入点方法、后置通知执行过程中抛出的异常绑定到该参数上

  • @After

    :用于定义最终通知,不管是否异常,该通知都会执行;使用时需要指定一个value属性,用于指定一个切入点表达式。
  • @Around

    :用于定义环绕通知,使用时需要指定一个value属性,用于指定一个切入点表达式。
/**
 * @Date: 2023/2/11
 * 切面类
 * @Aspect:用于定义该类为一个切面类
 * @Component:将组件注册到IoC容器中,否则单单使用@Aspect是不会被组件扫描注解检测到的
 */
@Aspect
@Component
public class AnnoAspect {

    /**
     * 切入点,该切入点匹配service包下所有类所有方法
     */
    @Pointcut("execution(* com.itan.aop.service..*.*(..))")
    public void pointCut() {}


    /**
     * 前置通知
     */
    @Before(value = "pointCut()")
    public void before() {
        System.out.println("前置通知");
    }

    /**
     * 异常通知
     */
    @AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowing(Exception e) {
        System.out.println("异常通知,异常为:" + e.getMessage());
    }

    /**
     * 后置通知
     */
    @AfterReturning(value = "pointCut()", returning = "result")
    public void afterReturning(Object result) {
        System.out.println("后置通知,返回值为:" + result);
    }

    /**
     * 最终通知
     */
    @After(value = "pointCut()")
    public void afterFinally() {
        System.out.println("最终通知");
    }

    /**
     * 环绕通知
     * 一定要有ProceedingJoinPoint类型的参数
     */
    @Around(value = "pointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        System.out.println("环绕通知 --- 进入方法");
        Object[] args = point.getArgs();
        System.out.println("外部传入的参数:" + Arrays.toString(args));
        System.out.println(point.toString());
        System.out.println(point.toShortString());
        System.out.println(point.toLongString());
        System.out.println(point.getThis());
        System.out.println(point.getTarget());
        System.out.println(point.getSignature());
        System.out.println(point.getSourceLocation());
        System.out.println(point.getKind());
        System.out.println(point.getStaticPart());
        // proceed方法表示调用切入点方法,否则方法不会执行,args表示参数,proceed就是切入点方法的返回值
        Object proceed = point.proceed(args);
        System.out.println("环绕通知 --- 退出方法");
        return proceed;
    }
}
           
/**
 * @Date: 2023/2/11
 * 目标接口实现类
 */
@Service
public class CalculateServiceImpl implements CalculateService {
    public Integer add(int i, int j) {
        return i + j;
    }

    public int sub(int i, int j) {
        return i + j;
    }

    public int multiply(int i, int j) {
        return i * j;
    }

    public double divide(int i, int j) {
        return i / j;
    }
}
           
@Test
public void test04() {
    // 通过配置文件创建容器对象
    ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
    CalculateService calc = context.getBean(CalculateService.class);
    System.out.println(calc.getClass());
    calc.add(2,3);
}
/**
 * 运行结果:可以看到默认情况下使用JDK代理:class com.sun.proxy.$Proxy28
 * class com.sun.proxy.$Proxy28
 * 环绕通知 --- 进入方法
 * 外部传入的参数:[2, 3]
 * execution(Integer com.itan.aop.service.CalculateService.add(int,int))
 * execution(CalculateService.add(..))
 * execution(public abstract java.lang.Integer com.itan.aop.service.CalculateService.add(int,int))
 * [email protected]
 * [email protected]
 * Integer com.itan.aop.service.CalculateService.add(int,int)
 * org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint$SourceLocationImpl@7357a011
 * method-execution
 * execution(Integer com.itan.aop.service.CalculateService.add(int,int))
 * 前置通知
 * 后置通知,返回值为:5
 * 最终通知
 * 环绕通知 --- 退出方法
 */
           

4、通知顺序

1、当同一个连接点方法中绑定了多个同一个类型的通知时,有时需要指定通知的执行顺序,在XML中通过

<aop:aspect />切面标签中的order属性指定执行通知顺序

,注解配置中也能实现通知执行顺序。

2、实现方式:

  • 通知类实现Ordered接口

  • 通知类标注@Order注解

  • 提示:未设置order值时,默认值为Integer.MAX_VALUE;值越小的切面,其内部的前置通知越先执行,后置通知越后执行。
/**
 * @Date: 2023/2/11
 * 通过@Order注解方式设置顺序
 */
@Aspect
@Component
@Order(Integer.MAX_VALUE - 2)
public class AnnoOrderAspect1 {
    /**
     * 切入点,该切入点匹配service包下所有类所有方法
     */
    @Pointcut("execution(* com.itan.aop.service..*.multiply(..))")
    public void pointCut() {}

    /**
     * 前置通知
     */
    @Before(value = "pointCut()")
    public void before() {
        System.out.println("AnnoOrderAspect1前置通知");
    }

    /**
     * 后置通知
     */
    @AfterReturning(value = "pointCut()", returning = "result")
    public void afterReturning(Object result) {
        System.out.println("AnnoOrderAspect1后置通知,返回值为:" + result);
    }

    /**
     * 最终通知
     */
    @After(value = "pointCut()")
    public void afterFinally() {
        System.out.println("AnnoOrderAspect1最终通知");
    }
}
           
/**
 * @Date: 2023/2/11
 * 通过实现Ordered接口方式设置顺序
 */
@Aspect
@Component
public class AnnoOrderAspect2 implements Ordered {
    /**
     * 切入点,该切入点匹配service包下所有类所有方法
     */
    @Pointcut("execution(* com.itan.aop.service..*.multiply(..))")
    public void pointCut() {}

    /**
     * 前置通知
     */
    @Before(value = "pointCut()")
    public void before() {
        System.out.println("AnnoOrderAspect2前置通知");
    }

    /**
     * 后置通知
     */
    @AfterReturning(value = "pointCut()", returning = "result")
    public void afterReturning(Object result) {
        System.out.println("AnnoOrderAspect2后置通知,返回值为:" + result);
    }

    /**
     * 最终通知
     */
    @After(value = "pointCut()")
    public void afterFinally() {
        System.out.println("AnnoOrderAspect2最终通知");
    }

    /**
     * 获取顺序值
     * @return
     */
    @Override
    public int getOrder() {
        return Integer.MAX_VALUE - 1;
    }
}
           
@Test
public void test05() {
    // 通过配置文件创建容器对象
    ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
    CalculateService calc = context.getBean(CalculateService.class);
    System.out.println(calc.getClass());
    calc.multiply(2,3);
}
/**
 * 运行结果:值越小的切面的前置通知越先执行,后置通知越后执行
 * AnnoOrderAspect1前置通知
 * AnnoOrderAspect2前置通知
 * AnnoOrderAspect2后置通知,返回值为:6
 * AnnoOrderAspect2最终通知
 * AnnoOrderAspect1后置通知,返回值为:6
 * AnnoOrderAspect1最终通知
 */