1.5萬字長文簡單總結(jié)SpringMVC請求參數(shù)接收

前提#

在日常使用SpringMVC進行開發(fā)的時候着憨,有可能遇到前端各種類型的請求參數(shù)憾儒,這里做一次相對全面的總結(jié)。SpringMVC中處理控制器參數(shù)的接口是HandlerMethodArgumentResolver,此接口有眾多子類俱济,分別處理不同(注解類型)的參數(shù)误阻,下面只列舉幾個子類:

  • RequestParamMethodArgumentResolver:解析處理使用了@RequestParam注解的參數(shù)债蜜、MultipartFile類型參數(shù)和Simple類型(如long、int等類型)參數(shù)究反。
  • RequestResponseBodyMethodProcessor:解析處理@RequestBody注解的參數(shù)寻定。
  • PathVariableMapMethodArgumentResolver:解析處理@PathVariable注解的參數(shù)。

實際上精耐,一般在解析一個控制器的請求參數(shù)的時候狼速,用到的是HandlerMethodArgumentResolverComposite,里面裝載了所有啟用的HandlerMethodArgumentResolver子類卦停。而HandlerMethodArgumentResolver子類在解析參數(shù)的時候使用到HttpMessageConverter(實際上也是一個列表向胡,進行遍歷匹配解析)子類進行匹配解析,常見的如MappingJackson2HttpMessageConverter(使用Jackson進行序列化和反序列化)惊完。

而HandlerMethodArgumentResolver子類到底依賴什么HttpMessageConverter實例實際上是由請求頭中的Content-Type(在SpringMVC中統(tǒng)一命名為MediaType僵芹,見org.springframework.http.MediaType)決定的,因此我們在處理控制器的請求參數(shù)之前必須要明確外部請求的Content-Type到底是什么小槐。上面的邏輯可以直接看源碼AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters拇派,思路是比較清晰的。在@RequestMapping注解中,produces和consumes屬性就是和請求的Accept或者響應(yīng)的Content-Type相關(guān)的:

  • consumes屬性:指定處理請求的提交內(nèi)容類型(Content-Type)攀痊,例如application/json桐腌、text/html等等,只有命中了對應(yīng)的Content-Type的值才會接受該請求苟径。
  • produces屬性:指定返回的內(nèi)容類型案站,僅當(dāng)某個請求的請求頭中的(Accept)類型中包含該指定類型才返回,如果返回的是JSON數(shù)據(jù)一般考慮使用application/json;charset=UTF-8棘街。

另外提一點蟆盐,SpringMVC中默認(rèn)使用Jackson作為JSON的工具包,如果不是完全理解透整套源碼的運作遭殉,一般不是十分建議修改默認(rèn)使用的MappingJackson2HttpMessageConverter(例如有些人喜歡使用FastJson石挂,實現(xiàn)HttpMessageConverter引入FastJson做HTTP消息轉(zhuǎn)換器,其實這種做法并不推薦)险污。

SpringMVC請求參數(shù)接收#

其實一般的表單或者JSON數(shù)據(jù)的請求都是相對簡單的痹愚,一些復(fù)雜的處理主要包括URL路徑參數(shù)、文件上傳蛔糯、數(shù)組或者列表類型數(shù)據(jù)等拯腮。另外,關(guān)于參數(shù)類型中存在日期類型屬性(例如java.util.Date蚁飒、java.sql.Date动壤、java.time.LocalDate、java.time.LocalDateTime淮逻、java.time.ZonedDateTime等等)琼懊,解析的時候一般需要自定義實現(xiàn)的邏輯實現(xiàn)String-->日期類型的轉(zhuǎn)換。其實道理很簡單爬早,日期相關(guān)的類型對于每個國家哼丈、每個時區(qū)甚至每個使用者來說認(rèn)知都不一定相同,所以SpringMVC并沒有對于日期時間類型的解析提供一個通用的解決方案筛严。在演示一些例子可能用到下面的模特類:

Copy@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體系中正確處理參數(shù)的例子進行分析醉旦,還會花精力整理SpringMVC體系中獨有的URL路徑參數(shù)處理的一些技巧以及最常見的日期參數(shù)處理的合理實踐(對于GET方法和POST方法提交的參數(shù)處理,基本囊括了其他如DELETE脑漫、PUT等方法的參數(shù)處理髓抑,隨機應(yīng)變即可)咙崎。

GET方法請求參數(shù)處理#

HTTP(s)協(xié)議使用GET方法進行請求的時候优幸,提交的參數(shù)位于URL模式的Query部分,也就是URL的?標(biāo)識符之后的參數(shù)褪猛,格式是key1=value1&key2=value2网杆。GET方法請求參數(shù)可以有多種方法獲取:

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

假設(shè)請求的URL為http://localhost:8080/get?name=doge&age=26馍资,那么控制器如下:

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

表單參數(shù)#

表單參數(shù),一般對應(yīng)于頁面上<form>標(biāo)簽內(nèi)的所有<input>標(biāo)簽的name-value聚合而成的參數(shù)关噪,一般Content-Type指定為application/x-www-form-urlencoded鸟蟹,表單參數(shù)值也就是會進行(URL)編碼。下面介紹幾種常見的表單參數(shù)提交的參數(shù)形式使兔。

  • 【非對象】- 非對象類型單個參數(shù)接收建钥。

對應(yīng)的控制器如下:

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

說實話,如果有毅力的話虐沥,所有的復(fù)雜參數(shù)的提交最終都可以轉(zhuǎn)化為多個單參數(shù)接收熊经,不過這樣做會產(chǎn)生十分多冗余的代碼,而且可維護性比較低欲险。這種情況下镐依,用到的參數(shù)處理器是RequestParamMapMethodArgumentResolver。

  • 【對象】 - 對象類型參數(shù)接收盯荤。

我們接著寫一個接口用于提交用戶信息馋吗,用到的是上面提到的模特類,主要包括用戶姓名秋秤、年齡和聯(lián)系人信息列表宏粤,這個時候,我們目標(biāo)的控制器最終編碼如下:

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

加入強行指定Content-Type為application/x-www-form-urlencoded灼卢,需要構(gòu)造請求參數(shù)格式如下:

因為沒有使用注解绍哎,最終的參數(shù)處理器為ServletModelAttributeMethodProcessor,主要是把HttpServletRequest中的表單參數(shù)封裝到MutablePropertyValues實例中鞋真,再通過參數(shù)類型實例化(通過構(gòu)造反射創(chuàng)建User實例)崇堰,反射匹配屬性進行值的填充。另外涩咖,請求復(fù)雜參數(shù)里面的列表屬性請求參數(shù)看起來比較奇葩海诲,實際上和在.properties文件中添加最終映射到Map類型的參數(shù)的寫法是一致的,所以對于嵌套數(shù)組或者列表類型的第一層索引要寫成firstLevel[index].fieldName的形式檩互。那么特幔,能不能把整個請求參數(shù)塞在一個字段中提交呢?

直接這樣做是不行的闸昨,因為實際提交的Form表單蚯斯,key是user字符串薄风,value實際上也是一個字符串,缺少一個String->User類型的轉(zhuǎn)換器拍嵌,實際上RequestParamMethodArgumentResolver依賴WebConversionService中Converter實例列表進行參數(shù)轉(zhuǎn)換遭赂,而默認(rèn)的Converter列表中肯定不會存在自定義轉(zhuǎn)換String->User類型的轉(zhuǎn)換器:

解決辦法還是有的,添加一個自定義的org.springframework.core.convert.converter.Converter實現(xiàn)即可:

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

上面這種做法屬于曲線救國的做法横辆,不推薦使用在生產(chǎn)環(huán)境撇他,但是如果有些第三方接口的對接無法避免這種參數(shù)(這個還真碰到多,有一些遠古的遺留系統(tǒng)比較容易出現(xiàn)各種奇葩的操作)狈蚤,可以選擇這種實現(xiàn)方式逆粹。

  • 【數(shù)組】 - 列表或者數(shù)組類型參數(shù)。

極度不推薦使用在application/x-www-form-urlencoded這種媒體類型的表單提交的形式下強行使用列表或者數(shù)組類型參數(shù)炫惩,除非是為了兼容處理歷史遺留系統(tǒng)的參數(shù)提交處理僻弹。例如提交的參數(shù)形式是:

Copylist = ["string-1", "string-2", "string-3"]

那么表單參數(shù)的形式要寫成:

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

控制器的代碼如下:

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

一個更加復(fù)雜的例子如下,假設(shè)想要提交的報文格式如下:

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

