HTTP请求和springmvc取参
问题背景概述
无论是使用HTML表单或者后端程序员常用的postman亦或ajax等向springmvc后台controller发起请求,在controller接收参数,常常碰到为什么HttpRequestServlet.getParameter(String key)方法获取不到客户端传来的参数呢?又或者为什么debug时看不到参数呢?亦或怎么上传文件并接收呢?你是否会碰到类似的问题?那么下面结合HTTP请求、Tomcat、springmvc、源码分析等来系统的探究下如何获取客户端传来参数的问题及原理,如有错误,请各位大神指正。
1、Http协议content-type介绍
Content-Type(内容类型),一般是指网页中存在的 Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件,这就是经常看到一些 PHP 网页点击的结果却是下载一个文件或一张图片的原因。Content-Type 标头告诉客户端实际返回的内容的内容类型。
语法格式:
Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=something
实例:
常见的媒体格式类型如下:
- 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格式发送到服务器(表单默认的提交数据的格式)
另外一种常见的媒体格式是上传文件之时使用的:
multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式。
2、常用媒体类型请求介绍
2.1 application/x-www-form-urlencoded类型
在get请求和post请求下的浏览器对表单数据进行key=value的方式进行处理。在对每个参数的key和value进行 URL 转码后,以key1=value1&key2=value2…的方式拼接起来作为请求参数,这个想必大家都明白。不同的是,get请求会将拼接起来的key=value形式的参数放在请求URL后请求,因为get请求没有请求体。而post请求是将拼接起来的key=value参数放在了请求体中。 在form表单中可以指定enctype属性,默认就是这个媒体类型。
form表单如下:
<form action="http://localhost:8080/mvc/registerInformation" enctype="multipart/form-data" method="post">
<div>
姓名:<input name="name" type="text" />
</div>
<div>
年龄:<input name="age" type="number" />
</div>
<div>
爱好:<input name="hobby" type="text" />
</div>
<div>
card:<input name="card" type="file" />
</div>
<div>
<input type="submit" value="提交" />
</div>
</form>
如下所示get请求拼接表单数据后的URL:
General
Request URL: http://localhost:8080/mvc/registerInformation?name=james&age=38&hobby=basketball
Request Method: GET
Status Code: 200 OK
Remote Address: [::1]:8080
Referrer Policy: no-referrer-when-downgrade
2.2 multipart/form-data类型
这个类型意味着要上传文件到服务器,所以从实际考虑出发,只进行post请求分析,不做get请求分析。在post请求下,浏览器会将表单的数据处理为一条消息,以标签为单元,用分隔符分开。既可以上传键值对,也可以上传文件。当上传的字段是文件时,会有Content-Type来标明文件类型;content-disposition,用来说明字段的一些信息;
由于有boundary隔离,所以multipart/form-data既可以上传文件,也可以上传键值对,它采用了键值对的方式,所以可以上传多个文件。请求体中的boundary参数指定的就是分隔体,可以看到请求内容被分为了两段,第一段对应filekey,第二段对应textkey。在form表单中可以指定enctype属性。
请求消息如下:
POST HTTP/1.1
Host: test.app.com
Cache-Control: no-cache
Postman-Token: 59227787-c438-361d-fbe1-75feeb78047e
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="filekey"; filename=""
Content-Type:
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="textkey"
tttttt
------WebKitFormBoundary7MA4YWxkTrZu0gW--
form-data与x-www-form-urlencoded的区别:
- multipart/form-data:可以上传文件或者键值对,最后都会转化为一条消息。
- x-www-form-urlencoded:只能上传键值对,而且键值对都是通过&间隔分开的。
2.3 postman中的raw
raw对应的是入参是任意格式的可以上传任意格式的【文本】,可以上传text、json、xml、html等,浏览器会将其放在请求体中(post请求)。
3 获取请求中参数方式
3.1 请求为GET
内容类型为:Content-Type: null (常用)
接收方式为:键名称 / 有键名属性的类
例子:
request:http://localhost:8080/form?name=张三param&age=20
接收:save(String name, Integer age) / save(User user)
内容类型为:Content-Type: multipart/form-data; boundary=...6936 (参数存放到body中)(不用)
接收方式为:键名称 / 有键名属性的类
例子:
request:http://localhost:8080/form
接收:save(String name, Integer age) / save(User user)
内容类型为:Content-Type: application/x-www-form-urlencoded (参数存放到body中)(不用)
接收方式为:不能接收
内容类型为:Content-Type: application/json (参数存放到body中)(常用)
接收方式为: 有键名属性的类+@RequestBody
例子:
request:http://localhost:8080/form
接收:save(@RequestBody User user)
3.2 请求为POST
内容类型为:Content-Type: null
接收方式为:键名称 / 有键名属性的类
例子:
request:http://localhost:8080/form?name=张三param&age=20
接收:save(String name, Integer age) / save(User user)
内容类型为:Content-Type:multipart/form-data; boundary=...936 (参数存放到body中)(文件上传)(常用)
接收方式为: 键名称 / 有键名属性的类
例子:
request:http://localhost:8080/form
接收:save(String name, Integer age) / save(User user)
内容类型为:Content-Type:application/x-www-form-urlencoded (参数存放到body中)(表单提交)(常用)
接收方式为: 键名称 / 有键名属性的类
例子:
request:http://localhost:8080/form
接收:save(String name, Integer age) / save(User user)
内容类型为:Content-Type: application/json (参数存放到body中)(常用)
接收方式为: 有键名属性的类+@RequestBody
例子:
request:http://localhost:8080/form
接收:save(@RequestBody User user)
内容类型为:Content-Type: text/plain
接收方式为: String字符串
例如:{"name":""tome,"age":18}
@RequestMapping(value = "notify")
public String receiveNotify(@RequestBody String params) {
User user = JSON.parseObject(params,User.class);
return "success";
}
3.3 Spring MVC中获取参数常用注解
@RequestAttribute (request.getAttribute(String name)):用于获取自定义在请求中的参数
@RequestParam(request.getParameter(String name)):用于获取 拼接在请求中的参数/表单提交中的参数
@RequestHeader(request.getHeader(String name)):用于获取请求头中的参数
@RequestBody:用于获取请求body中的json数据
@RequestPart:用于获取Content-Type:multipart/form-data中文件类型参数
@PathParam:用于获取请请求request中的参数
@PathVariable:用于获取URI中数据
4 请求参数封装及获取深层探索
由于常用Tomcat容器进行部署web服务,因此下面的探索也都是基于Tomcat容器的。
4.1 HttpServletRequest对象的封装及数据访问
tomcat或者其他的Servlet容器在调用程序员的Servlet之前已经帮我们做了很多事情了,比如解析HttpRequestLine(HTTP请求行)和解析HttpHeader(HTTP请求头)等等,但事实上在Servlet之前,服务器只解析到Header就停了。 如果请求是get请求,那么所有请求参数都在http协议的请求行中,如果是post请求,那么所有参数是封装在请求体body中的。为了方便后边便于开展探索,先给出controller层接口如下:
@RequestMapping(value="/registerInformation",produces = "application/json;charset=UTF-8")
public @ResponseBody Employee findMuId(String name,Integer age, HttpServletRequest httpServletRequest) throws IOException {
MultipartHttpServletRequest multipartHttpServletRequest=null;
Employee ret = new Employee();
String nameInHttpServletRequest = httpServletRequest.getParameter("name");
logger.info("______name In HttpServletRequest={}",nameInHttpServletRequest);
String hobby = httpServletRequest.getParameter("hobby");
logger.info("__________hobby In HttpServletRequest={}",hobby);
BufferedReader reader = httpServletRequest.getReader();
ret.setName(name); ret.setAge(age);
if (httpServletRequest instanceof MultipartHttpServletRequest){
multipartHttpServletRequest = (MultipartHttpServletRequest)httpServletRequest;
MultipartFile file = multipartHttpServletRequest.getFile("card");
logger.info("=====多部件表单上传请求");
if (file!=null) logger.info("______fileName={}",file.getName());
}else{
logger.info("=====原生表单请求");
}
return ret;
}
在Tomcat等servlet容器解析完请求行和请求头后,解析出的参数是最终会封装到controller中的接口参数HttpServletRequest里。但是请求的体的数据最终会被封装到一个输入InputStream流中,这个流封装在HttpServletRequest实例中。
如果类似表单提交,enctype=“multipart/form-data”,那么这时必须要在spring容器中添加MultipartResolver类型的Bean才可以进行文件上传。如:
@Bean
public MultipartResolver multipartResolver(){
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
multipartResolver.setDefaultEncoding("UTF-8");
multipartResolver.setMaxUploadSize(2097152);
return multipartResolver;
}
这时,MultipartResolver解析器会对HttpServletRequest对象进行解析转换为MultipartHttpServletRequest类型的参数对象,将HttpServletRequest对象解析为files文件以及parameters参数,并将HttpServletRequest对象包装到MultipartHttpServletRequest实例中,MultipartHttpServletRequest实例提供了对上传的files文件的访问,并提供了和HttpServletRequest一样方法【比如getParameter(String name)】,的对parameters参数的获取。
其实上面两个对象对参数的获取都是基于它们共同的父接口ServletRequest接口,这个接口中包含的访问数据的主要方法如下:
public ServletInputStream getInputStream() throws IOException;
public String getParameter(String name);
public BufferedReader getReader() throws IOException;
MultipartResolver解析HttpServletRequest的接口声明源码如下:
/**
* Parse the given HTTP request into multipart files and parameters,
* and wrap the request inside a
* {@link org.springframework.web.multipart.MultipartHttpServletRequest} object
* that provides access to file descriptors and makes contained
* parameters accessible via the standard ServletRequest methods.
* @param request the servlet request to wrap (must be of a multipart content type)
* @return the wrapped servlet request
* @throws MultipartException if the servlet request is not multipart, or if
* implementation-specific problems are encountered (such as exceeding file size limits)
* @see MultipartHttpServletRequest#getFile
* @see MultipartHttpServletRequest#getFileNames
* @see MultipartHttpServletRequest#getFileMap
* @see javax.servlet.http.HttpServletRequest#getParameter
* @see javax.servlet.http.HttpServletRequest#getParameterNames
* @see javax.servlet.http.HttpServletRequest#getParameterMap
*/
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
因为MultipartHttpServletRequest接口类型继承了HttpServletRequest接口,因此在controller接口参数中可以使用父类型HttpServletRequest进行接收。
根据上面提到的,除了file文件外,MultipartHttpServletRequest对parameters的访问和HttpServletRequest是相同方式,都是基于它们的功能父接口ServletRequest。
在MultipartResolver的接口声明中有说明,当springmvc的DispatcherServlet检测到一个multipart/form-data类型的请求后,会通过配置一个MultipartResolver实例进行解析,并把HttpServletRequest对象进行包装,也就是包装成上面提到的MultipartHttpServletRequest的实例。在controller中,当我们需要访问上传的文件的时候,我们可以将HttpServletRequest类型转换为
MultipartHttpServletRequest,这时我们就可以操作上传的一个或多个文件,上传的每个file文件被封装为MultipartFile对象。例如:
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
MultipartFile multipartFile = multipartRequest.getFile("image");
...
}
或者如我们上面实例的controller中对文件的获取,其实都一样的。
对multipart/form-data媒体类型的表单请求说完了,下面就以Content-Type:application/x-www-form-urlencoded媒体类型的form表单请求完成接下来的探索。上面聊完了get请求和post请求两种方式下,浏览器将请求参数分别通过HTTP协议的请求行和请求体发送给服务器。在第3节中【3 获取请求中参数方式】说明了在工作常用的获取参数的方式。接下来我们重点聊聊一些访问数据的接口细节吧,有点晚,好困,如果你看到这里了,别忘了给我点个赞,码这么多字好累zzz…,前面我们提到了,对于数据的访问主要是基于一个高级别接口ServletRequest接口,我们将针对以下ServletRequest接口的以下几个方法聊,将源码贴出来吧,如下:
void setCharacterEncoding(String env)
/**
* Overrides the name of the character encoding used in the body of this
* request. This method must be called prior to reading request parameters
* or reading input using getReader(). Otherwise, it has no effect.
*
* @param env <code>String</code> containing the name of
* the character encoding.
*
* @throws UnsupportedEncodingException if this ServletRequest is still
* in a state where a character encoding may be set,
* but the specified encoding is invalid
*/
public void setCharacterEncoding(String env) throws UnsupportedEncodingException;
把这个接口挑出来说一下,是因为好多人迷乱中文乱码的问题,HttpServletRequest中调用这个方法处理中文乱码,是有条件的。
第一:仅对request的请求体有效,也就是post请求的body里的内容。
第二:在读取请求体里的内容方法前或者使用getReader()【接下来马上提到】方法才有效。
String getParameter(String name)
/**
* Returns the value of a request parameter as a <code>String</code>,
* or <code>null</code> if the parameter does not exist. Request parameters
* are extra information sent with the request. For HTTP servlets,
* parameters are contained in the query string or posted form data.
*
* <p>You should only use this method when you are sure the
* parameter has only one value. If the parameter might have
* more than one value, use {@link #getParameterValues}.
*
* <p>If you use this method with a multivalued
* parameter, the value returned is equal to the first value
* in the array returned by <code>getParameterValues</code>.
*
* <p>If the parameter data was sent in the request body, such as occurs
* with an HTTP POST request, then reading the body directly via {@link
* #getInputStream} or {@link #getReader} can interfere
* with the execution of this method.
*
* @param name a <code>String</code> specifying the name of the parameter
*
* @return a <code>String</code> representing the single value of
* the parameter
*
* @see #getParameterValues
*/
public String getParameter(String name);
对这个方法,还是非常有值得注意的地方。
第一:使用这个方法的时候,你需要确保参数只有一个值,即key只有一个value,那么你可能会问,如果一个key有多个value怎么样呢?答案是,只获取第一个返回。没有的话返回null。
第二:如果一个key有多个value,怎么正确获取参数呢?答案是调用本接口的另外一个方法public String[] getParameterValues(String name);
第三:如果通过getInputStream()方法或者getReader()方法获取请求体中的数据,则可能对此方法的执行造成影响,具体什么影响,可能的影响,我们后边聊。
ServletInputStream getInputStream()
/**
* Retrieves the body of the request as binary data using
* a {@link ServletInputStream}. Either this method or
* {@link #getReader} may be called to read the body, not both.
*
* @return a {@link ServletInputStream} object containing
* the body of the request
*
* @exception IllegalStateException if the {@link #getReader} method
* has already been called for this request
*
* @exception IOException if an input or output exception occurred
*/
public ServletInputStream getInputStream() throws IOException;
获取请求体内容对应的二进制数据流,这个方法和接下来将要谈到的getReader()方法不能同时存在。
BufferedReader getReader()
/**
* Retrieves the body of the request as character data using
* a <code>BufferedReader</code>. The reader translates the character
* data according to the character encoding used on the body.
* Either this method or {@link #getInputStream} may be called to read the
* body, not both.
*
* @return a <code>BufferedReader</code> containing the body of the request
*
* @exception UnsupportedEncodingException if the character set encoding
* used is not supported and the text cannot be decoded
*
* @exception IllegalStateException if {@link #getInputStream} method
* has been called on this request
*
* @exception IOException if an input or output exception occurred
*
* @see #getInputStream
*/
public BufferedReader getReader() throws IOException;
获取请求体内容对应的字符数据BufferedReader,获取字符数据的时候,编码集按照setCharacterEncoding()设定来进行编码,并且,此方法和上面的getInputStream()只能调用一个,不能同时存在。
getInputStream() 和getReader()不能在一个逻辑里同时调用的原因分析:
前面有提到,post请求,会将请求体以输入流的方式封装在HttpServletRequest中。输入流InputStream是一种数据传输通道,本身不存储数据,只能被读取一次,不可来回读。因此,这就是两个方法不可以同时调用的原因。并且,在multipart/form-data媒体类型的请求中,HttpServletRequest被包装为MultipartHttpServletRequest类型,在用MultipartResolver包装的过程中,HttpServletRequest对象持有的请求体对应的输入流已经被打开,所以,在真实类型是MultipartHttpServletRequest的时候,如果调用getInputStream() 或getReader()方法,都会抛出IllegalStateException异常错误。
参考文献:
- https://www.cnblogs.com/pascall/p/10271690.html
- https://blog.csdn.net/shuangmulin45/article/details/78083992
- https://blog.csdn.net/qq_38082304/article/details/101430827
- https://www.cnblogs.com/h-c-g/p/11002380.html