作者:zhrowable
https://www.jianshu.com/p/5f6abd08ee08
SpringMVC請求參數接收
其實一般的表單或者JSON數據的請求都是相對簡單的,一些複雜的處理主要包括URL路徑參數、文件上傳、數組或者列表類型數據等。
另外,關於參數類型中存在日期類型屬性(例如java.util.Date、java.sql.Date、java.time.LocalDate、java.time.LocalDateTime),解析的時候一般需要自定義實現的邏輯實現String->日期類型的轉換。
其實道理很簡單,日期相關的類型對於每個國家、每個時區甚至每個使用者來說認知都不一定相同。在演示一些例子主要用到下面的模特類:
@Datapublic class User {
private String name;
private Integer age;
private List<Contact> contacts;}@Datapublic class Contact {
private String name;
private String phone;
}
表單參數
非對象類型單個參數接收:
這種是最常用的表單參數提交,ContentType指定爲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;
}
我們還是指定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實現即可:
@Componentpublic 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);
}
}
}
上面這種做法屬於曲線救國的做法,不推薦使用在生產環境,但是如果有些第三方接口的對接無法避免這種參數,可以選擇這種實現方式。
JSON參數
一般來說,直接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參數,或者叫請求路徑參數是基於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:MF,代碼如下:
@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:
@Configurationpublic 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用於讀取請求體的最後部分(文件的字節序列)。參數處理器用到的是RequestPartM
ethodArgumentResolver(記住一點,使用了@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
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)中,區別下面三種情況:
1、@ModelAttribute使用在方法(返回值)上,方法沒有返回值(void類型), Model(Map)參數需要自行設置。
2、@ModelAttribute使用在方法(返回值)上,方法有返回值(非void類型),返回值會添加到Model(Map)參數,key由@ModelAttribute的value指定,否則會使用返回值類型字符串(首寫字母變爲小寫)。
3、@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@NoArgsConstructorpublic 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參數
控制器方法的參數可以是@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類型參數處理例子。
不使用任何註解的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集合-批量文件上傳
批量文件上傳的時候,我們一般需要接收一個MultipartFile集合,可以有兩種選擇:
1、使用MultipartHttpServletRequest參數,直接調用
getFiles
方法獲取MultipartFile列表。2、使用@RequestParam註解修飾MultipartFile列表,參數處理器是RequestParamMethodArgumentResolver,其實就是第一種的封裝而已。
控制器方法代碼如下:
@PostMapping(value = "/parts")
public String partArgs(@RequestParam(name = "file") List<MultipartFile> parts) {
log.info("{}", parts);
return parts.toString();
}
日期類型參數處理
日期處理個人認爲是請求參數處理中最複雜的,因爲一般日期處理的邏輯不是通用的,過多的定製化處理導致很難有一個統一的標準處理邏輯去處理和轉換日期類型的參數。
不過,這裏介紹幾個通用的方法,以應對各種奇葩的日期格式。下面介紹的例子中全部使用Jdk8中引入的日期時間API,圍繞java.util.Date爲核心的日期時間API的使用方式類同。另外推薦大家關注下微信公衆號Java技術棧,在後臺回覆Java可以獲取我整理的 N 篇 Java 8+ 教程,都是乾貨。
一、統一以字符串形式接收
這種是最原始但是最奏效的方式,統一以字符串形式接收,然後自行處理類型轉換,下面給個小例子:
@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";
}
@Datapublic class UserDto {
private String userId;
private String birthdayTime;
private String graduationTime;}@Datapublic class UserEntity {
private String userId;
private LocalDateTime birthdayTime;
private LocalDateTime graduationTime;
}
二、使用註解@DateTimeFormat或者@JsonFormat
@DateTimeFormat註解配合@RequestBody的參數使用的時候,會發現拋出InvalidFormatException異常,提示轉換失敗,這是因爲在處理此註解的時候,只支持form提交(ContentType爲x-www-form-urlencoded),例子如下:
@Datapublic 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")LocalDateTime birthdayTime,
@RequestParam("name"="graduationTime")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的形式提交請求參數。關於Java序列化你應該知道的一切,這篇推薦看下。
這裏的例子是轉換請求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、
?
匹配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模式可以組合使用,千變萬化。RESTful API設計技巧經驗總結推薦看看。
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的源碼經驗總結了請求參數的處理相關的一些知識,希望幫到自己和大家。
關注公衆號Java技術棧回覆"面試"獲取我整理的2020最全面試題及答案。
推薦去我的博客閱讀更多:
2.Spring MVC、Spring Boot、Spring Cloud 系列教程
3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程
覺得不錯,別忘了點贊+轉發哦!