前言
我黃漢三又回來了,快半年沒更新博客了,這半年來的經歷實屬不易,
疫情當頭,本人實習的公司沒有跟員工共患難,直接辭掉了很多人。
作為一個實習生,本人也被無情開除了。所以本人又得重新准備找工作了。
算了,感慨一下,本來想昨天發的,但昨天是清明,哀悼時期,就留到了今天發。
話不多說,直接進入正題吧。這段時間本人在寫畢設,學校也遲遲沒有開學的消息,屬實難頂。
本來被開了本人只想回學校安度"晚年"算了,畢竟工作可以再找,但親朋好友以后畢業了就很少見了。
所以親們,一定要珍惜身邊的人噢。
因為這篇博文是現在本地typora上面寫好再放過博客園的,格式有點不統一
博客園的markdown編輯器還不是很好用,這點有點頭疼
還有一點是代碼格式問題,復制到markdown又變亂了
我哭了,本來就亂了,再加上博客篇幅的問題一擠壓,博文就亂完了
以后更文都用markdown了,所以關於排版的問題會越來越美化一下
通過本文讀者將可以學習到以下內容
- 注解的簡單使用和解析
- HandlerMethodArgumentResolver相關部分知識
起因
寫畢設,這周才把后台搭好,還有小程序端還沒開始。如題目所說,用了SpringBoot做后端搭建。
然后也當然應用了RESTful風格,當本人有一個url是/initJson/{id}的時候,直接就把用戶ID傳過來了。
本人就想能不能在前端簡單把ID加密一下,起碼不能眼睜睜看着ID直接就傳到后端。雖然只是一個畢設,
但還是稍微處理一下吧,處理的話我選擇用Base64好了。
本人現在是想把前端傳的一些簡單參數,用密文傳到后端再解密使用,避免明文傳輸。
當然在真正的環境中,肯定是使用更好的方案的。這里只是說有那么一種思路或者說那么一種場景。
給大家舉個例子之后可以拋磚引玉。
過程
1.前端
前端傳參的時候,加密
// encode是Base64加密的方法,可以自己隨便整一個
data.password = encode(pwd);
data.username= encode(username);
這樣子前端傳過去就是密文了。
2.后端
當參數傳到后端之后,想要把密文解析回明文,然后接下來就是本文的主旨所在了。
解密的時候,本人一開始是在接口里面解密的。
/**
* 此時參數接受到的內容是密文
*/
String login(String username, String password) {
username = Base64Util.decode(username);
password= Base64Util.decode(password);
}
看起來也沒啥是吧,但是萬一參數很多,或者說接口多,難道要每個接口都這么寫一堆解密的代碼嗎。
顯然還可以改造,怎么做?本人想到了注解,或者說想用注解試試,這樣自己也能加深對注解的學習。
2.1 注解
注解這個東西,本人當時學習的時候還以為是怎么起作用的,原來是可以自定義的(笑哭)。
我們在本文簡單了解下注解吧,如果有需要,后面本人可以更新一篇關於注解的博文。
或者讀者可以自行學習了解一下,說到這里,本人寫博客的理由是,網上沒有,或者網上找到的東西跟本人需要的不一樣時才會寫博客。
有的話就不寫了,以免都是同樣的東西,所以本人更新的博客並不算多,基本很久才一篇。
但好像這樣想並不對,寫博客無論是什么內容,不僅方便自己學習也可以方便他人,
所以以后應該更新頻率會好點吧希望。
回到正題,注解有三個主要的東西
- 注解定義(Annotation)
- 注解類型(ElementType)
- 注解策略(RetentionPolicy)
先來看看注解定義,很簡單
// 主要的就是 @interface 使用它定義的類型就是注解了,就跟class定義的類型是類一樣。
public @interface Base64DecodeStr {
/**
* 這里可以放一些注解需要的東西
* 像下面這個count()的含義是解密的次數,默認為1次
*/
int count() default 1;
}
然后再來看看注解類型
// 注解類型其實就是注解聲明在什么地方
public enum ElementType {
TYPE, /* 類、接口(包括注釋類型)或枚舉聲明 */
FIELD, /* 字段聲明(包括枚舉常量) */
METHOD, /* 方法聲明 */
PARAMETER, /* 參數聲明 */
CONSTRUCTOR, /* 構造方法聲明 */
LOCAL_VARIABLE, /* 局部變量聲明 */
ANNOTATION_TYPE, /* 注釋類型聲明 */
PACKAGE /* 包聲明 */
}
// 這個Target就是這么使用的
// 現在這個注解,本人希望它只能聲明在方法上還有參數上,別的地方聲明就會報錯
@Target({ElementType.METHOD, ElementType.PARAMETER})
public @interface Base64DecodeStr {
int count() default 1;
}
最后再來看看注解策略
public enum RetentionPolicy {
SOURCE, /* Annotation信息僅存在於編譯器處理期間,編譯器處理完之后就沒有該Annotation信息了*/
CLASS, /* 編譯器將Annotation存儲於類對應的.class文件中。默認行為 */
RUNTIME /* 編譯器將Annotation存儲於class文件中,並且可由JVM讀入 */
}
// 一般用第三個,RUNTIME,這樣的話程序運行中也可以使用
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Base64DecodeStr {
int count() default 1;
}
到此為止,一個注解就定義好了。但是在什么時候工作呢,這時我們就需要寫這個注解的解析了。
然后想想,定義這個注解的目的是,想直接在接口使用參數就是明文,所以應該在進入接口之前就把密文解密回明文並放回參數里。
這一步有什么好辦法呢,這時候就輪到下一個主角登場了,它就是HandlerMethodArgumentResolver
。
2.2 HandlerMethodArgumentResolver
關於HandlerMethodArgumentResolver的作用和解析,官方是這么寫的
/**
* Strategy interface for resolving method parameters into argument values in
* the context of a given request.
* 翻譯了一下
* 策略接口,用於在給定請求的上下文中將方法參數解析為參數值
* @author Arjen Poutsma
* @since 3.1
* @see HandlerMethodReturnValueHandler
*/
public interface HandlerMethodArgumentResolver {
/**
* MethodParameter指的是控制器層方法的參數
* 是否支持此接口
* ture就會執行下面的方法去解析
*/
boolean supportsParameter(MethodParameter parameter);
/**
* 常見的寫法就是把前端的參數經過處理再復制給控制器方法的參數
*/
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
所以這個接口,是很重要的,想想SpringMVC為何在控制器寫幾個注解,就能接收到參數,這個接口就是功不可沒的。
像常見的@PathVariable 就是用這個接口實現的。
本人的理解是,實現這個接口,就能在前端到后端接口之間處理方法和參數,所以剛好滿足上面的需求。
其實這個接口也是屬於SpringMVC源碼里面常見的一個,讀者依然也可自行了解下,
目前本人還沒有准備要寫Spring讀源碼的文章,因為本人也還沒系統的去看過,或許以后本人看了就會更新有關博客。
繼續,有了這樣的接口就可以用來寫解析自定義注解了,細心的同學可以發現,在這里寫注解解析,
那么這個注解就只能是在控制層起作用了,在服務層甚至DAO層都用不了,所以如果想全局用的話,
本人想到的是可以用AOP切一下,把需要用到的地方都切起來就可以了。
實現HandlerMethodArgumentResolver接口來寫解析。
public class Base64DecodeStrResolver implements HandlerMethodArgumentResolver {
private static final transient Logger log = LogUtils.getExceptionLogger();
/**
* 如果參數上有自定義注解Base64DecodeStr的話就支持解析
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(Base64DecodeStr.class)
|| parameter.hasMethodAnnotation(Base64DecodeStr.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
/**
* 因為這個注解是作用在方法和參數上的,所以要分情況
*/
int count = parameter.hasMethodAnnotation(Base64DecodeStr.class)
? parameter.getMethodAnnotation(Base64DecodeStr.class).count()
: parameter.getParameterAnnotation(Base64DecodeStr.class).count();
/**
* 如果是實體類參數,就把前端傳過來的參數構造成一個實體類
* 在系統中本人把所有實體類都繼承了BaseEntity
*/
if (BaseEntity.class.isAssignableFrom(parameter.getParameterType())) {
Object obj = parameter.getParameterType().newInstance();
webRequest.getParameterMap().forEach((k, v) -> {
try {
BeanUtils.setProperty(obj, k, decodeStr(v[0], count));
} catch (Exception e) {
log.error("參數解碼有誤", e);
}
});
// 這里的return就會把轉化過的參數賦給控制器的方法參數
return obj;
// 如果是非集合類,就直接解碼返回
} else if (!Iterable.class.isAssignableFrom(parameter.getParameterType())) {
return decodeStr(webRequest.getParameter(parameter.getParameterName()), count);
}
return null;
}
/**
* Base64根據次數恢復明文
*
* @param str Base64加密*次之后的密文
* @param count *次
* @return 明文
*/
public static String decodeStr(String str, int count) {
for (int i = 0; i < count; i++) {
str = Base64.decodeStr(str);
}
return str;
}
}
然后注冊一下這個自定義的Resolver。
這里就不用配置文件注冊了
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
//region 注冊自定義HandlerMethodArgumentResolver
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(base64DecodeStrResolver());
}
@Bean
public Base64DecodeStrResolver base64DecodeStrResolver() {
return new Base64DecodeStrResolver();
}
//endregion
}
在控制器層使用注解。
/**
* 先試試給方法加注解
*/
@Base64DecodeStr
public void login(@NotBlank(message = "用戶名不能為空") String username,
@NotBlank(message = "密碼不能為空") String password) {
System.out.println(username);
System.out.println(password);
}
看看效果
- 前端傳值
- 后端接收
至此整個功能上已經實現了,我們來看下關鍵api
// 這個就是一個參數,控制層的方法參數
MethodParameter parameter
// 常用方法
hasMethodAnnotation() 是否有方法注解
hasParameterAnnotation() 是否有參數注解
getMethodAnnotation() 獲取方法注解(傳入Class可以指定)
getParameterAnnotation() 獲取參數注解(傳入Class可以指定)
getParameterType() 獲取參數類型
// 這個可以理解為是前端傳過來的東西,里面可以拿到前端傳過來的密文,也就是初始值,沒有被處理過的
NativeWebRequest webRequest
// 常用方法 其實這幾個都是同一個 基於map的操作
getParameter()
getParameterMap()
getParameterNames()
getParameterValues()
2.3 深入探討
上面的例子是注解在方法上的,接下來試試注解在參數上。
/**
* 注解一個參數
*/
public void login(@NotBlank(message = "用戶名不能為空") @Base64DecodeStr String username,
@NotBlank(message = "密碼不能為空") String password) {
System.out.println(username);
System.out.println(password);
}
/*****************輸出******************************/
username
WTBkR2VtTXpaSFpqYlZFOQ==
/**
* 注解兩個參數
*/
public void login(@NotBlank(message = "用戶名不能為空") @Base64DecodeStr String username,
@NotBlank(message = "密碼不能為空") @Base64DecodeStr String password) {
System.out.println(username);
System.out.println(password);
}
/*****************輸出******************************/
username
password
可見注解在參數上也能用,接下來再來看看,同時注解在方法上和參數上,想一下。
假設方法上的注解優先,參數上的注解其次,會不會被解析兩次,
也就是說,密文先被方法注解解析成明文,然后之后被參數注解再次解析成別的東西。
/**
* 注解方法 注解參數
*/
@Base64DecodeStr
public void login(@NotBlank(message = "用戶名不能為空") @Base64DecodeStr String username,
@NotBlank(message = "密碼不能為空") @Base64DecodeStr String password) {
System.out.println(username);
System.out.println(password);
}
/*****************輸出******************************/
username
password
輸出的是正確的明文,也就是說上面的假設不成立,讓我們康康是哪里的問題。
回想一下,在解析的時候,我們都是用的webRequest
的getParameter,而webRequest
里面的值是從前端拿過來的,
所以decodeStr解密都是對前端的值解密,當然會返回正確的內容(明文),所以即使是方法注解先解密了,它解密的是前端的值,
然后再到屬性注解,它解密的也是前端的值,不會出現屬性注解解密的內容是方法注解解密出來的內容。
從這點來看,確實是這么一回事,所以即使方法注解和參數注解一起用也不會出現重復解密的效果。
但是,這只是一個原因,一開始本人還沒想到這個,然后就好奇打了斷點追蹤下源碼。
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 獲取參數的resolver,參數的定位是控制器.方法.參數位置 ,所以每個parameter都是唯一的
// 至於重載的啊,不知道沒試過,你們可以試下,XD
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" +
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
// argumentResolverCache是一個緩存,map,
// 從這里可以看出,每個控制器方法的參數都會被緩存起來,
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
// 調用supportsParameter看看是否支持
if (resolver.supportsParameter(parameter)) {
result = resolver;
// 一個參數可以有多個resolver
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
所以問題再細化一點,當我們同時注解方法和參數的時候,會調用幾次getArgumentResolver()呢,
為了便於觀察,本人將注解傳不同的參數。
在那之前,先放點小插曲,就是在調試的時候發現的問題
/**
* 注解方法
*/
@Base64DecodeStr( count = 10)
public void login(@NotBlank(message = "用戶名不能為空") String username,
@NotBlank(message = "密碼不能為空") String password) {
System.out.println(username);
System.out.println(password);
}
進去前
parameter是獲取不到方法上這個自定義注解的。
當代碼往下走,走到supportsParameter的時候
此時又有了,無語。
什么原因本人暫時沒找到。
言歸正傳,我們繼續調試
/**
* 注解方法 注解全部參數
*/
@Base64DecodeStr( count = 30)
public void login(@NotBlank(message = "用戶名不能為空") @Base64DecodeStr(count = 10) String username,
@NotBlank(message = "密碼不能為空") @Base64DecodeStr(count =20) String password) {
System.out.println(username);
System.out.println(password);
}
看看是先走方法注解還是參數注解。
- 第一次進來
可以看到是第一個參數username - 第二次進來
依然是第一個參數username - 第三次進來
看到是第二個參數password - 第四次進來
也是第二個參數password
所以可以看到,根本就沒有走方法注解,或者說方法注解會走兩次,參數注解一個一次,所以總共四次,這也沒問題。
這是怎么回事呢。要是不走方法注解,那方法注解怎么會生效呢,后面我找到了原因
/**
* 原來是因為這里,雖然不是因為方法注解進來的,但是這里優先取的是方法注解的值,
* 所以如果想讓屬性注解優先的話這里改一下就行
*/
int count = parameter.hasMethodAnnotation(Base64DecodeStr.class)
? parameter.getMethodAnnotation(Base64DecodeStr.class).count()
: parameter.getParameterAnnotation(Base64DecodeStr.class).count();
所以真相大白了,如果方法注解和屬性注解同時加上的話,會執行四次getArgumentResolver(),
其中只會調用兩次supportsParameter(),因為每個參數第二次都直接從map取到值了就不再走supportsParameter()了。
結束
至此我們完成了本次從前端到后端的旅途。
簡單總結一下。
- 注解
- 定義:@interface
- 類型:TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE,ANNOTATION_TYPE,PACKAGE
- 策略:SOURCE,CLASS,RUNTIME
- HandlerMethodArgumentResolver
- 作用:像攔截器一樣,在前端到后端中間的關卡
- 兩個方法
- supportsParameter:是否支持使用該Resolver
- resolveArgument:Resolver想要做的事
然后關於注解解析部分也不夠完善,比如如果參數是集合類型的話應該怎么處理,這都是后續了。
本篇內容都是本人真實遇到的問題並記錄下來,從開始想要加密加密參數到想辦法去實現這個功能,
這么一種思路,希望能給新人一點啟示,當然本人本身也還需要不斷學習,不然都找不到工作了,我只能邊忙畢設邊擠時間復習了。
人一惆悵話就多了,嘿嘿,不啰嗦了,現在是夜里兩點,准備睡了。