那么表單參數(shù)的形式要寫成:

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

控制器的代碼如下:

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

這種傳參格式其實并不靈活他嚷,甚至有可能降低開發(fā)效率和參數(shù)可讀性蹋绽。

JSON參數(shù)#

一般來說,直接在POST請求中的請求體提交一個JSON字符串這種方式對于SpringMVC來說是比較友好的筋蓖,只需要把Content-Type設(shè)置為application/json卸耘,然后直接上傳一個原始的JSON字符串即可,控制器方法參數(shù)使用@RequestBody注解處理:

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

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

因為使用了@RequestBody注解粘咖,最終使用到的參數(shù)處理器為RequestResponseBodyMethodProcessor蚣抗,實際上會用到MappingJackson2HttpMessageConverter進行參數(shù)類型的轉(zhuǎn)換,底層依賴到Jackson相關(guān)的包瓮下。推薦使用這種方式翰铡,這是最常用也是最穩(wěn)健的JSON參數(shù)處理方式

URL路徑參數(shù)#

URL路徑參數(shù)讽坏,或者叫請求路徑參數(shù)是基于URL模板獲取到的參數(shù)锭魔,例如/user/{userId}是一個URL模板(URL模板中的參數(shù)占位符是{}),實際請求的URL為/user/1路呜,那么通過匹配實際請求的URL和URL模板就能提取到userId為1迷捧。在SpringMVC中,URL模板中的路徑參數(shù)叫做Path Variable胀葱,對應(yīng)注解@PathVariable漠秋,對應(yīng)的參數(shù)處理器為PathVariableMethodArgumentResolver。注意一點是抵屿,@PathVariable的解析是按照value(name)屬性進行匹配庆锦,和URL參數(shù)的順序是無關(guān)的。舉個簡單的例子:

后臺的控制器如下:

Copy@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)的軟件架構(gòu)風(fēng)格晌该,個人覺得這種風(fēng)格是比較靈活和清晰的(從URL和請求方法就能完全理解接口的意義和功能)肥荔。下面再介紹兩種相對特殊的使用方式。

  • 帶條件的URL參數(shù)朝群。

其實路徑參數(shù)支持正則表達式燕耿,例如我們在使用/sex/{sex}接口的時候,要求sex必須是F(Female)或者M(Male)姜胖,那么我們的URL模板可以定義為/sex/{sex:M|F}誉帅,代碼如下:

Copy@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狀態(tài)碼蚜锨。這里僅僅是介紹了一個最簡單的URL參數(shù)正則表達式的使用方式,更強大的用法可以自行摸索慢蜓。

  • @MatrixVariable的使用亚再。

MatrixVariable也是URL參數(shù)的一種,對應(yīng)注解@MatrixVariable晨抡,不過它并不是URL中的一個值(這里的值指定是兩個"/"之間的部分)氛悬,而是值的一部分,它通過";"進行分隔耘柱,通過"="進行K-V設(shè)置如捅。說起來有點抽象速挑,舉個例子:假如我們需要打電話給一個名字為doge馁蒂,性別是男薯蝎,分組是碼畜的程序員傻谁,GET請求的URL可以表示為:/call/doge;gender=male;group=programmer胚泌,我們設(shè)計的控制器方法如下:

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

當(dāng)然鸟召,如果你按照上面的例子寫好代碼毙芜,嘗試請求一下該接口發(fā)現(xiàn)是報錯的:400 Bad Request - Missing matrix variable 'gender' for method parameter of type String撤逢。這是因為@MatrixVariable注解的使用是不安全的娄柳,在SpringMVC中默認(rèn)是關(guān)閉對其支持坚洽。要開啟對@MatrixVariable的支持,需要設(shè)置RequestMappingHandlerMapping#setRemoveSemicolonContent方法為false:

Copy@Configuration public class CustomMvcConfiguration implements InitializingBean { @Autowired private RequestMappingHandlerMapping requestMappingHandlerMapping; @Override public void afterPropertiesSet() throws Exception { requestMappingHandlerMapping.setRemoveSemicolonContent(false); } }

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

文件上傳#

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

假設(shè)在電腦的磁盤D盤根目錄有一個圖片文件叫doge.jpg需了,現(xiàn)在要通過本地服務(wù)接口把文件上傳跳昼,控制器的代碼如下:

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

