红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. SpringMVC 学习笔记
  3. 正文

SpringMVC 学习笔记2:请求与响应

2023年8月28日 1171点热度 0人点赞 0条评论

本文的示例项目为mvc-demo。

1.普通参数

这里的普通参数,指通过 GET 发送的 url 参数或者通过 POST 发送的在报文体中编码(form-data 或 x-www-form-urlencoded)的参数。

对于普通的查询参数,可以直接作为控制层方法的参数进行接收:

@Controller
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
​
    @RequestMapping("/save")
    @ResponseBody
    public String save(String name) {
        System.out.println(name);
        return String.format("{'result':true,'msg':'%s user saved.'}", name);
    }
    // ...
}

对应点的 API 工具调试信息可以查看 Apipost 接口文档。

这里有一个问题,如果请求参数中包含中文,在服务端打印出的参数是乱码。

显然这里需要对参数进行转码,这需要我们添加一个 Servlet 过滤器:

public class ServletContainerInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
    // ...
    @Override
    protected Filter[] getServletFilters() {
        return new Filter[]{new CharacterEncodingFilter("UTF-8")};
    }
}

CharacterEncodingFilter这个过滤器可以帮助我们进行转码。现在通过 POST 方法发送的请求参数中的中文可以在服务端正常显示(GET 方法发送的参数依然是乱码)。

1.1.@RequestParam

默认情况下,如果请求参数名与控制层中的方法形参名称一致,Spring 会自动进行映射和绑定。但如果请求值不一致,就需要我们手动进行绑定:

@RequestMapping("/hello")
@ResponseBody
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注解解决这个问题:

@RequestMapping("/hello")
@ResponseBody
public String hello(@RequestParam("user") String name) {
    // ...
}

1.2.组装对象

客户端传送过来的是一个个属性,我们可以选择将这些属性封装成对象进行接收。

比如请求是:http://localhost/user/save2?name=Tom&age=12

控制层:

@Data
private static class UserDTO{
    private String name;
    private String age;
}
​
@RequestMapping("/save2")
@ResponseBody
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 请求参数和对象属性进行匹配和绑定。

对于多层嵌套的对象,依然可以用普通参数传递和组装:

@Data
private static class UserDTO2{
    @Data
    private static class Address{
        private String country;
        private String province;
    }
    private String name;
    private String age;
    private Address address;
}
​
@RequestMapping("/save3")
@ResponseBody
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

控制层:

@RequestMapping("/hobbies")
@ResponseBody
public String hobbies(String[] hobby){
    System.out.println(hobby);
    return JSON.toJSONString(hobby);
}

1.4.List

相比数组,服务端更常见的是用 List 类型来接收序列值。上面的示例可以改写为:

@RequestMapping("/hobbiesList")
@ResponseBody
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解决这个问题:

@RequestMapping("/hobbiesList")
@ResponseBody
public String hobbiesList(@RequestParam List<String> hobby){
    System.out.println(hobby);
    return JSON.toJSONString(hobby);
}

2.json 数据

2.1.数组

json 数据是通过请求体发送的,接收方式:

@RequestMapping("/hobbiesJson")
@ResponseBody
public String hobbiesJson(@RequestBody List<String> hobbies){
    System.out.println(hobbies);
    return JSON.toJSONString(hobbies);
}

默认 Spring 没有开启对请求体中字符串的转换功能,需要在 Spring MVC 的配置类中开启:

@EnableWebMvc
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 直接传递一个实体类:

@Data
private static class UserDTO3{
    private String name;
    private Integer age;
}
​
@RequestMapping("/save4")
@ResponseBody
public String saveUser4(@RequestBody UserDTO3 dto){
    System.out.println(dto);
    return JSON.toJSONString(dto);
}

接口调用示例可以参考这里。

2.3.List

当然也可以用 List 而非 数组的方式接收一个数组形式的 json 串:

@RequestMapping("/save/many")
@ResponseBody
public String saveUsers(@RequestBody List<UserDTO3> users){
    System.out.println(users);
    return JSON.toJSONString(users);
}

接口调用示例可以参考这里。

3.日期参数

通常我们传输时间参数,格式都是类似于2011-10-01或2011-10-01 15:01:01这样的。

如果直接尝试在服务端用日期类型接收这样的参数:

@RequestMapping("/time")
@ResponseBody
public String time(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
                   @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time){
    System.out.println(date);
    System.out.println(time);
    return "";
}

会报错。

