4.1 Spring MVC介绍

以前,我们大量的使用JSP+Servlet技术开发web应用。通过使用jstl技术( Java server pages standarded tag library,即JSP标准标签库)封装一些常用组件,简化前端jsp页面的开发。

那个时候时间过得很慢,

慢到只能用一种技术规范,

开发web应用一辈子。

现在,我们更多的是使用前后端分离的MVVM技术来构建web应用。例如前端使用vue,后端使用rest方式(Representational State Transfer)开发业务功能接口,供前端通过ajax方式调用。

MVVM:Model-View-ViewModel的简写,它本质上就是MVC 的改进版。

无论是在以前还是现在,使用Spring开发web应用,首选的都是Spring MVC开发框架。

Spring MVC并不知道前端使用的视图技术,所以不会强迫您只使用 JSP 技术。实际上在Spring Boot 2.0框架中,Spring MVC推荐使用Thymeleaf模板技术。

Spring MVC分离了控制器、模型对象、分派器以及处理程序对象的角色,这种分离让它们更容易进行定制。

Spring 的 Web MVC 框架是围绕 DispatcherServlet 前端分发器设计的,它把请求分派给处理程序,同时带有可配置的处理程序映射、视图解析、本地语言(国际化,多语言支持)、主题解析以及上载文件等支持。默认的处理程序是非常简单的 Controller 接口,只有一个方法 ModelAndView handleRequest(request, response)。Spring 提供了一个控制器层次结构,可以派生子类。如果应用程序需要处理用户输入表单,那么可以继承 AbstractFormController。如果需要把多页输入处理到一个表单,那么可以继承 AbstractWizardFormController

4.1.1 常用注解

Spring MVC在发展过程中也经历了一段“黑暗”时间:非常繁杂的xml配置,简直就是“xml hell”(xml配置地狱,指繁杂到失控的xml配置信息)。后来,使用上注解以后,才简洁易用起来。

所以,要想了解会用Spring MVC就必须掌握其最常用的注解。

4.1.1.1 @Controller

在SpringMVC 中,控制器Controller 负责处理由前端分发器DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ,然后再把该Model 返回给对应的View 进行展示。

在Spring MVC中定义一个Controller不需要继承某个父类,也无需实现某个接口。你只需要使用@Controller 标记一个类是Controller即可。

前端分发器DispatcherServlet将会扫描使用了该注解的类的方法,并检测该方法是否使用了@RequestMapping 注解。@Controller 只是定义了一个控制器类,而使用@RequestMapping 注解的方法才是真正处理请求的处理器。单单使用@Controller 标记在一个类上还不能真正意义上的说它就是Spring MVC 的一个控制器类,因为这个时候Spring 还不认识它。那么要如何做Spring 才能认识它呢?这个时候就需要我们把这个控制器类交给Spring 来管理。

各位,是否还记得Spring Boot启动类的注解@SpringBootApplication,这个注解是个复合注解,其中包含@ComponentScan会扫描到启动类包及其子包下的Controller控制器类,并将其加载到Spring容器中。

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
    ...
}

4.1.1.2 @RequestMapping

RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。

RequestMapping注解有六个属性,下面我们把她分成三类进行说明(下面有相应示例)。

  1. value, method:

  2. value: 指定请求的实际地址,指定的地址可以是URI Template 模式(后面将会说明);

  3. method: 指定请求的method类型, GET、POST、PUT、DELETE等;

  4. consumes,produces:

  5. consumes:指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;

  6. produces: 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回;

  7. params,headers:

  8. params: 指定request中必须包含某些参数值是,才让该方法处理。

  9. headers: 指定request中必须包含某些指定的header值,才能让该方法处理请求。

4.1.1.2.1 @PathVariables

用于将请求URL中的模板变量映射到功能处理方法的参数上,即取出uri模板中的变量作为参数。

@Controller  
public class TestController {  

     @RequestMapping(value="/product/{productId}",method = RequestMethod.GET)  
     public String getProduct(@PathVariable("productId") String productId){  
           System.out.println("Product Id : " + productId);  
           return "hello";  
     }  

     @RequestMapping(value="/javabeat/{regexp1:[a-z-]+}", method = RequestMethod.GET) 
     public String getRegExp(@PathVariable("regexp1") String regexp1){  
           System.out.println("URI Part 1 : " + regexp1);  
           return "hello";  
     }  
}
4.1.1.2.2 @RequstParams

用于在Spring MVC后台控制层获取参数,类似一种是request.getParameter("name"),它有三个常用参数:defaultValue = "0", required = false, value = "isApp";defaultValue 表示设置默认值,required通过boolean值设置是否是必须要传入的参数,value 值表示接受的传入的参数类型。

多数情况下@RequestParam可以省略。

    @RequestMapping("genUsers")
    public String genUsers(@RequestParam int count) {
    //public String genUsers(int count) {
        for (int i=2; i<count+2; i++) {
            String str = UUID.randomUUID().toString().substring(16);
            User user = new User();
            user.setId(i);
            user.setUserName(str);
            user.setRealName(str);
            user.setPassWord(str);

            userService.saveUser(user);
        }
        return "生成了" + count + "个用户。";
    }

