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
,他的值共有以下幾種:
- CONSTRUCTOR:用於描述構造器
- FIELD:用於描述域即類成員變量
- LOCAL_VARIABLE:用於描述局部變量
- METHOD:用於描述方法
- PACKAGE:用於描述包
- PARAMETER:用於描述參數
- TYPE:用於描述類、接口(包括註解類型) 或enum聲明
注:在JDK1.8,新加了兩種類型,
8. TYPE_PARAMETER:表示這個 Annotation 可以用在 Type 的聲明式前,
9. TYPE_USE 表示這個 Annotation 可以用在所有使用 Type 的地方
@Retention
表示需要在什麼級別保存該註釋信息,用於描述註解的生命週期(即:被描述的註解在什麼範圍內有效)
他的取值範圍來自於枚舉類RetentionPolicy
,取值共以下幾種:
- SOURCE:在源文件中有效(即源文件保留)
- CLASS:在class文件中有效(即class保留)
- RUNTIME:在運行時有效(即運行時保留)
@Documented
@Documented用於描述其它類型的annotation應該被作爲被標註的程序成員的公共API,因此可以被例如javadoc此類的工具文檔化。Documented是一個標記註解,沒有成員。
@Inherited
@Inherited 元註解是一個標記註解,@Inherited闡述了某個被標註的類型是被繼承的。如果一個使用了@Inherited修飾的annotation類型被用於一個class,則這個annotation將被用於該class的子類。
4.常見註解
常用的第三方框架實現了非常多的註解,比如Mybatis
的Param
,Spring
的Component
,Service
,fastjson
的JSONfield
等等.
具體的實現方法這裏不多解釋了,有興趣的朋友可以取看一下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]
更多學習筆記見個人博客------>呼延十