SpringMVC請求參數接收總結

在日常使用 SpringMVC 進行開發的時候,有可能遇到前端各種類型的請求參數,這裏做一次相對全面的總結。 SpringMVC 中處理控制器參數的接口是 HandlerMethodArgumentResolver ,此接口有衆多子類,分別處理不同(註解類型)的參數,下面只列舉幾個子類:

RequestParamMethodArgumentResolver :解析處理使用了 @RequestParam 註解的參數、 MultipartFile 類型參數和 Simple 類型(如 long 、 int 等類型)參數。
RequestResponseBodyMethodProcessor :解析處理 @RequestBody 註解的參數。
PathVariableMapMethodArgumentResolver :解析處理 @PathVariable 註解的參數。
實際上,一般在解析一個控制器的請求參數的時候,用到的是 HandlerMethodArgumentResolverComposite ,裏面裝載了所有啓用的 HandlerMethodArgumentResolver 子類。而 HandlerMethodArgumentResolver 子類在解析參數的時候使用到 HttpMessageConverter (實際上也是一個列表,進行遍歷匹配解析)子類進行匹配解析,常見的如 MappingJackson2HttpMessageConverter (使用 Jackson 進行序列化和反序列化)。而 HandlerMethodArgumentResolver 子類到底依賴什麼 HttpMessageConverter 實例實際上是由請求頭中的 Content-Type (在 SpringMVC 中統一命名爲 MediaType ,見 org.springframework.http.MediaType )決定的,因此我們在處理控制器的請求參數之前必須要明確外部請求的 Content-Type 到底是什麼。上面的邏輯可以直接看源碼 AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters ,思路是比較清晰的。在 @RequestMapping 註解中, produces 和 consumes 屬性就是和請求或者響應的 Content-Type 相關的:

consumes 屬性:指定處理請求的提交內容類型( Content-Type ),例如 application/json、 text/html 等等,只有命中了對應的 Content-Type 的值纔會接受該請求。
produces 屬性:指定返回的內容類型,僅當某個請求的請求頭中的( Accept )類型中包含該指定類型才返回,如果返回的是JSON數據一般考慮使用 application/json;charset=UTF-8。
另外提一點, SpringMVC 中默認使用 Jackson 作爲JSON的工具包,如果不是完全理解透整套源碼的運作,一般不是十分建議修改默認使用的 MappingJackson2HttpMessageConverter (例如有些人喜歡使用 FastJson ,實現 HttpMessageConverter 引入 FastJson 做HTTP消息轉換器,這種做法並不推薦)。

SpringMVC請求參數接收

其實一般的表單或者JSON數據的請求都是相對簡單的,一些複雜的處理主要包括URL路徑參數、文件上傳、數組或者列表類型數據等。另外,關於參數類型中存在日期類型屬性(例如 java.util.Date 、 java.sql.Date 、 java.time.LocalDate 、 java.time.LocalDateTime 、 java.time.ZonedDateTime 等等),解析的時候一般需要自定義實現的邏輯實現 String-->日期類型 的轉換。其實道理很簡單,日期相關的類型對於每個國家、每個時區甚至每個使用者來說認知都不一定相同,所以 SpringMVC 並沒有對於日期時間類型的解析提供一個通用的解決方案。在演示一些例子可能用到下面的模特類:

@Data
public class User {

 private String name;
 private Integer age;
 private List<Contact> contacts;
}

@Data
public class Contact {

 private String name;
 private String phone;
}

下面主要以 HTTP 的 GET 方法和 POST 方法提交在 SpringMVC 體系中正確處理參數的例子進行分析,還會花精力整理 SpringMVC 體系中 獨有的 URL 路徑參數 處理的一些技巧以及最常見的 日期參數 處理的合理實踐(對於 GET 方法和 POST 方法提交的參數處理,基本囊括了其他如 DELETE 、 PUT 等方法的參數處理,隨機應變即可)。

GET方法請求參數處理

HTTP(s) 協議使用 GET 方法進行請求的時候,提交的參數位於 URL 模式的 Query 部分,也就是 URL 的 ? 之後的參數,格式是 key1=value1&key2=value2 。 GET 方法請求參數可以有多種方法獲取:

