关于 Spring MVC 处理 form 和文件的实例
关于 MediaType
在此之前,得先了解下 HTTP 的 MediaType,MediaType 用来标识内容,即请求体和响应体的编码格式,HTTP 请求的 Header 中的 Accept 和 Content-Type,HTTP 响应的 Header 中的 Content-Type 都使用 MediaType 来标识,其中 Accept 表示请求者希望响应者发送的数据的格式,Content-Type 代表请求体和响应体的内容格式,一些最常见的格式见下。
GET 请求不应当携带请求体!
MediaType | 描述 |
---|---|
text/html | HTML 文本 |
text/plain | 纯文本,空格转换为+ |
application/json | 序列化的 JSON 格式 |
application/x-www-form-urlencoded | 键值对形式 |
multipart/form-data | 似乎啥都有 |
MediaType 包含三个部分——type,subtype 和 charset,其中 type 是/前的值,subtype 是/后的值,比如对于 text/html,它的全格式是
text/html; charset=UTF-8
,它的 type 是 text,subtype 是 html。
在这里,application/x-www-form-urlencoded
和multipart/form-data
是这篇笔记的主角。
x-www-form-urlencoded
application/x-www-form-urlencoded 是最经典的格式了,它是键值对集合,其中键和值使用=分隔,键值对之间使用&分隔。容易意识到,URL 中?后面的部分就是这种格式,而实际上确实如此。
application/x-www-form-urlencoded 是浏览器原生发送表单的默认格式,比如当前端编写这样的表单时,默认发送的就是这样的格式。application/x-www-form-urlencoded 对于 GET 请求和 POST 请求,其行为不一样,处理方法也不一样。
form 元素有两个属性,method 和 enctype 需要配置,method 默认是 get,enctype 默认是 application/x-www-form-urlencoded,但对于 GET 请求,enctype 是无效的,
POST
考虑下面这个表单。
1 |
|
点击按钮后,会发出这样的请求,其中表单数据通过请求体去发送了,因此有 Content-Type。
1 |
|
相应接口的编写有这几种方式,可以看到这里一直都使用 RequestParam 去接受参数,这可能有些反直觉,因为在之前的实践中,我是认为 RequestParam 是专门用于获取 ? 后面的 kv 对的。
1 |
|
GET
然后看 get,考虑下面的这个表单。
1 |
|
点击 submit 后,会发出这个请求:
1 |
|
需注意:请求中没有 Content-type 属性,该表单的数据通过 url 传递!
因此,对于使用 get 请求的表单,相应的接口需这样编写:
1 |
|
可见,其编写方式和暴露一个普通的 GET 接口别无二致。
总结
对于 x-www-form-urlencoded,无论是 GET 请求还是 POST 请求,都使用 RequestParam 去拿到参数,最好应显式指定要接受的 MediaType。
form-data
form-data 也是键值对集合,但其相较于 x-www-form-urlencoded,能够方便地传递二进制数据(特别是文件),下面的实例不考虑 GET 请求。
考虑下面这样的表单。
1 |
|
点击 submit 按钮时,发送的请求的请求头如下:
1 |
|
请求体如下:
1 |
|
处理其的接口如下:
1 |
|
可见,这里使用 RequestPart 去拿到值,文件使用 MultipartFile 去拿。但仍旧可以使用 RequestParam,下面对注释的摘录介绍了 Requestparam 和 RequestPart 的区别:
In Spring MVC, “request parameters” map to query parameters, form data, and parts in multipart requests. This is because the Servlet API combines query parameters and form data into a single map called “parameters”, and that includes automatic parsing of the request body.
Note that @RequestParam annotation can also be used to associate the part of a “multipart/form-data” request with a method argument supporting the same method argument types. The main difference is that when the method argument is not a String or raw MultipartFile / Part, @RequestParam relies on type conversion via a registered Converter or PropertyEditor while RequestPart relies on HttpMessageConverters taking into consideration the ‘Content-Type’ header of the request part. RequestParam is likely to be used with name-value form fields while RequestPart is likely to be used with parts containing more complex content e.g. JSON, XML).
一些更复杂的使用需参考官方文档(tmd 这里全是宝啊!),有两点需注意:
- form-data 和 x-www-form-urlencoded 都允许重名的参数,可以通过 List 去全部获取到(需要使用 RequestParam)
- 多个文件能在同一个参数里,同一个参数的所有文件通过
List<MultipartFile>
去获取到,所有文件通过MultiValueMap<String, MultipartFile>
去获取到(需要用 RequestParam)
另外,对于这两种表单类型,想使用自定义类型(包括 Map)去接受参数时 Spring 会抱怨no matching editors or conversion strategy found
,需要去在控制器中去自定义相应带 InitBinder 注解的方法并(定义和)注册相应 PropertyEditor,下面的代码注册了一个从字符串到 SomeBean 的 Binder,这个配置仅用于获取 query param 或 form data,使用 RequestBody 时不会被影响。
1 |
|
只要定义了类型 T 的PropertyEditor
,则对List<T>
Spring MVC 也能处理。
关于文件的下载
前端上传文件说到底只有通过表单以 MultipartFile 上传这一种方式,而下载文件的方法也是需要学习的,Spring MVC 向前端返回文件有数种方式,这里都列举一下。
HttpServletResponse
直接把 InputStream 写入 HttpServletResponse 的流是使用最多,但最不优雅的方式,如果要使用它的话,就必须得把 HttpServletResponse 传递到服务层(如果保证控制器层不负责业务的话),这实在让人难以接受:
1 |
|
Resource,byte[]
org.springframework.core.io.Resource
可以用来返回文件,可能常用的实现包括ByteArrayResource
,ClassPathResource
,FileSystemResource
,InputStreamResource
。虽然是题外话,但 Resource 类读取文件非常方便,应当代替直接使用 ClassLoader 去读取文件的行为。
虽然
ClassPathResource
,FileSystemResource
能拿到文件名,但是 Spring 仍会将其当作text/html
来返回!
1 |
|
也可以直接返回byte[]
:
1 |
|
这两种都会默认按text/html
去返回,如果想要它们返回文件,则需要在注解上添加produces = MediaType.APPLICATION_OCTET_STREAM_VALUE
,这样虽然能让用户去下载到文件,但是文件名是无法修改的,这显然不太合适。
InputStream 作为返回值似乎是非法的。
ResponseEntity
要想自定义文件名,就需要能够对响应头进行更多自定义,如果不想注入 HttpResponse,就只能去返回一个 ResponseEntity 了,而 ResponseEntity 用于返回文件是轻而易举的:
1 |
|
我觉着这几乎是最友好的解决方案了,全程都没有使用魔法常量,并且符合通过返回值来回应前端请求这样的直觉,或许这就是最佳实践。
实际上可以再发散一下,操作文件总是和操作流有着密不可分的关系,Spring 对流作为请求体这种情景有很好的支持,包括但不限于异步等的支持,对其进行学习是有些必要的。
这里其实还有一个比较重要的问题:上面所介绍的方法在 OpenFeign 中可用吗?之后再说吧。
参考阅读
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!