【WEB系列】從0到1實現自定義web引數對映器
SpringBoot系列之從0到1實現自定義web引數對映器
在使用SpringMVC進行開發時,接收請求引數屬於基本功,當我們希望將傳參與專案中的物件關聯起來時,最常見的做法是預設的case(即傳參name與我們定義的name保持一致),當存在不一致,需要手動指定時,通常是藉助註解 @RequestParam
來實現,但是不知道各位小夥伴是否有發現,它的使用是有缺陷的
-
@RequestParam
不支援配置在類的屬性上
如果我們定義一個VO物件來接收傳承,這個註解用不了,如當我們定義一個Java bean(pojo)來接收引數時,若是get請求,post表單請求時,這個時候要求傳參name與pojo的屬性名完全匹配,如果我們有別名的需求場景,怎麼整?
最簡單的如傳參為: user_id=110&user_name=一灰灰
而接收引數的POJO為
public class ViewDo { private String uesrId; private String userName; }
接下來本文通過從0到1,手擼一個自定義的web傳參對映,帶你瞭解SpringMVC中的引數繫結知識點
I. 專案搭建
1. 專案依賴
本專案藉助 SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
進行開發
開一個web服務用於測試
<dependencies> <!-- 郵件傳送的核心依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
配置檔案application.yml
server: port: 8080
II. 別名對映
接下來我們的目的就是希望實現一個自定義的別名註解,來支援傳參的別名繫結,核心知識點就是自定義的引數解析器 HandlerMethodArgumentResolver
0. 知識點概要說明
在下面的實現之前,先簡單介紹一下我們要用到的知識點
引數處理類: HandlerMethodArgumentResolver
,兩個核心的介面方法
// 用於判斷當前這個是否可以用來處理當前的傳參 boolean supportsParameter(MethodParameter parameter); // 實現具體的引數對映功能,從請求引數中獲取對應的傳參,然後設定給目標物件 @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
所以我們的核心邏輯就是實現上面這個介面,然後實現上面的兩個方法即可;當然直接實現原始的介面,額外需要處理的內容就稍稍有點多了,我們這裡會用到SpringMVC本身提供的兩個實現類,進行能力的擴充套件
- ServletModelAttributeMethodProcessor:用於後續處理POJO類的屬性註解
- RequestParamMethodArgumentResolver:用於後續處理方法引數註解
1. 自定義註解
自定義的註解,支援掛在類成員上,也支援放在方法引數上
@Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ParamName { /** * new name */ String value(); }
2. 自定義引數處理器
接下來我們就是需要定義上面註解的解析器,鑑於方法引數註解與類的成員註解的處理邏輯的差異性(後面說為啥要區分開)
首先來看一下當方法引數上,有上面註解時,對應的解析類
public class ParamArgumentProcessor extends RequestParamMethodArgumentResolver { public ParamArgumentProcessor() { super(true); } // 當引數上擁有 ParanName 註解,且引數型別為基礎型別時,匹配 @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(ParamName.class) && BeanUtils.isSimpleProperty(parameter.getParameterType()); } // 根據自定義的對映name,從傳參中獲取對應的value @Override protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { ParamName paramName = parameter.getParameterAnnotation(ParamName.class); String ans = request.getParameter(paramName.value()); if (ans == null) { return request.getParameter(name); } return ans; } }
上面的實現比較簡單,判斷是否可以使用當前Resolver的方法實現
parameter.hasParameterAnnotation(ParamName.class) && BeanUtils.isSimpleProperty(parameter.getParameterType());
其次,另外一個實現方法 resolveName
就很直觀了,根據繫結的name獲取具體的傳參
- 注意:內部還做了一個相容,當繫結的傳參name找不到時,使用變數名來取傳參
- 舉例說明:
- 引數定義如
@ParamName("user_name") String userName
- 那麼上面這個傳參值,會從傳參列表中,取
user_name
對應的值,當user_name
不存在時,則取userName
對應的值
- 引數定義如
接下來則是針對引數為POJO的場景,此時我們的自定義引數解析器實現類 ServletModelAttributeMethodProcessor
,具體的實現邏輯如下
public class ParamAttrProcessor extends ServletModelAttributeMethodProcessor { private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<>(); public ParamAttrProcessor() { super(true); } // 要求引數為非基本型別,且引數的成員上存在@ParamName註解 @Override public boolean supportsParameter(MethodParameter parameter) { if (!BeanUtils.isSimpleProperty(parameter.getParameterType())) { for (Field field : parameter.getParameterType().getDeclaredFields()) { if (field.getDeclaredAnnotation(ParamName.class) != null) { return true; } } } return false; } // 主要是使用自定義的DataBinder,給傳參增加一些別名對映 @Override protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) { Object target = binder.getTarget(); Class<?> targetClass = target.getClass(); if (!replaceMap.containsKey(targetClass)) { Map<String, String> mapping = analyzeClass(targetClass); replaceMap.put(targetClass, mapping); } Map<String, String> mapping = replaceMap.get(targetClass); ParamDataBinder paramNameDataBinder = new ParamDataBinder(target, binder.getObjectName(), mapping); super.bindRequestParameters(paramNameDataBinder, nativeWebRequest); } // 避免每次都去解析targetClass對應的別名定義,在實現中新增一個快取 private static Map<String, String> analyzeClass(Class<?> targetClass) { Field[] fields = targetClass.getDeclaredFields(); Map<String, String> renameMap = new HashMap<>(); for (Field field : fields) { ParamName paramNameAnnotation = field.getAnnotation(ParamName.class); if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) { renameMap.put(paramNameAnnotation.value(), field.getName()); } } if (renameMap.isEmpty()) return Collections.emptyMap(); return renameMap; } }
雖然上面的實現相比較於第一個,程式碼量要長很多,但是邏輯其實也並不複雜
supportsParameter判斷是否可用
@ParamName
bindRequestParameters請求引數繫結
- 這個方法的核心訴求就是給傳參中的key=value,新增一個別名
- 舉例說明:
user_name = 一灰灰 @ParamName("user_name") String userName; user_name = userName
下面就是DataBinder的實現邏輯
public class ParamDataBinder extends ExtendedServletRequestDataBinder { private final Map<String, String> renameMapping; public ParamDataBinder(Object target, String objectName, Map<String, String> renameMapping) { super(target, objectName); this.renameMapping = renameMapping; } @Override protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { super.addBindValues(mpvs, request); for (Map.Entry<String, String> entry : renameMapping.entrySet()) { String from = entry.getKey(); String to = entry.getValue(); if (mpvs.contains(from)) { mpvs.add(to, mpvs.getPropertyValue(from).getValue()); } } } }
3. 註冊與測試
最終也是非常重要的一點就是需要註冊我們的自定義引數解析器,實現 WebMvcConfigurationSupport
,過載 addArgumentResolvers
方法即可
@SpringBootApplication public class Application extends WebMvcConfigurationSupport { @Override protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(new ParamAttrProcessor()); argumentResolvers.add(new ParamArgumentProcessor()); } }
最後給一個基本的測試
@RestController public class RestDemo { @Data @NoArgsConstructor @AllArgsConstructor public static class ViewDo { @ParamName("user_id") private Integer userId; @ParamName("user_name") private String userName; } /** * POJO 對應Spring中的引數轉換是 ServletModelAttributeMethodProcessor | RequestParamMethodArgumentResolver * * @param viewDo * @return */ @GetMapping(path = "getV5") public ViewDo getV5(ViewDo viewDo) { System.out.println("v5: " + viewDo); return viewDo; } /** * curl 'http://127.0.0.1:8080/postV1' -X POST -d 'user_id=123&user_name=一灰灰' * 注意:非json傳參,jackson的配置將不會生效,即上面這個請求是不會實現下劃線轉駝峰的; 但是返回結果會是下劃線的 * * @param viewDo * @return */ @PostMapping(path = "postV1") public ViewDo post(ViewDo viewDo) { System.out.println(viewDo); return viewDo; } @GetMapping(path = "ano") public ViewDo ano(@ParamName("user_name") String userName, @ParamName("user_id") Integer userId) { ViewDo viewDo = new ViewDo(userId, userName); System.out.println(viewDo); return viewDo; } }
上面提供了三個介面
@ParamName @ParamName
實測結果如下
4. 小結
本文主要通過實現自定義的引數對映解析器,來支援自定義的引數別名繫結,雖然內容不多,但其基本實現,則主要利用的是SpringMVC的引數解析這一塊知識點,當然本文作為應用篇,主要只是介紹瞭如何實現自定義的 HandlerMethodArgumentResolver
,當現有的引數解析滿足不了我們的訴求時,完全可以仿造上面的實現來實現自己的應用場景(相信也不會太難)
最後抽取一下本文中使用到的知識點
- 如何判斷一個類是否為基本物件:
org.springframework.beans.BeanUtils#isSimpleProperty
- 自定義引數解析器:實現介面 HandlerMethodArgumentResolver
- 方法1:supportsParameter,判斷當前這個解析器是否適用
- 方法2:resolveArgument,具體的引數解析實現邏輯
- RequestParamMethodArgumentResolver:預設的方法引數解析器,主要用於簡單引數型別的對映,內部封裝了型別適配相關邏輯
- ServletModelAttributeMethodProcessor:用於預設的POJO/ModelAttribute引數解析
III. 不能錯過的原始碼和相關知識點
0. 專案
- 工程: http://github.com/liuyueyi/spring-boot-demo
- 原始碼: http://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/202-web-params-camel
系列博文:
1. 微信公眾號: 一灰灰Blog
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
- 一灰灰Blog個人部落格 http://blog.hhui.top
- 一灰灰Blog-Spring專題部落格 http://spring.hhui.top
打賞 如果覺得我的文章對您有幫助,請隨意打賞。
- 程式設計師的浪漫:三十行程式碼實現用她的名字作幅畫
- 【WEB系列】內嵌Tomcat配置Accesslog日誌檔案生成位置原始碼探索
- 【搜尋系列】ES查詢常用例項演示
- 【搜尋系列】ES文件基本操作CURD例項演示
- 【搜尋系列】ES基本專案搭建
- Nosql儲存系統-叢集工作原理
- 例項演示,帶你瞭解終端神器ncat
- Redis:你真的會Redis麼,一文告訴你如何學習
- 常用設計模式彙總,告訴你如何學習設計模式
- 微服務閘道器:從對比到選型,由理論到實踐
- 【WEB系列】從0到1實現自定義web引數對映器
- SpringBoot系列Mybatis之批量插入的幾種姿勢
- SpringBoot系列Mybatis之ResultMap、ResultType返回結果使用姿勢
- 【DB系列】Mybatis之批量插入的幾種姿勢
- 【DB系列】Mybatis之ResultMap、ResultType返回結果使用姿勢
- 【中介軟體】Prometheus基於AOP實現埋點採集上報
- JDNI注入:RMI之繞過trustURLCodebase配置的注入例項演示三
- JDNI注入:RMI Reference引起的注入case二
- JDNI注入:RMI基本知識點介紹一
- JavaFun | 實現圖片轉字元輸出示例demo