@RequestParam
Query
HttpServletRequest

假設請求的 URL 爲 http://localhost:8080/get?name=doge&age=26 ,那麼控制器如下:

@Slf4j
@RestController
public class SampleController {

 @GetMapping(path = "/get1")
 public void get1(@RequestParam(name = "name") String name,
 @RequestParam(name = "age") Integer age) {
 log.info("name:{},age:{}", name, age);
 }

 @GetMapping(path = "/get2")
 public void get2(UserVo vo) {
 log.info("name:{},age:{}", vo.getName(), vo.getAge());
 }

 @GetMapping(path = "/get3")
 public void get3(HttpServletRequest request) {
 String name = request.getParameter("name");
 String age = request.getParameter("age");
 log.info("name:{},age:{}", name, age);
 }

 @Data
 public static class UserVo {

 private String name;
 private Integer age;
 }
}

表單參數

表單參數,一般對應於頁面上 <form> 標籤內的所有 <input> 標籤的 name-value 聚合而成的參數,一般 Content-Type 指定爲 application/x-www-form-urlencoded ,也就是會進行 URL 編碼。下面介紹幾種常見的表單參數提交的參數形式。

【非對象】- 非對象類型單個參數接收。
SpringMVC請求參數接收總結
對應的控制器如下:

@PostMapping(value = "/post")
public String post(@RequestParam(name = "name") String name,
 @RequestParam(name = "age") Integer age) {
 String content = String.format("name = %s,age = %d", name, age);
 log.info(content);
 return content;
}

說實話,如果有毅力的話,所有的複雜參數的提交最終都可以轉化爲多個單參數接收,不過這樣做會產生十分多冗餘的代碼,而且可維護性比較低。這種情況下,用到的參數處理器是 RequestParamMapMethodArgumentResolver 。

【對象】 - 對象類型參數接收。
我們接着寫一個接口用於提交用戶信息,用到的是上面提到的模特類,主要包括用戶姓名、年齡和聯繫人信息列表,這個時候,我們目標的控制器最終編碼如下:

@PostMapping(value = "/user")
public User saveUser(User user) {
 log.info(user.toString());
 return user;
}

我們還是指定 Content-Type 爲 application/x-www-form-urlencoded ,接着我們需要構造請求參數:

SpringMVC請求參數接收總結
因爲沒有使用註解,最終的參數處理器爲 ServletModelAttributeMethodProcessor ,主要是把 HttpServletRequest 中的表單參數封裝到 MutablePropertyValues 實例中,再通過參數類型實例化(通過構造反射創建 User 實例),反射匹配屬性進行值的填充。另外,請求複雜參數裏面的列表屬性請求參數看起來比較奇葩,實際上和在 .properties 文件中添加最終映射到 Map 類型的參數的寫法是一致的。那麼,能不能把整個請求參數塞在一個字段中提交呢?

SpringMVC請求參數接收總結
直接這樣做是不行的,因爲實際提交的 Form 表單, key 是 user 字符串, value 實際上也是一個字符串,缺少一個 String->User 類型的轉換器,實際上 RequestParamMethodArgumentResolver 依賴 WebConversionService 中 Converter 實例列表進行參數轉換:

SpringMVC請求參數接收總結
解決辦法還是有的,添加一個

org.springframework.core.convert.converter.Converter 實現即可:

@Component
public class StringUserConverter implements Converter<String, User> {

 @Autowaired
 private ObjectMapper objectMapper;

 @Override
 public User convert(String source) {
 try {
 return objectMapper.readValue(source, User.class);
 } catch (IOException e) {
 throw new IllegalArgumentException(e);
 }
 }
}

上面這種做法屬於曲線救國的做法,不推薦使用在生產環境,但是如果有些第三方接口的對接無法避免這種參數,可以選擇這種實現方式。

【數組】 - 列表或者數組類型參數。
極度不推薦使用在 application/x-www-form-urlencoded 這種媒體類型的表單提交的形式下強行使用列表或者數組類型參數,除非是爲了兼容處理歷史遺留系統的參數提交處理。例如提交的參數形式是:

