SpringMVC是基于Servlet规范封装的框架。我们使用SpringMVC框架时,不需要再编写Servlet相关的代码,只需要定义Controller并注册到Spring容器即可。
一个Controller对应一组请求,每一个Controller方法对应一个具体的HTTP请求。当用户的浏览器对某一资源发起请求后,DispatcherServlet首先收到请求,然后就会从Spring容器中获取并调用Controller组件,最后匹配对应的方法进行具体处理。流程如下图所示:
图片来自网络。
SpringMVC中,我们一般都是配置包扫描(参考前一篇笔记配置),并用注解标注在类上,来定义Controller:
@Controller
:定义一个控制器@RestController
:定义一个控制器,和上面区别是它相当于给每个请求处理方法标注了@ResponseBody
,返回的内容会自动以JSON序列化并输出例子:
@Controller
public class IndexController {
// ... 具体Controller类逻辑
}
另外一点要注意的是,SpringMVC的Controller默认为单例模式。
除此之外,SpringMVC也支持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
,推荐使用。
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
值为参数名,required
为true
。
除了URL参数,SpringMVC也支持路径参数,例子如下:
@GetMapping(value = "/demo/{id}")
public String demo(@PathVariable Integer id) {
// ... 具体业务逻辑
}
路径参数需要在@RequestMapping
注解中用{}
标出,并与方法参数名对应,方法参数需要使用@PathVariable
注解标注。
POST请求的表单格式常用的有application/x-www-form-urlencoded
、multipart/form-data
两种,在SpringMVC中处理方式没有区别。我们只需要指定一个JavaBean接收:
@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依赖。
AJAX请求一般使用JSON或XML格式请求体,和接收表单请求类似,JSON请求体也可以直接使用一个JavaBean来接收,SpringMVC能够集成Jackson
库解析请求,但要注意的是参数上需要标注@RequestBody
。
如下配置集成JSON自动解析到SpringMVC:
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="mappingJacksonHttpMessageConverter"/>
</list>
</property>
</bean>
<bean id="mappingJacksonHttpMessageConverter"
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</bean>
具体代码中,我们直接使用JavaBean接收请求对象即可:
@PostMapping(value = "/demo")
public String demo(@RequestBody @Valid LoginFormBean loginFormBean, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// ... 表单参数错误处理逻辑
}
// ... 具体业务逻辑
}
同样的,参数也支持@Valid
验证注解。
除了JSON格式,SpringMVC配合JAXB注解也支持对XML请求体的自动解析,用法与上面类似,这里就不多介绍了。
注:该功能需要引入对应Jackson或JAXB依赖。
文件上传比较常用的方式为multipart/form-data
表单格式,文件对应的表单字段则是一个较大的二进制数据,一般都通过数据流读取。SpringMVC内置了Apache的commons-fileupload
组件,能够很方便的处理上传请求。
这里要注意,我们需要为文件上传配置multipartResolver
:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>
处理文件上传的具体代码比较简单,直接通过MultipartFile
类型的参数接收即可:
@PostMapping(value = "/demo")
public String demo(@RequestParam(value = "file") MultipartFile file) {
// ... 具体业务逻辑
}
SpringMVC获取请求头有两种方式:比较简洁的方法是使用@RequestHeader
注解,另一种方式是从依赖注入的HttpServletRequest
对象中获取。
@RequestHeader
注解能够直接依赖注入请求头参数,下面例子代码中,从请求头中获取了token
参数:
@GetMapping(value = "/demo")
public String demo(@RequestHeader(value = "token") String token) {
// ... 具体业务逻辑
}
@RequestHeader
也具有required
属性,默认为true
,如果不满足会返回HTTP 400错误。
另一种方式是通过依赖注入HttpServletRequest
对象获取,这个对象我们都比较熟悉了,它就是ServletAPI中的请求对象:
@GetMapping(value = "/demo")
public String demo(HttpServletRequest request) {
// ... 具体业务逻辑
String token = request.getHeader("token");
}
Cookie和请求头处理类似,也支持两种方式:通过@CookieValue
注解获取,或从依赖注入的HttpServletRequest
对象中获取。
@GetMapping(value = "/demo")
public String demo(@CookieValue(value = "token") String token) {
// ... 具体业务逻辑
}
代码比较简单,这里就不赘述了。
之前解析请求JSON时我们介绍过Jackson库相关的配置,配置好后同样支持响应JSON的生成。
如果响应JSON数据,我们需要在Controller类上标注@RestController
或在方法上标注@ResponseBody
,比较推荐的用法是前者。
@RestController
@RequestMapping(value = "index")
public class IndexController {
@GetMapping(value = "/demo")
public ApiResult demo() {
return ApiResult.success();
}
}
XML也是类似的,这里就不多介绍了。
响应二进制数据一般用于文件下载的场景,文件下载通常要和Content-Diposition
、Content-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相关章节。
SpringMVC支持多种模板引擎,比如常用的JSP、Thymeleaf、Velocity等。响应模板视图需要我们配置ViewResolver
,下面是Thymeleaf的例子:
<!--thymeleaf template-->
<bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
<bean id="templateEngine" class="org.thymeleaf.spring5.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver"/>
</bean>
<bean id="templateResolver" class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
<property name="prefix" value="/template/"/>
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML"/>
<property name="characterEncoding" value="UTF-8"/>
<!--<property name="cacheable" value="false" />-->
</bean>
返回模板视图非常简单,控制器方法直接返回模板路径即可:
@Controller
@RequestMapping(value = "index")
public class IndexController {
@GetMapping(value = "/demo")
public String demo(Model model) {
// 向模板传递数据
model.addAttribute("username", "Alice");
// 响应模板webapp/template/index.html
return "index";
}
}
注意代码中,我们依赖注入了一个Model
,它的用法类似一个Map,用来向模板引擎传递数据。
模板中直接取用该数据即可:
<!doctype html>
<html lang="zh-cmn-Hans" 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领域),现在主流的前后端分离开发中,前端工程会独立出去并由静态文件服务器单独提供,而后端只负责输出接口数据。
在Spring核心功能相关章节中,我们学习过AOP的概念,实际上SpringMVC有一个地方迫切需要AOP切面功能,就是统一异常处理。
考虑这样的场景:我们的代码抛出运行时异常,但此时我们不希望SpringMVC展示出五花八门的错误页面,我们希望返回一个全局通用的错误信息,此时就可以用AOP切面统一拦截控制器方法的执行。
下面代码我们实现返回一个通用错误JSON信息:
package com.gacfox.demo.controller;
import com.gacfox.demo.bean.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
切面也支持全局数据预处理、全局数据绑定处理,不过正常使用基本不会用到,这里就不多介绍了。