天天看點

19 spring-注解驅動AOP--常用注解

文章目錄

    • 1 @EnableAspectJAutoProxy
      • 1.1 暴露代理對象:exposeProxy屬性
      • 1.2 代理方式:proxyTargetClass屬性
    • 2 @Aspect注解
      • 2.1 value屬性(了解)
    • 3 @Pointcut
      • 3.1 簡單使用
      • 3.2 修飾符
    • 4 用于配置通知的注解
      • 4.1 基本通知(前置,後置,最終,異常)
      • 4.2 執行順序
        • 4.2.1 不同通知類型的執行順序
          • 總結1:
        • 4.2.2 相同通知類型的執行順序(了解)
        • 4.2.3 通知類型的執行順序總結
      • 4.3 環繞通知:@Around注解
      • 4.3.1 環繞通知:實作其他四種基本通知
      • 4.3.2 ProceedingJoinPoint

1 @EnableAspectJAutoProxy

用于開啟注解AOP支援的,表示開啟spring對注解aop的支援。它有兩個屬性,分别是指定采用的代理方式和 是否暴露代理對象,通過AopContext可以進行通路。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

	// 指定是否采用cglib進行代理。預設值是false,表示使用jdk的代理。
	boolean proxyTargetClass() default false;
	// 指定是否暴露代理對象,通過AopContext可以進行通路。
	boolean exposeProxy() default false;
}
           

1.1 暴露代理對象:exposeProxy屬性

指定是否暴露代理對象,通過AopContext可以進行通路。啥意思呢?

  1. 聲明目标類的接口并實作該接口
public interface UserService {

    public void save(User user);

    public void batchSave(List<User> userList);
}
           
@Service
@Slf4j
public class UserServiceImpl implements UserService {
    @Override
    public void save(User user) {
        String uuid = UUID.randomUUID().toString();
        user.setId(uuid);
        log.info("模拟儲存使用者。。。。。");
        return user;
    }
	// 實作,内部調用的就是save方法
    @Override
    public void batchSave(List<User> userList) {
        for (User user : userList) {
            this.save(user);
        }
    }
}
           
  1. 切面類
@Component
@Aspect
public class LogUtils {

    /***
     * 前置通知: 在方法執行之前進行日志列印
     *
     * */
    @Before("execution(* study.wyy.spring.annotion.aop.service.*.*.*(..))")
    public void beforeLog() {
        System.out.println("執行日志列印");
    }
}
           
  1. 配置類
@ComponentScan("study.wyy.spring.annotion.aop")
@Configuration
@EnableAspectJAutoProxy
public class AopConfig1 {
}
           
  1. 啟動測試
public static void main(String[] args) {
    // 1 擷取容器
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig1.class);
    //2.擷取bean對象
    UserService userService = ac.getBean(UserService.class);
    System.out.println(userService);
    // 3.準備資料
    User user = new User();
    user.setId("1");
    user.setUsername("test");
    user.setNickname("泰斯特");
    userService.batchSave(Arrays.asList(user));
}
           

日志輸出:也是沒問題的

執行日志列印
模拟儲存使用者
           

但是呢如果,修改切入點表達式為:

"@Before("execution(* study.wyy.spring.annotion.aop.service.*.save.*(..))")"

,隻比對save開頭的方法

@Component
@Aspect
public class LogUtils {

    /***
     * 前置通知: 在方法執行之前進行日志列印
     * */
    //@Before("execution(* study.wyy.spring.annotion.aop.service.impl.*.*(..))")
    Before("execution(* study.wyy.spring.annotion.aop.service.impl.*.*save(..))")
    public void beforeLog() {
        System.out.println("執行日志列印");
    }
}
           

再次測試:發現沒有列印日志

模拟儲存使用者
           

原因是:切入點隻比對了save開頭的方法,是以batchSave這個方法就沒有被增強,雖然内部也是調用了save方法,但是此時由于已經過了切入的時機了,是以沒有列印日志

@Override
public void batchSave(List<User> userList) {
    for (User user : userList) {
   		 // 如果想要列印日志,就應該使用代理對象去執行save方法
        this.save(user);
    }
}
           

如何擷取代理對象?

  1. exposeProxy設為true
@ComponentScan("study.wyy.spring.annotion.aop")
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
public class AopConfig2 {
}
           
  1. 使用AopContext擷取代理對象,這個代理對象适合目前線程綁定的,是以是線程安全的
