這是公衆號《Throwable文摘》發佈的第22篇原創文章,暫時收錄於專輯《架構與實戰》。暫定下一篇發佈的長文是《圖文分析JUC同步器框架》,下一篇發佈的短文是《SpringBoot2.x入門:引入jdbc模塊與JdbcTemplate簡單使用》。html
在平常使用SpringMVC
進行開發的時候,有可能遇到前端各類類型的請求參數,這裏作一次相對全面的總結。SpringMVC
中處理控制器參數的接口是HandlerMethodArgumentResolver
,此接口有衆多子類,分別處理不一樣(註解類型)的參數,下面只列舉幾個子類:前端
RequestParamMethodArgumentResolver
:解析處理使用了@RequestParam
註解的參數、MultipartFile
類型參數和Simple
類型(如long
、int
等類型)參數。RequestResponseBodyMethodProcessor
:解析處理@RequestBody
註解的參數。PathVariableMapMethodArgumentResolver
:解析處理@PathVariable
註解的參數。實際上,通常在解析一個控制器的請求參數的時候,用到的是HandlerMethodArgumentResolverComposite
,裏面裝載了全部啓用的HandlerMethodArgumentResolver
子類。而HandlerMethodArgumentResolver
子類在解析參數的時候使用到HttpMessageConverter
(實際上也是一個列表,進行遍歷匹配解析)子類進行匹配解析,常見的如MappingJackson2HttpMessageConverter
(使用Jackson
進行序列化和反序列化)。java
而HandlerMethodArgumentResolver
子類到底依賴什麼HttpMessageConverter
實例其實是由請求頭中的Content-Type
(在SpringMVC
中統一命名爲MediaType
,見org.springframework.http.MediaType
)決定的,所以咱們在處理控制器的請求參數以前必需要明確外部請求的Content-Type
究竟是什麼。上面的邏輯能夠直接看源碼AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters
,思路是比較清晰的。在@RequestMapping
註解中,produces
和consumes
屬性就是和請求的Accept
或者響應的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
消息轉換器,其實這種作法並不推薦)。web
其實通常的表單或者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
等方法的參數處理,隨機應變便可)。spring
HTTP(s)
協議使用GET
方法進行請求的時候,提交的參數位於URL
模式的Query
部分,也就是URL
的?
標識符以後的參數,格式是key1=value1&key2=value2
。GET
方法請求參數能夠有多種方法獲取:sql
@RequestParam
註解處理。Query
中的參數名稱一致。HttpServletRequest
實例提供的方法(不推薦,存在硬編碼)。假設請求的URL
爲http://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
)編碼。下面介紹幾種常見的表單參數提交的參數形式。後端
對應的控制器以下:
@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
,須要構造請求參數格式以下:
由於沒有使用註解,最終的參數處理器爲ServletModelAttributeMethodProcessor
,主要是把HttpServletRequest
中的表單參數封裝到MutablePropertyValues
實例中,再經過參數類型實例化(經過構造反射建立User
實例),反射匹配屬性進行值的填充。另外,請求複雜參數裏面的列表屬性請求參數看起來比較奇葩,實際上和在.properties
文件中添加最終映射到Map
類型的參數的寫法是一致的,因此對於嵌套數組或者列表類型的第一層索引要寫成firstLevel[index].fieldName
的形式。那麼,能不能把整個請求參數塞在一個字段中提交呢?
直接這樣作是不行的,由於實際提交的Form
表單,key
是user
字符串,value
實際上也是一個字符串,缺乏一個String->User
類型的轉換器,實際上RequestParamMethodArgumentResolver
依賴WebConversionService
中Converter
實例列表進行參數轉換,而默認的Converter
列表中確定不會存在自定義轉換String->User
類型的轉換器:
解決辦法仍是有的,添加一個自定義的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; }
這種傳參格式其實並不靈活,甚至有可能下降開發效率和參數可讀性。
通常來講,直接在POST
請求中的請求體提交一個JSON
字符串這種方式對於SpringMVC
來講是比較友好的,只須要把Content-Type
設置爲application/json
,而後直接上傳一個原始的JSON
字符串便可,控制器方法參數使用@RequestBody
註解處理:
後端控制器的代碼也比較簡單:
@PostMapping(value = "/user-2") public User saveUser2(@RequestBody User user) { log.info(user.toString()); return user; }
由於使用了@RequestBody
註解,最終使用到的參數處理器爲RequestResponseBodyMethodProcessor
,實際上會用到MappingJackson2HttpMessageConverter
進行參數類型的轉換,底層依賴到Jackson
相關的包。推薦使用這種方式,這是最經常使用也是最穩健的JSON
參數處理方式。
URL
路徑參數,或者叫請求路徑參數是基於URL
模板獲取到的參數,例如/user/{userId}
是一個URL
模板(URL
模板中的參數佔位符是{}
),實際請求的URL
爲/user/1
,那麼經過匹配實際請求的URL
和URL
模板就能提取到userId
爲1。在SpringMVC
中,URL
模板中的路徑參數叫作Path Variable
,對應註解@PathVariable
,對應的參數處理器爲PathVariableMethodArgumentResolver
。注意一點是,@PathVariable的解析是按照value(name)屬性進行匹配,和URL參數的順序是無關的。舉個簡單的例子:
後臺的控制器以下:
@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
方式進行提交:
假設在電腦的磁盤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
軟件抓個包看下:
可知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
。簡單實用以下:
控制器方法代碼:
@PostMapping(value = "/header") public String header(@RequestHeader(name = "Content-Type") String contentType) { return contentType; }
Cookie
的值主要經過@CookieValue
註解的參數獲取,參數處理器爲ServletCookieValueMethodArgumentResolver
,須要在註解中指定Cookie
的Key
。控制器方法代碼以下:
@PostMapping(value = "/cookie") public String cookie(@CookieValue(name = "JSESSIONID") String sessionId) { return sessionId; }
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
註解處理的參數處理器爲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
的Spring
組件)中,若是存在一到多個使用了@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
的父接口,BindingResult
主要用於回調JSR
參數校驗異常的屬性項,若是JSR303
校驗異常,通常會拋出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){ // 這裏處理異常和返回值 } @ExceptionHandler(MethodArgumentNotValidException.class) public Response handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request){ // 這裏處理異常和返回值 } }
值得注意的是,SpringBoot某個版本以後,把JSR303相關的依賴抽離到spring-boot-starter-validation依賴中,若是要使用JSR303相關相關校驗功能,必須獨立引入此starter
控制器方法的參數能夠是@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<String,Object>
參數
這種狀況下參數實際上直接回調ModelAndViewContainer
中的ModelMap
實例,參數處理器爲MapMethodProcessor
,往Map
參數中添加的屬性將會帶到頁面中。
使用@RequestParam註解的Map<String,Object>
參數
這種狀況下的參數處理器爲RequestParamMapMethodArgumentResolver
,使用的請求方式須要指定Content-Type
爲x-www-form-urlencoded
,不能使用application/json
的方式:
控制器代碼爲:
@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
集合,能夠有兩種選擇:
MultipartHttpServletRequest
參數,直接調用getFiles
方法獲取MultipartFile
列表。@RequestParam
註解修飾MultipartFile
列表,參數處理器是RequestParamMethodArgumentResolver
,其實就是第1種方式的封裝而已。控制器方法代碼以下:
@PostMapping(value = "/parts") public String partArgs(@RequestParam(name = "file") List<MultipartFile> parts) { log.info("{}", parts); return parts.toString(); }
日期參數處理我的認爲是請求參數處理中最複雜的,由於通常日期處理的邏輯不是通用的,過多的定製化處理致使很難有一個統一的標準處理邏輯去處理和轉換日期類型的參數。不過,這裏介紹幾個通用的方法,以應對各類奇葩的日期格式。下面介紹的例子中所有使用JDK8
中引入的日期時間API
,圍繞java.util.Date
爲核心的日期時間API
的使用方式類同。
這種是最原始可是最奏效的方式,統一以字符串形式接收,而後自行處理類型轉換,下面給個小例子:
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; }
使用字符串接收後再轉換的缺點就是模板代碼太多,編碼風格不夠簡潔,重複性工做太多,若是有代碼潔癖或者相似筆者這樣是一個節能主義者,通常不會選用這種方式。
@DateTimeFormat
註解配合@RequestBody
的參數使用的時候,會發現拋出InvalidFormatException
異常,提示轉換失敗,這是由於在處理此註解的時候,只支持Form
表單提交(Content-Type
爲x-www-form-urlencoded
),例子以下:
@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的日期時間類型簡單
由於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
序列化和反序列化器對全局生效。
前面基本介紹完了主流的請求參數處理,其實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
。上面的例子只是列舉了SpringMVC
中URL
匹配的典型例子,並無深刻展開。
最後,org.springframework.util.AntPathMatcher
做爲一個工具類,能夠單獨使用,不只僅能夠用於匹配URL
,也能夠用於匹配系統文件路徑,不過須要使用其帶參數構造改變內部的pathSeparator
變量,例如:
AntPathMatcher antPathMatcher = new AntPathMatcher(File.separator);
筆者在前一段時間曾經花大量時間梳理和分析過Spring
、SpringMVC
的源碼,可是後面一段很長的時間須要進行業務開發,對架構方面的東西有點生疏了,畢竟東西不用就會生疏,這個是常理。這篇文章基於一些SpringMVC
的源碼經驗總結了請求參數的處理相關的一些知識,但願幫到本身和你們。
參考資料:
(本文完 c-7-d e-a-20180512 r-a-20200713 舊文重發 封面圖來源於日漫《神風怪盜》)
公衆號《Throwable文摘》(id:throwable-doge),不按期推送架構設計、併發、源碼探究相關的原創文章: