使用自定義註解實現接口參數校驗

1.前言

在接口的開發中,我們有時會想讓某個接口只可以被特定的人(來源)請求,那麼就需要在服務端對請求參數做校驗.

這種情況我們可以使用interceptor來統一進行參數校驗,但是如果很多個接口,有不同的的設定值,我們總不能寫很多個interceptor,然後按照patn逐一添加吧?

面對這種情況,我們可以選擇自定義一個註解,由註解來告訴我們,這個接口允許的訪問者是誰.

注:在本文的示例中,僅實現了對某一個字段的校驗,安全性並不高,實際項目中,可以採用多字段加密的方式,來保證安全性,原理和文中是一樣的.

2.java 註解介紹

Java Annotation是JDK5.0引入的一種註釋機制。

Annotation是代碼裏的特殊標記,這些標記可以在編譯、類加載、運行時被讀取,並執行相應的處理。

通過使用Annotation,程序員可以在不改變原有邏輯的情況下,在源文件中嵌入一些補充信息。

Annotation可以像修飾符一樣被使用,可以用於package、class、interface、constructor、method、member variable(成員變量)、parameter、local variable(局部變量)、annotation(註解),jdk 1.8之後,只要出現類型(包括類、接口、註解、枚舉)的地方都可以使用註解了。

我們可以使用JDK以及其它框架提供的Annotation,也可以自定義Annotation。

3.元註解(meta-annotation)

元註解是什麼呢?在我的理解裏,元註解是java官方提供的,用於修飾其他註解的幾個屬性.

因爲開放了自定義註解,所以所有的註解必須有章可循,他們的一些屬性必須要被定義.比如:這個註解用在什麼地方?類上還是方法上還是字段上?這個註解的生命週期是什麼?是保留在源碼裏供人閱讀就好,還是會生成在class文件中,對程序產生實際的作用?這些都需要被提前定義好,因此就有了:

這四個元註解@Target、@Retation、@Inherited、@Documented.

接下來對四個元註解逐一說明

@Target

用於描述註解的使用範圍(即:被描述的註解可以用在什麼地方)

他的取值範圍JDK定義了枚舉類ElementType,他的值共有以下幾種:

  1. CONSTRUCTOR:用於描述構造器
  2. FIELD:用於描述域即類成員變量
  3. LOCAL_VARIABLE:用於描述局部變量
  4. METHOD:用於描述方法
  5. PACKAGE:用於描述包
  6. PARAMETER:用於描述參數
  7. TYPE:用於描述類、接口(包括註解類型) 或enum聲明

注:在JDK1.8,新加了兩種類型,
8. TYPE_PARAMETER:表示這個 Annotation 可以用在 Type 的聲明式前,
9. TYPE_USE 表示這個 Annotation 可以用在所有使用 Type 的地方

@Retention

表示需要在什麼級別保存該註釋信息,用於描述註解的生命週期(即:被描述的註解在什麼範圍內有效)

他的取值範圍來自於枚舉類RetentionPolicy,取值共以下幾種:

  1. SOURCE:在源文件中有效(即源文件保留)
  2. CLASS:在class文件中有效(即class保留)
  3. RUNTIME:在運行時有效(即運行時保留)

@Documented

@Documented用於描述其它類型的annotation應該被作爲被標註的程序成員的公共API,因此可以被例如javadoc此類的工具文檔化。Documented是一個標記註解,沒有成員。

@Inherited

@Inherited 元註解是一個標記註解,@Inherited闡述了某個被標註的類型是被繼承的。如果一個使用了@Inherited修飾的annotation類型被用於一個class,則這個annotation將被用於該class的子類。

4.常見註解

常用的第三方框架實現了非常多的註解,比如MybatisParam,SpringComponent,Service,fastjsonJSONfield等等.

具體的實現方法這裏不多解釋了,有興趣的朋友可以取看一下fastjson的源碼,該項目相比spring等框架,簡單一些也更容易理解.

看到這種註解或簡單或複雜的功能之後,我們是否也可以自己來動手實現一個呢?

5.自定義註解

5.1.定義註解

首先我們來定義註解:

package com.huyan.demo.config;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * created by huyanshi on 2019/1/20
 */
@Target(ElementType.METHOD) // 該註解使用在方法上
@Retention(RetentionPolicy.RUNTIME) //運行時註解
@Documented
public @interface CheckSource {
  //該註解的參數,是一個string數組
  String[] sources() default {"all"};

}

我們需要的註解用於校驗參數,因此它的使用範圍是方法,生命週期是運行時保留.此外,註解有一個類型爲string數組的參數,用來表示當前方法允許的source列表.

5.2.編寫註解解析器

其實一開始我在這裏糾結了許久,因爲我不能理解一個註解應該在哪裏以什麼方式調用.

按照我的思路,每個註解應該有一個字段(或者類似的東西),來指示應該去哪裏調用這個註解的真正使用.

後來經過細細思考,發現這是不現實的,因爲註解的作用完全沒有規律可言,你可以實現任何你想要的功能,返回值可以使任意值,裏面的邏輯也是任意的.

那麼就意味着,你需要爲你的註解負責,否則他沒有任何作用.也就是說,你需要爲自己的註解編寫註解解析器,來定義什麼時候用到這個註解,用它幹什麼?

@純個人觀點,慎看
經過在網上衝浪,我發現註解解析器的主要形式有三種:

1.interceptor

