Controller控制器

SpringMVC是基于JavaEE Servlet规范封装的Web表现层框架。我们使用SpringMVC框架时,不需要再编写Servlet相关的代码,只需要定义Controller(控制器)并注册到Spring容器即可。实际开发中,通常每个Controller对应一组业务功能相关的请求,每个Controller方法对应一个具体的HTTP请求。当用户的浏览器对资源发起请求后,DispatcherServlet首先收到请求,然后就会从Spring容器中获取并调用Controller组件,最后匹配对应的方法进行具体处理,整个流程如下图所示。

定义Controller

SpringMVC中,我们需要使用注解标注来声明一个类是控制器。

@Controller:定义一个控制器,常用于传统MVC开发模式

@RestController:定义一个控制器,和上面区别是它相当于给每个请求处理方法标注了@ResponseBody,返回的内容会自动以JSON序列化并输出,常用于现代前后端分离开发模式,服务端只提供JSON接口的情况

默认情况下,SpringMVC的控制器是单例的,因此你不能用类成员变量记录请求或会话作用域的信息。

下面是控制器类写法的例子。

@Controller
public class IndexController {
    // ... 具体Controller类逻辑
}

除了使用注解定义Controller,SpringMVC也支持在Spring配置文件中使用XML方式声明控制器类,但这种方式太过麻烦早已经不再使用,后文将不再涉及此种方式。

请求处理

请求路径匹配

Controller中,通过@RequestMapping注解匹配请求对应的控制器和方法。该注解可以标注在控制器类和方法上,下面是一个例子。

@Controller
@RequestMapping(value = "/index")
public class IndexController {

    @RequestMapping(value = "/demo", method = RequestMethod.GET)
    public String demo() {
        // ... 具体业务逻辑
    }
}
  • value:匹配的路径,支持数组匹配多个路径
  • method:HTTP请求方法,如果不指定,那么代表GET、POST均支持,建议写明

如上述写法,用户请求/index/demo路径时,SpringMVC就会自动匹配并获取IndexController这个Bean,并调用demo方法。

除了@RequestMapping,SpringMVC还提供了@GetMapping@PostMapping等注解,相当于已经指定好了method,实际开发中推荐使用后者。

接收URL参数

Controller接收基本GET请求参数非常简单,例如请求路径为GET /demo?id=1,直接在控制器方法上声明该参数,SpringMVC会自动进行参数注入。

@GetMapping(value = "/demo")
public String demo(@RequestParam(value = "id", required = false) Integer id) {
    // ... 具体业务逻辑
}
  • value:请求参数名
  • required:是否为必须参数,如果必须参数未传,SpringMVC会返回HTTP 400错误

代码中@RequestParam注解不是必须的,默认value值为参数名,requiredtrue

除了URL参数,SpringMVC也支持路径参数,例子如下。

@GetMapping(value = "/demo/{id}")
public String demo(@PathVariable Integer id) {
    // ... 具体业务逻辑
}

路径参数需要在@RequestMapping注解中用{}标出,并与方法参数名对应,方法参数需要使用@PathVariable注解标注。

接收表单请求

POST请求的表单格式常用的有application/x-www-form-urlencodedmultipart/form-data两种,在SpringMVC中处理方式没有区别。我们只需要指定一个DTO类来接收。

@PostMapping(value = "/demo")
public String demo(LoginFormBean loginFormBean) {
    // ... 具体业务逻辑
}

SpringMVC也对JSR303验证注解进行了集成,我们如果需要验证请求表单的参数,直接使用相关注解即可。

@PostMapping(value = "/demo")
public String demo(@Valid LoginFormBean loginFormBean, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        // ... 表单参数错误处理逻辑
    }
    // ... 具体业务逻辑
}

代码中,LoginFormBean内部标注了相关验证注解,方法参数上标注了@Valid,这样请求调用控制器方法前,SpringMVC会对参数进行验证,如果有错误则自动注入bindingResult参数,我们对其判断即可。

注:验证功能需要引入相关Maven依赖。

接收JSON请求体

AJAX请求一般使用JSON格式请求体,和接收表单请求类似,JSON请求体也可以直接使用一个DTO类来接收。对于SpringBoot工程,实际上已默认集成了Jackson库解析JSON请求,但要注意的是参数上需要标注@RequestBody,下面是一个例子。

@PostMapping(value = "/demo")
public String demo(@RequestBody @Valid LoginFormBean loginFormBean, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        // ... 表单参数错误处理逻辑
    }
    // ... 具体业务逻辑
}

同样的,参数也支持@Valid验证注解。

接收文件上传请求

文件上传比较常用的方式为multipart/form-data表单格式,文件对应的表单字段则是一个较大的二进制数据,一般都通过数据流读取。SpringMVC内置了Apache的commons-fileupload组件,能够很方便的处理上传请求。处理文件上传的具体代码比较简单,直接通过MultipartFile类型的参数接收即可。

