用了这么多年 Spring MVC,你真的了解它吗?(二)

简介: 今天,正式介绍一下Java极客技术知识星球Spring 源码分析:不得不重视的 Transaction 事务Spring 源码学习(八) AOP 使用和实现原理这么火的 OKR,你不了解下?Java:控制反转(IoC)与依赖注入(DI)

DispatcherServlet 初始化

该类是 spring-mvc 的核心,该类进行真正逻辑实现,DisptacherServlet 实现了 Servlet 接口。

22.jpg

介绍:

servlet 是一个 Java 编写的程序,基于 Http 协议,例如我们常用的 Tomcat,也是按照 servlet 规范编写的一个 Java

servlet 的生命周期是由 servlet 的容器来控制,分为三个阶段:初始化、运行和销毁。

servlet 初始化阶段会调用其 init 方法:

HttpServletBean#init

public final void init() throws ServletException {
    // 解析 init-param 并封装到 pvs 变量中
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    // 将当前的这个 Servlet 类转换为一个 BeanWrapper,从而能够以 Spring 的方式对 init—param 的值注入
    BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
    ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
    // 注册自定义属性编辑器,一旦遇到 Resource 类型的属性将会使用 ResourceEditor 进行解析
    bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
    // 空实现,留给子类覆盖
    initBeanWrapper(bw);
    bw.setPropertyValues(pvs, true);
    // 初始化 servletBean (让子类实现,这里它的实现子类是 FrameworkServlet)
    initServletBean();
}

在这里初始化 DispatcherServlet,主要是通过将当前的 servlet 类型实例转换为 BeanWrapper 类型实例,以便使用 Spring 中提供的注入功能进行相应属性的注入。

从上面注释,可以看出初始化函数的逻辑比较清晰,封装参数、转换成 BeanWrapper 实例、注册自定义属性编辑器、属性注入,以及关键的初始化 servletBean


容器初始化

下面看下初始化关键逻辑:

FrameworkServlet#initServletBean

剥离了日志打印后,剩下的两行关键代码

protected final void initServletBean() throws ServletException {
    // 仅剩的两行关键代码
    this.webApplicationContext = initWebApplicationContext();
    // 留给子类进行覆盖实现,但我们例子中用的 DispatcherServlet 并没有覆盖,所以先不用管它
    initFrameworkServlet();
}

WebApplicationContext 的初始化

FrameworkServlet#initWebApplicationContext

该函数的主要工作就是创建或刷新 WebApplicationContext 实例并对 servlet 功能所使用的变量进行初始化。

protected WebApplicationContext initWebApplicationContext() {
    // 从根容器开始查找
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;
    if (this.webApplicationContext != null) {
        // 有可能在 Spring 加载 bean 时,DispatcherServlet 作为 bean 加载进来了
        // 直接使用在构造函数被注入的 context 实例
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                if (cwac.getParent() == null) {
                    cwac.setParent(rootContext);
                }
                // 刷新上下文环境
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        // 根据 contextAttribute 属性加载 WebApplicationContext
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // 经过上面步骤都没找到,那就来创建一个
        wac = createWebApplicationContext(rootContext);
    }
    if (!this.refreshEventReceived) {
        synchronized (this.onRefreshMonitor) {
            // 刷新,初始化很多策略方法
            onRefresh(wac);
        }
    }
    if (this.publishContext) {
        // Publish the context as a servlet context attribute.
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }
    return wac;
}

根容器查找

我们最常用到的 spring-mvc,是 spring 容器和 web 容器共存,这时 rootContext 父容器就是 spring 容器。

在前面的 web.xml 配置的监听器 ContextLaoderListener,已经将 Spring 父容器进行了加载

WebApplicationContextUtils#getWebApplicationContext(ServletContext)

public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
    // key 值 :WebApplicationContext.class.getName() + ".ROOT"
    // (ServletContext) sc.getAttribute(attrName) ,
    return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}

同时,根据上面代码,了解到 Spring 父容器,是以 key 值为 : WebApplicationContext.class.getName() + ".ROOT" 保存到 ServletContext 上下文中。


根据 contextAttribute 寻找

虽然有默认 key,但用户可以重写初始化逻辑(在 web.xml 文件中设定 servlet 参数 contextAttribute),使用自己创建的 WebApplicaitonContext,并在 servlet 的配置中通过初始化参数 contextAttribute 指定 key

protected WebApplicationContext findWebApplicationContext() {
    String attrName = getContextAttribute();
    if (attrName == null) {
        return null;
    }
    // attrName 就是用户在`web.xml` 文件中设定的 `servlet` 参数 `contextAttribute`
    WebApplicationContext wac =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
    if (wac == null) {
        throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
    }
    return wac;
}

重新创建实例

通过前面的方法都没找到,那就来重新创建一个新的实例:

