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信息接收和发送的过程

Spring/Boot/Cloud系列知识:SpringMVC进行HTTP信息接收和发送的过程

Spring/Boot/Cloud系列知识:SpringMVC进行HTTP信息接收和发送的过程