4.1.1.3 @ResponseBody

该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。

返回的数据不是html页面,而是其他某种格式的数据时(如json、xml等)使用。

4.1.1.4 @ModelAndView

用来存储处理完后的结果数据,以及显示该数据的视图。从名字上看ModelAndView中的Model代表模型,View代表视图,这个名字就很好地解释了该类的作用。业务处理器调用模型层处理完用户请求后,把结果数据存储在该类的model属性中,把要返回的视图信息存储在该类的view属性中,然后让该ModelAndView返回该Spring MVC框架。框架通过调用配置文件中定义的视图解析器,对该对象进行解析,最后把结果数据显示在指定的页面上。

    public ModelAndView handleRequestInternal(HttpServletRequest request,  
        HttpServletResponse response)throws Exception{  
        ...  
        Map<String,Object> model = new HashMap<String,Object>();  
        if(courtName != null){  
            model.put("courtName",courtName);  
            model.put("reservations",reservationService.query(courtName));  
        }  
        return new ModelAndView("reservationQuery",model);  
    }

4.1.1.5 @RestController

@RestController是一个复合注解,其作用等同于@Controller + @ResponseBody。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {

    /**
     * The value may indicate a suggestion for a logical component name,
     * to be turned into a Spring bean in case of an autodetected component.
     * @return the suggested component name, if any (or empty String otherwise)
     * @since 4.0.1
     */
    @AliasFor(annotation = Controller.class)
    String value() default "";

}

这个注解,大量地用在前后端分离的应用中。

4.1.2 Spring MVC剖析

要想彻底掌握Spring MVC,就必须深入其设计、运行机制,才能做到“知其然,也知其所以然”。

当然,彻底掌握任何一个框架,都是通过阅读官方文档(绝大多数时候是英文的)和源码来完成的。

最早,Spring MVC大部分是和JSP一起使用的,这里我们也以JSP为例,以便于讲解其运行机理。

4.1.2.1 加载过程

Spring MVC应用在启动过程中会创建两个Spring容器:基础容器(含@Repository,@Service,@Component等组件)和控制器容器(含@Controller)。

控制器容器容器“继承”自基础容器,所以在我们写的Controller里面可以注入@Service和@Component组件(当然,也可以注入@Repository,只是基于事务管理绝对不推荐这样做),而@Service组件中不能注入@Controller组件。

从依赖倒置原则来讲,@Service组件绝不应该依赖@Controller组件。

项目中,我们写在Controller中的标注了@RequestMapping的方法(如下面代码片段的getUser方法),在启动时会被扫描后装入DispatcherServlet的handlerMappings属性中。

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping("get/{id}")
    public String getUser(@PathVariable int id){
        return userService.selectUser(id).toString();
    }
...

DispatcherServlet的handlerMappings属性中存放了Spring MVC Web应用的所有路径映射处理器。

@SuppressWarnings("serial")
public class DispatcherServlet extends FrameworkServlet {
...
    /** List of HandlerMappings used by this servlet. */
    @Nullable
    private List<HandlerMapping> handlerMappings;
...

HandlerMapping接口的最常用实现就是RequestMappingHandlerMapping类,具体继承层级如下图:

image-20191120195949041

当然,启动过程还会在DispatcherServlet对象中加载ViewResolver、MultipartResolver等对象,这里我们不过多介绍。感兴趣的同学可以通过断点Debug的方式详细观察DispatcherServlet的加载过程。

4.1.2.2 前端分发器

DispatcherServlet就是Spring MVC的前端分发器。

在运行时,从前端(一般都是浏览器发起一个http请求)过来的请求,被DispatcherServlet截获后,就调用doDispatch方法,寻求合适的处理器(参考下面代码片段中的第17行,mappedHandler = getHandler(processedRequest))处理这个请求。

    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;
                    }
                }

                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // Actually invoke the handler.
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                applyDefaultViewName(processedRequest, mv);
                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);
            }
            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);
                }
            }
        }
    }

更详细的处理过程,请在运行时跟踪doDispatch了解更多。

4.1.2.3 控制器方法

在Controller中的标注了@RequestMapping的方法就是控制器方法,也叫处理器(handler)。

如前所述,web应用启动时,会将所有的处理器扫描到DispatcherServlet的handlerMappings属性中,供其doDispatch方法匹配合适的处理器,然后执行处理器中的逻辑。

控制器方法(处理器)一般完成前端数据的准备封装工作,然后调用服务层方法完成特定的业务逻辑功能处理。

    @RequestMapping("get/{id}")
    public String getUser(@PathVariable int id){
        return userService.selectUser(id).toString();
    }

如上,getUser方法就是控制器方法,在运行时就是一个处理器。

4.1.2.4 视图解析器

视图解析器决定了控制器方法返回的视图如何处理,比如InternalResourceViewResolver就是用JSP视图技术的解析器。

image-20191120203703968