@Override
    public void batchSave(List<User> userList) {
        // 實作,内部調用的就是save方法
        for (User user : userList) {
            // 擷取代理對象
            UserService userService = (UserService) AopContext.currentProxy();
            userService.save(user);
        }
    }
           

再測試:

執行日志列印
模拟儲存使用者
           

1.2 代理方式:proxyTargetClass屬性

指定是否采用cglib進行代理。預設值是false,表示使用jdk的代理。

如果将UserServiceImpl使用final修飾,也就是會所UserServiceImpl無法有子類,進行測試

  1. 配置類
@ComponentScan("study.wyy.spring.annotion.aop")
@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}
           
  1. 測試
public static void main(String[] args) {
     // 1 擷取容器
     AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
     //2.擷取bean對象
     UserService userService = ac.getBean(UserService.class);
     userService.save(new User());
 }
           

發現沒有沒有問題,照樣可以列印日志,此時優先使用jdk代理

如果将proxyTargetClass設為true,強制使用cglib代理:

@ComponentScan("study.wyy.spring.annotion.aop")
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig4 {
}
           

發現報錯:

Cannot subclass final class study.wyy.spring.annotion.aop.service.impl.UserServiceImpl

,大緻就是不能繼承final類,因為cglib是通過繼承

目标類實作的,而現在目标類被final修飾,無法被被繼承,是以報錯

2 @Aspect注解

聲明目前類是一個切面類。

屬性value: 預設我們的切面類應該為單例的。但是當切面類為一個多例類時,指定預 處理的切入點表達式。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Aspect {

    /**
     * Per clause expression, defaults to singleton aspect
     * <p/>
     * Valid values are "" (singleton), "perthis(...)", etc
     * 預設我們的切面類應該為單例的。但是當切面類為一個多例類時,指定預處理的切入點表達式。
     * 它支援指定切入點表達式,或者是用@Pointcut修飾的方法名稱(要求全限定方法名)
     */
    public String value() default "";
}
           

2.1 value屬性(了解)

  1. 切面類為多例模式: @Scope(“prototype”)
@Component
@Aspect("perthis(execution(* study.wyy.spring.annotion.aop.service.impl.*.*(..)))")
public class LogUtils {

    /***
     * 前置通知: 在方法執行之前進行日志列印
     *
     * */
    @Before("execution(* study.wyy.spring.annotion.aop.service.impl.*.*(..))")
    public void beforeLog() {
        System.out.println("執行日志列印");
    }
}
           
  1. 測試
public static void main(String[] args) {
      // 1 擷取容器
      AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig1.class);
      //2.擷取bean對象
      UserService userService = ac.getBean(UserService.class);
      // 3.準備資料
      User user = new User();
      userService.save(user);
  }
           

測試報錯:

Bean with name 'logUtils' is a singleton, but aspect instantiation model is not singleton

原因在于,此時我們的切面是多例的,但是我們的logUtils這bean是單例的,是以改為多例;

@Component
@Aspect("perthis(execution(* study.wyy.spring.annotion.aop.service.impl.*.*(..)))")
@Scope("prototype")
public class LogUtils {

    /***
     * 前置通知: 在方法執行之前進行日志列印
     *
     * */
    @Before("execution(* study.wyy.spring.annotion.aop.service.impl.*.*(..))")
    public void beforeLog() {
        System.out.println("執行日志列印");
    }
}
           

3 @Pointcut

此注解是用于指定切入點表達式的。此注解就是代替xml中的aop:pointcut标簽,實作切入點表達 式的通用化。

簡單來說就是抽取切入點表達式,存在多種通知,但是這幾種通知是比對同一個切入點表達式,就可以使用改注解抽出一個通用的切入點表達式

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Pointcut {

    /**
     * The pointcut expression
     * We allow "" as default for abstract pointcut
     * 用于指定切入點表達式。
     */
    String value() default "";
    
    /**
     * When compiling without debug info, or when interpreting pointcuts at runtime,
     * the names of any arguments used in the pointcut are not available.
     * Under these circumstances only, it is necessary to provide the arg names in 
     * the annotation - these MUST duplicate the names used in the annotated method.
     * Format is a simple comma-separated list.
     * 用于指定切入點表達式的參數。參數可以是execution中的,也可以是 args中的。通常情況下不使用此屬性也可以獲得切入點方法參數。
     */
    String argNames() default "";
}
           

3.1 簡單使用

  1. 目标類
package study.wyy.spring.annotion.aop.service;
import study.wyy.spring.annotion.aop.model.User;
import java.util.List;

