AOP 與 註解的那些事兒~

持續原創輸出,點擊上方藍字關注我

目錄

  • 前言
  • 什麼是AOP?
  • AOP的相關概念(面試常客)
  • Spring Boot 如何整合AOP自定義一個註解?
  • 使用攔截器如何自定義註解?
  • 內部調用導致AOP註解失效
  • 總結

前言

註解相信大家都用過,尤其是Spring Boot 這個框架,比如@Controller

這篇文章就來介紹下Spring Boot 中如何自定義一個註解,順帶介紹一下Spring BootAOP如何整合。

什麼是AOP?

AOP即是面向切面,是Spring的核心功能之一,主要的目的即是針對業務處理過程中的橫向拓展,以達到低耦合的效果。

舉個栗子,項目中有記錄操作日誌的需求、或者流程變更是記錄變更履歷,無非就是插表操作,很簡單的一個save操作,都是一些記錄日誌或者其他輔助性的代碼。一遍又一遍的重寫和調用。不僅浪費了時間,又將項目變得更加的冗餘,實在得不償失。

此時AOP的就該出場了,能夠在不改變原邏輯的基礎上實現相關功能。

AOP的相關概念(面試常客)

要理解Spring Boot整合Aop的實現,就必須先對面向切面實現的一些Aop的概念有所瞭解,不然也是雲裏霧裏。

切面(Aspect):一個關注點的模塊化。以註解@Aspect的形式放在類上方,聲明一個切面。

連接點(Joinpoint):在程序執行過程中某個特定的點,比如某方法調用的時候或者處理異常的時候都可以是連接點。

通知(Advice):通知增強,需要完成的工作叫做通知,就是你寫的業務邏輯中需要比如事務、日誌等先定義好,然後需要的地方再去用。增強包括如下五個方面:

  1. @Before:在切點之前執行
  2. @After:在切點方法之後執行
  3. @AfterReturning:切點方法返回後執行
  4. @AfterThrowing:切點方法拋異常執行
  5. @Around:屬於環繞增強,能控制切點執行前,執行後,用這個註解後,程序拋異常,會影響@AfterThrowing這個註解。

切點(Pointcut):其實就是篩選出的連接點,匹配連接點的斷言,一個類中的所有方法都是連接點,但又不全需要,會篩選出某些作爲連接點做爲切點。

引入(Introduction):在不改變一個現有類代碼的情況下,爲該類添加屬性和方法,可以在無需修改現有類的前提下,讓它們具有新的行爲和狀態。其實就是把切面(也就是新方法屬性:通知定義的)用到目標類中去。

目標對象(Target Object):被一個或者多個切面所通知的對象。也被稱做被通知(adviced)對象。既然Spring AOP是通過運行時代理實現的,這個對象永遠是一個被代理(proxied)對象。

AOP代理(AOP Proxy)AOP框架創建的對象,用來實現切面契約(例如通知方法執行等等)。在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。

織入(Weaving):把切面連接到其它的應用程序類型或者對象上,並創建一個被通知的對象。這些可以在編譯時(例如使用AspectJ編譯器),類加載時和運行時完成。Spring和其他純Java AOP框架一樣,在運行時完成織入。

Spring Boot 如何整合AOP自定義一個註解?

在實際開發中對於橫向公共的邏輯需要抽取出來,這時候就需要使用AOP,比如日誌的記錄、權限的驗證等等,這些功能都可以用註解輕鬆的完成。

下面介紹如何在Spring Boot使用AOP定義一個註解。

添加依賴starter

AOP整合Spring Boot有一個starter,只需要添加依賴即可,如下:

<!--springboot集成Aop-->
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>

開啓AOP

在配置類上標註@EnableAspectJAutoProxy註解即可開啓AOP,這個註解有什麼用呢,源碼如下:

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

最重要的是如下一行代碼:

@Import(AspectJAutoProxyRegistrar.class)

@Import這個註解很熟悉了吧,快速注入一個類,這裏是注入一個AnnotationAwareAspectJAutoProxyCreator

自定義一個註解

就以日誌處理爲例子,定義一個日誌處理的註解,如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
    String value() default "";
}

定義一個切面

一個切面的滿足條件如下:

  1. 類上標註了@Aspect註解
  2. 注入到IOC容器中,比如@Component註解

定義的日誌切面如下:

@Component
@Aspect
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SysLogAspect {
}

@Order指定了切面執行的優先級,假如有多個切面,肯定是要有先後的執行順序,這樣才能保證邏輯性。

定義切點表達式

這裏需要攔截的肯定是@SysLog這個註解,只要方法上標註了該註解都將會被攔截,表達式如下:

@Pointcut("@annotation(com.example.annotation_demo.annotation.SysLog)")
public void pointCut() {}

添加通知方法