FrameworkServlet#createWebApplicationContext(WebApplicationContext)

protected WebApplicationContext createWebApplicationContext(@Nullable WebApplicationContext parent) {
    return createWebApplicationContext((ApplicationContext) parent);
}
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    // 允许我们自定义容器的类型,通过 contextClass 属性进行配置
    // 但是类型必须要继承 ConfigurableWebApplicationContext,不然将会报错
    Class<?> contextClass = getContextClass();
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException();
    }
    // 通过反射来创建 contextClass
    ConfigurableWebApplicationContext wac =
            (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
    wac.setEnvironment(getEnvironment());
    wac.setParent(parent);
    // 获取 contextConfigLocation 属性,配置在 servlet 初始化函数中
    String configLocation = getContextConfigLocation();
    wac.setConfigLocation(configLocation);
    // 初始化 Spring 环境包括加载配置环境
    configureAndRefreshWebApplicationContext(wac);
    return wac;
}

获取上下文类 contextClass

默认使用的是 XmlWebApplicationContext,但如果需要配置自定义上下文,可以在 web.xml 中的 <init-param> 标签中修改 contextClass 属性对应的 value,但需要注意图中提示:

23.jpg

configureAndRefreshWebApplicationContext

使用该方法,用来对已经创建的 WebApplicaitonContext 进行配置以及刷新

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    // 遍历 ApplicationContextInitializer,执行 initialize 方法
    applyInitializers(wac);
    // 关键的刷新,加载配置文件及整合 parent 到 wac
    wac.refresh();
}

ApplicationContextInitializer

该类可以通过 <init-param>contextInitializerClasses 进行自定义配置:

<init-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value>自定义类,需继承于 `ApplicationContextInitializer`</param-value>
</init-param>

正如代码中的顺序一样,是在 mvc 容器创建前,执行它的 void initialize(C applicationContext) 方法:

protected void applyInitializers(ConfigurableApplicationContext wac) {
    AnnotationAwareOrderComparator.sort(this.contextInitializers);
    for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
        initializer.initialize(wac);
    }
}

所有如果没有配置的话,默认情况下 contextInitializers 列表为空,表示没有 ApplicationContextInitializer 需要执行。


加载 Spring 配置

wac.refresh(),实际调用的是我们之前就很熟悉的刷新方法:

org.springframework.context.support.AbstractApplicationContext#refresh

24.jpg

从图中能够看出,刷新方法的代码逻辑与之前一样,通过父类 AbstractApplicationContextrefresh 方法,进行了配置文件的加载。

从图中能够看出,刷新方法的代码逻辑与之前一样,通过父类 AbstractApplicationContextrefresh 方法,进行了配置文件的加载。

在例子中的 web.xml 配置中,指定了加载 spring-mvc.xml 配置文件

<!-- 配置 DispatcherServlet -->
<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring-mvc.xml</param-value>
    </init-param>
</servlet>

注册 mvc 解析器

由于我们配置了 contextConfigLocation,指定了加载资源的路径,所以在 XmlWebApplicationContext 初始化的时候,加载的 Spring 配置文件路径是我们指定 spring-mvc.xml


25.jpg

spring-mvc.xml 配置中,主要配置了三项

<!--扫描包,自动注入bean-->
<context:component-scan base-package="web.controller"/>
<!--使用注解开发spring mvc-->
<mvc:annotation-driven/>
<!--视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/"/>
    <property name="suffix" value=".jsp"/>
</bean>

同样老套路,使用了 <mvc:annotation> 自定义注解的话,要注册相应的解析器后,Spring 容器才能解析元素:

org.springframework.web.servlet.config.MvcNamespaceHandler

public void init() {
    // MVC 标签解析需要注册的解析器
    registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
    registerBeanDefinitionParser("default-servlet-handler", new DefaultServletHandlerBeanDefinitionParser());
    registerBeanDefinitionParser("interceptors", new InterceptorsBeanDefinitionParser());
    registerBeanDefinitionParser("resources", new ResourcesBeanDefinitionParser());
    registerBeanDefinitionParser("view-controller", new ViewControllerBeanDefinitionParser());
    registerBeanDefinitionParser("redirect-view-controller", new ViewControllerBeanDefinitionParser());
    registerBeanDefinitionParser("status-controller", new ViewControllerBeanDefinitionParser());
    registerBeanDefinitionParser("view-resolvers", new ViewResolversBeanDefinitionParser());
    registerBeanDefinitionParser("tiles-configurer", new TilesConfigurerBeanDefinitionParser());
    registerBeanDefinitionParser("freemarker-configurer", new FreeMarkerConfigurerBeanDefinitionParser());
    registerBeanDefinitionParser("groovy-configurer", new GroovyMarkupConfigurerBeanDefinitionParser());
    registerBeanDefinitionParser("script-template-configurer", new ScriptTemplateConfigurerBeanDefinitionParser());
    registerBeanDefinitionParser("cors", new CorsBeanDefinitionParser());
}

