互联网应用主流框架整合之SpringMVC初始化及各组件工作原理

Spring MVC的初始化和流程

MVC理念的发展

SpringMVC是Spring提供给Web应用领域的框架设计,MVC分别是Model-View-Controller的缩写,它是一个设计理念,不仅仅存在于Java中,各类语言及开发均可用,其运转流程和各组件的应用是MVC的根本

在早期的Java Web开发中,主要采用JSP+JavaBean模式,其设计理念在于解耦各个模块

很快,就发现JSP和JavaBean之间出现了严重的耦合,Java和HTML也耦合在了一起,这样开发者不仅需要掌握Java还需要掌握前端技术,更严重的是在软件工程过程中,出现了前端需要等后端,后端需要等前端,才能完成有效地测试乃至后续流程,而且每个过程几乎无法复制使用,甚至JSP完成了很多业务逻辑混杂着许多页面逻辑功能,很快这种方式就被Servlet+JSP+JavaBean代替了,就出现了新的模型

模型多了一个Servlet组件,主要作用是控制器,接受请求并调度JavaBean工作,然后结果返回给JSP展示给用户,使用了这种模式之后,前后端得到了一定的分离,控制器和模型层的分离也使得大量的Java代码可以重复使用,但后端代码仍存在一定的耦合,此时Struts1/2和作为模型层的Hibernate出现了

随着互联网的发展,页面大部分采用Ajax请求,他们之间的交互只需要JSON数据,对于JSP的依赖大大降低,而无论Struts1还是2和SJP都有比较紧密的联系,有大量关于JSP的jar包,加上Struts2出现的漏洞,开发者慢慢转向了SpringMVC

在互联网应用中,对于性能和灵活性的高要求又使得Hibernate无法强有力的支撑,并且数据库的迁移几率很小Hibernate用于移植数据库的特性也难发挥,MyBatis就变得更受开发者拥护;而随着NoSQL的强势和在互联网应用的发挥,已经不是传统的持久层框架能够完全处理的了,于是SpringMVC也给出了方案

在Service下可以用Spring的声明式事务操作数据访问层处理数据,在业务层上还允许拥护访问NoSQL,从而大大的提升互联网应用的性能;并且Spring MVC最大的特色是流程和组件是松散的,可以在Spring MVC中使用各类视图,包括JSON、JSP、XML、PDF等等能够满足手机端、平板端、PC端各类请求

Spring MVC组件与流程

Spring MVC的核心在于其流程和各类组件,这是Spring MVC的基础,它是一种基于Servlet的技术,提供了核心控制器DispatcherServlet和相关的组件,并制定了松散的结构,从而适应灵活的需要,Spring MVC框架是围绕着DispatcherServlet工作的,它是个Servlet,可以拦截HTTP发送过来的请求,在Servlet初始化(调用init方法)时,Spring MVC会根据配置获取信息,从而得到URI(Uniform Resource Identifier)和处理器(Handler)之间的映射关系,为了更加灵活并增强功能,SpringMVC还给处理器加入拦截器,所以还可以在处理器执行官前后加入自己的代码,这样就构成了一个处理器的执行链(HandlerExecutionChain);根据上下文初始化视图解析器等内容,当处理器返回时可以通过视图解析器定位视图,将数据模型渲染到视图中,用来响应用户的请求

当一个请求到来时,DispatcherServlet通过请求和事前解析好的HandlerMapping配置,找到对应的处理器(Handler),准备开始运行处理器和拦截器组成的执行链,而运行处理器需要一个对应的环境,这样它就有了一个处理器的适配器(HandlerAdaper),通过这个适配器能运行对应的处理器和拦截器,这里的处理器包含了控制器的内容和其他增强的功能;在处理器返回模型和视图给DispatcherServlet后,DispatcherServlet就会把对应的视图信息传递给视图解析器(ViewResolver),而视图解析器不是必需的,取决于是否使用逻辑视图,如果使用逻辑视图,那么视图解析器就会解析它,把模型渲染到视图中去,响应用户的请求,如果不适用逻辑视图,则不会进行处理,直接通过视图渲染数据库模型,这就是Spring MVC完整的流程,SpringMVC也提供了大量的类库用于支撑这个体系,大部分组件不需要开发者自己实现

Spring MVC实例

SpringMVC的体系既可以使用XML配置的方式也可以使用注解的方式,首先引入所需要的依赖包

xml 复制代码
    <!-- 引入Spring Web和MVC框架的依赖 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>
    <!-- 引入Jackson JSON处理依赖,用于Spring MVC的模型绑定和JSON序列化/反序列化 -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.10.1</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.10.1</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-annotations</artifactId>
      <version>2.10.1</version>
    </dependency>

配置WEB项目的web.xml文件

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1"
         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID">

  <!-- 配置Spring IoC(Inversion of Control)容器的配置文件路径 -->
  <!-- 默认值是/WEB-INF/applicationContext.xml文件 -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
  </context-param>
  <!-- 配置ContextLoaderListener监听器,用于初始化Spring IoC容器 -->
  <!-- 当Web应用启动时,该监听器会读取在<context-param>中设置的配置文件 -->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <!-- 配置Spring MVC的核心组件DispatcherServlet -->
  <!-- 它负责处理HTTP请求,并将请求分发到相应的控制器 -->
  <servlet>
    <!-- servlet名称,用于在web.xml中引用和在URL中匹配 -->
    <servlet-name>dispatcher</servlet-name>
    <!-- 指定Servlet实现类 -->
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 指定servlet在服务器启动时加载的优先级 -->
    <!-- 数字越小,优先级越高。默认值是-1,表示不自动加载 -->
    <!-- 这里设置为2,意味着在ContextLoaderListener之后加载 -->
    <load-on-startup>2</load-on-startup>
  </servlet>
  <!-- 配置Servlet的映射规则 -->
  <!-- 所有以/mvc/开头的URL请求都将被DispatcherServlet处理 -->
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/mvc/*</url-pattern>
  </servlet-mapping>
  <!-- 欢迎文件列表配置 -->
  <!-- 当用户访问没有明确资源的目录时,服务器会尝试显示这些欢迎文件 -->
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
</web-app>
  • 系统变量contextConfigLocation会告诉Spring MVC其Spring IoC的配置文件位置,Spring根据位置加载,如果是多个文件用逗号隔开,也可以使用正则式进行模糊匹配,默认值是/WEB-INF/applicationContext.xml
  • ContextLoaderListener实现了接口ServletContextListener,ServletContextListener的作用是在整个Web项目前后加入自定义代码,从而可以在Web项目初始化之前,完成对SpringIoC容器的初始化,可以在Web项目关闭的时候,完成对SpringIoC容器资源的释放
  • 在配置DispatcherServlet时,设置servlet-name为"dispatcher",在SpringMVC中需要一个/WEB-INF/dispatcher-servlet.xml文件(注意servlet-name和文件名的对应关系)与之对应,并且设置在服务器启动期间就初始化它
  • 配置DispatcherServlet,并且拦截匹配正则式/mvc/*的请求,这样可以限定拦截范围
  • applicationContext.xml的配置代码中未进行配置,这样SpringIoC就不会装载自己的类
  • dispatcher-servlet.xml文件内容如下,其中定义了视图解析器,解析器定义了前缀和后缀,这样视图就知道找到什么样的文件作为视图响应用户请求了
xml 复制代码
<?xml version='1.0' encoding='UTF-8' ?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
	    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/tx 
        http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/mvc 
        http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
	<!-- 使用注解驱动 -->
	<mvc:annotation-driven />

	<!-- 定义扫描装载的包 -->
	<!-- 会自动搜索com包及其子包下所有的组件 -->
	<context:component-scan base-package="com.*" />

	<!-- 定义视图解析器 -->
	<!-- 解析请求并返回对应的WEB-INF/jsp下的JSP页面 -->
	<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />
</beans>

定义一个Controller,代码如下

java 复制代码
package com.ssmvc.controller;

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

/**
 * DemoController类用于处理与演示相关的HTTP请求。
 * 通过@Controller注解,该类被标识为一个Spring MVC控制器。
 * @Controller("demoController")注解为该控制器指定了一个别名,便于在配置文件中引用。
 */
