天天看點

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最終通知
 */