Spring/Boot/Cloud系列知识:SpringMVC 传参详解(上)
Posted 说好不能打脸
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring/Boot/Cloud系列知识:SpringMVC 传参详解(上)相关的知识,希望对你有一定的参考价值。
本文所述内容适用于Spring Boot 2.1.5(Spring 5.1.7)及以上版本。
1、HTTP请求参数传入的常见情况
SpringMVC组件接收参数值的方式,根据HTTP/HTTPS请求信息格式的不同而不同。而HTTP/HTTPS请求通常的值传入方式包括以下几种(本文不涉及介绍HTTP协议的基本结构,并且会默认读者已知晓这些结构):
1.1、通过URL结构的Query部分进行传递
以下为URL的标准格式:
protocol: // hostname [:port] / path / [;parameters][?query]#fragment
通过URL结构的Query部分传递参数,就是通过以上部分中“?”符号和“#”符合之间的内容进行参数值传递,以下示例均是采用这样的方式进行参数传递:
# 传递的参数为 tstatus=0
http://localhost:8081/XXXXXX/findByConditions?tstatus=0
# 传递的参数为 tstatus=0&tstatus=1,这两个名称一样的参数,服务器端都可以接收到(以数组的方式)
http://localhost:8081/XXXXXXX?tstatus=0&tstatus=1
# 传递的参数为 key1=value1&key12=value2
http://localhost:8081/somepath?key1=value1&key12=value2
以下示例为Method-Type为Get类型时,使用URL的Query部分进行参数传递的HTTP请求信息(部分示例):
GET /v1/xxxxxxx?tenantCode=test_tenant HTTP/1.1
Connection: keep-alive
Referer: https://XXXXXXX
......
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: HWWAFSESID=2efaf73ff736667445; HWWAFSESTIME=1620642560190;
......
当然,后端服务除了可以接收URL的Query部分以外,实际上还可以接收到直接请求的URL内容中任意部分的内容(请注意各种代理程序对URL地址的过滤、重写和转发),只是这个内容不属于本文的主要内容,所以这里就不进行展开说明了。
1.2、通过HTTP协议的Body部分进行传递
除了通过URL的Query部分进行参数传递外,还可以通过HTTP/HTTPS请求的Body部分进行参数传递,而后者是Web开发人员在实际工作中最常使用的传值方式,也是本文重点讨论的内容。实际上这种传参方式主要由HTTP协议Body部分的描述方式决定,常见场景如下:
- 使用application/x-www-form-urlencoded格式描述的Body结构进行参数传递
这种方式实际上就是传统意义上所称的提交表单传参(不带文件附件)的方式。通过各种HTTP调试工具,我们能够观察到类似如下传参内容:
.......
POST http://XXXXXXXX HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
......
username=CkfFGDR%2BAMG0HZUblcS5Aw%3D%3D&password=EvM0r1MMhMjRtlj%2BnvqhOQ%3D%3D
......
以上示例内容通过HTTP请求的Body部分传递了两个参数(并使用Base64进行值内容编码),这两个参数名为username和password,后文将介绍服务端如何进行这两个参数值的接收。
- 使用multipart/form-data格式描述的Body结构进行参数传递
这实际上也是传统意义上所称的页面表单传参(带文件附件)的方式。通过各种HTTP调用工具,我们能够观察到类似如下的传参内容:
POST /XXXXX/yyyyyy HTTP/1.1
Connection: keep-alive
Content-Length: 282053
......
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXszL5fnKB39bmMSS
......
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
......
------WebKitFormBoundaryXszL5fnKB39bmMSS
Content-Disposition: form-data; name="file"; filename="无标题.png"
Content-Type: image/png
-- 文件内容(不同的文件内容将呈现不同的显示效果) --
------WebKitFormBoundaryXszL5fnKB39bmMSS--
....
以上示例内容比较多,这里只是截取了其中一部分进行展示。multipart/form-data信息格式的实质是多区块表单,它通过一个定义的分隔符将HTTP信息的Body部分分为若干个区域,并在这些区域描述不同的内容。这也是为什么multipart/form-data信息格式能够同时上传多个文件的原因——不同文件被使用分隔符由Body部分的不同内容块进行描述。
请注意示例内容中 “----WebKitFormBoundaryXszL5fnKB39bmMSS” 就是对分隔符的申明,后续Body部分的内容就会使用这个分隔符进行分割,以便保证提交的参数值和提交的附件文件内容(二进制编码)不会混淆。分隔符是客户端按照HTTP协议规范自动生成的,以便保证分隔符本身和提交的Body内容不会重复。
- 使用application/json格式描述的Body结构
处理以上两种我们传统意义上所称的表单参数外(分为带附件和不带附件),现在越来越多的应用服务还使用JSON结构借助Body部分进行内容提交。这种通过application/json信息格式描述的Body结构,通过各种HTTP调试工具,我们能够观察到类似如下的传参内容:
POST 8081/v1/xxxxxx
Content-Type: application/json
......
{ "userName":"username1", "account":"yinwenjie", "phone":18108285502}
这里特别说明一下关于Method-Type为GET方式时,不应使用HTTP请求的Body部分进行参数传递的问题。这涉及到一个兼容性的历史原因。实际上,在HTTP/1.1协议中并没有严格禁止在GET方式的请求时,使用HTTP请求的Body部分进行内容传递,但是在早期HTTP协议中明确不建议GET方式的请求携带Body部分。
所以各种浏览器、调试工具、HTTP代理工具的早期版本中一般都不支持甚至会明确忽略Get方式请求的Body部分(例如大家常用的Postman工具的早期版本就不支持Get方式的HTTP请求携带Body部分)。这就导致正式生产环境中如果使用Get方式的HTTP请求携带Body部分,很有可能经过各种请求转发后,Body部分内容无故消失而且很难排查。
1.3、通过HTTP协议的Head部分进行传递
除了通过HTTP协议的URL部分、Body部分取得参数以外,HTTP协议的Head部分也是开发人员取得参数的重要位置。而Head部分携带的参数主要可分为以下几种场景:
- 由HTTP协议Head部分特定的属性携带参数:
HTTP协议在Head部分定义了许多特定意义属性,这些属性明确写在HTTP协议规范中而且对HTTP请求的性质起着至关重要的作用,有一些关键属性是开发人员必须牢记的。例如(只是部分举例):
- Cache-Control: 指定请求和响应遵循的缓存机制,例如当该属性的值为no-cache是,表示请求或响应消息不能缓存。
- Content-Encoding: web服务器支持的返回内容编码类型。
- Content-Type: 标识HTTP请求或者响应的Body部分,采用什么样的信息格式进行描述。前文介绍的“application/x-www-form-urlencoded”、“multipart/form-data”、“application/json”都属于信息格式。
- Set-Cookie: 在响应信息中,如果服务器需要浏览器设置一个 Cookie,则会返回该属性内容。
- Referer: referer信息描述了本次HTTP请求的最初源头地址。注意这个源头地址并非IP地址和端口,而是
请注意,Head部分的特定属性和属性值,有一部分是HTTP-Request(请求)特有的,有一部分是HTTP-Response(响应)特有的,有一部分是可以共通使用的。读者具体可参见HTTP/1.1官方协议文档(https://datatracker.ietf.org/doc/rfc2616/)。
- 由Head部分对cookies信息进行描述:
这里特别说明一下Head部分如何进行cookies信息的操作,由于HTTP请求是无状态的,如果需要将多个HTTP请求关联起来让它们产生上下文联系,就需要在每次HTTP请求时传递一些状态信息。这些状态信息一般来说就通过Head部分Cookie属性进行传递。
对cookies信息的操作主要分为对cookies信息的查询和修改。HTTP请求信息将可以携带客户端的cookies信息提交到服务器端,HTTP响应信息可以根据服务器端的要求修改记录在客户端上的cookies信息。对客户端上cookies信息的修改,主要有HTTP响应信息中的Set-Cookie属性完成。对于Cookie属性的格式规范,读者可以参看一些第三方文档。
- 由开发人员自定义Head部分的属性携带的参数:
当然,HTTP协议的Head部分除了那些由协议本身规定的特有属性外,还允许开发者自行定义相关属性,这种方式也是一种非常重要的参数传递方式,一般用于一些应用程序内部特异性要求的传参场景,例如保证特定用户多次HTTP请求之间的状态性(HTTP本身是无状态的,要保证多次HTTP请求的相关性,就需要依靠各种参数记录一些状态值)。如下所示的HTTP请求中,开发人员使用Head部分自定义的tenant属性,记录“租户”信息:
POST http://XXXXXXXX HTTP/1.1
tenant: eyJnYXRld2F5SXAiOiIxMzkuOS4yMjcuMTMiLCJnYXRld2F5UG9sIjp0cnVlfV0sInRlbmFudE5hbWUiOiLkuInlhagifQ
......
Content-Type: application/json
cache-control: no-cache
......
通过HTTP协议的Head部分携带自定义参数并不是本文推荐的方式,也不是本文重点讨论的场景,如果读者目前不清楚怎样取得这些参数值,那么就只需要知道通过servlet提供的HttpServletRequest对象的操作功能,开发人员可以从Head部分或者向Head部分写入各种内容(后文还会说明相关内容)。另外,通过Head进行参数传递时也需要注意安全性问题,例如是否允许这些属性在跨域场景中被传递。
1.4、其它传入方式
当然,由于HTTP协议的Body部分支持多种格式描述,例如text/xml、text/html,而这些特定的描述格式还适用于其它由HTTP协议所承载的功能,例如XML-RPC、WebSocket等等。本专题的后续内容将涉及这些内容的讨论。
2、使用Spring MVC接受各种请求传参
在介绍完HTTP请求的请求者有哪些手段进行参数传递后,本文再介绍作为HTTP请求的接收者有哪些手段进行参数的接收。请注意,根据请求者使用的请求场景不同,接收端对于参数的接收手段也不一样。
2.1、通过HttpServletRequest对象接受请求传参
SpringMVC组件,针对客户端所有的参数传递场景,都可以通过HttpServletRequest对象进行接受。其中又以getAttribute(String)方法和getParameter(String)方法最为常用。
两者的区别实际上还是比较大的,getAttribute(String)方法主要是从本次请求的Servlet容器中获取数据,Servlet容器保证了本次请求所涉及的多个服务端处理逻辑的信息可以互通,例如开发人员可以在Filter中通过setAttribute(String , String)方法向Servlet容器设置一个键值信息,并且在后续的Filter中或者后续的Controller中,再通过getAttribute(String)方法从Servlet容器中取出这个键值信息。
Servlet容器会在服务器端接收到本次请求后被初始化,其容器内部会写入一些初始化的attribute信息,例如可以直接使用“org.springframework.session.SessionRepository.CURRENT_SESSION”的Key信息,取得HttpSession对象。
getParameter(String)方法可以取出当次请求的URL地址中以Query部分描述的参数信息、Body信息中以表单形式描述的参数信息,且getParameter(String)方法只能返回字符串对象java.lang.String。示例代码如下:
// ...... 省略不重要的代码
@PostMapping("find")
public String find(HttpServletRequest request) {
// 可以直接从Servelt容器中取得session对象
// 其效果与request.getSession()相同(更推荐后一方式)
HttpSession result = (HttpSession)request.getAttribute("org.springframework.session.SessionRepository.CURRENT_SESSION");
result = request.getSession();
// 以下方式可以取得的query部分的参数,如/xxxx?test=yinwenjie
// 或者表单中名为test的参数值
String test = request.getParameter("test");
test = request.getParameter("test");
return "3456789";
}
// ......
那么如果HTTP请求中存在多个名称相同的参数,这时候服务端该怎么接收呢?例如以下URL请求信息:
/xxxxxx/find?test=valueA&test=valueB&test=valueC
又或者以表单形式提交的参数中,存在多个参数名相同的参数:…
POST http://XXXXXXXX HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
…
test=valueA&test=valueB&test=valueC
…
这时,可以通过getParameterValues(String name)方法取得参数值的数组,且这个数组中的值的排列顺序和HTTP请求中的参数顺序相同。示例代码如下:
// ......
@PostMapping("find")
public String find(HttpServletRequest request) {
// 可以以数组方式返回多个参数名相同的传参
String[] tests = request.getParameterValues("test");
return "something";
}
// ......
实际上在HTTP请求到达服务端后各种参数就已经装载完毕,开发人员可以通过getAttributeNames()方法或者getParameterNames()方法分别获取servlet容器中的要素信息,以及url的query部分和表单部分携带的要素信息。
如果示例代码如下:
// ......
// 获得servlet容器中所有的要素信息
Enumeration<String> attributeNames = request.getAttributeNames();
while (attributeNames.hasMoreElements()) {
String attributeName = attributeNames.nextElement();
System.out.println("attributeName = " + attributeName);
}
// 获得所有url携带的和表单提交的要素信息
Enumeration<String> parameterNames = request.getParameterNames();
while(parameterNames.hasMoreElements()) {
String parameterName = parameterNames.nextElement();
System.out.println("parameterName = " + parameterName);
}
// ......
当然配合使用的常用的方法还包括:
-
getParameterValues(String): 该方法可以获取指定参数名的传参值,请注意该方法返回的是一个数组,即使调用者只传递了一个参数,返回的也是一个数组(数组中只有一个元素)。
-
getParameterMap(): 该方法可以返回调用者通过URL的Query部分和提交表单的字段属性部分中所有K-V形式传递的参数值,其Key值就是参数名,其Value值也是一个字符串数组(即使调用者只传递了一个参数)。
2.2、通过@RequestParam和@RequestAttribute注解接收请求传参
上一小节介绍的直接通过写代码的获取参数传值的方式,是非常浪费体力的也不具备任何灵活性可言。在SpringMVC中提供了简便的注解方式,帮助开发人员获取传参信息。有两个注解:@RequestParam注解的使用效果类似于getParameter(String)方法或者getParameterValues(String)方法的调用效果,这主要看开发人员希望注解接收的是字符串还是字符串数组;@RequestAttribute注解的使用效果类似于getParameterValues(String)方法的调用效果。示例使用方法如下所示:
@PostMapping("find")
public void find(HttpServletRequest request ,
@RequestParam("keys") String[] values ,
@RequestParam("key") String value ,
@RequestAttribute("org.springframework.session.SessionRepository.CURRENT_SESSION") HttpSession session) {
// ..... 你的处理逻辑过程
}
========(接下篇)
以上是关于Spring/Boot/Cloud系列知识:SpringMVC 传参详解(上)的主要内容,如果未能解决你的问题,请参考以下文章
Spring/Boot/Cloud系列知识:SpringMVC 传参详解(下)
Spring/Boot/Cloud系列知识:SpringMVC 传参详解(下)
Spring/Boot/Cloud系列知识:SpringMVC 传参详解(下)
Spring/Boot/Cloud系列知识:SpringMVC进行HTTP信息接收和发送的过程