content-type定义
MediaType,即是Internet Media Type,互联网媒体类型;也叫做MIME类型,在Http协议消息头中,使用Content-Type来表示具体请求中的媒体类型信息。
常见媒体格式如下:
- text/html : HTML格式
- text/plain :纯文本格式
- text/xml : XML格式
- image/gif :gif图片格式
- image/jpeg :jpg图片格式
- image/png:png图片格式
以application开头的媒体格式类型:
- application/xhtml+xml :XHTML格式
- application/xml : XML数据格式
- application/atom+xml :Atom XML聚合格式
- application/json : JSON数据格式
- application/pdf :pdf格式
- application/msword : Word文档格式
- application/octet-stream :
二进制流数据(如常见的文件下载) - application/x-www-form-urlencoded :
默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
b. content-type 实例说明
上面算是基本定义和取值,下面结合实例对典型的几种方式进行说明
application/x-www-form-urlencoded:数据被编码为名称/值对。这是标准的编码格式。
multipart/form-data: 数据被编码为一条消息,页上的每个控件对应消息中的一个部分。
text/plain: 数据以纯文本形式(text/json/xml/html)进行编码,其中不含任何控件或格式字符
对于前端使用而言,form表单的enctype属性为编码方式,常用有两种:application/x-www-form-urlencoded和multipart/form-data,默认为application/x-www-form-urlencoded。
在日常使用SpringMVC进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结。SpringMVC中处理控制器参数的接口是HandlerMethodArgumentResolver,此接口有众多子类,分别处理不同(注解类型)的参数,下面只列举几个子类:
- RequestParamMethodArgumentResolver:解析处理使用了@RequestParam注解的参数、MultipartFile类型参数和Simple类型(如long、int等类型)参数。
- RequestResponseBodyMethodProcessor:解析处理@RequestBody注解的参数。
- PathVariableMapMethodArgumentResolver:解析处理@PathVariable注解的参数。
实际上,一般在解析一个控制器的请求参数的时候,用到的是HandlerMethodArgumentResolverComposite,里面装载了所有启用的HandlerMethodArgumentResolver子类。而HandlerMethodArgumentResolver子类在解析参数的时候使用到HttpMessageConverter(实际上也是一个列表,进行遍历匹配解析)子类进行匹配解析,常见的如MappingJackson2HttpMessageConverter(使用Jackson进行序列化和反序列化)。而HandlerMethodArgumentResolver子类到底依赖什么HttpMessageConverter实例实际上是由请求头中的Content-Type(在SpringMVC中统一命名为MediaType,见org.springframework.http.MediaType)决定的,因此我们在处理控制器的请求参数之前必须要明确外部请求的Content-Type到底是什么。上面的逻辑可以直接看源码AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters,思路是比较清晰的。在@RequestMapping注解中,produces和consumes属性就是和请求或者响应的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消息转换器,这种做法并不推荐)。
Get请求
发起Get请求时,浏览器用application/x-www-form-urlencoded方式,将表单数据转换成一个字符串(key1=value1&key2=value2…)拼接到url上,这就是我们常见的url带请求参数的情况
RequestBody同样支持GET方法?????
Post表单
发起post请求时,如果没有传文件,浏览器也是将form表单的数据封装成k=v的结果丢到http body中,拿开源中国的博客提交的表单为例,一个典型的post表单,上传的数据拼装在form data中,为kv结构
如果有传文件的场景,Content-Type类型会升级为multipart/form-data,这一块不详细展开
Post json对象
post表单除了前面一种方式之外,还有一种也是我们常见的,就是讲所有的表单数据放在一个大的json串中,然后丢给后端,这里也有一个在线的实例,某电商平台的商品发表,截图如下
注意看上面的Request Payload,是一个大的json串,和前面差别明显
代码示例:
Ajax的默认格式为:application/x-www-form-urlencoded,相当于(username=“admin”&password=123)来传递数据(这是GET请求的固定格式)
前端代码:
当Ajax以默认格式上传时,data数据直接使用JSON对象user,不用转换为JSON字符串(很方便)
var user= {
"username" : username,
"password" : password,
"rememberMe":rememberMe
};
$.ajax({
url : "http://...../jsontest.do",
type : "POST",
async : true,
data : user,
dataType : 'json',
success : function(data) {
}
});
后端使用@RequestParam注解或省略:
【推荐】
//直接省略注解
@RequestMapping("/jsontest.do")
public void test(User user,String username,String password,Boolean rememberMe){
System.out.println(user);
System.out.println("username: " + username);
System.out.println("password: " + password);
System.out.println("rememberMe: " + rememberMe);
}
//加上注解
@RequestMapping("/jsontest.do")
public void test(@RequestParam String username,
@RequestParam String password,@RequestParam Boolean rememberMe,){
System.out.println("username: " + username);
System.out.println("password: " + password);
System.out.println("rememberMe: " + rememberMe);
}
优点:
1.前端传递数据不用转换为json字符串:JSON.stringify(user)
2.后端接受的参数很灵活,即可以封装为User对象,亦可以使用单个参数username,rememberMe,甚至User对象和单个rememberMe参数混合使用都可以
【对象】 - 对象类型参数接收
我们接着写一个接口用于提交用户信息,用到的是上面提到的模特类,主要包括用户姓名、年龄和联系人信息列表,这个时候,我们目标的控制器最终编码如下:
@Data
public class User {
private String name;
private Integer age;
private List contacts;
}
@Data
public class Contact {
private String name;
private String phone;
}
@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类型的参数的写法是一致的。**那么,能不能把整个请求参数塞在一个字段中提交呢?
直接这样做是不行的,因为实际提交的Form表单,key是user字符串,value实际上也是一个字符串,缺少一个String->User类型的转换器,实际上RequestParamMethodArgumentResolver依赖WebConversionService中Converter实例列表进行参数转换:
解决办法还是有的,添加一个org.springframework.core.convert.converter.Converter实现即可:
@Component public class StringUserConverter implements Converter{ @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") Listlist) { 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") Listusers) { log.info(users); } @Data public class UserVo{ private String name; private Integer age; }
Post json字符串
@RequestBody使得REST接口接收的不再content-type为application/x-www-form-urlencoded的请求, 反而需要显示指定为application/json
也是在body中添加json串格式的请求,使用此方式的同时,还是老老实实的选择POST方法比较合适,无法通过javax.servlet.ServletRequest#getParameter获取参数。
代码示例:
前端代码:
var user= {
"username" : username,
"password" : password
};
$.ajax({
url : "http://...../jsontest.do",
type : "POST",
async : true,
contentType: "application/json; charset=utf-8",
data : JSON.stringify(user),
dataType : 'json',
success : function(data) {
}
});
后端必须使用@RequestBody 注解:
//这种方式下所有的参数都只能封装在User对象中,不能单独设置参数
@RequestMapping("/jsontest")
public void test(@RequestBody User user ){
String username = user.getUsername();
String password = user.getPassword();
}
或者
@RequestMapping("/jsontest")
public void test(@RequestBody Map map ){
String username = map.get("username").toString();
String password = map.get("password").toString();
}
或者
public void test(@RequestBody String jsonData) {
JSONObject jsonObject = JSON.parseObject(jsonData);
String username= jsonObject.getString("username");
String username= jsonObject.getString("password");
}
缺点:
1.前端需要使用JSON.stringify()将JSON对象转换为JSON字符串
2.后端在接受参数的时候比较麻烦,没有第1种简单,也没有第一种灵活
postman写入参数如下:
后端控制器的代码也比较简单:
@PostMapping(value = "/user-2")
public User saveUser2(@RequestBody User user) {
log.info(user.toString());
return user;
}
因为使用了@RequestBody注解,最终使用到的参数处理器为RequestResponseBodyMethodProcessor,实际上会用到MappingJackson2HttpMessageConverter进行参数类型的转换,底层依赖到Jackson相关的包。推荐使用这种方式,这是最常用也是最稳健的JSON参数处理方式。
@PathVariable
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参数正则表达式的使用方式,更强大的用法可以自行摸索。
文件上传#
文件上传在使用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;
}
请求头#
请求头的值主要通过@RequestHeader注解的参数获取,参数处理器是RequestHeaderMethodArgumentResolver,需要在注解中指定请求头的Key。简单实用如下:
控制器方法代码:
@PostMapping(value = "/header")
public String header(@RequestHeader(name = "Content-Type") String Content-Type) {
return Content-Type;
}
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实例中带到页面中进行渲染。
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"));
}
}
spring-web.xml中需要如下配置
text/html;charset=UTF-8
application/json;charset=UTF-8
application/xml;charset=UTF-8
AllowArbitraryCommas
AllowUnQuotedFieldNames
DisableCircularReferenceDetect
部分原理参考:https://juejin.cn/post/6844903648858734600