public interface UserService {

     void save(User user);

}
           
package study.wyy.spring.annotion.aop.service.impl;

import org.springframework.stereotype.Service;
import study.wyy.spring.annotion.aop.model.User;
import study.wyy.spring.annotion.aop.service.UserService;

@Service
public class UserServiceImpl implements UserService {
    @Override
    public void save(User user) {
        System.out.println("模拟儲存使用者");
    }
}
           
  1. 切面類
package study.wyy.spring.annotion.aop.aop;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LogUtils {

 
    @Before("execution(* study.wyy.spring.annotion.aop.service.impl.*.*(..))")
    public void beforeLog() {
        System.out.println("beforeLog: 執行日志列印");
    }

    @After("execution(* study.wyy.spring.annotion.aop.service.impl.*.*(..))")
    public void afterLog() {
        System.out.println("afterLog: 執行日志列印");
    }
}

           
  1. 配置類
package study.wyy.spring.annotion.aop.config;

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

@ComponentScan("study.wyy.spring.annotion.aop")
@Configuration
@EnableAspectJAutoProxy
public class AopConfig1 {
}

           
  1. 測試
public class Test01 {

    public static void main(String[] args) {
        // 1 擷取容器
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig1.class);
        //2.擷取bean對象
        UserService userService = ac.getBean(UserService.class);
        // 3.準備資料
        User user = new User();
        userService.save(user);
    }
}
           
  1. 測試結果
beforeLog: 執行日志列印
模拟儲存使用者
afterLog: 執行日志列印
           

**之前的這種存在一個問題,before和after配置的切入點是一樣的,這個時候就需要把這個切入點表達式,抽取出來,做到

一次修改,處處生效

**

使用@Pointcut注解抽取即可:

package study.wyy.spring.annotion.aop.aop;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LogUtils {
	// 使用Pointcut抽取切入點表達式
    @Pointcut("execution(* study.wyy.spring.annotion.aop.service.impl.*.*(..))")
    private void pointCut(){};

	// 使用Pointcut指定的切入點表達式
    @Before("pointCut()")
    public void beforeLog() {
        System.out.println("beforeLog: 執行日志列印");
    }

    @After("pointCut()")
    public void afterLog() {
        System.out.println("afterLog: 執行日志列印");
    }
}
           

3.2 修飾符

@Pointcut("execution(* study.wyy.spring.annotion.aop.service.impl.*.*(..))")
    private void pointCut(){};
           

配置pointCut的方法的修飾符private有啥作用呢?和方法的修飾符作用一樣,private表示在隻能在本類中使用

如果另一個切面類想要使用這個pointCut聲明的切入點表達式,需要滿足一下兩點

  1. 方法的修飾符是public或者protected,至于他倆的差別,學過java的的都知道,就不多說了
  2. 引用的時候使用全類名+方法名
@Before("study.wyy.spring.annotion.aop.aop.LogUtils.pointCut()")
    public void beforeLog() {
        System.out.println("beforeLog: 執行日志列印");
    }
    @After("study.wyy.spring.annotion.aop.aop.LogUtils.pointCut()")
    public void afterLog() {
        System.out.println("afterLog: 執行日志列印");
    }
           

4 用于配置通知的注解

4.1 基本通知(前置,後置,最終,異常)

  1. @Before: 被此注解修飾的方法為前置通知。前置通知的執行時間點是在切入點方法執行之前
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before {

    /**
     * 用于指定切入點表達式。可以是表達式,也可以是表達式的引用。
     */
    String value();
    
    // 用于指定切入點表達式參數的名稱。它要求和切入點表達式中的參數名稱 一緻。通常不指定也可以擷取切入點方法的參數内容。
    String argNames() default "";

}
           
  1. AfterReturning: 用于配置後置通知。後置通知的執行是在切入點方法正常執行之後執行。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterReturning {

    /**
     * 用于指定切入點表達式,可以是表達式,也可以是表達式的引用。
     */
    String value() default "";

    /**
     * 它的作用和value是一樣的。
     */
    String pointcut() default "";

    /**
     * The name of the argument in the advice signature to bind the returned value to
     */
    String returning() default "";
    
    /**
    	用于指定切入點表達式參數的名稱。它要求和切入點表達式中的參數名稱 一緻。通常不指定也可以擷取切入點方法的參數内容。
     */
    String argNames() default "";

}
           
  1. AfterThrowing: 用于配置異常通知。用此注解修飾的方法執行時機是在切入點方法執行産生異常之後執行。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterThrowing {
    String value() default "";

    String pointcut() default "";

    /**
     * 指定切入點方法執行産生異常時的異常對象變量名稱。它必須和異常變量
名稱一緻。
     */
    String throwing() default "";
   
    String argNames() default "";

}
           
  1. After: 用于指定最終通知。 最終通知的執行時機,是在切入點方法執行完成之後執行,無論切入點方法執行是

    否産生異常最終通知都會執行。是以被此注解修飾的方法,通常都是做一些清理操作。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface After {

 
    String value();
    
    String argNames() default "";
}
           
  1. Around: 用于指定環繞通知。環繞通知有别于前面介紹的四種通知類型。它不是指定增強方法執行時機的,而是

    spring為我們提供的一種可以通過編碼的方式手動控制增強方法何時執行的機制。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Around {

  
    String value();
    
  
    String argNames() default "";

}
           