其构造器方法,会为其设置视图的前缀和后缀。

    public InternalResourceViewResolver(String prefix, String suffix) {
        this();
        setPrefix(prefix);
        setSuffix(suffix);
    }

在下面的配置片段场景中,如果控制器方法中返回“hello”字符串,则Spring MVC框架会找到/WEB-INF/views/hello.jsp这个页面,来渲染给浏览器。

    <!-- 这个类用于Spring MVC视图解析 -->
    <beans:bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <beans:property name="prefix" value="/WEB-INF/views/" />
        <beans:property name="suffix" value=".jsp" />
    </beans:bean>

4.1.2.5 动态资源

传统上,动态资源就是JSP文件。

一般而言,我们都会为DispatcherServlet配置拦截所有的前端请求,在控制器(Controller)方法返回的视图(View)中加载业务数据(Model)。

<servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:META-INF/spring/springmvc-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

4.1.2.6 静态资源

服务器上的视图(页面)资源,不都是JSP文件,还包括JavaScript文件、图片、CSS样式文件等,这些就是静态资源。静态资源都是在客户浏览器端加载的,显然并不需要DispatcherServlet拦截处理,更不能解析到/WEB-INF/views/arrow.png这样的错误位置上。

Spring MVC为静态资源提供了过滤机制。所有过滤掉的静态资源,DispatcherServlet直接放行。

<mvc:resources mapping="/images/**" location="/images/" />

4.1.2.7 一个完整的请求过程

前面,我们已经大致了解了Spring的运行机制,下面以浏览器发起http请求到接收到返回结果的全过程来加深理解Spring MVC的处理流程。

image-20191120234549136

  1. Spring MVC的Spring容器及前端分发器加载SpringMVC.xml配置文件,完成Spring MVC的初始化;
  2. 浏览器发起一个http请求(Request),被DispatcherServlet拦截到;
  3. DispatcherServlet查询handlerMappings匹配一个handler;
  4. 调用handler;
  5. handler调用服务层对应的方法,完成业务逻辑处理;
  6. 业务逻辑返回的结果被handler包装到一个ModelAndView中;
  7. DispatcherServlet通过ViewResolver获取解析视图;
  8. 将Model上的数据加载到View上;
  9. View(JSP页面)向浏览器返回http响应(Response)。

4.1.3 传统Spring MVC示例

为了更好的观察Spring MVC的配置、加载、运行机制,我们以一个用JSP页面的Spring MVC项目为例,来介绍我们前面介绍到的一些知识点。

本示例,使用的8.5版本的Tomcat。

创建maven项目,选择maven-archetype-webapp骨架。

image-20191121010319572

在pom文件中添加Spring MVC的依赖。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>

为项目配置web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    id="WebApp_ID" version="3.0">
    <display-name>spring-mvc-jsp</display-name>
    <servlet>
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>SpringMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

为Spring MVC前端分发器DispatcherServlet指定的配置文件为classpath:spring-context.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/mvc 
        http://www.springframework.org/schema/mvc/spring-mvc.xsd 
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.example"/>
    <mvc:annotation-driven/>
    <mvc:resources location="/images/" mapping="/images/**"/>
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>

</beans>

这个文件中配置了组件扫描路径(12行)、注解驱动(13行)和静态资源过滤(14行)。

为JSP视图解析器InternalResourceViewResolver配置了前缀、后缀属性。

创建一个控制器,提供一个控制器方法(请求处理器),服务项目上下文的/sayHello请求路径,返回一个ModelAndView:

  • Model中存储了模拟服务层返回的sayHello + who + "."内容,其键值为sayHello
  • View指定为/hello,根据视图解析器配置换算成真实文件为/WEB-INF/views/hello.jsp
package com.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class HelloController {

    @RequestMapping("/sayHello")
    public ModelAndView sayHello(String who) {
        ModelAndView mv = new ModelAndView();
        //模拟调用Service方法,返回问候语sayHello
        String sayHello = "Greeting! Hello ";
        mv.addObject("sayHello", sayHello + who + ".");
        mv.setViewName("/hello");
        return mv;
    }

}

编写hello.jsp文件,接收Model中携带的sayHello业务对象值(Model)。

并在其中插入一张图片,演示静态资源的使用。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Say Hello to WHO.</title>
</head>
<body>
    <img src="<%=request.getContextPath()%>/images/RoyElephant.png" width="128" height="128" />
    ${sayHello}
</body>
</html>

运行,测试,正确返回期望的结果。

image-20191121092015331

更进一步:可以在Debug模式下,设置断点,观察其内部数据。如DispatcherServlet的handlerMappings这个List里面注册了我们在控制器里面写的“请求处理器”sayHello(String)这个方法。

image-20191121012948657

以及我们在前面配置的视图处理器InternalResourceViewResolver

image-20191121013332130

可以清晰地看到当前请求所对应的处理器。

image-20191121013623431

更多源码跟踪解读,请读者自行完成。

本小节示例项目代码:

https://github.com/gyzhang/SpringBootCourseCode/tree/master/spring-mvc-jsp

results matching ""

    No results matching ""