可以看到,mvc 提供了很多便利的注解,有拦截器、资源、视图等解析器,但我们常用的到的是 anntation-driven 注解驱动,这个注解通过 AnnotationDrivenBeanDefinitionParser 类进行解析,其中会注册两个重要的 bean :

class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
    public static final String HANDLER_MAPPING_BEAN_NAME = RequestMappingHandlerMapping.class.getName();
    public static final String HANDLER_ADAPTER_BEAN_NAME = RequestMappingHandlerAdapter.class.getName();
    ...
}

跳过其他熟悉的 Spring 初始化配置,通过上面的步骤,完成了 Spring 配置文件的解析,将扫描到的 bean 加载到了 Spring 容器中。

那么下面就正式进入 mvc 的初始化。


mvc 初始化

onRefresh 方法是 FrameworkServlet 类中提供的模板方法,在子类 DispatcherServlet 进行了重写,主要用来刷新 SpringWeb 功能实现中所必须用到的全局变量:

protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
    // 初始化 multipartResolver 文件上传相关
    initMultipartResolver(context);
    // 初始化 LocalResolver 与国际化相关
    initLocaleResolver(context);
    // 初始化 ThemeResolver 与主题更换相关
    initThemeResolver(context);
    // 初始化 HandlerMapping 与匹配处理器相关
    initHandlerMappings(context);
    // 初始化 HandlerAdapter 处理当前 Http 请求的处理器适配器实现,根据处理器映射返回相应的处理器类型
    initHandlerAdapters(context);
    // 初始化 HandlerExceptionResolvers,处理器异常解决器
    initHandlerExceptionResolvers(context);
    // 初始化 RequestToViewNameTranslator,处理逻辑视图名称
    initRequestToViewNameTranslator(context);
    // 初始化 ViewResolver 选择合适的视图进行渲染
    initViewResolvers(context);
    // 初始化 FlashMapManager 使用 flash attributes 提供了一个请求存储属性,可供其他请求使用(重定向时常用)
    initFlashMapManager(context);
}

该函数是实现 mvc 的关键所在,先来大致介绍一下初始化的套路:

  1. 寻找用户自定义配置
  2. 没有找到,使用默认配置

显然,Spring 给我们提供了高度的自定义,可以手动设置想要的解析器,以便于扩展功能。

如果没有找到用户配置的 bean,那么它将会使用默认的初始化策略: getDefaultStrategies 方法



相关文章
|
30天前
|
缓存 前端开发 Java
Spring MVC 面试题及答案整理,最新面试题
Spring MVC 面试题及答案整理,最新面试题
85 0
|
30天前
ssm(Spring+Spring mvc+mybatis)——updateDept.jsp
ssm(Spring+Spring mvc+mybatis)——updateDept.jsp
10 0
|
29天前
|
SQL JavaScript Java
springboot+springm vc+mybatis实现增删改查案例!
springboot+springm vc+mybatis实现增删改查案例!
23 0
|
29天前
|
SQL Java 数据库连接
挺详细的spring+springmvc+mybatis配置整合|含源代码
挺详细的spring+springmvc+mybatis配置整合|含源代码
35 1
|
7天前
|
数据采集 前端开发 Java
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
21 3
|
7天前
|
存储 前端开发 Java
会话锦囊:揭示Spring MVC如何巧妙使用@SessionAttributes
会话锦囊:揭示Spring MVC如何巧妙使用@SessionAttributes
13 1
|
7天前
|
前端开发 Java Spring
数据之桥:深入Spring MVC中传递数据给视图的实用指南
数据之桥:深入Spring MVC中传递数据给视图的实用指南
24 3
|
16天前
|
前端开发 安全 Java
使用Java Web框架:Spring MVC的全面指南
【4月更文挑战第3天】Spring MVC是Spring框架的一部分,用于构建高效、模块化的Web应用。它基于MVC模式,支持多种视图技术。核心概念包括DispatcherServlet(前端控制器)、HandlerMapping(请求映射)、Controller(处理请求)、ViewResolver(视图解析)和ModelAndView(模型和视图容器)。开发流程涉及配置DispatcherServlet、定义Controller、创建View、处理数据、绑定模型和异常处理。
使用Java Web框架:Spring MVC的全面指南
|
23天前
|
敏捷开发 监控 前端开发
Spring+SpringMVC+Mybatis的分布式敏捷开发系统架构
Spring+SpringMVC+Mybatis的分布式敏捷开发系统架构
55 0
|
30天前
|
Java Apache vr&ar
springmvc报错 nested exception is org.mybatis.spring.MyBatisSystemException:
springmvc报错 nested exception is org.mybatis.spring.MyBatisSystemException:
15 0