4.2 執行順序

4.2.1 不同通知類型的執行順序

  1. 目标類
package study.wyy.spring.anno.aop.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import study.wyy.spring.anno.aop.model.User;
import study.wyy.spring.anno.aop.service.UserService;

import java.util.List;
import java.util.UUID;

@Service
@Slf4j
public final class UserServiceImpl implements UserService {
    @Override
    public User save(User user) {
        if(null == user){
           throw new IllegalArgumentException("user.is.null");
        }
        System.out.println("模拟儲存");
        return user;
    }

}
           
  1. 切面類
package study.wyy.spring.anno.aop.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogUtils {
    @Pointcut("execution(* study.wyy.spring.anno.aop.service.impl.*.*save(..))")
    private void pointCut(){};
    /***
     * 前置通知: 在方法執行之前進行日志列印
     *
     * */
    @Before("pointCut()")
    public void beforeLog() {
        System.out.println("[前置通知]: 在方法執行之前進行日志列印");
    }

    /***
     * 後置通知: 在目标方法執行後實施增強 在方法執行結束後,列印日志
     * */
    @AfterReturning("pointCut()")
    public void afterReturningLog() {
        System.out.println("[後置通知]: 在目标方法執行後實施增強 在方法執行結束後,列印日志");
    }

    /***
     * 異常通知
     *  發生異常的時候執行
     * */
    @AfterThrowing("pointCut()")
    public void afterThrowingLog() {
        System.out.println("[異常通知]: 發生異常的時候執行");
    }

    /***
     * 最終通知: 無論切入點方法執行是否産生異常最終通知都會執行
     * */
    @After("pointCut()")
    public void afterLog() {
        System.out.println("[最終通知]: 無論切入點方法執行是否産生異常最終通知都會執行");
    }
}
           
  1. spring配置類
package study.wyy.spring.anno.aop.config;

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

@Configuration
@ComponentScan("study.wyy.spring.anno.aop")
@EnableAspectJAutoProxy(exposeProxy = true) // 開啟AspectJ
public class SpringConfiguration {
}
           
  1. 測試和結果
public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        UserService userService = context.getBean(UserService.class);
        userService.save(new User());
    }
           
[前置通知]: 在方法執行之前進行日志列印
模拟儲存
[最終通知]: 無論切入點方法執行是否産生異常最終通知都會執行
[後置通知]: 在目标方法執行後實施增強 在方法執行結束後,列印日志
           

為何最終通知在後置通知之前執行了?

  • 正常按照我們的了解來說來說,通知的執行應該是符合下面這段僞代碼的:
public static void main(String[] args) {
    try {
        System.out.println("前置通知");
        System.out.println("切入點方法");
        System.out.println("後置通知");
    }catch (Exception e){
        System.out.println("異常通知");
    }finally {
        System.out.println("最終通知");
    }
}
           
  • 但是spring并不是這麼處理,而是這樣:
public static void main(String[] args) {
    try {
        try {
            System.out.println("前置通知");
            System.out.println("切入點方法");
        }finally {
            System.out.println("最終通知");
        }
        System.out.println("後置通知");
    }catch (Exception e){
        System.out.println("異常通知");
    }
}
           
  • 是以不僅僅後置通知在最終通知後面執行,異常通知也是在最終通知後面執行:
public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
    UserService userService = context.getBean(UserService.class);
    // 模拟異常
    userService.save(null);
}
           