控制臺輸出是:

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

可能有點疑惑,參數(shù)是怎么來的肋乍,我們可以用Fildder軟件抓個包看下:

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

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

其他參數(shù)#

其他參數(shù)主要包括請求頭、Cookie吮廉、Model苞尝、Map等相關(guān)參數(shù),還有一些并不是很常用或者一些相對原生的屬性值獲然侣(例如HttpServletRequest宙址、HttpServletResponse或者它們內(nèi)置的實例方法等)不做討論。

請求頭#

請求頭的值主要通過@RequestHeader注解的參數(shù)獲取调卑,參數(shù)處理器是RequestHeaderMethodArgumentResolver曼氛,需要在注解中指定請求頭的Key。簡單實用如下:

控制器方法代碼:

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

Cookie#

Cookie的值主要通過@CookieValue注解的參數(shù)獲取令野,參數(shù)處理器為ServletCookieValueMethodArgumentResolver舀患,需要在注解中指定Cookie的Key∑疲控制器方法代碼如下:

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

Model類型參數(shù)#

Model類型參數(shù)的處理器是ModelMethodProcessor聊浅,實際上處理此參數(shù)是直接返回ModelAndViewContainer實例中的Model(具體是ModelMap類型),因為要橋接不同的接口和類的功能现使,因此回調(diào)的實例是BindingAwareModelMap類型低匙,此類型繼承自ModelMap同時實現(xiàn)了Model接口。舉個例子:

Copy@GetMapping(value = "/model") public String model(Model model, ModelMap modelMap) { log.info("{}", model == modelMap); return "success"; }

注意調(diào)用此接口碳锈,控制臺輸出INFO日志內(nèi)容為:true顽冶。還要注意一點:ModelMap或者Model中添加的屬性項會附加到HttpRequestServlet實例中帶到頁面中進行渲染,使用模板引擎的前提下可以直接在模板文件內(nèi)容中直接使用占位符提取這些屬性值售碳。

@ModelAttribute參數(shù)#

@ModelAttribute注解處理的參數(shù)處理器為ModelAttributeMethodProcessor强重,@ModelAttribute的功能源碼的注釋如下:

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

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

  1. @ModelAttribute使用在方法(返回值)上间景,方法沒有返回值(void類型),?Model(Map)參數(shù)需要自行設(shè)置艺智。
  2. @ModelAttribute使用在方法(返回值)上倘要,方法有返回值(非void類型),返回值會添加到Model(Map)參數(shù)十拣,key由@ModelAttribute的value指定封拧,否則會使用返回值類型字符串(首寫字母變?yōu)樾懼揪椋绶祷刂殿愋蜑镮nteger,則key為integer)泽西。
  3. @ModelAttribute使用在方法參數(shù)中曹铃,則可以獲取同一個控制器中的已經(jīng)設(shè)置的@ModelAttribute對應(yīng)的值。

在一個控制器(使用了@Controller的Spring組件)中尝苇,如果存在一到多個使用了@ModelAttribute的方法,這些方法總是在進入控制器方法之前執(zhí)行埠胖,并且執(zhí)行順序是由加載順序決定的(具體的順序是帶參數(shù)的優(yōu)先糠溜,并且按照方法首字母升序排序),舉個例子:

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

調(diào)用此接口直撤,控制臺輸出日志如下:

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

可以印證排序規(guī)則和參數(shù)設(shè)置非竿、獲取的結(jié)果和前面的分析是一致的。

Errors或者BindingResult參數(shù)#

Errors其實是BindingResult的父接口谋竖,BindingResult主要用于回調(diào)JSR參數(shù)校驗異常的屬性項红柱,如果JSR303校驗異常,一般會拋出MethodArgumentNotValidException異常蓖乘,并且會返回400(Bad Request)锤悄,見全局異常處理器DefaultHandlerExceptionResolver。Errors類型的參數(shù)處理器為ErrorsMethodArgumentResolver嘉抒。舉個例子:

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

調(diào)用接口控制臺Warn日志如下:

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

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

Copy@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相關(guān)的依賴抽離到spring-boot-starter-validation依賴中蚂会,如果要使用JSR303相關(guān)相關(guān)校驗功能,必須獨立引入此starter

@Value參數(shù)#