這種方式比較方便,可以直接攔截所有的請求,檢查該請求進入的類及方法上有沒有特定的註解,如果有怎麼怎麼操作一波.

但是侷限性比較大,我們又不是隻在controller裏面會用到註解.

2.AOP

這種方式也比較方便,擴展性比較好,當你需要在新的地方用到該註解,新增一個切點就好.

3.封裝成方法,隨時調用

這種是大部分人喜聞樂見的(其實最喜聞樂見的是每次用到就寫一遍唄),但是如果不經常重構一下代碼,你會發現導出充滿了你對某一個註解的使用代碼,那就很崩潰了,你需要儘量將其封裝一下,放在統一的工具類,每次需要的時候調用即可.

@個人觀點結束!

由於我們這次的需求是攔截不合法的請求,所以當然是第一種方式比較靠譜,因此我們寫了一個攔截器:

package com.huyan.demo.config;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

/**
 * created by huyanshi on 2019/1/20
 */
public class CheckSourceInterceptor extends HandlerInterceptorAdapter {

  private static Logger LOG = LoggerFactory.getLogger(CheckSourceInterceptor.class);


  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    if (!(handler instanceof HandlerMethod)) {
      LOG.warn("UnSupport handler");
      throw new IllegalArgumentException("Interceptor only supports HandlerMethod handler");
    }
    //拿到請求參數裏面的source參數
    String source = request.getParameter("source");
    String errorMsg = null;
    //如果source爲空,返回錯誤
    if (null == source || "".equals(source)) {
      errorMsg = "No source in params";
    }
    if (errorMsg != null) {
      response.setStatus(500);
      LOG.info(errorMsg);
      response.getWriter().write(errorMsg);
      return false;
    }
    //拿到該方法上的註解對象
    CheckSource checkSource = getCheckSource((HandlerMethod) handler);
    //如果拿到的對象爲空,說明沒有此註解,直接放行
    if (checkSource != null) {
      //拿到註解對象的屬性,即允許通行的source列表
      String[] sources = checkSource.sources();
      if (sources.length == 0 || sources[0].equals("all")) {
        //列表爲空或者爲默認值,放行
        return true;
      }
      //遍歷列表,如果傳入的參數在其中,則放行
      for (String s : sources) {
        if (s.equals(source)) {
          return true;
        }
      }
      //如果傳入的source參數不在允許的參數列表中,則攔截請求,並返回錯誤信息
      errorMsg = "source is not support";
      response.getWriter().write(errorMsg);
      return false;
    }
    return true;
  }

  /**
   * 拿到該方法上的checksource註解對象
   */
  private CheckSource getCheckSource(HandlerMethod handlerMethod) {
    if (handlerMethod.getBeanType().isAnnotationPresent(CheckSource.class)) {
      return handlerMethod.getBeanType().getAnnotation(CheckSource.class);
    } else if (handlerMethod.getMethod().isAnnotationPresent(CheckSource.class)) {
      return handlerMethod.getMethod().getAnnotation(CheckSource.class);
    }
    return null;
  }
}

代碼中添加了比較詳細的註釋,這裏只寫一下大概的思路:

通過攔截器的機制,拿到該方法上的CheckSource對象,該對象可能爲空,不爲空的時候拿到它的sources屬性,之後依次遍歷,判斷傳入的source是否在允許的列表中.

在這個攔截器中,我們定義了:

1.何時使用這個註解?

在我們配置的,使用這個攔截器的時候,進入controller層的某一個方法時.

2.怎麼使用這個註解?

拿傳入的source參數和這個註解的屬性sources列表一一匹配,有匹配上的則允許請求,無匹配值則返回錯誤信息.

5.3.實際使用註解

5.3.1.首先配置這個攔截器,攔截status接口

package com.huyan.demo.config;


import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * created by huyanshi on 2019/1/20
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

  CheckSourceInterceptor checkSourceInterceptor = new CheckSourceInterceptor();

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(checkSourceInterceptor).addPathPatterns("/status");
  }

}

5.3.2.status接口

package com.huyan.demo.controller;

import com.huyan.demo.config.CheckSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * created by pfliu on 2018/9/2
 */
@RestController
public class StatusController {

  private Logger logger = LoggerFactory.getLogger(this.getClass());

  @CheckSource(sources = {"huyan", "huihui"})
  @GetMapping(value = "/status")
  public Object status(@RequestParam("source") String source) {
    return "哈哈哈";
  }
}

好,編碼全部完成了.

啓動項目,看一下結果.

5.3.3.測試結果

  • 不帶source參數

  • 錯誤的source參數

  • 正確的source參數

6.總結

java的註解機制並不算太難理解,但是重點是,我們日常中很難想到去應用他,一來是因爲我們對其不夠熟悉,二來是我們的業務,沒有那麼通用的邏輯.

註解機制被大量的使用在各種框架中,足以證明他是一種優秀的機制,值得我們去學習並努力的應用在自己的工作中.

7.參考鏈接

https://josh-persistence.iteye.com/blog/2226493
https://www.ibm.com/developerworks/cn/java/j-lo-java8annotation/index.html

完。





ChangeLog

2019-01-20 完成

以上皆爲個人所思所得,如有錯誤歡迎評論區指正。

歡迎轉載,煩請署名並保留原文鏈接。

聯繫郵箱:[email protected]

更多學習筆記見個人博客------>呼延十

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