[前置通知]: 在方法執行之前進行日志列印
[最終通知]: 無論切入點方法執行是否産生異常最終通知都會執行
[異常通知]: 發生異常的時候執行
Exception in thread "main" java.lang.IllegalArgumentException: user.is.null
           
總結1:

執行不同類型通知的執行順序為:

  1. 前置通知: 在方法執行之前進行日志列印
  2. 執行切入點方法
  3. 最終通知: 無論切入點方法執行是否産生異常最終通知都會執行
  4. 是否發生異常
    • 否:後置通知: 在目标方法執行後實施增強 在方法執行結束後,列印日志
    • 是: 異常通知: 發生異常的時候執行
  5. 最終通知: 無論切入點方法執行是否産生異常最終通知都會執行

4.2.2 相同通知類型的執行順序(了解)

  1. 在切入點裡面在加入兩個前置通知
@Before("pointCut()")
    public void beforeLog() {
        System.out.println("[前置通知:beforeLog]: 在方法執行之前進行日志列印");
    }

    @Before("pointCut()")
    public void ceforeLog() {
        System.out.println("[前置通知:ceforeLog]: 在方法執行之前進行日志列印");
    }

    @Before("pointCut()")
    public void baforeLog() {
        System.out.println("[前置通知:baforeLog]: 在方法執行之前進行日志列印");
    }
           
  1. 測試和結果
public static void main(String[] args) {
     AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
     UserService userService = context.getBean(UserService.class);
     userService.save(new User());
 }
           
[前置通知:baforeLog]: 在方法執行之前進行日志列印
[前置通知:beforeLog]: 在方法執行之前進行日志列印
[前置通知:ceforeLog]: 在方法執行之前進行日志列印
模拟儲存
[最終通知]: 無論切入點方法執行是否産生異常最終通知都會執行
[後置通知]: 在目标方法執行後實施增強 在方法執行結束後,列印日志
           

可見是根據方法名的ASCII碼的順序執行的

  • baforeLog和beforeLog在ceforeLog之前執行,因為b在c之前
  • baforeLog在beforeLog之前:因為a在e之前

4.2.3 通知類型的執行順序總結

執行不同類型通知的執行順序為:

  1. 前置通知: 在方法執行之前進行日志列印
  2. 執行切入點方法
  3. 最終通知: 無論切入點方法執行是否産生異常最終通知都會執行
  4. 是否發生異常
    • 否:後置通知: 在目标方法執行後實施增強 在方法執行結束後,列印日志
    • 是: 異常通知: 發生異常的時候執行
  5. 最終通知: 無論切入點方法執行是否産生異常最終通知都會執行

相同類型的通知則根據方法名的字母順序的順序執行

4.3 環繞通知:@Around注解

環繞通知有别于前面介紹的四種通知類型。它不是指定增強方法執行時機的,而是

spring為我們提供的一種可以通過編碼的方式手動控制增強方法何時執行的機制。

@Around注解用于指定環繞通知。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Around {

 	// 指定切入點表達式
    String value();
	// 用于指定切入點表達式參數的名稱。它要求和切入點表達式中的參數名稱 一緻。通常不指定也可以擷取切入點方法的參數内容。
    String argNames() default "";

}
           

4.3.1 環繞通知:實作其他四種基本通知

/***
  * 環繞通知
  * */
 @Around("pointCut()")
 public void aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
     try {
         try {
             System.out.println("[環繞通知]:前置通知");
             // 執行目标方法
             proceedingJoinPoint.proceed();
         }finally {
             System.out.println("[環繞通知]:最終通知");
         }
         System.out.println("[環繞通知]:後置通知");
     }catch (Exception e){
         System.out.println("[環繞通知]:異常通知");
     }
 }
           

執行結果:

[環繞通知]:前置通知
[前置通知:baforeLog]: 在方法執行之前進行日志列印
[前置通知:beforeLog]: 在方法執行之前進行日志列印
[前置通知:ceforeLog]: 在方法執行之前進行日志列印
模拟儲存
[環繞通知]:最終通知
[環繞通知]:後置通知
[最終通知]: 無論切入點方法執行是否産生異常最終通知都會執行
[後置通知]: 在目标方法執行後實施增強 在方法執行結束後,列印日志
           

環繞通知會比其他的通知先執行

4.3.2 ProceedingJoinPoint

這個類就是對切入點的封裝。可以擷取切入點方法的形參還有方法簽名:

// 擷取形參
Object[] args = proceedingJoinPoint.getArgs();
// 擷取方法簽名
Signature signature = proceedingJoinPoint.getSignature();