@Controller("demoController")
@RequestMapping("/HelloFresh")
public class MyController {

    /**
     * 处理访问/index路径的HTTP请求。
     * 该方法返回一个ModelAndView对象,其中viewName属性设置为"index"。
     * 这意味着请求将被重定向到名为index的视图。
     *
     * @return ModelAndView对象,其中包含用于渲染视图的信息。
     */
    @RequestMapping("/index")
    public ModelAndView index() {
        ModelAndView mv = new ModelAndView();
        mv.setViewName("index");
        return mv;
    }

}
  • @Controller指明当前类为控制器,SpringMVC扫描的时候就会把它作为控制器加载进来
  • @RequestMapping指定了所匹配请求的URI,SpringMVC在初始化的时候会将这些信息解析保存,于是便有了HandlerMapping,当发生请求时,DispatcherServlet在拦截请求后,可以通过请求的URL和相关信息在HandlerMapping机制中找到对应的控制器和方法
  • 方法定义返回ModelAndView,在方法中把视图名称定义为index,由于配置的前缀是/WEB-INF/jsp/后缀是.jsp,因此它会选择使用/WEB-INF/jsp/index.jsp作为最后的响应,jsp代码如下
jsp 复制代码
<%@page contentType="text/html" 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>Chapter15</title>
	</head>
	<body>
		<h1>Hello, Spring MVC</h1>
	</body>
</html>

IDEA启动Tomcat,并访问链接http://localhost:8080/SSMVC_war/mvc/HelloFresh/indexTomat9+JDK1.8+Spring5.2.1.RELEASE,其他的环境会有兼容性问题,再次不多赘述各组件版本之间兼容性,Tomcat服务器配置如下



如图所示,图中展示了实例的组件和流程,Spring MVC启动的时候就会去解析MyController的注解,生成对应URI和请求的映射关系,并注册对应的方法;当请求来了的时候,SpringMVC首先根据URI找到对应的HandlerMapping,然后组织为一个执行链(HandlerExecutionChain),通过请求类型找到RequestMappingHandlerAdaper实例,该实例是在DispatcherServlet初始化的时候创建的;接下来通过它去执行HandlerExecutionChain的内容,最终在MyController的方法中将"index"视图返回DispatcherServlet,由于配置的视图解析器(InternalResourceViewResolver)前缀为/WEB-INF/jsp/后缀为.jsp视图名为index,所以最后它会找到/WEB-INF/jsp/index.jsp文件作为视图,响应最终的请求
以上就完成了一个Spring MVC最简单的一个实例

Spring MVC初始化

整个Spring MVC的流程中,配置了DispatcherServlet和ContextLoaderListener,在初始化时,它们要初始化Spring IoC容器上下文和映射请求上下文,其中映射请求上下文是基于Spring IoC上下文扩展出来的,以适应Java Web项目的需要

初始化Spring IoC上下文

在Web容器的规范中,存在一个ServletContextListener接口,它允许我们在Web容器初始化和结束期执行一定的逻辑,换句话说就是通过实现它可以在DispatcherServlet初始化之前完成SpringIoC容器的初始化,可以再结束期销毁Spring IoC容器

java 复制代码
package org.springframework.web.context;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
 * ContextLoaderListener 类实现了 ServletContextListener 接口,
 * 用于在 Servlet 应用上下文中初始化和销毁 Spring 的 WebApplicationContext。
 * 它是 Spring Web 应用程序的核心组件之一,负责在 Servlet 应用程序启动时创建和配置ApplicationContext,
 * 并在应用程序停止时关闭ApplicationContext,以管理应用程序的生命周期。
 */
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    /**
     * 默认构造函数,用于创建一个没有关联 WebApplicationContext 的 ContextLoaderListener。
     * 在实例化后,通常会通过 ServletContext 的 init 参数来指定要关联的 ApplicationContext。
     */
    public ContextLoaderListener() {
    }

    /**
     * 带有 WebApplicationContext 参数的构造函数,用于明确指定要与此 ContextLoaderListener 关联的 ApplicationContext。
     *
     * @param context 要关联的 WebApplicationContext。
     */
    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }

    /**
     * ServletContextListener 接口的实现方法。
     * 当 ServletContext 初始化时调用此方法,用于初始化 WebApplicationContext。
     *
     * @param event ServletContextEvent,包含 ServletContext 相关的信息。
     */
    public void contextInitialized(ServletContextEvent event) {
        this.initWebApplicationContext(event.getServletContext());
    }

    /**
     * ServletContextListener 接口的实现方法。
     * 当 ServletContext 销毁时调用此方法,用于关闭 WebApplicationContext 并进行清理工作。
     *
     * @param event ServletContextEvent,包含 ServletContext 相关的信息。
     */
    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

通过这个类可以在Web项目的上下文使用Spring IoC容器管理整个Web项目的资源

初始化映射请求上下文

映射请求上下文是通过DispatcherServlet初始化的,它和普通的Servlet是一样的,可以根据自己的需要设置它在启动时初始化,或者在等待用户第一次请求时候初始化,如果在Web项目中并没有注册ContextLoaderListener,这个时候DispatcherServlet就会在其初始化的时候对Spring IoC容器进行初始化

由于初始化Spring IoC容器是一个耗时的操作,这个工作不应该放到用户请求上,在大部分场景下,都应该让DispatcherServlet在服务器启动期间完成Spring IoC容器的初始化,可以是Web容器启动时,也可以在Web容器载入DispatcherServlet的时候进行初始化,最好是在Web容器刚启动的时候对其进行初始化,因为在整个Web项目的初始化过程中,不只DispatcherServlet需要使用到Spring IoC的资源,在开始时初始化可以让Web中的各个组件共享资源;当然可以指定Web容器中组件得初始化顺序,让DispatcherServlet第一个初始化,只是这样就增加了配置的复杂度,因此在绝大多数情况下使用ContextLoaderListener初始化Spring IoC容器更佳

从类的关系图中可以看出,DispatcherServlet的父类是FrameworkServlet,FrameworkServlet的父类是HttpServletBean,HttpServletBean继承了Web容器提供的HttpServlet

Web容器初始化,会调用其init方法,对于DispatcherServlet也是如此,这个方法位于其父类HttpServletBean中,源码如下