既然是日誌記錄,肯定是在方法執行前,執行後都需要記錄,因此需要定義一個環繞通知,如下:

  @Around("pointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        //邏輯開始時間
        long beginTime = System.currentTimeMillis();

        //執行方法
        Object result = point.proceed();

        //todo,保存日誌,自己完善
        saveLog(point,beginTime);

        return result;
    }

測試

以上配置完成後即可使用,只需要在需要的方法上標註@SysLog註解即可,如下:

@SysLog
@PostMapping("/add")
public String add(){
  return "";
}

使用攔截器如何自定義註解?

使用AOP自定義的註解在每個方法上都會被攔截驗證,首先效率上就不高。

然而攔截器是在每個Controller方法執行之前進行攔截,其他的方法都不會生效,比如service方法。

比如權限的驗證、防止瞬間重複點擊等等需求就適合使用攔截器自定義的註解。

自定義一個註解

就以防止瞬間重複點擊的例子來創建一個註解,如下:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
    /**
     * 默認失效時間5秒
     */

    long seconds() default 5;
}

自定義攔截器

需要在請求執行之前完成驗證,邏輯很簡單,就是判斷方法上有沒有標註@RepeatSubmit註解,代碼如下:

/**
 * description:重複提交註解的攔截器
 */

@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod){
            //只攔截標註了@RepeatSubmit該註解
            HandlerMethod handlerMethod=(HandlerMethod)handler;
            //獲取controller方法上標註的註解
            RepeatSubmit repeatSubmit = AnnotationUtils.findAnnotation(handlerMethod.getMethod(),RepeatSubmit.class);
            //沒有限制重複提交,直接跳過
            if (Objects.isNull(repeatSubmit))
                return true;
            //todo 一個值,標誌這個請求的唯一性,比如IP+userId+uri+請求參數
            String flag="";
            //存在即返回false,不存在即返回true
            Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(flag, "", repeatSubmit.seconds(), TimeUnit.SECONDS);
            if (ifAbsent!=null&&!ifAbsent)
                //todo: 此處拋出異常,需要在全局異常解析器中捕獲
                throw new RepeatSubmitException();
        }
        return true;
    }
}

注入的攔截器

將上述自定義的攔截器注入到Sprign Boot中,這裏不再演示了,前面教程有介紹過,請看:Spring Boot 第六彈,攔截器如何配置,看這兒~

測試

在需要攔截方法上添加@RepeatSubmit註解即可,如下:

    @RepeatSubmit
    @GetMapping("/add")
    public String add(){
        return "";
    }

內部調用導致AOP註解失效

這個問題在事務中也是經常被忽略的問題,網上很多人說是AOPBug,其實在我看來這真不是一個BUG,並且也是有辦法解決的。

先來看一下失效的案例,如下:

public class ArticleServiceImpl{
  @SysLog
  public void A(){
    ......
  }
  
  
  public void B(){
    this.A();
  }
}

在上述的代碼中,如果執行方法B,則@SysLog註解將會失效。

失效的原因

AOP使用的是動態代理的機制,它會給類生成一個代理類,事務的相關操作都在代理類上完成。內部方式使用this調用方式時,使用的是實例調用,並沒有通過代理類調用方法,所以會導致事務失效。

解決方法

其實解決方法有很多,下面將會一一介紹。

1. 引入自身的Bean

在類內部通過@Autowired將本身bean引入,然後通過調用自身bean,從而實現使用AOP代理操作。代碼如下:

public class ArticleServiceImpl{
  /**
  * 注入自身的Bean
  */

  @Autowired
  private ArticleService articleService;
  
  @SysLog
  public void A(){
    ......
  }
  
  public void B(){
    articleService.A();
  }
}

2. 通過ApplicationContext引入bean

通過ApplicationContext獲取bean,通過bean調用內部方法,就使用了bean的代理類。

需要先創建一個ApplicationContext的工具類獲取ApplicationContext,然後才能調用getBean()方法,代碼如下:

public class ArticleServiceImpl{
  
  @SysLog
  public void A(){
    ......
  }
  
  public void B(){
    ApplicationContextUtils.getApplicationContext().getBean(ArticleService.class).A();
  }
}

3. 通過AopContext獲取當前類的代理類

此種方法需要設置@EnableAspectJAutoProxy中的exposeProxytrue

使用AopContext獲取當前的代理對象,代碼如下:

public class ArticleServiceImpl{
  
  @SysLog
  public void A(){
    ......
  }
  
  public void B(){
    ((ArticleService)AopContext.currentProxy()).A();
  }
}

總結

這篇文章介紹了AOP的相關概念、AOP實現自定義註解以及攔截器實現自定義註解,都是日常開發中必備的知識點,希望這篇文章對各位有所幫助。

源碼已經上傳,回覆關鍵詞AOP註解獲取。

最後,別忘了點贊哦!!!

另外作者的第一本PDF書籍已經整理好了,由淺入深的詳細介紹了Mybatis基礎以及底層源碼,有需要的朋友回覆關鍵詞Mybatis進階即可獲取,目錄如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章