控制器方法的參數(shù)可以是@Value注解修飾的參數(shù)耗式,會從Environment實例中裝配和轉(zhuǎn)換屬性值到對應(yīng)的參數(shù)中(也就是參數(shù)的來源并不是請求體胁住,而是上下文中已經(jīng)加載和處理完成的環(huán)境屬性值),參數(shù)處理器為ExpressionValueMethodArgumentResolver刊咳。舉個例子:

Copy@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類型參數(shù)#

Map類型參數(shù)的范圍相對比較廣芦缰,對應(yīng)一系列的參數(shù)處理器企巢,注意區(qū)別使用了上面提到的部分注解的Map類型和完全不使用注解的Map類型參數(shù),兩者的處理方式不相同让蕾。下面列舉幾個相對典型的Map類型參數(shù)處理例子浪规。

不使用任何注解的Map<String,Object>參數(shù)

這種情況下參數(shù)實際上直接回調(diào)ModelAndViewContainer中的ModelMap實例或听,參數(shù)處理器為MapMethodProcessor,往Map參數(shù)中添加的屬性將會帶到頁面中笋婿。

使用@RequestParam注解的Map<String,Object>參數(shù)

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

控制器代碼為:

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

使用@RequestHeader注解的Map<String,Object>參數(shù)

這種情況下的參數(shù)處理器為RequestHeaderMapMethodArgumentResolver缸濒,作用是獲取請求的所有請求頭的Key-Value足丢。

使用@PathVariable注解的Map<String,Object>參數(shù)

這種情況下的參數(shù)處理器為PathVariableMapMethodArgumentResolver,作用是獲取所有路徑參數(shù)封裝為Key-Value結(jié)構(gòu)庇配。

MultipartFile集合-批量文件上傳#

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

  1. 使用MultipartHttpServletRequest參數(shù)捞慌,直接調(diào)用getFiles方法獲取MultipartFile列表耀鸦。
  2. 使用@RequestParam注解修飾MultipartFile列表,參數(shù)處理器是RequestParamMethodArgumentResolver啸澡,其實就是第1種方式的封裝而已袖订。

控制器方法代碼如下:

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

日期類型參數(shù)處理#

日期參數(shù)處理個人認(rèn)為是請求參數(shù)處理中最復(fù)雜的,因為一般日期處理的邏輯不是通用的嗅虏,過多的定制化處理導(dǎo)致很難有一個統(tǒng)一的標(biāo)準(zhǔn)處理邏輯去處理和轉(zhuǎn)換日期類型的參數(shù)洛姑。不過,這里介紹幾個通用的方法皮服,以應(yīng)對各種奇葩的日期格式吏口。下面介紹的例子中全部使用JDK8中引入的日期時間API,圍繞java.util.Date為核心的日期時間API的使用方式類同冰更。

一产徊、統(tǒng)一以字符串形式接收#

這種是最原始但是最奏效的方式,統(tǒng)一以字符串形式接收蜀细,然后自行處理類型轉(zhuǎn)換舟铜,下面給個小例子:

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

使用字符串接收后再轉(zhuǎn)換的缺點就是模板代碼太多,編碼風(fēng)格不夠簡潔奠衔,重復(fù)性工作太多谆刨,如果有代碼潔癖或者類似筆者這樣是一個節(jié)能主義者,一般不會選用這種方式归斤。

二痊夭、使用注解@DateTimeFormat或者@JsonFormat#

@DateTimeFormat注解配合@RequestBody的參數(shù)使用的時候,會發(fā)現(xiàn)拋出InvalidFormatException異常脏里,提示轉(zhuǎn)換失敗她我,這是因為在處理此注解的時候,只支持Form表單提交(Content-Type為x-www-form-urlencoded),例子如下:

Copy@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請求參數(shù)的場景番舆,因此更推薦使用@JsonFormat注解酝碳,不過注意需要指定時區(qū)(timezone屬性,例如在中國是東八區(qū)GMT+8)恨狈,否則有可能導(dǎo)致出現(xiàn)時差疏哗,舉個例子:

Copy@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作為日期字段參數(shù)的類型,因為它的轉(zhuǎn)換相對于其他JDK8的日期時間類型簡單

三禾怠、Jackson序列化和反序列化定制#