java 复制代码
    /**
     * 创建一个标准的Servlet环境。
     * 
     * @return ConfigurableEnvironment 一个可配置的环境实例,用于Servlet环境配置。
     */
    protected ConfigurableEnvironment createEnvironment() {
        return new StandardServletEnvironment();
    }

    /**
     * 初始化Servlet。
     * 此方法用于初始化Servlet实例,包括设置Servlet属性、初始化BeanWrapper以及初始化Servlet bean。
     * 
     * @throws ServletException 如果初始化过程中出现错误。
     */
    public final void init() throws ServletException {
        // 从Servlet配置中创建属性值集合
        PropertyValues pvs = new ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
        // 如果属性值不为空,则进行属性设置
        if (!pvs.isEmpty()) {
            try {
                // 创建BeanWrapper用于属性设置和bean访问
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                // 创建ServletContextResourceLoader,用于加载资源
                ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
                // 注册自定义编辑器,用于处理Resource类型的属性
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
                // 初始化BeanWrapper,可以被子类重写以提供额外的初始化逻辑
                this.initBeanWrapper(bw);
                // 设置属性值到BeanWrapper中,强制覆盖现有值
                bw.setPropertyValues(pvs, true);
            } catch (BeansException var4) {
                // 如果在设置bean属性过程中出现BeansException,则记录错误并重新抛出异常
                if (this.logger.isErrorEnabled()) {
                    this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);
                }

                throw var4;
            }
        }

        // 初始化Servlet bean,此方法可以被子类重写以提供特定的初始化逻辑
        this.initServletBean();
    }

    /**
     * 初始化BeanWrapper。
     * 此方法为保护类型的抽象方法,用于子类提供BeanWrapper的初始化逻辑。
     * 
     * @param bw 要初始化的BeanWrapper实例。
     * @throws BeansException 如果初始化过程中出现错误。
     */
    protected void initBeanWrapper(BeanWrapper bw) throws BeansException {
    }

    /**
     * 初始化Servlet bean。
     * 此方法为保护类型的抽象方法,用于子类进行特定的Servlet bean初始化。
     * 
     * @throws ServletException 如果初始化过程中出现错误。
     */
    protected void initServletBean() throws ServletException {
    }

在HttpServletBean中可以看到initServletBean方法,在FrameworkServlet中也可以看到,子类方法会覆盖父类的方法,FrameworkServlet的initServletBean()源码如下

java 复制代码
    /**
     * 初始化Servlet Bean。
     * 这个方法负责Spring Servlet的初始化过程,包括日志记录、初始化WebApplicationContext和FrameworkServlet。
     * 如果在初始化过程中遇到RuntimeException或ServletException,将记录错误并重新抛出异常。
     * @throws ServletException 如果在初始化Servlet时发生错误。
     */
    protected final void initServletBean() throws ServletException {
        // 记录Servlet初始化开始的日志。
        this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
        }

        // 记录初始化开始时间。
        long startTime = System.currentTimeMillis();

        try {
            // 初始化WebApplicationContext和FrameworkServlet。
            this.webApplicationContext = this.initWebApplicationContext();
            this.initFrameworkServlet();
        } catch (RuntimeException | ServletException var4) {
            // 记录初始化失败的日志并抛出异常。
            this.logger.error("Context initialization failed", var4);
            throw var4;
        }

        // 根据enableLoggingRequestDetails的值,记录关于请求参数和头信息日志的详细程度。
        if (this.logger.isDebugEnabled()) {
            String value = this.enableLoggingRequestDetails ? "shown which may lead to unsafe logging of potentially sensitive data" : "masked to prevent unsafe logging of potentially sensitive data";
            this.logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value);
        }

        // 记录初始化完成的日志。
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
        }

    }

    /**
     * 初始化Web应用程序上下文。
     * 此方法负责获取或创建WebApplicationContext,并根据需要配置和刷新它。
     * 如果已经存在webApplicationContext,则检查它是否是一个可配置的上下文,并且是否尚未激活。
     * 如果尚未激活,它将被配置并刷新。如果不存在webApplicationContext,则尝试查找或创建一个新的上下文。
     * 最后,如果配置了发布上下文属性,则将上下文发布到Servlet上下文中。
     *
     * @return WebApplicationContext 应用程序上下文实例
     */
    protected WebApplicationContext initWebApplicationContext() {
        // 尝试从当前Servlet上下文中获取根应用程序上下文
        WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
        WebApplicationContext wac = null;
        
        // 如果已经存在webApplicationContext,则进行进一步的配置和检查
        if (this.webApplicationContext != null) {
            wac = this.webApplicationContext;
            // 如果当前上下文是可配置的,并且尚未激活
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
                // 如果当前上下文没有父上下文,则设置根上下文作为其父上下文
                if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
                        cwac.setParent(rootContext);
                    }
                    // 配置并刷新当前上下文
                    this.configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }

        // 如果当前上下文为null,则尝试查找或创建一个新的上下文
        if (wac == null) {
            wac = this.findWebApplicationContext();
        }
        if (wac == null) {
            wac = this.createWebApplicationContext(rootContext);
        }

        // 如果尚未接收到刷新事件,则同步刷新上下文
        if (!this.refreshEventReceived) {
            synchronized(this.onRefreshMonitor) {
                this.onRefresh(wac);
            }
        }

        // 如果配置了发布上下文属性,则将其发布到Servlet上下文中
        if (this.publishContext) {
            String attrName = this.getServletContextAttributeName();
            this.getServletContext().setAttribute(attrName, wac);
        }

        // 返回最终配置和刷新的应用程序上下文
        return wac;
    }

当IoC容器没有对应的初始化时,DispatcherServlet会常识去初始化它,调度onRefresh方法,该方法将初始化Spring MVC各个组件,该方法源码如下

java 复制代码
    /**
     * 在上下文刷新时初始化策略。
     * 此方法在应用程序上下文被刷新时被调用,用于初始化一系列的策略对象,这些对象是处理Web请求所必需的。
     * @param context 应用程序上下文,提供对应用程序范围内的Bean和其他服务的访问。
     */
    protected void onRefresh(ApplicationContext context) {
        this.initStrategies(context);
    }
        /**
     * 初始化各种策略实例。
     * 此方法负责在应用程序上下文加载后,初始化处理请求、解析请求参数、处理异常、解析视图等各个环节所需的策略实例。
     * 这些策略包括但不限于:多部分解析器、地域解析器、主题解析器、处理器映射、处理器适配器、异常解析器、请求到视图名的翻译器、视图解析器和闪现映射管理器。
     * 通过这种方式,框架可以灵活地根据配置来调整其行为,提供高度可定制的服务。
     *
     * @param context 应用程序上下文,用于获取配置和服务实例。
     */
    protected void initStrategies(ApplicationContext context) {
        // 初始化多部分解析器,用于处理上传文件和表单数据。
        this.initMultipartResolver(context);
        // 初始化地域解析器,用于处理地域相关的请求和响应。
        this.initLocaleResolver(context);
        // 初始化主题解析器,用于处理主题相关的请求和响应。
        this.initThemeResolver(context);
        // 初始化处理器映射,用于映射请求到特定的处理器。
        this.initHandlerMappings(context);
        // 初始化处理器适配器,用于适配和调用各种类型的处理器。
        this.initHandlerAdapters(context);
        // 初始化异常解析器,用于处理处理器执行过程中抛出的异常。
        this.initHandlerExceptionResolvers(context);
        // 初始化请求到视图名的翻译器,用于将处理结果翻译成对应的视图名。
        this.initRequestToViewNameTranslator(context);
        // 初始化视图解析器,用于解析视图名并渲染相应的视图。
        this.initViewResolvers(context);
        // 初始化闪现映射管理器,用于管理闪现消息和数据。
        this.initFlashMapManager(context);
    }