@PostMapping(value = "/demo")
public String demo(@RequestParam(value = "file") MultipartFile file) {
    // ... 具体业务逻辑
}

获取HTTP请求头

SpringMVC中获取请求头有两种方式,比较简洁的方法是使用@RequestHeader注解,另一种方式是从自动注入的HttpServletRequest对象中获取。

@RequestHeader注解能够直接注入请求头作为参数,下面例子代码中,我们从请求头中获取了名为token的参数。

@GetMapping(value = "/demo")
public String demo(@RequestHeader(value = "token") String token) {
    // ... 具体业务逻辑
}

@RequestHeader也具有required属性,默认为true,如果不满足会返回HTTP 400错误。

另一种方式是通过注入HttpServletRequest对象获取,这个对象我们都比较熟悉了,它就是Servlet API中的请求对象。

@GetMapping(value = "/demo")
public String demo(HttpServletRequest request) {
    // ... 具体业务逻辑
    String token = request.getHeader("token");
}

获取Cookie

Cookie和请求头处理类似,也支持两种方式,通过@CookieValue注解获取,或从注入的HttpServletRequest对象中获取。

@GetMapping(value = "/demo")
public String demo(@CookieValue(value = "token") String token) {
    // ... 具体业务逻辑
}

代码比较简单,这里就不赘述了。

响应处理

响应JSON数据

对于响应JSON数据,我们需要在Controller类上标注@RestController或在方法上标注@ResponseBody,比较推荐的用法是前者。

@RestController
@RequestMapping(value = "index")
public class IndexController {

    @GetMapping(value = "/demo")
    public ApiResult demo() {
        return ApiResult.success();
    }
}

响应二进制数据

响应二进制数据一般用于文件下载的场景,文件下载通常要和Content-DipositionContent-Type响应头配合使用,下面是一个例子。

@RestController
@RequestMapping(value = "index")
public class IndexController {

    @GetMapping(value = "/demo")
    public ResponseEntity<byte[]> demo() {
        // 模拟响应的二进制数据
        String str = "Hello, world!";
        byte[] data = str.getBytes();
        // 响应头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentDispositionFormData("attachment", "1.txt");
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        // 返回响应
        return new ResponseEntity<>(data, headers, HttpStatus.OK);
    }
}

SpringMVC对响应体进行了一个封装,即ResponseEntity对象,我们对其指定输出的二进制数据、响应头和相应状态码,就能实现任意数据的响应了。

除了使用ResponseEntity对象,我们也可以直接使用依赖注入的HttpServletResponse对象,获取其输出流并写入数据。这种方式在老版本SpringMVC工程中使用也比较多,具体可以参考Servlet相关章节。

响应模板视图

对于SpringBoot工程,模板引擎一般使用Thymeleaf,这需要引入相关的起步依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

返回模板视图非常简单,控制器方法直接返回模板路径即可。

@Controller
@RequestMapping(value = "index")
public class IndexController {

    @GetMapping(value = "/demo")
    public String demo(Model model) {
        // 向模板传递数据
        model.addAttribute("username", "Alice");
        // 响应模板在resources/templates/index.html
        return "index";
    }
}

注意代码中,我们在方法参数中注入了一个Model对象,它的用法类似一个Map,用来向模板引擎传递数据。

模板中直接取用该数据即可。

<!doctype html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Demo</title>
</head>
<body>
    <div th:text="${username}"></div>
</body>
</html>

有关模板引擎的详细使用方法将在其他章节详细介绍。

注:使用后端模板引擎输出HTML页面已经属于过时的技术了(尤其大型项目常用的Java领域),现在主流的前后端分离开发中,前端工程会独立出去并由静态文件服务器单独提供,而后端只负责输出接口数据。

ControllerAdvice控制器切面

在Spring核心功能相关章节中我们学习过AOP的概念,实际上SpringMVC有一个地方迫切需要AOP切面功能,就是统一异常处理。

考虑这样的场景:我们的代码抛出运行时异常,但此时我们不希望SpringMVC展示出五花八门的错误页面,我们希望返回一个全局通用的错误信息,此时就可以用AOP切面统一拦截控制器方法的执行。下面代码我们实现返回一个通用错误JSON信息。

package com.gacfox.demo.config;

import com.gacfox.demo.model.ApiResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class GlobalExceptionHandler {
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public ApiResult<?> handleException() {
        return ApiResult.failure();
    }
}

如果按照正常访问流程走到的控制器方法抛出异常,那么该切面就会生效,并返回通用错误JSON信息。

除了全局错误信息拦截,@ControllerAdvice切面也支持全局数据预处理、全局数据绑定处理,不过正常使用基本不会用到,这里就不多介绍了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。