实际上 Spring 默认的确会尝试将字符串转换为我们期望的事件类型,但问题在于其默认格式并不是中文互联网常用的格式(对于日期,是yyyy/MM/dd),所以就会转换出错。

这个问题可以用@DateTimeFormat注解解决:

@RequestMapping("/time")
@ResponseBody
public String time(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
                   @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time){
    // ...
}

如果是 Spring Boot,可以修改 Spring 默认的处理的日期格式,具体可以参考这篇文章。

4.响应

4.1.返回 json

之前让 HTTP 响应返回 json 对象,都是用 fastjson 编码实现的:

@RequestMapping("/save/many")
@ResponseBody
public String saveUsers(@RequestBody List<UserDTO3> users){
    System.out.println(users);
    return JSON.toJSONString(users);
}

实际上 Spring 默认使用的 json 编码和解码工具是 jackson,只要我们整合了 jackson,Spring 就可以自动帮我们将返回的对象编码为 json 后添加到 HTTP 响应报文中返回:

@RequestMapping("/save/many")
@ResponseBody
public List<UserDTO3> saveUsers(@RequestBody List<UserDTO3> users){
    System.out.println(users);
    return users;
}

4.2.页面

让 Spring 返回一个 Html 页面很简单:

@RequestMapping("/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, @Nullable MediaType var2);
​
    boolean canWrite(Class<?> var1, @Nullable MediaType var2);
​
    List<MediaType> getSupportedMediaTypes();
​
    T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;
​
    void write(T var1, @Nullable MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}

这个接口有众多实现类可以完成具体转换:

image-20230828181341187

比如如果项目添加了 Jackson 的支持,就可以用 Jackson 的相关实现类完成转换。

5.特殊对象

5.1.HttpServletRequest

虽然使用 SpringMVC 不会强制我们在控制层方法中添加HttpServletRequest和HttpServletResponse参数,但是我们依然可以使用这两个对象:

@RequestMapping("/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.域属性

在多个控制层方法之间进行转发请求时,通常需要这样获取传输的域属性:

@RequestMapping("/url1")
public String url1(HttpServletRequest request){
    request.setAttribute("msg", "hello");
    // 转发到 url2
    return "/user/url2";
}
​
@RequestMapping("/url2")
public String url2(HttpServletRequest request){
    // 获取域中的属性
    String msg = (String) request.getAttribute("msg");
    System.out.println(msg);
    return "/index.jsp";
}

在 SpingMVC 中,可以使用一个@RequestAttribute注解方便地获取 Request 域的属性:

@RequestMapping("/url3")
public String url3(HttpServletRequest request){
    request.setAttribute("msg", "hello");
    // 转发到 url2
    return "/user/url2";
}
​
@RequestMapping("/url4")
public String url4(@RequestAttribute("msg") String msg){
    // 获取域中的属性
    System.out.println(msg);
    return "/index.jsp";
}

这样做的好处是,不需要为了获取域属性注入一个HttpServletRequest对象,且省去了手动类型转换。

5.3.请求头

同样,SpringMVC 提供一个方便的注解@RequestHeader用于获取请求报文中的请求头:

@GetMapping("/header")
public void header(@RequestHeader("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注解获取所有的请求头:

@GetMapping("/allHeaders")
public void allHeaders(@RequestHeader 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 的值:

@GetMapping("/cookie")
public void cookie(@CookieValue("JSESSIONID") String sessionId){
    System.out.println(sessionId);
}

5.5.Session

SpringMVC 提供注解@SessionAttribute 用于获取指定的 Session 属性值:

@GetMapping("/session/read")
@ResponseBody
public String readSession(@SessionAttribute("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
     */
    @Bean
    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.保存上传文件

在控制层实现文件上传:

@Controller
@RequestMapping("/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();
        }
    }
​
    @PostMapping("/upload")
    public String upload(@RequestBody MultipartFile userUploadFile) throws IOException {
        @Cleanup InputStream inputStream = userUploadFile.getInputStream();
        String originalFilename = userUploadFile.getOriginalFilename();
        @Cleanup 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,谢谢阅读。

本文的完整示例可以从这里获取。

7.参考资料

  • 从零开始 Spring Boot 24:处理时间 - 红茶的个人站点 (icexmoon.cn)

  • 从零开始 Spring Boot 29:类型转换 - 红茶的个人站点 (icexmoon.cn)

  • 黑马程序员SSM框架教程

  • 黑马程序员新版Spring零基础入门到精通

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: mvc spring 参数
最后更新:2023年9月21日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号