事实上,对于这些组件,DispatcherServlet会根据其配置文件DispatcherServlet.properties进行初始化

properties 复制代码
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.
# 为DispatcherServlet的策略接口提供默认的实现类。
# 当在DispatcherServlet的应用上下文中找不到匹配的bean时,这些配置用作备用选项。
# 不建议应用程序开发者对此进行自定义。

# 配置默认的LocaleResolver实现,用于解析请求中的地域信息。
# 默认使用AcceptHeaderLocaleResolver,根据请求头中的Accept-Language字段解析地域信息。
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

# 配置默认的ThemeResolver实现,用于解析请求中的主题信息。
# 默认使用FixedThemeResolver,固定的主题解析策略。
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

# 配置默认的HandlerMapping实现,用于映射请求到处理器。
# 列出了多个实现,按照优先级从高到低排列,DispatcherServlet会按顺序尝试解析请求。
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
	org.springframework.web.servlet.function.support.RouterFunctionMapping

# 配置默认的HandlerAdapter实现,用于适配不同的处理器并执行它们。
# 列出了多个实现,支持处理不同类型的操作,包括HttpRequestHandler、Controller、HandlerFunction等。
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
	org.springframework.web.servlet.function.support.HandlerFunctionAdapter

# 配置默认的HandlerExceptionResolver实现,用于处理处理器执行过程中抛出的异常。
# 列出了多个实现,按照优先级从高到低排列,用于映射不同的异常到对应的处理逻辑。
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
	org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

# 配置默认的RequestToViewNameTranslator实现,用于将请求映射到视图名称。
# 默认使用DefaultRequestToViewNameTranslator,根据请求路径规则推断视图名称。
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

# 配置默认的ViewResolver实现,用于解析视图。
# 默认使用InternalResourceViewResolver,支持JSP和其他内部资源的解析。
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

# 配置默认的FlashMapManager实现,用于管理Flash属性。
# 默认使用SessionFlashMapManager,将Flash属性存储在会话中,以支持请求之间的数据传递。
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

这便解释了在启动期间,DispatcherServlet会加载这些配置的组件进行初始化,不需要很多配置就能够使用Spring MVC的原因

使用注解配置方式初始化

Servlet3.0之后的规范允许取消web.xml配置,只使用注解方式,Spring3.1之后的版本也提供了注解方式的配置,使用注解方式很简单,先继承一个抽象类AbstractAnnotationConfigDispatcherServletInitializer,再实现它所以定义的方法,使用起来非常方便,如下代码所示

java 复制代码
package com.ssmvc.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import com.ssmvc.backend.config.BackendConfig;
import com.ssmvc.web.config.WebConfig;
/**
 * 自定义Web应用程序初始化器,继承自Spring框架的AbstractAnnotationConfigDispatcherServletInitializer类。
 * 该类用于配置Spring MVC的DispatcherServlet初始化参数,以及应用程序的上下文配置。
 */
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * 配置根应用程序上下文的Java配置类。
     * 根上下文通常用于配置共享于整个应用程序的bean,如数据源、事务管理器等。
     *
     * @return Java配置类的数组。
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        // 可以返回Spring的Java配置文件数组
        return new Class<?>[] { BackendConfig.class };
    }

    /**
     * 配置DispatcherServlet实例的应用程序上下文的Java配置类。
     * Servlet上下文通常用于配置与Web层相关的bean,如控制器、视图解析器等。
     * 
     * @return Java配置类的数组。
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        // 可以返回Spring的Java配置文件数组
        return new Class<?>[] { WebConfig.class };
    }

    /**
     * 配置DispatcherServlet的URL映射。
     * 该方法定义了哪些URL路径应该由DispatcherServlet处理。
     * 
     * @return URL模式的数组。
     */
    @Override
    protected String[] getServletMappings() {
        return new String[] { "/mvc/*" };
    }
}

继承了AbstractAnnotationConfigDispatcherServletInitializer,Spring MVC就会加载这个类文件,Servlet3.0之后的版本允许扫描加载Servlet,只需要按照规范实现ServletContainerInitializer接口,Web容器就会把对应的Servlet加载进来,于是Spring MVC框架在自己的包内实现了一个类SpringServletContainerInitializer,它实现了ServletContainerInitializer接口,可以通过它去加载用户提供的MyWebAppInitializer,看一下源码

java 复制代码
package org.springframework.web;

import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;

/**
 * 实现{@link ServletContainerInitializer},以便在Servlet 3.0+容器中自动检测和初始化Spring的{@link WebApplicationInitializer}。
 * <p>
 * 该类通过反射实例化找到的所有WebApplicationInitializer实现,并按照它们的顺序调用{@link WebApplicationInitializer#onStartup(ServletContext)}方法。
 * 如果没有找到任何实现,则不执行任何操作。
 */
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    /**
     * 默认构造函数。
     */
    public SpringServletContainerInitializer() {
    }

    /**
     * 当Servlet容器启动时调用此方法。
     * <p>
     * 它负责检测并实例化所有的{@link WebApplicationInitializer}实现,然后按照它们的顺序初始化Spring应用程序。
     *
     * @param webAppInitializerClasses Servlet容器检测到的WebApplicationInitializer类的集合。
     * @param servletContext           Servlet上下文对象,用于初始化Spring应用程序。
     * @throws ServletException 如果在实例化WebApplicationInitializer实现时发生错误。
     */
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
        List<WebApplicationInitializer> initializers = new LinkedList<>();
        // 如果提供了WebApplicationInitializer类的集合,则遍历该集合
        if (webAppInitializerClasses != null) {
            for (Class<?> waiClass : webAppInitializerClasses) {
                // 检查类是否为接口或抽象类,以及是否实现WebApplicationInitializer接口
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        // 通过无参构造函数实例化WebApplicationInitializer实现
                        initializers.add((WebApplicationInitializer) ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance());
                    } catch (Throwable var7) {
                        // 如果实例化失败,抛出ServletException
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
                    }
                }
            }
        }

        // 如果没有找到任何WebApplicationInitializer实现,记录日志
        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
        } else {
            // 如果找到了实现,记录日志并按排序顺序调用每个initializer的onStartup方法
            servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
            AnnotationAwareOrderComparator.sort(initializers);
            for (WebApplicationInitializer initializer : initializers) {
                initializer.onStartup(servletContext);
            }
        }
    }
}

从源码中可以看到,只要实现了WebApplicationInitializer接口的onStartup方法,Spring MVC就会把类当作一个初始化器加载进来

图中可以看到,有5个类实现了WebApplicationInitializer接口,该接口是一个通用的Spring Web初始化接口:

  • AbstractContextLoaderInitializer抽象类用于初始化Spring IoC容器
  • AbstractDispatcherServletInitializer用于初始化DispatcherServlet,映射(HandlerMapping)关系
  • AbstractAnnotationConfigDispatcherServletInitializer暴露给用户使用的Spring初始化器方法
  • AbstractReactiveWebInitializer是一个响应式编程的初始化器

除了AbstractReactiveWebInitializer,另外的抽象类之间也是继承的关系

