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

实例:
http请求
常见的媒体格式类型如下:

  • 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异常错误。

参考文献:

  1. https://www.cnblogs.com/pascall/p/10271690.html
  2. https://blog.csdn.net/shuangmulin45/article/details/78083992
  3. https://blog.csdn.net/qq_38082304/article/details/101430827
  4. https://www.cnblogs.com/h-c-g/p/11002380.html
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值