1. 背景
Sping MVC 是在 Spring 之上的框架,用于开发 Web 程序。
2. 初步了解 Spring MVC
2.1 知识
Spring MVC 是建立在 Servlet API 之上的 Web 框架,包含在 Spring Framework 中。MVC 是指 模型,视图,控制器的意思,Spring MVC 实现了这种思想。
Spring MVC 分离了控制器、模型对象、过滤器以及处理程序对象的角色,这种分离让它们更容易进行定制。
Spring MVC 不依赖 JSP,可以使用其他模板引擎(JSP,thymeleaf等)。RESTful API 返回的 JSON 格式可以理解为 json View,也是 MVC。
Spring MVC 与许多其他 Web 框架一样,是围绕前端控制器模式( front controller )设计的,其中DispatcherServlet为请求处理提供统一入口,而实际工作由委托组件处理。
一个HTTP请求经过 Spring MVC 需要经历的过程如下:
image.png
- 1、 HTTP 请求 携带用户请求的内容,比如表单等 到达 DispactcherServlet。
- 2和3、DispactcherServlet 需要将请求委托给其他组件来执行,它查询 处理器映射(Handler Mapping)以确定具体将请求转发到哪个 控制器 (Controller)
- 4和5、DispactcherServlet 将请求转发到具体选定的控制器(Controller),Controller 负责访问服务和数据库获得 模型( Model ),并返回一个视图名称。
- 6和7、DispactcherServlet 将模型和视图 发送到一个 视图解析器 ( View Resolver), 由视图解析器 使用模型渲染输出到视图。
- 8和9、DispactcherServlet 将视图的呈现内容返回,响应到请求内容给客户端。
2.2 编写一个精简的 MVC 项目
刚刚说了 一个请求所要经历的过程,提到了几个组件,下面我们通过搭建基础版的项目进一步了解。
传统的web项目需要一个web.xml进行配置,包括 Servlet的配置映射,请求映射,视图解析,异常处理,委托组件等。DispatcherServlet 需要知道这些配置。
我们这里不这么做,而由 java 代码配置 DispatcherServlet 。 通过继承 AbstractAnnotationConfigDispatcherServletInitializer 来实现,当它部署在 sevlet 3.0的容器中时,容器会自动发现它并应用配置,示例:
代码语言:javascript复制/**
* web app 的初始化辅助类。等同于 web.xml
*/
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{RootConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{ServletConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
上面的代码中配置说明如下:
- getRootConfigClasses() 返回的类将会用来配置 ContextLoaderListener 创建的应用上下文中的Bean。
- getServletConfigClasses() 返回的类用来配置 web 应用,比如控制器,视图解析器等。
通常在一个 web 应用中有这么两个上下文:
- Servlet WebApplicationContext : 包含 控制器,视图解析器,Handler映射等。
- Root WebApplicationContext: 通常包含基础架构 bean,例如数据存储库和业务服务Bean。
关系如下:
image.png
我这里 RootConfig 是空的。
代码语言:javascript复制@Configuration
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)})
public class RootConfig {
}
ServletConfig 类中配置一个 jsp 视图解析器。
代码语言:javascript复制@Configuration
@EnableWebMvc
@ComponentScan
public class ServletConfig extends WebMvcConfigurerAdapter {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
写一个简单的 Controller
代码语言:javascript复制@Controller
@RequestMapping("/main")
public class HelloWorldController {
@RequestMapping(value = "/say", method = RequestMethod.GET)
public String sayHello(Model model) {
model.addAttribute("yourname", "zhangsan");
return "welcome";
}
}
welcome.jsp 文件代码:
代码语言:javascript复制<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>HelloWorld page</title>
</head>
<body>
<h1>这是首页</h1>
Your Name : ${yourname}
</body>
</html>
使用 Spring 结合 Spring MVC 的一个精简版的项目就搭建好了。 我的完整代码示例见:https://github.com/vir56k/java_demo/tree/master/spring_mvc_demo_1
2.3 读取请求中的参数
支持下述参数:
- 查询字符串
- Form 表单中的
- 请求路径中的
示例:
使用 @RequestParam 读取查询字符串中或表单数据的参数值
代码语言:javascript复制 @GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) {
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
使用 @PathVariable 注解读取 青汽路径的参数。
代码语言:javascript复制@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
public void findPet(@PathVariable String petId) {
// ...
}
2.4 参数的合法性校验
Spring Framework 提供对 Java Bean Validation API 的支持。
示例:
代码语言:javascript复制public class PersonForm {
@NotNull
@Size(max=64)
private String name;
@Min(0)
private int age;
}
代码语言:javascript复制public class User {
private String email;
@NotNull @Email
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
public class UserService {
public void createUser(@Email String email,
@NotNull String name) {
...
}
}
更多请参考:https://beanvalidation.org/
2.5 视图渲染
而类似 JSP 开发的方式已是古老的方法。较新一点的是 Thymeleaf 框架。当前(本文写作时间2021-07-06)比较流行的开发方式是前后端分离的技术,使用 ReactJS,VUE 单独开发项目。本文不再多介绍。
2.6 使用HTTP消息转换器
消息转换器 消息转换(message conversion)提供了一种更为直接的方式,它能够将控制器产生的数据转换为服务于客户端的表述形式(JSON,XML等)。
当使用消息转换功能时,DispatcherServlet不再将模型数据传送到视图中,它直接通过消息转换器
直接转换成指定格式。
比如 如果 Jackson JSON在类路径下,那么处理方法返回的对象将交给 MappingJacksonHttpMessageConverter 来处理。
@RestController注解 正常情况下,当处理方法返回Java对象时,这个对象会放在模型中并在视图中渲染使用。但是,如果使用了消息转换功能的话,我们需要告诉Spring 跳过正常的模型/视图流程,并使用消息转换器。最简单 的方法是为控制器方法添加@ResponseBody注解。
代码语言:javascript复制@RequestMapping("/login")
@ResponseBody
public Object login(String name, String password, HttpSession session) {
...
return new JsonResult(user);
}
如果在控制器类上使用@RestController来代替@Controller的话,Spring将会为该控制器的所有处理方法应用消息转换功能。我们不必为每个方法都添加@ResponseBody了。
代码语言:javascript复制@RestController
public class XxxController{
...
}
返回ResponseEntity对象 控制器方法可以返回一个ResponseEntity对象。ResponseEntity中可以包含响应相关的元数据(如头部信息和状态码)以及要转换的对象实体。
代码语言:javascript复制@GetMapping("/custom")
ResponseEntity<String> custom() {
HttpHeaders headers = new HttpHeaders();
headers.add("Custom-Header", "foo");
return new ResponseEntity<>(
"Custom header set", headers, HttpStatus.OK);
}
2.7 扩展
UriComponentsBuilder Spring提供了UriComponentsBuilder,可以给我们一 些帮助。它是一个构建类,通过逐步指定URL中的各种组成部分(如host、端口、路径以及 查询),我们能够使用它来构建UriComponents实例。
代码语言:javascript复制UriComponents uriComponents=UriComponentsBuilder
.fromHttpUrl("http://localhost:8080//hello")
.queryParams(params).build()
String uri=uriComponents.toUriString();
@RequestMapping @RequestMapping 标识了这是一个请求映射,还有一些基于此扩展的方法,像下面这些,从名字就能看出具体的含义。
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- @PatchMapping
自定义 servlet 和 filter 扩展一下,通过继承 WebApplicationInitializer 可以注册自定义 servlet 和 filter:
代码语言:javascript复制/**
* 初始化 web 应用
*/
public class MyWebApplicationInitializer implements WebApplicationInitializer {
public void onStartup(ServletContext servletContext) throws ServletException {
// 注册一个 servlet
ServletRegistration.Dynamic servlet = servletContext.addServlet("myservlet1", MyServlet.class);
servlet.addMapping("/custom/**");
// 注册一个过滤器
FilterRegistration.Dynamic filter1 = servletContext.addFilter("filter1", MyFilter1.class);
filter1.addMappingForUrlPatterns(null, false, "/custom/*");
}
}
Multipart 解析器和文件上传 注册一个 Multipart 解析器
代码语言:javascript复制public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
}
}
接收上传文件
代码语言:javascript复制@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(@RequestParam("name") String name,
@RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
byte[] bytes = file.getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
异常处理 Spring MVC 提供了多种形式将异常转化成 响应:
- 特定的 Spring 异常将自动映射到 HTTP 的状态码
- 异常上使用 @ReponseStatus 注解,可以将其对应到某个 HTTP 状态码
- 方法上使用 @ExceptionHandler 注解,标识处理异常。
@ResponseStatus 注解的自定义异常,将自动映射到 HTTP 的状态码:
代码语言:javascript复制@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "啊呜,找不见了...")
public class MyException extends RuntimeException {
}
使用 @ExceptionHandler 注解捕获异常: @ExceptionHandler 作用在 controller 类上时,可以捕获这个 controller 的异常。
代码语言:javascript复制 // @ExceptionHandler 注解,在这里会捕获这个类异常
@ExceptionHandler(MyException2.class)
public String handleException() {
return "errorrrr";
}
@ExceptionHandler 注解 结合“ 控制器通知 ” 可以捕获所有 控制器的异常。
控制器通知( controller advise ) 是指 被标注了@ControllerAdvice 注解的类。这个类可以配合以下的注解使用:
- @ExceptionHandle 标注的方法
- @InitBinder 标注的方法
- @ModelAttribute 标注的方法 在标注了 @ControllerAdvice 的类中,上述的三个方法会运用到整个应用程序所有控制器中带有 @RequestMapping 方法上。 示例:
@ControllerAdvice
public class AppExceptionHandler {
// @ExceptionHandler 注解,在这里会捕获这个类异常
@ExceptionHandler(MyException3.class)
public String handleException() {
return "errorrrr";
}
}
3. 示例
我的代码示例见:https://github.com/vir56k/java_demo/tree/master/spring_mvc_demo_2
4.参考:
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html
http://websystique.com/springmvc/spring-4-mvc-helloworld-tutorial-annotation-javaconfig-full-example/