因為SpringMVC默認(rèn)使用Jackson處理@RequestBody的參數(shù)轉(zhuǎn)換返奉,因此可以通過定制序列化器和反序列化器來實現(xiàn)日期類型的轉(zhuǎn)換,這樣我們就可以使用application/json的形式提交請求參數(shù)吗氏。這里的例子是轉(zhuǎn)換請求JSON參數(shù)中的字符串為LocalDateTime類型芽偏,屬于JSON反序列化,因此需要定制反序列化器:

Copy@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對于日期類型處理默認(rèn)的序列化器和反序列化器关面,這樣就能全局生效坦袍,不需要再使用其他注解或者定制序列化方案(當(dāng)然,有些時候需要特殊處理定制)等太,或者說捂齐,在需要特殊處理的場景才使用其他注解或者定制序列化方案。使用鉤子接口Jackson2ObjectMapperBuilderCustomizer可以實現(xiàn)對容器中的ObjectMapper單例中的屬性定制:

Copy@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匹配#

前面基本介紹完了主流的請求參數(shù)處理,其實SpringMVC中還會按照URL的模式進行匹配瞻想,使用的是Ant路徑風(fēng)格压真,處理工具類為org.springframework.util.AntPathMatcher,從此類的注釋來看蘑险,匹配規(guī)則主要包括下面四點

  1. ?匹配1個字符滴肿。
  2. *匹配0個或者多個字符
  3. **匹配路徑中0個或者多個目錄佃迄。
  4. 正則支持泼差,如{spring:[a-z]+}將正則表達式[a-z]+匹配到的值,賦值給名為spring的路徑變量呵俏。

舉些例子:

'?'形式的URL

Copy@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

Copy@GetMapping(value = "/pattern*") public String pattern() { return "success"; } /pattern 200 OK /pattern/ 200 OK /patternd 200 OK /pattern/a 404 Not Found

'**'形式的URL

Copy@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

Copy@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模式可以組合使用堆缘,千變?nèi)f化。

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

Copy@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,也可以用于匹配系統(tǒng)文件路徑臂痕,不過需要使用其帶參數(shù)構(gòu)造改變內(nèi)部的pathSeparator變量伯襟,例如:

CopyAntPathMatcher antPathMatcher = new AntPathMatcher(File.separator);

小結(jié)#

筆者在前一段時間曾經(jīng)花大量時間梳理和分析過Spring、SpringMVC的源碼握童,但是后面一段很長的時間需要進行業(yè)務(wù)開發(fā)姆怪,對架構(gòu)方面的東西有點生疏了,畢竟東西不用就會生疏澡绩,這個是常理稽揭。這篇文章基于一些SpringMVC的源碼經(jīng)驗總結(jié)了請求參數(shù)的處理相關(guān)的一些知識,希望幫到自己和大家肥卡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末溪掀,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子步鉴,更是在濱河造成了極大的恐慌揪胃,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氛琢,死亡現(xiàn)場離奇詭異喊递,居然都是意外死亡,警方通過查閱死者的電腦和手機阳似,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門骚勘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人障般,你說我怎么就攤上這事调鲸。” “怎么了挽荡?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵藐石,是天一觀的道長。 經(jīng)常有香客問我定拟,道長于微,這世上最難降的妖魔是什么逗嫡? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮株依,結(jié)果婚禮上驱证,老公的妹妹穿的比我還像新娘。我一直安慰自己恋腕,他們只是感情好抹锄,可當(dāng)我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荠藤,像睡著了一般伙单。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上哈肖,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天吻育,我揣著相機與錄音,去河邊找鬼淤井。 笑死布疼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的币狠。 我是一名探鬼主播游两,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼总寻!你這毒婦竟也來了器罐?” 一聲冷哼從身側(cè)響起梢为,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤渐行,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后铸董,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體祟印,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年粟害,在試婚紗的時候發(fā)現(xiàn)自己被綠了蕴忆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡悲幅,死狀恐怖套鹅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情汰具,我是刑警寧澤卓鹿,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站留荔,受9級特大地震影響吟孙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一杰妓、第九天 我趴在偏房一處隱蔽的房頂上張望藻治。 院中可真熱鬧,春花似錦巷挥、人聲如沸桩卵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吸占。三九已至,卻和暖如春凿宾,著一層夾襖步出監(jiān)牢的瞬間矾屯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工初厚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留件蚕,地道東北人。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓产禾,卻偏偏與公主長得像排作,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子亚情,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,697評論 2 351