在平常使用SpringMVC進行開發的時候,有可能遇到前端各類類型的請求參數,這裏作一次相對全面的總結。SpringMVC中處理控制器參數的接口是HandlerMethodArgumentResolver,此接口有衆多子類,分別處理不一樣(註解類型)的參數,下面只列舉幾個子類:html
實際上,通常在解析一個控制器的請求參數的時候,用到的是HandlerMethodArgumentResolverComposite,裏面裝載了全部啓用的HandlerMethodArgumentResolver子類。而HandlerMethodArgumentResolver子類在解析參數的時候使用到HttpMessageConverter(實際上也是一個列表,進行遍歷匹配解析)子類進行匹配解析,常見的如MappingJackson2HttpMessageConverter。而HandlerMethodArgumentResolver子類到底依賴什麼HttpMessageConverter實例其實是由請求頭中的ContentType(在SpringMVC中統一命名爲MediaType,見org.springframework.http.MediaType)決定的,所以咱們在處理控制器的請求參數以前必需要明確外部請求的ContentType究竟是什麼。上面的邏輯能夠直接看源碼AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters
,思路是比較清晰的。在@RequestMapping註解中,produces和consumes就是和請求或者響應的ContentType相關的:前端
另外提一點,SpringMVC中默認使用Jackson做爲JSON的工具包,若是不是徹底理解透整套源碼的運做,通常不是十分建議修改默認使用的MappingJackson2HttpMessageConverter(例若有些人喜歡使用FastJson,實現HttpMessageConverter引入FastJson作轉換器)。java
其實通常的表單或者JSON數據的請求都是相對簡單的,一些複雜的處理主要包括URL路徑參數、文件上傳、數組或者列表類型數據等。另外,關於參數類型中存在日期類型屬性(例如java.util.Date、java.sql.Date、java.time.LocalDate、java.time.LocalDateTime),解析的時候通常須要自定義實現的邏輯實現String->日期類型的轉換。其實道理很簡單,日期相關的類型對於每一個國家、每一個時區甚至每一個使用者來講認知都不必定相同。在演示一些例子主要用到下面的模特類:程序員
@Data public class User { private String name; private Integer age; private List<Contact> contacts; } @Data public class Contact { private String name; private String phone; }
非對象類型單個參數接收:web
這種是最經常使用的表單參數提交,ContentType指定爲application/x-www-form-urlencoded,也就是會進行URL編碼。正則表達式
對應的控制器以下:spring
@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。sql
對象類型參數接收:json
咱們接着寫一個接口用於提交用戶信息,用到的是上面提到的模特類,主要包括用戶姓名、年齡和聯繫人信息列表,這個時候,咱們目標的控制器最終編碼以下:後端
@PostMapping(value = "/user") public User saveUser(User user) { log.info(user.toString()); return user; }
咱們仍是指定ContentType爲application/x-www-form-urlencoded,接着咱們須要構造請求參數:
由於沒有使用註解,最終的參數處理器爲ServletModelAttributeMethodProcessor,主要是把HttpServletRequest中的表單參數封裝到MutablePropertyValues實例中,再經過參數類型實例化(經過構造反射建立User實例),反射匹配屬性進行值的填充。另外,請求複雜參數裏面的列表屬性請求參數看起來比較奇葩,實際上和在.properties文件中添加最終映射到Map類型的參數的寫法是一致的。那麼,能不能把整個請求參數塞在一個字段中提交呢?
直接這樣作是不行的,由於實際提交的form表單,key是user,value其實是一個字符串,缺乏一個String->User類型的轉換器,實際上RequestParamMethodArgumentResolver依賴WebConversionService中Converter列表進行參數轉換:
解決辦法仍是有的,添加一個org.springframework.core.convert.converter.Converter實現便可:
@Component public class StringUserConverter implements Converter<String, User> { private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public User convert(String source) { try { return MAPPER.readValue(source, User.class); } catch (IOException e) { throw new IllegalArgumentException(e); } } }
上面這種作法屬於曲線救國的作法,不推薦使用在生產環境,可是若是有些第三方接口的對接沒法避免這種參數,能夠選擇這種實現方式。
通常來講,直接POST一個JSON字符串這種方式對於SpringMVC來講是比較友好的,只須要把ContentType設置爲application/json,提交一個原始的JSON字符串便可:
後端控制器的代碼也比較簡單:
@PostMapping(value = "/user-2") public User saveUser2(@RequestBody User user) { log.info(user.toString()); return user; }
由於使用了@RequestBody註解,最終使用到的參數處理器爲RequestResponseBodyMethodProcessor,實際上會用到MappingJackson2HttpMessageConverter進行參數類型的轉換,底層依賴到Jackson相關的包。
URL參數,或者叫請求路徑參數是基於URL模板獲取到的參數,例如/user/{userId}是一個URL模板(URL模板中的參數佔位符是{}),實際請求的URL爲/user/1,那麼經過匹配實際請求的URL和URL模板就能提取到userId爲1。在SpringMVC中,URL模板中的路徑參數叫作PathVariable,對應註解@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)中,區別下面三種狀況:
在一個控制器(使用了@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的父接口,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,覆蓋對應的方法。
控制器方法的參數能夠是@Value註解修飾的參數,會從Environment中裝配和轉換屬性值到對應的參數中(也就是參數的來源並非請求體),參數處理器爲ExpressionValueMethodArgumentResolver。舉個例子:
@GetMapping(value = "/value") public String value(@Value(value = "${spring.application.name}") String name) { log.info("spring.application.name={}", name); return name; }
Map類型參數的範圍相對比較廣,對應一系列的參數處理器,注意區別使用了上面提到的部分註解的Map類型和徹底不使用註解的Map類型參數,二者的處理方式不相同。下面列舉幾個相對典型的Map類型參數處理例子。
不使用任何註解的Map<String,Object>參數
這種狀況下參數實際上直接回調ModelAndViewContainer中的ModelMap實例,參數處理器爲MapMethodProcessor,往Map參數中添加的屬性將會帶到頁面中。
使用@RequestParam註解的Map<String,Object>參數
這種狀況下的參數處理器爲RequestParamMapMethodArgumentResolver,使用的請求方式須要指定ContentType爲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集合,能夠有兩種選擇:
getFiles
方法獲取MultipartFile列表。控制器方法代碼以下:
@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; }
@DateTimeFormat註解配合@RequestBody的參數使用的時候,會發現拋出InvalidFormatException異常,提示轉換失敗,這是由於在處理此註解的時候,只支持form提交(ContentType爲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; }
由於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
。
最後,org.springframework.util.AntPathMatcher
做爲一個工具類,能夠單獨使用,不只僅能夠用於匹配URL,也能夠用於匹配系統文件路徑,不過須要使用其帶參數構造改變內部的pathSeparator變量,例如:
AntPathMatcher antPathMatcher = new AntPathMatcher(File.separator);
筆者在前一段時間曾經花大量時間梳理和分析過Spring、SpringMVC的源碼,可是後面一段很長的時間須要進行業務開發,對架構方面的東西有點生疏了,畢竟東西不用就會生疏,這個是常理。這篇文章基於一些SpringMVC的源碼經驗總結了請求參數的處理相關的一些知識,但願幫到本身和你們。
參考資料:
(本文完)