2.5萬字長文簡單總結SpringMVC請求參數接收

這是公衆號《Throwable文摘》發佈的第22篇原創文章,暫時收錄於專輯《架構與實戰》。暫定下一篇發佈的長文是《圖文分析JUC同步器框架》,下一篇發佈的短文是《SpringBoot2.x入門:引入jdbc模塊與JdbcTemplate簡單使用》。html

前提

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

  • RequestParamMethodArgumentResolver:解析處理使用了@RequestParam註解的參數、MultipartFile類型參數和Simple類型(如longint等類型)參數。
  • RequestResponseBodyMethodProcessor:解析處理@RequestBody註解的參數。
  • PathVariableMapMethodArgumentResolver:解析處理@PathVariable註解的參數。

實際上,通常在解析一個控制器的請求參數的時候,用到的是HandlerMethodArgumentResolverComposite,裏面裝載了全部啓用的HandlerMethodArgumentResolver子類。而HandlerMethodArgumentResolver子類在解析參數的時候使用到HttpMessageConverter(實際上也是一個列表,進行遍歷匹配解析)子類進行匹配解析,常見的如MappingJackson2HttpMessageConverter(使用Jackson進行序列化和反序列化)。java

spmvc-p-14

HandlerMethodArgumentResolver子類到底依賴什麼HttpMessageConverter實例其實是由請求頭中的Content-Type(在SpringMVC中統一命名爲MediaType,見org.springframework.http.MediaType)決定的,所以咱們在處理控制器的請求參數以前必需要明確外部請求的Content-Type究竟是什麼。上面的邏輯能夠直接看源碼AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters,思路是比較清晰的。在@RequestMapping註解中,producesconsumes屬性就是和請求的Accept或者響應的Content-Type相關的:程序員

  • consumes屬性:指定處理請求的提交內容類型(Content-Type),例如application/jsontext/html等等,只有命中了對應的Content-Type的值纔會接受該請求。
  • produces屬性:指定返回的內容類型,僅當某個請求的請求頭中的(Accept)類型中包含該指定類型才返回,若是返回的是JSON數據通常考慮使用application/json;charset=UTF-8

另外提一點,SpringMVC中默認使用Jackson做爲JSON的工具包,若是不是徹底理解透整套源碼的運做,通常不是十分建議修改默認使用的MappingJackson2HttpMessageConverter(例若有些人喜歡使用FastJson,實現HttpMessageConverter引入FastJsonHTTP消息轉換器,其實這種作法並不推薦)。web

SpringMVC請求參數接收

其實通常的表單或者JSON數據的請求都是相對簡單的,一些複雜的處理主要包括URL路徑參數、文件上傳、數組或者列表類型數據等。另外,關於參數類型中存在日期類型屬性(例如java.util.Datejava.sql.Datejava.time.LocalDatejava.time.LocalDateTimejava.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;
}

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

GET方法請求參數處理

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

  1. 使用@RequestParam註解處理。
  2. 使用對象接收,注意對象的屬性名稱要和Query中的參數名稱一致。
  3. 使用HttpServletRequest實例提供的方法(不推薦,存在硬編碼)。

假設請求的URLhttp://localhost:8080/get?name=doge&age=26,那麼控制器以下:json

@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)編碼。下面介紹幾種常見的表單參數提交的參數形式。後端

  • 【非對象】- 非對象類型單個參數接收。

spmvc-p-1

對應的控制器以下:

@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-Typeapplication/x-www-form-urlencoded,須要構造請求參數格式以下:

spmvc-p-2

由於沒有使用註解,最終的參數處理器爲ServletModelAttributeMethodProcessor,主要是把HttpServletRequest中的表單參數封裝到MutablePropertyValues實例中,再經過參數類型實例化(經過構造反射建立User實例),反射匹配屬性進行值的填充。另外,請求複雜參數裏面的列表屬性請求參數看起來比較奇葩,實際上和在.properties文件中添加最終映射到Map類型的參數的寫法是一致的,因此對於嵌套數組或者列表類型的第一層索引要寫成firstLevel[index].fieldName的形式。那麼,能不能把整個請求參數塞在一個字段中提交呢?

spmvc-p-3

直接這樣作是不行的,由於實際提交的Form表單,keyuser字符串,value實際上也是一個字符串,缺乏一個String->User類型的轉換器,實際上RequestParamMethodArgumentResolver依賴WebConversionServiceConverter實例列表進行參數轉換,而默認的Converter列表中確定不會存在自定義轉換String->User類型的轉換器:

spmvc-p-4

解決辦法仍是有的,添加一個自定義的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"]

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

name value
list[0] string-1
list[1] string-2
list[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}]

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

name value
user[0].name doge-1
user[0].age 21
user[1].name doge-2
user[1].age 22

控制器的代碼以下:

@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註解處理:

spmvc-p-5

後端控制器的代碼也比較簡單:

@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,那麼經過匹配實際請求的URLURL模板就能提取到userId爲1。在SpringMVC中,URL模板中的路徑參數叫作Path Variable,對應註解@PathVariable,對應的參數處理器爲PathVariableMethodArgumentResolver注意一點是,@PathVariable的解析是按照value(name)屬性進行匹配,和URL參數的順序是無關的。舉個簡單的例子:

spmvc-p-6

後臺的控制器以下:

@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-dataPOST方式進行提交:

spmvc-p-8

假設在電腦的磁盤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軟件抓個包看下:

spmvc-p-7

可知MultipartFile實例的主要屬性分別來自Content-DispositionContent-TypeContent-Length,另外,InputStream用於讀取請求體的最後部分(文件的字節序列)。參數處理器用到的是RequestPartMethodArgumentResolver(記住一點,使用了@RequestPartMultipartFile必定是使用此參數處理器)。在其餘狀況下,使用@RequestParamMultipartFile或者僅僅使用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;
}

其餘參數

其餘參數主要包括請求頭、CookieModelMap等相關參數,還有一些並非很經常使用或者一些相對原生的屬性值獲取(例如HttpServletRequestHttpServletResponse或者它們內置的實例方法等)不作討論。

請求頭

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

spmvc-p-9

控制器方法代碼:

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

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

@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)中,區別下面三種狀況:

  1. @ModelAttribute使用在方法(返回值)上,方法沒有返回值(void類型), Model(Map)參數須要自行設置。
  2. @ModelAttribute使用在方法(返回值)上,方法有返回值(非void類型),返回值會添加到Model(Map)參數,key@ModelAttributevalue指定,不然會使用返回值類型字符串(首寫字母變爲小寫,如返回值類型爲Integer,則keyinteger)。
  3. @ModelAttribute使用在方法參數中,則能夠獲取同一個控制器中的已經設置的@ModelAttribute對應的值。

在一個控制器(使用了@ControllerSpring組件)中,若是存在一到多個使用了@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參數校驗異常的屬性項,若是JSR303校驗異常,通常會拋出MethodArgumentNotValidException異常,而且會返回400(Bad Request),見全局異常處理器DefaultHandlerExceptionResolverErrors類型的參數處理器爲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){
        // 這裏處理異常和返回值
    }

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

值得注意的是,SpringBoot某個版本以後,把JSR303相關的依賴抽離到spring-boot-starter-validation依賴中,若是要使用JSR303相關相關校驗功能,必須獨立引入此starter

@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-Typex-www-form-urlencoded,不能使用application/json的方式:

spmvc-p-10

控制器代碼爲:

@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集合,能夠有兩種選擇:

  1. 使用MultipartHttpServletRequest參數,直接調用getFiles方法獲取MultipartFile列表。
  2. 使用@RequestParam註解修飾MultipartFile列表,參數處理器是RequestParamMethodArgumentResolver,其實就是第1種方式的封裝而已。

spmvc-p-11

控制器方法代碼以下:

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

日期類型參數處理

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

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

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

static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

@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;
}

spmvc-p-12

使用字符串接收後再轉換的缺點就是模板代碼太多,編碼風格不夠簡潔,重複性工做太多,若是有代碼潔癖或者相似筆者這樣是一個節能主義者,通常不會選用這種方式。

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

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

spmvc-p-13

@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;
}

通常選用LocalDateTime做爲日期字段參數的類型,由於它的轉換相對於其餘JDK8的日期時間類型簡單

3、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"));
    }
}

4、最佳實踐

前面三種方式都存在硬編碼等問題,其實最佳實踐是直接修改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. ?匹配1個字符。
  2. *匹配0個或者多個字符
  3. **匹配路徑中0個或者多個目錄
  4. 正則支持,如{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。上面的例子只是列舉了SpringMVCURL匹配的典型例子,並無深刻展開。

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

AntPathMatcher antPathMatcher = new AntPathMatcher(File.separator);

小結

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

參考資料:

  • spring-boot-web-starter:2.3.0.RELEASE源碼。

(本文完 c-7-d e-a-20180512 r-a-20200713 舊文重發 封面圖來源於日漫《神風怪盜》)

公衆號《Throwable文摘》(id:throwable-doge),不按期推送架構設計、併發、源碼探究相關的原創文章:

相關文章
相關標籤/搜索