代码中定义的MyWebAppInitializer类继承了抽象类AbstractAnnotationConfigDispatcherServletInitializer,并实现了三个方法,其中

  • getRootConfigClasses()用于获取Spring IoC容器的Java配置类,用以装载各类Spring Bean
  • getServletConfigClasses()用于获取各类Spring MVC的URI和控制器的配置关系类,用于生成Web请求的上下
  • getServletMappings()定义DispatcherServlet拦截请求的范围,比如通过正则式"/mvc/*"限制拦截

getRootConfigClasses方法返回配置BackendConfig类,用于Spring应用程序的配置,包括数据源、MyBatis、事务管理器等的初始化,BackendConfig如下所示

java 复制代码
package com.ssmvc.backend.config;

import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;
import org.springframework.web.bind.annotation.RestController;


@Configuration
// 扫描所有com.ssmvc包下的组件,但排除@Controller和@RestController注解的类
@ComponentScan(basePackages = "com.ssmvc.*", excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = {Controller.class, RestController.class}))
@EnableTransactionManagement // 开启事务管理
@MapperScan(basePackages = "com.ssmvc", annotationClass = Mapper.class) // 扫描Mapper接口
public class BackendConfig implements TransactionManagementConfigurer {

    // 数据源实例
    // 数据源
    private DataSource dataSource = null;

    /**
     * 初始化数据源。
     * 使用Apache Commons DBCP2创建数据源实例。
     *
     * @return DataSource 数据源实例
     */
    @Bean(name = "dataSource")
    public DataSource initDataSource() {
        if (dataSource != null) {
            return dataSource;
        }
        Properties props = new Properties();
        // 配置数据库连接信息和连接池参数
        props.setProperty("driverClassName", "com.mysql.jdbc.Driver");
        props.setProperty("url", "jdbc:mysql://localhost:3306/ssm");
        props.setProperty("username", "root");
        props.setProperty("password", "123456");
        props.setProperty("maxActive", "200");
        props.setProperty("maxIdle", "20");
        props.setProperty("maxWait", "30000");
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

    /**
     * 初始化JdbcTemplate。
     * 为Spring提供的简化JDBC操作的工具类配置数据源。
     *
     * @param dataSource 数据源
     * @return JdbcTemplate 实例
     */
    @Bean(name = "jdbcTemplate")
    public JdbcTemplate initjdbcTemplate(@Autowired DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    /**
     * 配置基于注解的事务管理器。
     * 使用DataSourceTransactionManager作为事务管理器,并配置数据源。
     *
     * @return PlatformTransactionManager 事务管理器实例
     */
    @Override
    @Bean(name = "transactionManager")
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        DataSourceTransactionManager transactionManager
                = new DataSourceTransactionManager();
        transactionManager.setDataSource(initDataSource());
        return transactionManager;
    }

    /**
     * 创建SqlSessionFactory。
     * 配置MyBatis的SqlSessionFactoryBean,并指定数据源和配置文件位置。
     *
     * @param dataSource 数据源
     * @return SqlSessionFactory 实例
     * @throws Exception
     */
    @Bean("sqlSessionFactory")
    public SqlSessionFactory createSqlSessionFactoryBean(
            @Autowired DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean
                = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        // 指定MyBatis配置文件位置
        String cfgFile = "mybatis-config.xml";
        Resource configLocation = new ClassPathResource(cfgFile);
        sqlSessionFactoryBean.setConfigLocation(configLocation);
        return sqlSessionFactoryBean.getObject();
    }

}

getServletConfigClasses方法返回配置WebConfig类,如下是WebConfig代码,它是一个URI和控制器的映射关系类,由此产生Web请求的上下文,代码如下

java 复制代码
package com.ssmvc.web.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * Spring MVC的配置类
 * 通过@Configuration注解标记这个类是一个配置类,等同于Spring XML配置文件
 * 使用@EnableWebMvc注解开启Spring MVC的功能
 * 使用@ComponentScan注解指定Spring要扫描的组件包,这些包中的组件会被自动注册为Spring Bean
 */
@Configuration
@ComponentScan("com.ssmvc.controller")
@EnableWebMvc
public class WebConfig {

    /**
     * 配置InternalResourceViewResolver,作为Spring MVC的视图解析器
     * 通过@Bean注解标记这个方法会返回一个Bean,该Bean会被注册到Spring的ApplicationContext中
     * 方法名称viewResolver和@Bean注解中的name属性一起定义了这个Bean的名称
     * @return 返回一个配置好的InternalResourceViewResolver实例
     */
    @Bean(name = "viewResolver")
    public ViewResolver initViewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}

这样就通过Java配置的方式完成了之前在XML(web.xml和Spring IoC容器所需要的XML文件)里的配置,执行效果一样,注意不能共存,如果使用Java配置,那之前的xml配置文件要删掉(web.xml&dispatcher-servlet.xml&applicationContext.xml)

WebMvcConfigurer接口

上面代码中配置了视图解析器,实际上,为了方便配置各类组件,Spring MVC提供了接口WebMvcConfigurer,这是一个允许定制SpringMVC组件得接口,有时候使用它会更佳方便和简便,接口源码如下:

java 复制代码
package org.springframework.web.servlet.config.annotation;

/**
 * 配置Spring MVC的接口。
 * 该接口提供了多种方法,用于定制Spring MVC的各个方面的行为,例如路径匹配、内容协商、异步支持等。
 * 通过实现这个接口,开发者可以精细地控制Spring MVC的应用程序行为。
 */
public interface WebMvcConfigurer {
    /**
     * 配置路径匹配的设置。
     * 使用{@link PathMatchConfigurer}可以定制路径匹配的行为,例如启用Ant风格的路径模式、配置路径变量的解析等。
     *
     * @param configurer 用于配置路径匹配的工具类。
     */
    default void configurePathMatch(PathMatchConfigurer configurer) {
    }

    /**
     * 配置内容协商的行为。
     * 内容协商允许服务器根据客户端的请求头(如Accept)来选择合适的响应格式。通过这个方法,可以定制内容协商的策略和配置。
     *
     * @param configurer 用于配置内容协商的工具类。
     */
    default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    }