list = ["string-1", "string-2", "string-3

那麼表單參數的形式要寫成:

namevaluelist[0]string-1list[1]string-2list[2]string-3

控制器的代碼如下:

@PostMapping(path = "/list")
public void list(@RequestParam(name="list") List<String> list) {
 log.info(list);
}

一個更加複雜的例子如下,假設想要提交的報文格式如下:

user = [{"name":"doge-1","age": 21},{"name":"doge-2","age": 22}]

那麼表單參數的形式要寫成:

namevalueuser[0].namedoge-1user[0].age21user[1].namedoge-2user[1].age22

控制器的代碼如下:

@PostMapping(path = "/user")
public void saveUsers(@RequestParam(name="user") List<UserVo> users) {
 log.info(users);
}

@Data
public class UserVo{

    private String name;
    private Integer age;

JSON參數

一般來說,直接在 POST 請求中的請求體提交一個JSON字符串這種方式對於 SpringMVC 來說是比較友好的,只需要把 Content-Type 設置爲 application/json ,提交一個原始的JSON字符串即可,控制器方法參數使用 @RequestBody 註解處理:

SpringMVC請求參數接收總結
後端控制器的代碼也比較簡單:

@PostMapping(value = "/user-2")
public User saveUser2(@RequestBody User user) {
 log.info(user.toString());
 return user;
}

因爲使用了 @RequestBody 註解,最終使用到的參數處理器爲 RequestResponseBodyMethodProcessor ,實際上會用到 MappingJackson2HttpMessageConverter 進行參數類型的轉換,底層依賴到 Jackson 相關的包。推薦使用這種方式,這是最常用也是最穩健的 JSON 參數處理方式。

URL路徑參數

URL 路徑參數,或者叫請求路徑參數是基於URL模板獲取到的參數,例如 /user/{userId} 是一個 URL 模板( URL 模板中的參數佔位符是{}),實際請求的 URL 爲 /user/1 ,那麼通過匹配實際請求的 URL 和 URL 模板就能提取到 userId 爲1。在 SpringMVC 中, URL 模板中的路徑參數叫做 PathVariable ,對應註解 @PathVariable ,對應的參數處理器爲 PathVariableMethodArgumentResolver 。 注意一點是,@PathVariable的解析是按照value(name)屬性進行匹配,和URL參數的順序是無關的 。舉個簡單的例子:

SpringMVC請求參數接收總結
後臺的控制器如下:

@GetMapping(value = "/user/{name}/{age}")
public String findUser1(@PathVariable(value = "age") Integer age,
                        @PathVariable(value = "name") String name) {
    String content = String.format("name = %s,age = %d", name, age);
    log.info(content);
    return content;
}

這種用法被廣泛使用於 Representational State Transfer(REST) 的軟件架構風格,個人覺得這種風格是比較靈活和清晰的(從URL和請求方法就能完全理解接口的意義和功能)。下面再介紹兩種相對特殊的使用方式。

帶條件的 URL 參數。
其實路徑參數支持正則表達式,例如我們在使用 /sex/{sex} 接口的時候,要求 sex 必須是 F(Female) 或者 M(Male) ,那麼我們的URL模板可以定義爲 /sex/{sex:M|F} ,代碼如下:

@GetMapping(value = "/sex/{sex:M|F}")
public String findUser2(@PathVariable(value = "sex") String sex){
 log.info(sex);
 return sex;
}

只有 /sex/F 或者 /sex/M 的請求才會進入 findUser2() 控制器方法,其他該路徑前綴的請求都是非法的,會返回404狀態碼。這裏僅僅是介紹了一個最簡單的 URL 參數正則表達式的使用方式,更強大的用法可以自行摸索。

@MatrixVariable 的使用。
MatrixVariable 也是 URL 參數的一種,對應註解 @MatrixVariable ,不過它並不是 URL 中的一個值(這裏的值指定是兩個”/“之間的部分),而是值的一部分,它通過”;”進行分隔,通過”=”進行K-V設置。說起來有點抽象,舉個例子:假如我們需要打電話給一個名字爲doge,性別是男,分組是碼畜的程序員, GET 請求的 URL 可以表示爲: /call/doge;gender=male;group=programmer ,我們設計的控制器方法如下:

@GetMapping(value = "/call/{name}")
public String find(@PathVariable(value = "name") String name,
 @MatrixVariable(value = "gender") String gender,
 @MatrixVariable(value = "group") String group) {
 String content = String.format("name = %s,gender = %s,group = %s", name, gender, group);
 log.info(content);
 return content;
}

當然,如果你按照上面的例子寫好代碼,嘗試請求一下該接口發現是報錯的: 400 Bad Request - Missing matrix variable 'gender' for method parameter of type String 。這是因爲 @MatrixVariable 註解的使用是不安全的,在 SpringMVC 中默認是關閉對其支持。要開啓對 @MatrixVariable 的支持,需要設置

RequestMappingHandlerMapping#setRemoveSemicolonContent 方法爲 false :

@Configuration
public class CustomMvcConfiguration implements InitializingBean {

    @Autowired
    private RequestMappingHandlerMapping requestMappingHandlerMapping;

    @Override
    public void afterPropertiesSet() throws Exception {
        requestMappingHandlerMapping.setRemoveSemicolonContent(false);
    }
}

除非有很特殊的需要,否則不建議使用 @MatrixVariable 。

文件上傳

文件上傳在使用 POSTMAN 模擬請求的時候需要選擇 form-data , POST 方式進行提交:

SpringMVC請求參數接收總結
假設我們在D盤有一個圖片文件叫doge.jpg,現在要通過本地服務接口把文件上傳,控制器的代碼如下:

@PostMapping(value = "/file1")
public String file1(@RequestPart(name = "file1") MultipartFile multipartFile) {
    String content = String.format("name = %s,originName = %s,size = %d",
    multipartFile.getName(), multipartFile.getOriginalFilename(), multipartFile.getSize());
    log.info(content);
    return content;
}

控制檯輸出是:

name = file1,originName = doge.jpg,size = 68727

可能有點疑惑,參數是怎麼來的,我們可以用 Fildder 軟件抓個包看下:

SpringMVC請求參數接收總結
可知 MultipartFile 實例的主要屬性分別來自 Content-Disposition 、 Content-Type 和 Content-Length ,另外, InputStream 用於讀取請求體的最後部分(文件的字節序列)。參數處理器用到的是 RequestPartMethodArgumentResolver (記住一點,使用了 @RequestPart 和 MultipartFile 一定是使用此參數處理器)。在其他情況下,使用 @RequestParam 和 MultipartFile 或者僅僅使用 MultipartFile (參數的名字必須和 POST 表單中的 Content-Disposition 描述的 name 一致)也可以接收上傳的文件數據,主要是通過 RequestParamMethodArgumentResolver 進行解析處理的,它的功能比較強大,具體可以看其 supportsParameter 方法,這兩種情況的控制器方法代碼如下:

@PostMapping(value = "/file2")
public String file2(MultipartFile file1) {
    String content = String.format("name = %s,originName = %s,size = %d",
                file1.getName(), file1.getOriginalFilename(), file1.getSize());
    log.info(content);
    return content;
}

@PostMapping(value = "/file3")
public String file3(@RequestParam(name = "file1") MultipartFile multipartFile) {
    String content = String.format("name = %s,originName = %s,size = %d",
            multipartFile.getName(), multipartFile.getOriginalFilename(), multipartFile.getSize());
    log.info(content);
    return content;
}

其他參數

其他參數主要包括請求頭、 Cookie 、 Model 、 Map 等相關參數,還有一些並不是很常用或者一些相對原生的屬性值獲取(例如 HttpServletRequest 、 HttpServletResponse 等)不做討論。

請求頭

請求頭的值主要通過 @RequestHeader 註解的參數獲取,參數處理器是 RequestHeaderMethodArgumentResolver ,需要在註解中指定請求頭的 Key 。簡單實用如下:

SpringMVC請求參數接收總結
控制器方法代碼:

@PostMapping(value = "/header")
public String header(@RequestHeader(name = "Content-Type") String Content-Type) {
 return Content-Type;
}
Cookie

Cookie 的值主要通過 @CookieValue 註解的參數獲取,參數處理器爲 ServletCookieValueMethodArgumentResolver ,需要在註解中指定 Cookie 的 Key 。控制器方法代碼如下:

@PostMapping(value = "/cookie")
public String cookie(@CookieValue(name = "JSESSIONID") String sessionId) {
    return sessionId;
}

Model類型參數

Model 類型參數的處理器是 ModelMethodProcessor ,實際上處理此參數是直接返回 ModelAndViewContainer 實例中的 Model ( ModelMap 類型),因爲要橋接不同的接口和類的功能,因此回調的實例是 BindingAwareModelMap 類型,此類型繼承自 ModelMap 同時實現了 Model 接口。舉個例子:

@GetMapping(value = "/model")
public String model(Model model, ModelMap modelMap) {
log.info("{}", model == modelMap);
return "success";
}
注意調用此接口,控制檯輸出INFO日誌內容爲:true。還要注意一點: ModelMap 或者 Model中添加的屬性項會附加到 HttpRequestServlet 實例中帶到頁面中進行渲染。

@ModelAttribute參數

@ModelAttribute 註解處理的參數處理器爲 ModelAttributeMethodProcessor , @ModelAttribute 的功能源碼的註釋如下:

Annotation that binds a method parameter or method return value to a named model attribute, exposed to a web view.

簡單來說,就是通過 key-value 形式綁定方法參數或者方法返回值到 Model(Map) 中,區別下面三種情況:

@ModelAttribute 使用在方法(返回值)上,方法沒有返回值( void 類型), Model(Map) 參數需要自行設置。
@ModelAttribute 使用在方法(返回值)上,方法有返回值(非 void 類型),返回值會添加到 Model(Map) 參數, key 由 @ModelAttribute 的 value 指定,否則會使用返回值類型字符串(首寫字母變爲小寫,如返回值類型爲 Integer ,則 key 爲 integer )。
@ModelAttribute 使用在方法參數中,則可以獲取同一個控制器中的已經設置的 @ModelAttribute 對應的值。
在一個控制器(使用了 @Controller )中,如果存在一到多個使用了 @ModelAttribute 的方法,這些方法總是在進入控制器方法之前執行,並且執行順序是由加載順序決定的(具體的順序是帶參數的優先,並且按照方法首字母升序排序),舉個例子:

@Slf4j
@RestController
public class ModelAttributeController {

    @ModelAttribute
    public void before(Model model) {
        log.info("before..........");
        model.addAttribute("before", "beforeValue");
    }

    @ModelAttribute(value = "beforeArg")
    public String beforeArg() {
        log.info("beforeArg..........");
        return "beforeArgValue";
    }

    @GetMapping(value = "/modelAttribute")
    public String modelAttribute(Model model, @ModelAttribute(value = "beforeArg") String beforeArg) {
        log.info("modelAttribute..........");
        log.info("beforeArg..........{}", beforeArg);
        log.info("{}", model);
        return "success";
    }

    @ModelAttribute
    public void after(Model model) {
        log.info("after..........");
        model.addAttribute("after", "afterValue");
    }

    @ModelAttribute(value = "afterArg")
    public String afterArg() {
        log.info("afterArg..........");
        return "afterArgValue";
    }
}

調用此接口,控制檯輸出日誌如下:

after..........
before..........
afterArg..........
beforeArg..........
modelAttribute..........
beforeArg..........beforeArgValue
{after=afterValue, before=beforeValue, afterArg=afterArgValue, beforeArg=beforeArgValue}

可以印證排序規則和參數設置、獲取的結果和前面的分析是一致的。

Errors或者BindingResult參數

Errors 其實是 BindingResult 的父接口, BindingResult 主要用於回調JSR參數校驗異常的屬性項,如果JSR校驗異常,一般會拋出 MethodArgumentNotValidException 異常,並且會返回400(Bad Request),見全局異常處理器 DefaultHandlerExceptionResolver 。 Errors 類型的參數處理器爲 ErrorsMethodArgumentResolver 。舉個例子:

@PostMapping(value = "/errors")
public String errors(@RequestBody @Validated ErrorsModel errors, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        for (ObjectError objectError : bindingResult.getAllErrors()) {
            log.warn("name={},message={}", objectError.getObjectName(), objectError.getDefaultMessage());
        }
    }
    return errors.toString();
}

//ErrorsModel
@Data
@NoArgsConstructor
public class ErrorsModel {
 @NotNull(message = "id must not be null!")
 private Integer id;
 @NotEmpty(message = "errors name must not be empty!")
 private String name;
}

調用接口控制檯Warn日誌如下:

name=errors,message=errors name must not be empty!

一般情況下,不建議用這種方式處理JSR校驗異常的屬性項,因爲會涉及到大量的重複的硬編碼工作,建議:方式一直接繼承 ResponseEntityExceptionHandler 覆蓋對應的方法或者方式二同時使用 @ExceptionHandler 和 @(Rest)ControllerAdvice 註解進行異常處理。例如:

@RestControllerAdvice
public class ApplicationRestControllerAdvice{

    @ExceptionHandler(BusinessException.class)
    public Response handleBusinessException(BusinessException e, HttpServletRequest request){
 // 這裏處理異常和返回值
    }
}

@Value參數

控制器方法的參數可以是 @Value 註解修飾的參數,會從 Environment 實例中裝配和轉換屬性值到對應的參數中(也就是參數的來源並不是請求體),參數處理器爲 ExpressionValueMethodArgumentResolver 。舉個例子:

@GetMapping(value = "/value")
public String value(@Value(value = "${spring.application.name}") String name) {
 log.info("spring.application.name={}", name);
 return name;
}

spring.application.name 屬性一般在配置文件中指定,在加載配置文件屬性的時候添加到全局的 Environment 中。

Map類型參數

Map 類型參數的範圍相對比較廣,對應一系列的參數處理器,注意區別使用了上面提到的部分註解的 Map 類型和完全不使用註解的 Map 類型參數,兩者的處理方式不相同。下面列舉幾個相對典型的 Map 類型參數處理例子。

不使用任何註解的Map\<String,Object>參數

這種情況下參數實際上直接回調 ModelAndViewContainer 中的 ModelMap 實例,參數處理器爲 MapMethodProcessor ,往 Map 參數中添加的屬性將會帶到頁面中。

使用@RequestParam註解的Map\<String,Object>參數

這種情況下的參數處理器爲 RequestParamMapMethodArgumentResolver ,使用的請求方式需要指定 Content-Type 爲 x-www-form-urlencoded ,不能使用 application/json 的方式:

SpringMVC請求參數接收總結
控制器代碼爲:

@PostMapping(value = "/map")
public String mapArgs(@RequestParam Map<String, Object> map) {
 log.info("{}", map);
 return map.toString();
}

使用@RequestHeader註解的Map\<String,Object>參數

這種情況下的參數處理器爲 RequestHeaderMapMethodArgumentResolver ,作用是獲取請求的所有請求頭的 Key-Value 。

使用@PathVariable註解的Map\<String,Object>參數

這種情況下的參數處理器爲 PathVariableMapMethodArgumentResolver ,作用是獲取所有路徑參數封裝爲 Key-Value 結構。

MultipartFile集合-批量文件上傳

批量文件上傳的時候,我們一般需要接收一個 MultipartFile 集合,可以有兩種選擇:

使用 MultipartHttpServletRequest 參數,直接調用 getFiles 方法獲取 MultipartFile 列表。
使用 @RequestParam 註解修飾 MultipartFile 列表,參數處理器是 RequestParamMethodArgumentResolver ,其實就是第1種方式的封裝而已。
SpringMVC請求參數接收總結
控制器方法代碼如下:

@PostMapping(value = "/parts")
public String partArgs(@RequestParam(name = "file") List<MultipartFile> parts) {
 log.info("{}", parts);
 return parts.toString();
}

日期類型參數處理

日期參數處理個人認爲是請求參數處理中最複雜的,因爲一般日期處理的邏輯不是通用的,過多的定製化處理導致很難有一個統一的標準處理邏輯去處理和轉換日期類型的參數。不過,這裏介紹幾個通用的方法,以應對各種奇葩的日期格式。下面介紹的例子中全部使用Jdk8中引入的日期時間API,圍繞 java.util.Date 爲核心的日期時間API的使用方式類同。

一、統一以字符串形式接收

這種是最原始但是最奏效的方式,統一以字符串形式接收,然後自行處理類型轉換,下面給個小例子:

@PostMapping(value = "/date1")
public String date1(@RequestBody UserDto userDto) {
 UserEntity userEntity = new UserEntity();
 userEntity.setUserId(userDto.getUserId());
 userEntity.setBirthdayTime(LocalDateTime.parse(userDto.getBirthdayTime(), FORMATTER));
 userEntity.setGraduationTime(LocalDateTime.parse(userDto.getGraduationTime(), FORMATTER));
 log.info(userEntity.toString());
 return "success";
}

@Data
public class UserDto {

    private String userId;
    private String birthdayTime;
    private String graduationTime;
}

@Data
public class UserEntity {

    private String userId;
    private LocalDateTime birthdayTime;
    private LocalDateTime graduationTime;
}

SpringMVC請求參數接收總結
使用字符串接收後再轉換的缺點就是模板代碼太多,編碼風格不夠簡潔,重複性工作太多。

二、使用註解@DateTimeFormat或者@JsonFormat

@DateTimeFormat 註解配合 @RequestBody 的參數使用的時候,會發現拋出 InvalidFormatException 異常,提示轉換失敗,這是因爲在處理此註解的時候,只支持 Form 表單提交( Content-Type 爲 x-www-form-urlencoded ),例子如下:

SpringMVC請求參數接收總結

@Data
public class UserDto2 {

    private String userId;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime birthdayTime;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime graduationTime;
}

@PostMapping(value = "/date2")
public String date2(UserDto2 userDto2) {
 log.info(userDto2.toString());
 return "success";
}

//或者像下面這樣
@PostMapping(value = "/date2")
public String date2(@RequestParam("name"="userId")String userId,
 @RequestParam("name"="birthdayTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime birthdayTime,
 @RequestParam("name"="graduationTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime graduationTime) {
 return "success";
}

而 @JsonFormat 註解可使用在Form表單或者JSON請求參數的場景,因此更推薦使用 @JsonFormat 註解,不過注意需要指定時區( timezone 屬性,例如在中國是東八區 GMT+8 ),否則有可能導致出現 時差 ,舉個例子:

@PostMapping(value = "/date2")
public String date2(@RequestBody UserDto2 userDto2) {
log.info(userDto2.toString());
return "success";
}

@Data
public class UserDto2 {

private String userId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime birthdayTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime graduationTime;
}

三、Jackson序列化和反序列化定製

因爲 SpringMVC 默認使用 Jackson 處理 @RequestBody 的參數轉換,因此可以通過定製序列化器和反序列化器來實現日期類型的轉換,這樣我們就可以使用 application/json 的形式提交請求參數。這裏的例子是轉換請求Json參數中的字符串爲 LocalDateTime 類型,屬於Json反序列化,因此需要定製反序列化器:

@PostMapping(value = "/date3")
public String date3(@RequestBody UserDto3 userDto3) {
log.info(userDto3.toString());
return "success";
}

@Data
public class UserDto3 {

private String userId;
@JsonDeserialize(using = CustomLocalDateTimeDeserializer.class)
private LocalDateTime birthdayTime;
@JsonDeserialize(using = CustomLocalDateTimeDeserializer.class)
private LocalDateTime graduationTime;

}

public class CustomLocalDateTimeDeserializer extends LocalDateTimeDeserializer {

public CustomLocalDateTimeDeserializer() {
    super(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

}

四、最佳實踐

前面三種方式都存在硬編碼等問題,其實最佳實踐是直接修改 MappingJackson2HttpMessageConverter 中的 ObjectMapper 對於日期類型處理默認的序列化器和反序列化器,這樣就能全局生效,不需要再使用其他註解或者定製序列化方案(當然,有些時候需要特殊處理定製),或者說,在需要特殊處理的場景才使用其他註解或者定製序列化方案。使用鉤子接口 Jackson2ObjectMapperBuilderCustomizer 可以實現對容器中的 ObjectMapper 單例中的屬性定製:

@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(){
return customizer->{
customizer.serializerByType(LocalDateTime.class,new LocalDateTimeSerializer(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
customizer.deserializerByType(LocalDateTime.class,new LocalDateTimeDeserializer(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
};
}

這樣就能定製化 MappingJackson2HttpMessageConverter 中持有的 ObjectMapper ,上面的 LocalDateTime 序列化和反序列化器對全局生效。

請求URL匹配

前面基本介紹完了主流的請求參數處理,其實 SpringMVC 中還會按照 URL 的模式進行匹配,使用的是 Ant 路徑風格,處理工具類爲 org.springframework.util.AntPathMatcher ,從此類的註釋來看,匹配規則主要包括下面四點:

? 匹配1個字符。
* 匹配0個或者多個 字符 。
** 匹配路徑中0個或者多個 目錄 。
正則支持,如 {spring:[a-z]+} 將正則表達式[a-z]+匹配到的值,賦值給名爲 spring 的路徑變量。
舉些例子:
‘?’形式的URL:

@GetMapping(value = "/pattern?")
public String pattern() {
return "success";
}

/pattern 404 Not Found
/patternd 200 OK
/patterndd 404 Not Found
/pattern/ 404 Not Found
/patternd/s 404 Not Found

‘*‘形式的URL:

@GetMapping(value = "/pattern*")
public String pattern() {
return "success";
}

/pattern 200 OK
/pattern/ 200 OK
/patternd 200 OK
/pattern/a 404 Not Found
‘**‘形式的URL:

@GetMapping(value = "/pattern/**/p")
public String pattern() {
return "success";
}

/pattern/p 200 OK
/pattern/x/p 200 OK
/pattern/x/y/p 200 OK
{spring:[a-z]+}形式的URL:

@GetMapping(value = "/pattern/{key:[a-c]+}")
public String pattern(@PathVariable(name = "key") String key) {
return "success";
}

/pattern/a 200 OK
/pattern/ab 200 OK
/pattern/abc 200 OK
/pattern 404 Not Found
/pattern/abcd 404 Not Found

上面的四種URL模式可以組合使用,千變萬化。

URL 匹配還遵循 精確匹配原則 ,也就是存在兩個模式對同一個 URL 都能夠匹配成功,則 選取最精確的 URL 匹配 ,進入對應的控制器方法,舉個例子:

@GetMapping(value = "/pattern/**/p")
public String pattern1() {
return "success";
}

@GetMapping(value = "/pattern/p")
public String pattern2() {
return "success";
}

上面兩個控制器,如果請求 URL 爲 /pattern/p ,最終進入的方法爲 pattern2 。

最後, org.springframework.util.AntPathMatcher 作爲一個工具類,可以單獨使用,不僅僅可以用於匹配 URL ,也可以用於匹配系統文件路徑,不過需要使用其帶參數構造改變內部的 pathSeparator 變量,例如:

AntPathMatcher antPathMatcher = new AntPathMatcher(File.separator);


小結

筆者在前一段時間曾經花大量時間梳理和分析過 Spring 、 SpringMVC 的源碼,但是後面一段很長的時間需要進行業務開發,對架構方面的東西有點生疏了,畢竟東西不用就會生疏,這個是常理。這篇文章基於一些 SpringMVC 的源碼經驗總結了請求參數的處理相關的一些知識,希望幫到自己和大家。

原文鏈接: http://www.throwable.club/2019/12/04/spring-mvc-param-handle-summary-1/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章