本文的示例项目为。
这里的普通参数,指通过 GET 发送的 url 参数或者通过 POST 发送的在报文体中编码(form-data 或 x-www-form-urlencoded)的参数。
对于普通的查询参数,可以直接作为控制层方法的参数进行接收:
"/user")
(public class UserController {
private UserService userService;
"/save")
(
public String save(String name) {
System.out.println(name);
return String.format("{'result':true,'msg':'%s user saved.'}", name);
}
// ...
}
对应点的 API 工具调试信息可以查看 。
这里有一个问题,如果请求参数中包含中文,在服务端打印出的参数是乱码。
显然这里需要对参数进行转码,这需要我们添加一个 Servlet 过滤器:
public class ServletContainerInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
protected Filter[] getServletFilters() {
return new Filter[]{new CharacterEncodingFilter("UTF-8")};
}
}
CharacterEncodingFilter
这个过滤器可以帮助我们进行转码。现在通过 POST 方法发送的请求参数中的中文可以在服务端正常显示(GET 方法发送的参数依然是乱码)。
1.1.@RequestParam
默认情况下,如果请求参数名与控制层中的方法形参名称一致,Spring 会自动进行映射和绑定。但如果请求值不一致,就需要我们手动进行绑定:
"/hello")
(
public String hello(String name) {
String msg = String.format("hello, %s!", name);
System.out.println(msg);
return String.format("{'result':true,'msg':'%s'}", msg);
}
在服务端,要接收的请求参数名是name
,如果客户端发送的参数名不同,服务端就接收不到。
比如客户端的请求:http://localhost/user/hello?user=Tom
可以使用@RequestParam
注解解决这个问题:
"/hello")
(
public String hello( ("user") String name) {
// ...
}
1.2.组装对象
客户端传送过来的是一个个属性,我们可以选择将这些属性封装成对象进行接收。
比如请求是:http://localhost/user/save2?name=Tom&age=12
控制层:
private static class UserDTO{
private String name;
private String age;
}
"/save2")
(
public String save2(UserDTO dto) {
System.out.println(String.format("User %s will be saved.", dto));
return JSON.toJSONString(dto);
}
控制台输出:
User UserController.UserDTO(name=Tom, age=12) will be saved.
依然可以正常识别和接收参数,Spring 在这里很“智能”,如果控制层方法的形参是对象,它就会尝试将 HTTP 请求参数和对象属性进行匹配和绑定。
对于多层嵌套的对象,依然可以用普通参数传递和组装:
private static class UserDTO2{
private static class Address{
private String country;
private String province;
}
private String name;
private String age;
private Address address;
}
"/save3")
(
public String save3(UserDTO2 dto) {
System.out.println(String.format("User %s will be saved.", dto));
return JSON.toJSONString(dto);
}
请求:http://localhost/user/save3?name=Tom&age=17&address.country=china&address.province=beijin
1.3.数组
HTTP 请求发送的参数中的参数名是可以重复出现的,最常见的是用多个复选框(checkbox)提交表单。此时在服务端我们就需要用数组来接收参数。
假设请求为:http://localhost/user/hobbies?hobby=music&hobby=photo&hobby=travel
控制层:
"/hobbies")
(
public String hobbies(String[] hobby){
System.out.println(hobby);
return JSON.toJSONString(hobby);
}
1.4.List
相比数组,服务端更常见的是用 List
类型来接收序列值。上面的示例可以改写为:
"/hobbiesList")
(
public String hobbiesList(List hobby){
System.out.println(hobby);
return JSON.toJSONString(hobby);
}
请求:http://localhost/user/hobbiesList?hobby=music&hobby=travel&hobby=photo
但是实际测试会报错:
ava.lang.IllegalArgumentException: Cannot generate variable name for non-typed Collection parameter type ...
这是因为需要为List
指定泛型参数:
public String hobbiesList(List<String> hobby)
但依然会报错:
java.lang.NoSuchMethodException: java.util.List.<init>()
这是因为List
是一个引用类型(对象),Spring 对于引用类型,会认为你是要将多个普通参数组装成一个对象,所以会尝试用请求参数和对象属性进行匹配和绑定。显然这里我们不想让请求参数绑定到List
的属性上,而是想让List
作为一个接收多个同名参数的容器。
可以使用@RequestParam
解决这个问题:
"/hobbiesList")
(
public String hobbiesList( List<String> hobby){
System.out.println(hobby);
return JSON.toJSONString(hobby);
}
2.json 数据
2.1.数组
json 数据是通过请求体发送的,接收方式:
"/hobbiesJson")
(
public String hobbiesJson( List<String> hobbies){
System.out.println(hobbies);
return JSON.toJSONString(hobbies);
}
默认 Spring 没有开启对请求体中字符串的转换功能,需要在 Spring MVC 的配置类中开启:
public class SpringMvcConfig {
}
Spring MVC 默认使用 jackson 将请求体中的 json 格式的字符串转换为对象,所以需要添加 jackson 相关依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.0</version>
</dependency>
具体的接口调用示例可以参考。
2.2.POJO
更常见的是通过 json 直接传递一个实体类:
private static class UserDTO3{
private String name;
private Integer age;
}
"/save4")
(
public String saveUser4( UserDTO3 dto){
System.out.println(dto);
return JSON.toJSONString(dto);
}
接口调用示例可以参考。
2.3.List
当然也可以用 List 而非 数组的方式接收一个数组形式的 json 串:
"/save/many")
(
public String saveUsers( List<UserDTO3> users){
System.out.println(users);
return JSON.toJSONString(users);
}
接口调用示例可以参考。
3.日期参数
通常我们传输时间参数,格式都是类似于2011-10-01
或2011-10-01 15:01:01
这样的。
如果直接尝试在服务端用日期类型接收这样的参数:
"/time")
(
public String time( (pattern = "yyyy-MM-dd") LocalDate date,
pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time){
( System.out.println(date);
System.out.println(time);
return "";
}
会报错。
实际上 Spring 默认的确会尝试将字符串转换为我们期望的事件类型,但问题在于其默认格式并不是中文互联网常用的格式(对于日期,是yyyy/MM/dd
),所以就会转换出错。
这个问题可以用@DateTimeFormat
注解解决:
"/time")
(
public String time( (pattern = "yyyy-MM-dd") LocalDate date,
pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time){
( // ...
}
如果是 Spring Boot,可以修改 Spring 默认的处理的日期格式,具体可以参考。
4.响应
4.1.返回 json
之前让 HTTP 响应返回 json 对象,都是用 fastjson 编码实现的:
"/save/many")
(
public String saveUsers( List<UserDTO3> users){
System.out.println(users);
return JSON.toJSONString(users);
}
实际上 Spring 默认使用的 json 编码和解码工具是 jackson,只要我们整合了 jackson,Spring 就可以自动帮我们将返回的对象编码为 json 后添加到 HTTP 响应报文中返回:
"/save/many")
(
public List<UserDTO3> saveUsers( List<UserDTO3> users){
System.out.println(users);
return users;
}
4.2.页面
让 Spring 返回一个 Html 页面很简单:
"/helloPage")
(public String helloPage(){
return "index.jsp";
}
这里的实际请求路径是/user/helloPage
,所以 Spring 会寻找/user/index.jsp
,添加一个对应的 JSP 文件:
<html>
<body>
<h2>Hello World!</h2>
</body>
</html>
用浏览器请求就能看到效果。
4.3.HtppMessageConverter
就像之前展示的,用@ResponseBody
标记的控制层方法,Spring 会将返回值作为 HTTP 响应报文体。如果返回的类型不是 String 而是一个引用类型,Spring 会尝试将其转换为 json 字符串。
具体来说,这种转换是使用HttpMessageConverter<T>
接口实现的:
package org.springframework.http.converter;
// ...
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> var1, MediaType var2);
boolean canWrite(Class<?> var1, MediaType var2);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;
void write(T var1, MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}
这个接口有众多实现类可以完成具体转换:
比如如果项目添加了 Jackson 的支持,就可以用 Jackson 的相关实现类完成转换。
5.特殊对象
5.1.HttpServletRequest
虽然使用 SpringMVC 不会强制我们在控制层方法中添加HttpServletRequest
和HttpServletResponse
参数,但是我们依然可以使用这两个对象:
"/request")
(public void request(HttpServletRequest request){
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
System.out.println(headerName);
}
}
它们的用法与 Servlet 中的用法完全相同,这里不再赘述。
5.2.域属性
在多个控制层方法之间进行转发请求时,通常需要这样获取传输的域属性:
"/url1")
(public String url1(HttpServletRequest request){
request.setAttribute("msg", "hello");
// 转发到 url2
return "/user/url2";
}
"/url2")
(public String url2(HttpServletRequest request){
// 获取域中的属性
String msg = (String) request.getAttribute("msg");
System.out.println(msg);
return "/index.jsp";
}
在 SpingMVC 中,可以使用一个@RequestAttribute
注解方便地获取 Request 域的属性:
"/url3")
(public String url3(HttpServletRequest request){
request.setAttribute("msg", "hello");
// 转发到 url2
return "/user/url2";
}
"/url4")
(public String url4( ("msg") String msg){
// 获取域中的属性
System.out.println(msg);
return "/index.jsp";
}
这样做的好处是,不需要为了获取域属性注入一个HttpServletRequest
对象,且省去了手动类型转换。
5.3.请求头
同样,SpringMVC 提供一个方便的注解@RequestHeader
用于获取请求报文中的请求头:
"/header")
(public void header( ("Accept-Language") String al){
System.out.println(al);
}
控制台输出:
zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
需要注意的是,这里获取到的请求头的值是没有进行分割过的,比如上边的信息,实际上是由;
分隔的多个值,需要我们自行分割。
还可以用@ReuqestHeader
注解获取所有的请求头:
"/allHeaders")
(public void allHeaders( Map<String,String> headers){
headers.forEach((name,values)->{
System.out.printf("%s:%s%n", name, values);
});
}
控制台输出:
host:localhost connection:keep-alive pragma:no-cache cache-control:no-cache sec-ch-ua:"Microsoft Edge";v="117", "Not;A=Brand";v="8", "Chromium";v="117" sec-ch-ua-mobile:?0 sec-ch-ua-platform:"Windows" ...
5.4.Cookie
SpringMVC 提供注解@CookieValue
用于方便地获取 Cookie 的值:
"/cookie")
(public void cookie( ("JSESSIONID") String sessionId){
System.out.println(sessionId);
}
5.5.Session
SpringMVC 提供注解@SessionAttribute
用于获取指定的 Session 属性值:
"/session/read")
(
public String readSession( ("username") String username){
System.out.println(username);
return "";
}
6.上传文件
6.1.表单
前端页面通常使用表单提交的方式上传文件:
<%page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form enctype="multipart/form-data" action="/file/upload" method="post">
<input type="file" name="userUploadFile"/>
<input type="submit" value="提交"/>
</form>
</body>
</html>
需要注意的是,与普通的表单不同,上传文件的表单enctype
属性必须是multipart/form-data
,且method
是post
。
6.2.文件上传解析器
SpringMVC 默认不开启文件上传的解析器,需要手动开启:
public class SpringMvcConfig {
/**
* 文件上传解析器(Bean 名称必须是 multipartResolver)
* @return
*/
public MultipartResolver multipartResolver(){
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
// 默认文件编码
multipartResolver.setDefaultEncoding("UTF-8");
// 总上传文件的最大容量
multipartResolver.setMaxUploadSize(3145728);
// 单个上传文件的最大容量
multipartResolver.setMaxUploadSizePerFile(1048576);
// 上传文件缓存大小
multipartResolver.setMaxInMemorySize(1048576);
return multipartResolver;
}
}
需要注意的是,解析器 Bean 名称必须是multipartResolver
,底层按照名称获取 Bean。
6.3.依赖
这个解析器还依赖一个第三方包,需要添加依赖:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
6.4.保存上传文件
在控制层实现文件上传:
"/file")
(public class FileController {
private static final String PROJECT_HOME = System.getProperty("user.dir");
private static final String UPLOAD_DIR = PROJECT_HOME+"/upload";
static {
File dir = new File(UPLOAD_DIR);
if(!dir.exists()){
dir.mkdir();
}
}
"/upload")
( public String upload( MultipartFile userUploadFile) throws IOException {
InputStream inputStream = userUploadFile.getInputStream();
String originalFilename = userUploadFile.getOriginalFilename();
OutputStream outputStream = new FileOutputStream(UPLOAD_DIR+"/"+originalFilename);
IOUtils.copy(inputStream, outputStream);
return "redirect:/file/upload.jsp";
}
}
上传的文件会保存在 Web 应用执行时的目录下的/upload
目录中。
控制层方法参数@RequestBody MultipartFile userUploadFile
获取的对象就是上传的文件,参数名userUploadFile
对应页面表单中的<input type="file"/>
标签的name
属性。
将上传文件的输入流拷贝到输出流这里使用了工具类IOUtils
,该类属于commons-io
这个依赖,不过该依赖被包含在commons-fileupload
依赖中,所以不需要额外引用,直接可以使用。
The End,谢谢阅读。
本文的完整示例可以从获取。
文章评论