    /**
     * 配置异步请求的支持。
     * 使用{@link AsyncSupportConfigurer}可以配置Spring MVC如何处理异步请求,例如设置异步请求的超时时间、配置异步请求的处理器等。
     *
     * @param configurer 用于配置异步支持的工具类。
     */
    default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    }

    /**
     * 配置默认Servlet的处理行为。
     * 通过这个方法,可以决定Spring MVC如何处理静态资源请求,或者将某些请求代理给默认的Servlet处理。
     *
     * @param configurer 用于配置默认Servlet处理的工具类。
     */
    default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    }

    /**
     * 注册格式化器(Formatter)。
     * 格式化器用于将字符串转换为对象,或者将对象转换为字符串。通过这个方法,可以向Spring MVC添加自定义的格式化器。
     *
     * @param registry 用于注册格式化器的工具类。
     */
    default void addFormatters(FormatterRegistry registry) {
    }

    /**
     * 注册拦截器。
     * 拦截器可以在请求处理之前、之后或者发生异常时执行自定义逻辑。通过这个方法,可以向Spring MVC添加自定义的拦截器。
     *
     * @param registry 用于注册拦截器的工具类。
     */
    default void addInterceptors(InterceptorRegistry registry) {
    }

    /**
     * 注册资源处理器。
     * 资源处理器用于处理静态资源请求,例如CSS、JavaScript文件。通过这个方法,可以向Spring MVC添加自定义的资源处理器。
     *
     * @param registry 用于注册资源处理器的工具类。
     */
    default void addResourceHandlers(ResourceHandlerRegistry registry) {
    }

    /**
     * 注册CORS(跨域资源共享)映射。
     * CORS允许Web应用程序从不同的域访问资源。通过这个方法,可以向Spring MVC添加自定义的CORS映射规则。
     *
     * @param registry 用于注册CORS映射的工具类。
     */
    default void addCorsMappings(CorsRegistry registry) {
    }

    /**
     * 注册ViewController。
     * ViewController将特定的URL路径直接映射到视图,而不需要通过控制器处理。通过这个方法,可以向Spring MVC添加自定义的ViewController。
     *
     * @param registry 用于注册ViewController的工具类。
     */
    default void addViewControllers(ViewControllerRegistry registry) {
    }

    /**
     * 配置视图解析器。
     * 视图解析器负责将逻辑视图名解析为实际的视图对象。通过这个方法,可以向Spring MVC添加自定义的视图解析器或者配置已有的视图解析器。
     *
     * @param registry 用于配置视图解析器的工具类。
     */
    default void configureViewResolvers(ViewResolverRegistry registry) {
    }

    /**
     * 注册方法参数解析器。
     * 方法参数解析器用于解析控制器方法的参数。通过这个方法,可以向Spring MVC添加自定义的方法参数解析器。
     *
     * @param resolvers 方法参数解析器的列表。
     */
    default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    }

    /**
     * 注册方法返回值处理器。
     * 方法返回值处理器用于处理控制器方法的返回值。通过这个方法,可以向Spring MVC添加自定义的方法返回值处理器。
     *
     * @param handlers 方法返回值处理器的列表。
     */
    default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
    }

    /**
     * 配置HTTP消息转换器。
     * HTTP消息转换器用于将HTTP请求体中的数据转换为Java对象,或将Java对象转换为HTTP响应体中的数据。通过这个方法,可以向Spring MVC添加自定义的HTTP消息转换器或者配置已有的消息转换器。
     *
     * @param converters HTTP消息转换器的列表。
     */
    default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

    /**
     * 扩展HTTP消息转换器。
     * 这个方法与configureMessageConverters类似,但是它允许在已有的消息转换器列表上进行扩展,而不是替换。
     *
     * @param converters HTTP消息转换器的列表。
     */
    default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

    /**
     * 配置异常处理器。
     * 异常处理器用于处理控制器方法抛出的异常。通过这个方法,可以向Spring MVC添加自定义的异常处理器。
     *
     * @param resolvers 异常处理器的列表。
     */
    default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    /**
     * 扩展异常处理器。
     * 这个方法与configureHandlerExceptionResolvers类似,但是它允许在已有的异常处理器列表上进行扩展,而不是替换。
     *
     * @param resolvers 异常处理器的列表。
     */
    default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    /**
     * 获取验证器。
     * 这个方法允许返回一个自定义的验证器,用于验证控制器方法的参数或者模型对象。如果不需要自定义验证器,可以返回null。
     *
     * @return 验证器实例,或者null。
     */
    @Nullable
    default Validator getValidator() {
        return null;
    }

    /**
     * 获取消息代码解析器。
     * 消息代码解析器用于解析验证错误的消息代码。通过这个方法,可以返回一个自定义的消息代码解析器,或者null。
     *
     * @return 消息代码解析器实例,或者null。
     */
    @Nullable
    default MessageCodesResolver getMessageCodesResolver() {
        return null;
    }
}

其中有两个比较常用的方法,分别是configureViewResolversaddViewControllers, 据此,改造一下WebConfig类,代码如下

java 复制代码
package com.ssmvc.web.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;


/**
 * Spring MVC的配置类
 * 通过@Configuration注解标记这个类是一个配置类,等同于Spring XML配置文件
 * 使用@EnableWebMvc注解开启Spring MVC的功能
 * 使用@ComponentScan注解指定Spring要扫描的组件包,这些包中的组件会被自动注册为Spring Bean
 */
@Configuration
@ComponentScan("com.ssmvc.controller")
@EnableWebMvc
public class WebConfigII implements WebMvcConfigurer {

    /**
     * 配置视图解析器
     * 本方法用于设置Spring MVC中视图解析的前缀和后缀,指定视图文件在项目中的位置和格式。
     * 使用InternalResourceViewResolver作为视图解析器,它能够处理JSP视图。
     * 设置前缀为"/WEB-INF/jsp/",确保视图文件位于WEB-INF目录下的jsp子目录中。
     * 设置后缀为".jsp",指明视图文件的格式为JSP。
     * @param registry ViewResolverRegistry实例,用于注册和配置视图解析器
     */
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
        registry.viewResolver(viewResolver);

    }

    /**
     * 添加ViewController
     * 本方法用于配置默认的控制器,即无需处理业务逻辑的简单页面跳转。
     * 通过ViewControllerRegistry添加一个控制器,将"/config/index"路径映射到"index"视图。
     * 这样当请求"/config/index"时,会直接展示对应的"index.jsp"页面,无需额外的控制器逻辑。
     * @param registry ViewControllerRegistry实例,用于注册和配置ViewController
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/config/index").setViewName("index");
    }
}

Spring MVC开发流程

开发Spring MVC的核心是控制器的开发,在Spring MVC中会将控制器的方法封装为一个处理器(Handler), 然后增强它的功能,主要包括解析控制器的方法参数和结果等等,控制器(Controller)是通过注解@Controller标注的,只要把它配置在IoC容器的扫描路径中,就可以通过扫描装配了,控制器往往要结合注解@RequestMapping使用,注解@RequestMapping可以配置在类或方法上,它的作用是将指定的URI和类或者方法进行绑定,这样请求对应的URI就可以找到对应的方法进行处理了

在SpringMVC4.3之后增加了@RestController@GetMapping@PostMapping等注解,通过这些注解,用户能够方便的构建REST风格的Spring MVC

Spring MVC还给处理器添加了拦截器机制,当启动SpringMVC的时候,首先会解析控制器中注解@RequestMapping的配置,将方法绑定为一个处理器,然后结合所配置的拦截器,存放到一个执行链(HandlerExecutionChain)中,最后将注解@RequestMappint定义的路径和执行链映射,这样当请求到达服务器时,通过请求路径找到对应的执行链并运行它,就可以运行拦截器和处理器了

@RequestMapping
java 复制代码
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

/**
 * 注解用于映射HTTP请求到处理方法。
 * 
 * @Target({ElementType.TYPE, ElementType.METHOD}) 指定该注解可以用于类或方法上。
 * @Retention(RetentionPolicy.RUNTIME) 指定该注解在运行时可被读取。
 * @Documented 指示Javadoc工具应包含此注释。
 * @Mapping 表明这是一个映射注解,用于Spring MVC中。
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
    /**
     * 映射的名称,可选。
     * 
     * @return 映射的名称,默认为空字符串。
     */
    String name() default "";

    /**
     * 映射的URL路径,可以使用通配符。
     * 
     * @return 映射的URL路径数组,默认为空数组。
     */
    @AliasFor("path")
    String[] value() default {};

    /**
     * 映射的URL路径,与value()相同。
     * 
     * @return 映射的URL路径数组,默认为空数组。
     */
    @AliasFor("value")
    String[] path() default {};

    /**
     * 映射支持的HTTP请求方法。
     * 
     * @return 支持的HTTP请求方法数组,默认为空数组。
     */
    RequestMethod[] method() default {};

    /**
     * 映射方法参数,用于指定请求参数的条件。
     * 
     * @return 请求参数数组,默认为空数组。
     */
    String[] params() default {};

    /**
     * 映射请求头,用于指定请求头的条件。
     * 
     * @return 请求头数组,默认为空数组。
     */
    String[] headers() default {};

    /**
     * 映射消费的内容类型,用于指定请求体的MIME类型。
     * 
     * @return 消费的内容类型数组,默认为空数组。
     */
    String[] consumes() default {};

    /**
     * 映射产生的内容类型,用于指定响应体的MIME类型。
     * 
     * @return 产生的内容类型数组,默认为空数组。
     */
    String[] produces() default {};
}

