文章目录
- 一、概述及示例
-
- 1.1 MVC 思想介绍
- 1.2 SpringMVC 概述
- 1.3 HelloWorld
- 1.4 请求响应流程
- 二、请求映射
-
- 2.1 注解的使用位置
- 2.2 value、method 属性
- 2.3 params、headers 属性
- 2.4 Ant 风格编程
- 2.5 路径中的占位符
- 三、请求参数获取
-
- 3.1 原生 Servlet
- 3.2 形参列表
- 3.3 @RequestParam
- 3.4 @RequestHeader
- 3.5 @CookieValue
- 3.6 POJO 对象
- 3.7 中文乱码
- 四、域数据共享
-
- 4.1 原生 Servlet
- 4.2 ModalAndView
- 4.3 Model
- 4.4 Map
- 4.5 ModelMap
- 4.6 request 域数据共享的选择
- 4.7 session、servletContext 域数据共享
- 五、视图
-
- 5.1 SpringMVC 中的视图
- 5.2 ThymeleafView
- 5.3 InternalResourceView
- 5.4 RedirectView
- 5.5 view-controller
- 5.6 配置 JSP 视图解析器
- 六、RESTful
-
- 6.1 RESTful 风格介绍
- 6.2 HiddenHttpMethodFilter
- 6.3 GET 请求获取信息
- 6.4 POST 请求保存信息
- 6.4 DELETe 请求删除信息
- 6.5 PUT 请求修改信息
- 6.6 DefaultServletHandler
- 七、信息报文转换
-
- 7.1 HttpMessageConverter
- 7.2 @RequestBody
- 7.3 RequestEntity
- 7.4 @ResponseBody
- 7.5 ReponseEntity
- 7.6 @RestController
- 八、文件上传、JSON、AJAX
-
- 8.1 文件上传
- 8.2 JSON 处理
- 8.3 AJAX 处理
- 九、拦截器
-
- 9.1 拦截器的配置与使用
- 9.2 多个拦截器的执行顺序
- 十、异常处理器
-
- 10.1 异常处理器概述
- 10.2 异常处理器(xml)
- 10.3 异常处理器(注解)
- 十一、SpringMVC 总结
- 11.1 常用组件
- 11.2 WebApplicationContext 初始化流程
- 11.3 DispatcherServlet 处理请求流程
- 11.4 SpringMVC 的执行流程总结归纳
- 11.5 SpringMVC 基于 xml 文件的配置
- 11.6 SpringMVC 基于注解的配置
一、概述及示例
1.1 MVC 思想介绍
-
MVC 是一种软件架构的思想,将软件按照模型(model)、视图(view)、控制器(controller)来划分
-
M(Model):模型层,指工程中的 JavaBean,作用是处理数据。JavaBean分为以下两类
- 一类称为实体 Bean:用于存储业务数据,如 Student、User 等
- 一类称为业务 Bean:指 Service 或 Dao 对象,专门用于处理业务逻辑和数据访问
-
V(View):视图层,指工程中的 html 或 JSP 等页面,作用是与用户进行交互,展示数据
-
C(Controller):控制层,指工程中的 Servlet,作用是接收请求和响应浏览器
-
MVC 的工作流程
- 用户通过视图层(view)发送请求到服务器,在服务器中请求被 Controller 接收
- Controller 调用相应的 Model 层处理请求
- 模型层处理完毕后将结果返回到Controller
- Controller 根据请求处理的结果找到相应的 View
- 由视图层渲染数据后最终响应给浏览器
1.2 SpringMVC 概述
SpringMVC 是 Spring 的一个后续产品,是 Spring 的一个子项目。SpringMVC 是 Spring 为表述层开发提供的一整套完备的解决方案,在表述层框架历经 Strust、WebWork、Strust2 等诸多产品的历代更迭之后,目前业界普遍选择了 SpringMVC 作为 JavaEE 项目表述层开发的首选方案。相比其它框架,SpringMVC 具有以下显著特点
- Spring 家族原生产品,与 IOC 容器等基础组件无缝对接
- 基于原生的 Servlet,通过功能强大的前端控制器 DispatcherServlet 对请求和响应进行统一处理
- 可插拔式组件即插即用,内部组件化程度高,想要什么功能配置相应组件即可
- 性能卓著,尤其适合现代大型、超大型互联网项目要求
1.3 HelloWorld
-
新建 maven 工程,给普通的 maven 工程添加 web 模块(添加 web.xml、指定工程上下文路径到 webapp)
-
在 pom.xml 中修改 maven 工程的打包方式为 war 包并引入 SpringMVC 相关依赖
war org.springframework spring-webmvc 5.3.1 ch.qos.logback logback-classic 1.2.3 javax.servlet javax.servlet-api 3.1.0 provided org.thymeleaf thymeleaf-spring5 3.0.12.RELEASE
-
在 web.xml 中注册前端控制器 DispatcherServlet
DispatcherServlet org.springframework.web.servlet.DispatcherServlet <!-- 指定 SpringMVC 配置文件路径,若不指定则 mvc.xml 应位于 WEB-INF 目录下且名称为 -servlet.xml --> contextConfigLocation classpath:mvc.xml 1 DispatcherServlet <!-- / 不能匹配 .jsp 请求路径的请求(避免在访问 jsp 页面时,该请求被 DispatcherServlet 处理,从而找不到相应的页面) @Controller public class HelloController { @RequestMapping("/") public String toIndex() { // 返回视图名称即 hello.html,返回 hello 后由 Thymeleaf 视图解析器解析 return "hello"; } }
-
创建前端页面 hello.html
Hello World Hello World
-
部署 Tomcat 访问
-
项目结构如下
-
访问成功
1.4 请求响应流程
- 浏览器发送请求,若请求地址符合前端控制器的 url-pattern,该请求就会被前端控制器 DispatcherServlet 捕获
- 前端控制器会读取 SpringMVC 的核心配置文件,通过扫描组件找到控制器,将请求地址和控制器中 @RequestMapping 注解的 value 属性值进行匹配
- 若匹配成功,该注解所标识的控制器方法需要返回一个字符串类型的视图名称,该视图名称会被视图解析器解析
- 视图解析器加上前缀和后缀组成视图的路径,通过 Thymeleaf 对视图进行渲染,最终转发到视图所对应页面
二、请求映射
2.1 注解的使用位置
- @RequestMapping 标识一个类:设置映射请求路径初始信息
- @RequestMapping 标识一个方法:设置映射请求路径的具体信息
@Controller @RequestMapping("/test") public class RequestMappingController { // 此时浏览器具体请求路径为:/test/testRequestMapping @RequestMapping("/testRequestMapping") public String testRequestMapping(){ return "success"; } }
2.2 value、method 属性
-
@RequestMapping 注解的 value 属性通过请求的请求地址匹配请求映射(value 属性必不可少)。value 属性是一个 String 类型的数组,表示该控制器方法可以响应多个对应的请求地址
@RequestMapping(value = {"/testRequestMapping", "/test"}) public String testRequestMapping(){ return "success"; }
首页 get request
-
@RequestMapping 注解的 method 属性是通过请求的请求方式来匹配请求映射。method 属性是一个 RequestMethod 类型的数组,表示该请求映射能够匹配多种请求方式的请求(不配置 method 属性的时表示当前控制器方法可以匹配所有请求方式的请求)
@RequestMapping(value = {"/testRequestMapping", "/test"}, method = {RequestMethod.GET, RequestMethod.POST}) public String testRequestMapping(){ return "success"; }
-
若当前请求的请求地址满足请求映射的 value 属性,但是请求方式不满足 method 属性,则浏览器报错 405:Request method ‘POST’ not supported
-
method 的派生注解:目前浏览器只支持 get 和 post,若在 form 表单提交时,为 method 设置了其他请求方式的字符串(put 或 delete),则按照默认的请求方式 get 进行处理
- @PostMapping:处理 post 请求的映射(增)
- @DeleteMapping:处理 delete 请求的映射(删)
- @PutMapping:处理 put 请求的映射(改)
- @GetMapping:处理 get 请求的映射(查)
2.3 params、headers 属性
-
@RequestMapping 注解的 params 属性是一个字符串类型的数组,可以通过四种表达式设置请求参数和请求映射的匹配关系
- param:请求必须携带 param 参数
- !param:请求不能携带 param 参数
- param=value:请求必须携带 param 参数且 param=value
- param!=value:请求必须携带 param 参数且 param!=value
// 请求必须携带 username 和 password 参数且值为 bear(否则报错 400参数不匹配) // 请求头中必须携带 Connection 和 Host 参数且值为指定值(否则报错 404) @RequestMapping( value = "/testParamsAndHeaders", params = {"username=bear", "password=bear"}, headers = {"Connection=keep-alive", "Host=localhost:8080"}) public String testParamsAndHeaders() { return "success"; }
1. testParamsAndHeaders
-
headers 属性的使用方法与 params 一致
2.4 Ant 风格编程
// 1. ? 匹配任意单个字符 @RequestMapping("/?testAnt") // 2. * 零到多个字符 @RequestMapping("testAnt")
1. testAnt -> ? 2. testAnt -> * 3. testAnt -> **
2.5 路径中的占位符
@RequestMapping("/testPath/{username}/{password}") public String testPath(@PathVariable("username") String username, @PathVariable("password") String password) { System.out.println("username:" + username); System.out.println("password:" + password); return "success"; }
测试 RequestMapping 中的占位符
三、请求参数获取
3.1 原生 Servlet
// 不推荐(侮辱 SpringMVC?) @RequestMapping("/testServletApi") public String testServletApi(HttpServletRequest request) { String username = request.getParameter("username"); String password = request.getParameter("password"); System.out.println(username + " " + password); return "success"; }
测试 Servlet Api 获取请求参数
3.2 形参列表
@RequestMapping("/testParam") public String testParam(String username, String password, String[] hobby) { System.out.println(username + " " + password); System.out.println(Arrays.toString(hobby)); return "success"; }
3.3 @RequestParam
@RequestMapping("/testParam") public String testParam(@RequestParam(value = "user_name", required = false, defaultValue = "bear") String username) { System.out.println(username); return "success"; }
3.4 @RequestHeader
- @RequestHeader 是将请求头信息和控制器方法的形参创建映射关系
- @RequestHeader 注解一共有三个属性:value、required、defaultValue,用法同 @RequestParam
3.5 @CookieValue
- @CookieValue 是将 cookie 数据和控制器方法的形参创建映射关系
- @CookieValue 注解一共有三个属性:value、required、defaultValue,用法同 @RequestParam
- 第一次创建 session 会话时,服务器创建 Cookie 并响应给浏览器(浏览器 Cookie 信息存在于响应报文中),此后的请求 Cookie 存在于请求报文中
3.6 POJO 对象
// 若请求参数中不存在的 POJO 对象中的字段时则对应字段值为 null @RequestMapping("/testPojo") public String testPojo(User user) { System.out.println(user); return "success"; }
3.7 中文乱码
-
在 web.xml 中配置编码过滤器 CharacterEncodingFilter ,需将其配置为第一个过滤器,因为过滤器的拦截顺序安装配置的顺序依次拦截
CharacterEncodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 forceRequestEncoding true forceResponseEncoding true CharacterEncodingFilter @GetMapping("/employee/{id}") public String getEmployeeById(@PathVariable("id") Integer id, Model model){ Employee employee = employeeDao.get(id); model.addAttribute("employee", employee); return "employee_update"; }
Update Employee lastName:
email:
gender:male female
@PutMapping("/employee") public String updateEmployee(Employee employee){ employeeDao.save(employee); return "redirect:/employee"; }
6.6 DefaultServletHandler
<!-- 处理静态资源,若只设置该标签,则只能访问静态资源,其他请求无法访问 此时必须开启注解驱动 以处理其它非静态资源请求 -->
七、信息报文转换
7.1 HttpMessageConverterHttpMessageConverter(报文信息转换器)可以实现将请求报文转换为 Java 对象,或将 Java 对象转换为响应报文。HttpMessageConverter 提供了两个注解和两种类型:@RequestBody、@ResponseBody、RequestEntity、ResponseEntity 以实现数据间的转换
7.2 @RequestBody
// @RequestBody 获取请求体内容 @RequestMapping("/testRequestBody") public String testRequestBody(@RequestBody String requestBody){ // Output:username=admin&password=123456 System.out.println("requestBody:"+requestBody); return "success"; }
用户名:
密码:
7.3 RequestEntity
// RequestEntity 获取请求头、请求体内容 @PostMapping("/testRequestEntity") public String testRequestEntity(RequestEntity requestEntity) { System.out.println("请求头:" + requestEntity.getHeaders()); System.out.println("请求体:" + requestEntity.getBody()); System.out.println("请求方法:" + requestEntity.getMethod()); System.out.println("请求地址:" + requestEntity.getUrl()); return "success"; }
首页
用户名:
密码:
7.4 @ResponseBody
用 @ResponseBody 标识一个控制器方法,方法的返回值就是响应到浏览器的数据。
@ResponseBody @RequestMapping("/testResponseBody") public String testResponseBody(){ // 此时浏览器原请求页面打印 success return "success"; }
7.5 ReponseEntity
ResponseEntity 用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文。可以通过设置自定义的 ResponseEntity 实现文件下载
@RequestMapping("/download") public ResponseEntity download(HttpSession session) throws IOException { ServletContext servletContext = session.getServletContext(); // 获取文件在服务器中真实的磁盘路径 String realPath = servletContext.getRealPath("/static/img/img.png"); InputStream inputStream = new FileInputStream(realPath); // 创建与文件输入流中字节数等大的字节数组 byte[] buffer = new byte[inputStream.available()]; inputStream.read(buffer); // 创建 HttpHeaders 对象设置响应头信息 MultiValueMap headers = new HttpHeaders(); // 设置下载的方式及下载文件名 headers.add("Content-Disposition", "attachment;filename=1.jpg"); // 设置响应状态码 HttpStatus httpStatus = HttpStatus.OK; // 创建 ResponseEntity 对象 ResponseEntity responseEntity = new ResponseEntity(buffer, headers, httpStatus); inputStream.close(); return responseEntity; }
7.6 @RestController
@RestController 注解是 SpringMVC 提供的一个复合注解,标识在控制器的类上,就相当于为当前类添加了 @Controller 注解,并且为其中的每个方法添加了 @ResponseBody 注解
八、文件上传、JSON、AJAX
8.1 文件上传-
在 pom.xml 中引入文件上传所需依赖
commons-fileupload commons-fileupload 1.3.1
-
在 mvc.xml 中配置文件解析器
-
前端 form 表单提交文件数据: form 表单的请求方式必须为 post,并且添加属性 enctype=”multipart/form-data”
Photo:
PDF:
-
后台控制器接收并保存文件数据:SpringMVC 中将上传的文件封装到 MultipartFile 对象中,通过此对象可以获取文件相关信息
@PostMapping("/upload") public String upload(HttpSession session, MultipartFile photo, MultipartFile pdf) throws IOException { // 获取指定目录在磁盘上的真实路径 String realPath = session.getServletContext().getRealPath("/upload"); File directory = new File(realPath); // 若目录不存在则新建目录 if (!directory.exists()) { directory.mkdir(); } // 获取浏览器上传的文件的文件名并获取文件名后缀 String photoName = photo.getOriginalFilename(); String photoSuffix = photoName.substring(photoName.lastIndexOf(".")); String pdfName = pdf.getOriginalFilename(); String pdfSuffix = pdfName.substring(pdfName.lastIndexOf(".")); // 以系统生成的随机 UUID 作为新的文件名 String photoPath = realPath + File.separator + UUID.randomUUID() + photoSuffix; String pdfPath = realPath + File.separator + UUID.randomUUID() + pdfSuffix; // 保存文件到磁盘目录 pdf.transferTo(new File(pdfPath)); photo.transferTo(new File(photoPath)); return "success"; }
8.2 JSON 处理
-
pom.xml 中引入 JSON 依赖
com.fasterxml.jackson.core jackson-databind 2.12.1
-
在 mvc.xml 中开启注解驱动,此时在 HandlerAdaptor 中会自动装配一个消息转换器 MappingJackson2HttpMessageConverter,可以将响应到浏览器的 Java 对象自动转换为 JSON 格式的字符串
-
在控制器方法上使用 @ResponseBody 注解进行标识,将 Java 对象直接作为控制器方法的返回值返回,就会自动转换为 JSON 格式的字符串
@ResponseBody @RequestMapping("/testResponseUser") public User testResponseUser(){ // 浏览器 Output:{"id":1001,"username":"admin","password":"123456","age":23,"sex":"男"} return new User(1001,"admin","123456",23,"男"); }
8.3 AJAX 处理
@ResponseBody @GetMapping("/testAjax") public String testAjax(User user) { System.out.println(user); return "hello,ajax"; }
九、拦截器
9.1 拦截器的配置与使用-
自定义拦截器需要自定义类实现 HandlerInterceptor 接口
@Component public class FirstInterceptor implements HandlerInterceptor { // 控制器方法执行之前执行 preHandle() ,其 boolean 类型的返回值表示是否调用相应的控制器方法 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("FirstInterceptor preHandle()"); return true; } // 控制器方法执行之后执行 postHandle() @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("FirstInterceptor postHandle()"); } // 处理完视图和模型数据,视图渲染完毕之后执行 afterComplation() @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("FirstInterceptor afterCompletion()"); } }
-
在 mvc.xml 中配置拦截器
<mvc:mapping path=" @ControllerAdvice public class ExceptionHandler { @org.springframework.web.bind.annotation.ExceptionHandler(value = {ArithmeticException.class, NullPointerException.class}) public ModelAndView handleException(Exception ex) { ModelAndView modelAndView = new ModelAndView(); // 异常发生时转发到的视图名称 modelAndView.setViewName("error"); // request 域中共享的错误信息,key 为 ex,value 为 ex modelAndView.addObject("ex", ex); return modelAndView; } }
十一、SpringMVC 总结
11.1 常用组件组件名 描述 作用 DispatcherServlet前端控制器,不需要工程师开发,由框架提供统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求HandlerMapping处理器映射器,不需要工程师开发,由框架提供根据请求的 url、method 等信息查找 Handler,即控制器方法Handler处理器(控制器),需要工程师开发在 DispatcherServlet 的控制下通过 Handler 对具体的用户请求进行处理HandlerAdapter处理器适配器,不需要工程师开发,由框架提供通过 HandlerAdapter 对处理器(控制器方法)进行执行ViewResolver视图解析器,不需要工程师开发,由框架提供进行视图解析,得到相应的视图,例如:ThymeleafView、InternalResourceView、RedirectViewView视图即页面将模型数据通过页面展示给用户
11.2 WebApplicationContext 初始化流程-
调用 FrameworkServlet 类中的 initWebApplicationContext 方法实现 context 对象的初始化,在初始化方法中先调用其同类的方法 createWebApplicationContext 创建 context 对象(37 行)
protected WebApplicationContext initWebApplicationContext() { WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; if (this.webApplicationContext != null) { // A context instance was injected at construction time -> use it wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; if (!cwac.isActive()) { // The context has not yet been refreshed -> provide services such as // setting the parent context, setting the application context id, etc if (cwac.getParent() == null) { // The context instance was injected without an explicit parent -> set // the root application context (if any; may be null) as the parent cwac.setParent(rootContext); } configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { // No context instance was injected at construction time -> see if one // has been registered in the servlet context. If one exists, it is assumed // that the parent context (if any) has already been set and that the // user has performed any initialization such as setting the context id wac = findWebApplicationContext(); } if (wac == null) { // No context instance is defined for this servlet -> create a local one // 1. 创建 WebApplicationContext wac = createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { // Either the context is not a ConfigurableApplicationContext with refresh // support or the context injected at construction time had already been // refreshed -> trigger initial onRefresh manually here. synchronized (this.onRefreshMonitor) { // 2. 刷新 WebApplicationContext onRefresh(wac); } } if (this.publishContext) { // Publish the context as a servlet context attribute. // 将 IOC 容器在应用域共享 String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac); } return wac; } protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) { Class contextClass = getContextClass(); if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException( "Fatal initialization error in servlet with name '" + getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext"); } // 通过反射创建 IOC 容器对象 ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); wac.setEnvironment(getEnvironment()); // 设置父容器 wac.setParent(parent); String configLocation = getContextConfigLocation(); if (configLocation != null) { wac.setConfigLocation(configLocation); } configureAndRefreshWebApplicationContext(wac); return wac; }
-
接着调用刷新上下文对象的方法 onRefresh(wac)(46 行),该方法在 FrameworkServlet 的子类 DispatcherServlet 中进行了重写并调用了 initStrategies(context)方法初始化策略,即初始化各个组件
@Override protected void onRefresh(ApplicationContext context) { initStrategies(context); } protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); }
11.3 DispatcherServlet 处理请求流程
-
FrameworkServlet 重写了 HttpServlet 中的 service() 和 doXxx(),这些方法中调用了 FrameworkServlet 类中的 processRequest(request, response)。在此方法中又调用了 doService(request, response) 方法处理请求(子类 DispatcherServlet 重写了 doService(request, response) 方法)
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long startTime = System.currentTimeMillis(); Throwable failureCause = null; LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); LocaleContext localeContext = buildLocaleContext(request); RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor()); initContextHolders(request, localeContext, requestAttributes); try { // 执行服务,doService() 是一个抽象方法,在 DispatcherServlet 中进行了重写 doService(request, response); } catch (ServletException | IOException ex) { failureCause = ex; throw ex; } catch (Throwable ex) { failureCause = ex; throw new NestedServletException("Request processing failed", ex); } finally { resetContextHolders(request, previousLocaleContext, previousAttributes); if (requestAttributes != null) { requestAttributes.requestCompleted(); } logResult(request, response, failureCause, asyncManager); publishRequestHandledEvent(request, response, startTime, failureCause); } }
-
DispatcherServlet 中的 doService() 方法调用同类的 doDispatch() 方法分发处理请求
@Override protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { logRequest(request); // Keep a snapshot of the request attributes in case of an include, // to be able to restore the original attributes after the include. Map attributesSnapshot = null; if (WebUtils.isIncludeRequest(request)) { attributesSnapshot = new HashMap(); Enumeration attrNames = request.getAttributeNames(); while (attrNames.hasMoreElements()) { String attrName = (String) attrNames.nextElement(); if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) { attributesSnapshot.put(attrName, request.getAttribute(attrName)); } } } // Make framework objects available to handlers and view objects. request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource()); if (this.flashMapManager != null) { FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); if (inputFlashMap != null) { request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap)); } request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); } RequestPath requestPath = null; if (this.parseRequestPath && !ServletRequestPathUtils.hasParsedRequestPath(request)) { requestPath = ServletRequestPathUtils.parseAndCache(request); } try { // 处理请求和响应 doDispatch(request, response); } finally { if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Restore the original attribute snapshot, in case of an include. if (attributesSnapshot != null) { restoreAttributesAfterInclude(request, attributesSnapshot); } } if (requestPath != null) { ServletRequestPathUtils.clearParsedRequestPath(request); } } } protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. // 通过控制器方法创建相应的处理器适配器,调用所对应的控制器方法 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } // 1. 调用拦截器的 preHandle() if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. // 2. 由处理器适配器调用具体的控制器方法(controller),最终获得 ModelAndView 对象 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); // 3. 调用拦截器的 postHandle() mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { // As of 4.3, we're processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and other scenarios. dispatchException = new NestedServletException("Handler dispatch failed", err); } // 4. 处理模型数据和渲染视图,完成后调用拦截器的 afterCompletion() processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } } // 处理模型数据、渲染视图、调用拦截器的 afterCompletion() private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } } // Did the handler return a view to render? if (mv != null && !mv.wasCleared()) { // 处理模型数据和渲染视图 render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else { if (logger.isTraceEnabled()) { logger.trace("No view rendering, null ModelAndView returned."); } } if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Concurrent handling started during a forward return; } if (mappedHandler != null) { // Exception (if any) is already handled.. // 调用拦截器的afterCompletion() mappedHandler.triggerAfterCompletion(request, response, null); } }
11.4 SpringMVC 的执行流程总结归纳
-
用户向服务器发送请求,请求被 SpringMVC 前端控制器 DispatcherServlet 捕获
-
DispatcherServlet 对请求 URL 进行解析,得到请求资源标识符(URI),判断请求 URI 对应的映射:
请求映射失败:查看是否配置了 mvc:default-servlet-handler 处理静态资源,如果没配置,则报错 404(DispatcherServlet)
-
请求映射成功,根据该 URI,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以 HandlerExecutionChain 执行链对象的形式返回
-
DispatcherServlet 根据获得的 Handler,选择一个合适的 HandlerAdapter,如果成功获得 HandlerAdapter,此时将开始执行拦截器的 preHandler(…) 方法(按定义顺序正序执行)
-
提取 Request 中的模型数据,填充 Handler入参,开始执行 Handler(Controller) 方法处理请求。在填充 Handler 的入参过程中,根据配置,Spring 将做一些额外的工作:
- HttpMessageConveter: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息
- 数据转换:对请求消息进行数据转换。如 String 转换成 Integer、Double 等
- 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
- 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到 BindingResult 或 Error 中
-
Handler 执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象
-
此时将开始执行拦截器的 postHandle(…) 方法(按定义顺序逆序执行)
-
根据返回的 ModelAndView(此时会判断是否存在异常:如果存在异常,则执行 HandlerExceptionResolver 进行异常处理)选择一个适合的 ViewResolver 进行视图解析,根据 Model 和 View 来渲染视图
-
渲染视图完毕执行拦截器的 afterCompletion(…) 方法(按定义顺序逆序执行)
-
将渲染结果返回给客户端
11.5 SpringMVC 基于 xml 文件的配置
-
pom.xml 引入相关依赖
war org.springframework spring-webmvc 5.3.1 ch.qos.logback logback-classic 1.2.3 javax.servlet javax.servlet-api 3.1.0 provided org.thymeleaf thymeleaf-spring5 3.0.12.RELEASE com.fasterxml.jackson.core jackson-databind 2.12.1 commons-fileupload commons-fileupload 1.3.1
-
web.xml 配置过滤器、前端控制器
CharacterEncodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 forceRequestEncoding true forceResponseEncoding true CharacterEncodingFilter public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class[] getRootConfigClasses() { return new Class[]{SpringConfig.class}; } @Override protected Class[] getServletConfigClasses() { return new Class[]{SpringMvcConfig.class}; } @Override protected String[] getServletMappings() { return new String[]{"/"}; } @Override protected Filter[] getServletFilters() { // 编码过滤器 CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter("UTF-8", true); // 请求方法过滤器 HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter(); return new Filter[]{characterEncodingFilter, hiddenHttpMethodFilter}; } }
-
Spring 的配置类
@Configuration public class SpringConfig { }
-
SpringMVC 的配置类
@Configuration @ComponentScan("com.bear.mvc.controller") @EnableWebMvc public class SpringMvcConfig implements WebMvcConfigurer { @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } @Override public void addInterceptors(InterceptorRegistry registry) { FirstInterceptor firstInterceptor = new FirstInterceptor(); // 添加拦截规则 registry.addInterceptor(firstInterceptor).addPathPatterns(" @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); } @Override public void configureHandlerExceptionResolvers(List resolvers) { SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver(); Properties prop = new Properties(); // 添加异常及跳转的页面名称 prop.setProperty("java.lang.ArithmeticException", "error"); exceptionResolver.setExceptionMappings(prop); // 设置共享异常信息的键 exceptionResolver.setExceptionAttribute("ex"); resolvers.add(exceptionResolver); } @Bean public CommonsMultipartResolver multipartResolver() { return new CommonsMultipartResolver(); } @Bean public ITemplateResolver templateResolver() { WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext(); ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(webApplicationContext.getServletContext()); templateResolver.setPrefix("/WEB-INF/pages/"); templateResolver.setSuffix(".html"); templateResolver.setCharacterEncoding("UTF-8"); templateResolver.setTemplateMode(TemplateMode.HTML); return templateResolver; } @Bean public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver); return templateEngine; } @Bean public ViewResolver viewResolver(SpringTemplateEngine templateEngine) { ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setCharacterEncoding("UTF-8"); viewResolver.setTemplateEngine(templateEngine); return viewResolver; } }
-
-