例如如下代码,使用了value和method

java 复制代码
    @RequestMapping(value = "/index2", method = RequestMethod.GET)
    public ModelAndView index2() {
        // 创建 ModelAndView 对象
        ModelAndView mv = new ModelAndView();
        // 设置视图名称为 "index"
        mv.setViewName("index");
        // 返回 ModelAndView 对象
        return mv;
    }

这样就可以对/mvc/HelloFresh/index2的HTTP GET请求提供响应了,如果没有配置method那么就会响应所有的请求

控制器开发

控制器开发是SpringMVC的核心,大致分为三个步骤,获取请求参数,处理业务逻辑,绑定模型和视图

首先看一下获取请求参数,在SpringMVC中接收参数的方法很多,建议不要使用Servlet容器给与的API,避免控制器依赖Servlet容器,例如如下代码

java 复制代码
    /**
     * 通过参数 获取Servlet规范中的HttpServletRequest和HttpSession对象
     *
     * @param session -- 会话对象
     * @param request -- 请求对象
     * @return
     */
    @RequestMapping(value = "/index3")
    public ModelAndView index3(HttpSession session, HttpServletRequest request) {
        ModelAndView mv = new ModelAndView();
        mv.setViewName("index");
        return mv;
    }

SpringMVC会自动解析代码中的方法参数session和request,传递关于Servlet容器的API参数,所以是可以获取到的,通过request或者session都可以很容易的获取HTTP的请求参数,但并非一个好的方法,这样做index3方法就和Servlet容器耦合在了一起,不利于扩展和测试

SpringMVC提供了更多的方法和注解用来获取参数,例如要获取一个HTTP请求的参数id(长整型),可以使用注解@RequestParam,index方法写成如下所示代码

java 复制代码
    /**
     * 通过@RequestParam获取参数
     *
     * @param id 参数,长整型
     * @return ModelAndView
     */
    @RequestMapping(value = "/index4", method = RequestMethod.GET)
    public ModelAndView index4(@RequestParam("id") Long id) {
        System.out.println("params[id] = " + id);
        ModelAndView mv = new ModelAndView();
        mv.setViewName("index");
        return mv;
    }

@RequestParam源码如下

java 复制代码
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

/**
 * 用于标记方法参数,指示该参数应作为HTTP请求参数加以处理。
 * <p>
 * 此注解用于说明方法参数与HTTP请求参数之间的映射关系。它允许指定参数的名称、是否必需,以及默认值。
 * 参数的名称和值可以通过注解的属性来定义,提供了一种灵活的方式来处理不同的请求场景。
 * 
 * @Target({ElementType.PARAMETER}) 指定注解可以应用于方法参数上
 * @Retention(RetentionPolicy.RUNTIME) 指定注解在运行时可见
 * @Documented 指示Javadoc应该包含这个注解
 */
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    /**
     * 参数的名称,用于映射到HTTP请求参数。
     * <p>
     * 此属性与value属性具有相同的作用,提供了不同的命名选项,以增强代码的可读性和灵活性。
     * 默认情况下,如果未显式指定名称,则会使用方法参数的名称。
     * 
     * @return 参数的名称
     */
    @AliasFor("name")
    String value() default "";

    /**
     * 参数的名称,用于映射到HTTP请求参数。
     * <p>
     * 此属性与name属性具有相同的作用,提供了不同的命名选项,以增强代码的可读性和灵活性。
     * 默认情况下,如果未显式指定名称,则会使用方法参数的名称。
     * 
     * @return 参数的名称
     */
    @AliasFor("value")
    String name() default "";

    /**
     * 指示参数是否必需。
     * <p>
     * 如果设置为true,则在请求中必须存在对应的参数,否则将导致错误。
     * 如果设置为false,则可以省略参数,此时方法可以使用默认值。
     * 
     * @return 是否必需的布尔值,默认为true
     */
    boolean required() default true;

    /**
     * 指定参数的默认值。
     * <p>
     * 当请求中未提供参数时,将使用此默认值。此属性仅在required属性设置为false时有效。
     * 默认值的特殊字符串表示是为了在文档生成和处理中提供更好的可读性和区分度。
     * 
     * @return 参数的默认值,默认为一个特殊的空白字符串,用于指示没有提供默认值
     */
    String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

假设一个登陆系统已经再Session中设置了userName,那么应该如何获取Session中内容呢,Spring MVC提供了注解@SessionAttribute获取Session中的数据

java 复制代码
    /**
     * 通过注解@SessionAttribute获得会话属性
     *
     * @param userName 用户名
     * @return ModelAndView
     */
    @RequestMapping(value = "/index5", method = RequestMethod.GET)
    public ModelAndView index5(@SessionAttribute("userName") String userName) {
        System.out.println("session[userName] = " + userName);
        ModelAndView mv = new ModelAndView();
        mv.setViewName("index");
        return mv;
    }

获取参数之后,便是处理业务逻辑和绑定视图,一般而言,实现的逻辑和数据库有关,既可以在applicationContext.xml中配置关于数据库的部分,也可以使用Java配置的方式,例如在BackendConfig中加入Bean,如下代码所示

java 复制代码
package com.ssmvc.backend.config;

import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;
import org.springframework.web.bind.annotation.RestController;

/**
 * 后端配置类,用于Spring应用程序的配置,包括数据源、MyBatis、事务管理器等的初始化。
 */
@Configuration
// 扫描所有com.ssmvc包下的组件,但排除@Controller和@RestController注解的类
@ComponentScan(basePackages = "com.ssmvc.*", excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = {Controller.class, RestController.class}))
@EnableTransactionManagement // 开启事务管理
@MapperScan(basePackages = "com.ssmvc", annotationClass = Mapper.class) // 扫描Mapper接口
public class BackendConfig implements TransactionManagementConfigurer {

    // 数据源实例
    private DataSource dataSource = null;

    /**
     * 初始化数据源。
     * 使用Apache Commons DBCP2创建数据源实例。
     *
     * @return DataSource 数据源实例
     */
    @Bean(name = "dataSource")
    public DataSource initDataSource() {
        if (dataSource != null) {
            return dataSource;
        }
        Properties props = new Properties();
        // 配置数据库连接信息和连接池参数
        props.setProperty("driverClassName", "com.mysql.jdbc.Driver");
        props.setProperty("url", "jdbc:mysql://localhost:3306/ssm");
        props.setProperty("username", "root");
        props.setProperty("password", "123456");
        props.setProperty("maxActive", "200");
        props.setProperty("maxIdle", "20");
        props.setProperty("maxWait", "30000");
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

    /**
     * 初始化JdbcTemplate。
     * 为Spring提供的简化JDBC操作的工具类配置数据源。
     *
     * @param dataSource 数据源
     * @return JdbcTemplate 实例
     */
    @Bean(name = "jdbcTemplate")
    public JdbcTemplate initjdbcTemplate(@Autowired DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    /**
     * 配置基于注解的事务管理器。
     * 使用DataSourceTransactionManager作为事务管理器,并配置数据源。
     *
     * @return PlatformTransactionManager 事务管理器实例
     */
    @Override
    @Bean(name = "transactionManager")
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        DataSourceTransactionManager transactionManager
                = new DataSourceTransactionManager();
        transactionManager.setDataSource(initDataSource());
        return transactionManager;
    }

    /**
     * 创建SqlSessionFactory。
     * 配置MyBatis的SqlSessionFactoryBean,并指定数据源和配置文件位置。
     *
     * @param dataSource 数据源
     * @return SqlSessionFactory 实例
     * @throws Exception
     */
    @Bean("sqlSessionFactory")
    public SqlSessionFactory createSqlSessionFactoryBean(
            @Autowired DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean
                = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        // 指定MyBatis配置文件位置
        String cfgFile = "mybatis-config.xml";
        Resource configLocation = new ClassPathResource(cfgFile);
        sqlSessionFactoryBean.setConfigLocation(configLocation);
        return sqlSessionFactoryBean.getObject();
    }

}
java 复制代码
package com.ssmvc.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;

import com.ssmvc.pojo.Role;
import com.ssmvc.service.RoleService;

/**
 * 角色控制器,负责处理与角色相关的请求。
 */
@Controller
@RequestMapping("/role")
public class RoleController {
    // 自动注入角色服务,用于处理角色相关的业务逻辑
    // 注入角色服务类
    @Autowired
    private RoleService roleService = null;

    /**
     * 获取角色详情,返回角色信息的ModelAndView。
     *
     * @param id 角色ID,用于查询特定的角色。
     * @return ModelAndView,包含角色详情的视图和模型。
     */
    @RequestMapping(value = "/details", method = RequestMethod.GET)
    public ModelAndView getRole(@RequestParam("id") Long id) {
        Role role = roleService.getRole(id);
        ModelAndView mv = new ModelAndView();
        mv.setViewName("role_details");
        // 给数据模型添加一个角色对象
        mv.addObject("role", role);
        return mv;
    }

    /**
     * 获取角色详情,返回以JSON格式表示的角色信息。
     *
     * @param id 角色ID,用于查询特定的角色。
     * @return ModelAndView,包含以JSON格式表示的角色详情。
     */
    //获取角色
    @RequestMapping(value="/details/json", method=RequestMethod.GET)
    public ModelAndView getRoleJson(@RequestParam("id") Long id) {
        Role role = roleService.getRole(id);
        ModelAndView mv = new ModelAndView();
        mv.addObject("role", role);
        //指定视图类型
        mv.setView(new MappingJackson2JsonView());
        return mv;
    }
}

例如有这样的一个角色控制器,在代码中注入RoleService,就可以通过这个服务类使用传递的参数id获取角色,最后把查询出来的角色添加给模型和视图以便将来使用;至此只是完成了业务逻辑,并没有实现视图渲染,还需要把从数据库查询出来的数据,通过某种方式渲染到视图中展示给用户

视图渲染

一般而言,可以把视图定义为一个JSP文件,实际上SpringMVC存在大量的的视图模式,例如JSP、JSTL、JSON、Thymeleaf等等,通过这些可以方便的渲染数据响应用户请求,例如上边的例子,根据配置,它需要一个/WEB-INF/jsp/role_details.jsp文件来渲染数据,代码如下

jsp 复制代码
<%@ page pageEncoding="utf-8"%>
<%@ page import="com.ssmvc.pojo.Role"%>
<html>
	<head>
		<title>角色详情</title>
	</head>
	<%
	// 获得数据模型
	Role role = (Role)request.getAttribute("role");
	%>
	<body>
		<center>
			<table border="1">
				<tr>
					<td>标签</td>
					<td>值</td>
				</tr>
				<tr>
					<td>角色编号</td>
					<td><%=role.getId() %></td>
				</tr>
				<tr>
					<td>角色名称</td>
					<td><%=role.getRoleName()%></td>
				</tr>
				<tr>
					<td>角色备注</td>
					<td><%=role.getNote()%></td>
				</tr>
			</table>
		</center>
	</body>
</html>

启动Tomcat,然后用浏览器请求地址http://localhost:8080/SSMVC_war/mvc/role/details?id=2,将会得到如下页面

在很多使用Ajax技术的环境中,后台往往返回JSON数据给前端,SpringMVC在模型和视图上也支持,如下代码所示

java 复制代码
    /**
     * 获取角色详情,返回以JSON格式表示的角色信息。
     *
     * @param id 角色ID,用于查询特定的角色。
     * @return ModelAndView,包含以JSON格式表示的角色详情。
     */
    //获取角色
    @RequestMapping(value="/details/json", method=RequestMethod.GET)
    public ModelAndView getRoleJson(@RequestParam("id") Long id) {
        Role role = roleService.getRole(id);
        ModelAndView mv = new ModelAndView();
        mv.addObject("role", role);
        //指定视图类型
        mv.setView(new MappingJackson2JsonView());
        return mv;
    }

代码中视图类型为MappingJackson2JsonView, 然后请求地址http://localhost:8080/SSMVC_war/mvc/role/details/json?id=2,便会得到如下页面

这样就返回JSON数据,提供给Ajax异步请求使用,MappingJackson2JsonView是一个非逻辑视图,因此不需要视图解析器定位MappingJackson2JsonView会直接把ModelAndView中的数据模型通过JSON视图转换出来就得到了JSON数据

这不是将控制器返回的结果变为JSON的唯一方式,使用注解@ResponseBody或者@RestController是更简单和广泛的方法

相关推荐
cyforkk20 小时前
Spring 异常处理器:从混乱到有序,优雅处理所有异常
java·后端·spring·mvc
Cloud-Future3 天前
Spring MVC 处理请求的流程
java·spring·mvc
FrankYoou5 天前
Spring Boot + Spring MVC 项目结构
spring boot·spring·springmvc
optimistic_chen5 天前
【Java EE进阶 --- SpringBoot】Spring IoC
spring boot·后端·spring·java-ee·mvc·loc
wuk9985 天前
在Spring MVC中使用查询字符串与参数
java·spring·mvc
原来是好奇心6 天前
深入剖析Spring Boot中Spring MVC的请求处理流程
spring boot·spring·mvc
xkroy6 天前
创建Spring MVC和注解
学习·spring·mvc
期待のcode6 天前
SpringMVC的请求接收与结果响应
java·后端·spring·mvc
Pure03197 天前
Spring MVC BOOT 中体现的设计模式
spring·设计模式·mvc
The Sheep 20237 天前
.NetCore MVC
mvc·.netcore