IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> SpringMVC源码分析(二)—DispacherServlet初始化(SpringMVC的父子容器消亡史) -> 正文阅读

[Java知识库]SpringMVC源码分析(二)—DispacherServlet初始化(SpringMVC的父子容器消亡史)

接上篇,我们实现了一个简单的SpringMVC小程序。麻雀虽小,五脏俱全,里面包含了一个web工程基本的组件。现在我们开始剖析源码,分为如下几个步骤

  • 1.Tomcat如何启动两个容器—Spring容器、SpringMVC容器
  • 2.处理@RequestMapping注解,建立<url,controller(中的方法)>关系。
  • 3.初始化DispacherServlet
  • 4.DispacherServlet如何处理请求(前三步都是为该步骤打下基础,具体内容放到下一篇)

注Spring的版本是5.2.12.RELEASE,不同版本略有差异,但核心逻辑是不变的。

一、Tomcat如何启动两个容器—Spring容器、SpringMVC容器

前面我们提到web项目会启动两个容器,两个容器启动过程中有一个共同点就是,初始化的方法位于AbstractApplicationContext类的refresh方法
?

1.先启动刷新Spring容器

该功能由ContextLoader类的initWebApplicationContext方法实现,省略一些次要代码

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {

    try {
        if (this.context == null) {
            //1.创建Spring容器
            this.context = createWebApplicationContext(servletContext);
        }
        if (this.context instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
            if (!cwac.isActive()) {
                if (cwac.getParent() == null) {
                    ApplicationContext parent = loadParentContext(servletContext);
                    cwac.setParent(parent);
                }
                //2.刷新该容器,调用了AbstractApplicationContext类的refresh方法
                configureAndRefreshWebApplicationContext(cwac, servletContext);
            }
        }
        return this.context;
    }
}

栈调用如下图
在这里插入图片描述

看下刷新完后的spring容器中bean
在这里插入图片描述

2.接着启动刷新SpringMVC容器

该功能主要由FrameworkServlet类的createWebApplicationContext方法实现

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    
    //1.创建容器实例
    ConfigurableWebApplicationContext wac =
        (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

    wac.setEnvironment(getEnvironment());
    //2.设置刚才创建的spring容器为父容器。父容器可以在ServletContext中拿到
    wac.setParent(parent);
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        wac.setConfigLocation(configLocation);
    }
    3.刷新容器
    configureAndRefreshWebApplicationContext(wac);

    return wac;
}

上面方法的调用过程如下
在这里插入图片描述

刷新完的SpringMVC容器,只包含了UserController的实例,没有UserService的实例。图有点长,就不截了,大家可以自己验证。
?

3.父子容器描述

1.概括上述步骤

上述步骤可以总结成下图
在这里插入图片描述

简单来说就是初始化listener时初始化Spring容器,初始化Servlet时初始化SpringMVC容器。
?
需要说明的是,在本例中父子容器其实都是XmlWebApplicationContext的实例,只是两个实例做了一个引用关系

2.这个过程需要注意以下几个点

如果把bean全部放到Spring容器会怎么样?

如果将controller的bean都放入的spring容器中,如果访问该项目会出现404的错误。
因为在解析@RequestMapping过程中,initHandlerMethods()方法只是对SpringMVC容器中的bean进行处理的,并没有找入父容器的bean。此时url和controller中的方法(或者是handler)没有生成对应关系,也就是说没有方法能够处理该请求,出现404的错误。

如果把bean全部放在SpringMVC容器会怎么样?

在使用事务时会导致失效

3.为什么要分父子容器

SpringMVC是从struct发展过来的,也就是说方便两者进行切换,而不用动Spring容器中的bean。

二、处理@RequestMapping注解,建立url和方法的关系

上面提的关系就是<url,controller(中的方法)>Map结构。要注意的是?第二部分是在SpringMVC容器初始化过程中完成。

1.说下过程中涉及的类

  • RequestMappingInfo: 这个类是对请求映射的一个抽象,它包含了请求路径,请求方法,请求头等信息。其实可以看做是@RequestMapping的一个对应类。
  • HandlerMethod: 这个类封装了处理器实例(Controller bean)和处理方法实例(Method)以及方法参数数组(MethodParameter[])
  • HandlerMapping :该接口的实现类用来定义请求和处理器之前的映射关系,其中定义了一个方法getHandler。
  • AbstractHandlerMethodMapping :这是HandlerMapping的一个基本实现类,该类定义了请求与HandlerMethod实例的映射关系。
  • RequestMappingHandlerMapping: 它将@RequestMapping注解转化为RequestMappingInfo实例,并为父类使用。

2.怎么将上面的类串联起来呢?分为三个步骤

  • 1.遍历注解:SpringMVC容器初始化过程中,具体在创建RequestMappingHandlerMapping类实例时,对标注了@Controller或@RequestMapping注解的类中方法进行遍历。

  • 2.封装RequestMappingInfo实例:将类和方法上的**@RequestMapping注解值进行合并,封装成一个RequestMappingInfo实例**。以controller中的方法对象为key,对应的RequestMappingInfo实例为value存入map中,遍历map开始注册

  • 3.注册到Map中:将这个Controller实例、方法及方法参数信息封装到HandlerMethod中。以RequestMappingInfo为key,HandlerMethod为value存储到map结构。将url(@RequestMapping注解value值)为key,以RequestMappingInfo为value存到map结构中

3.上面步骤实现细节

1.遍历注解

入口开始在RequestMappingHandlerMapping类的afterPropertiesSet方法

//创建实例过程中回调此方法
public void afterPropertiesSet() {
    this.config = new RequestMappingInfo.BuilderConfiguration();
    this.config.setUrlPathHelper(getUrlPathHelper());
    this.config.setPathMatcher(getPathMatcher());
    this.config.setSuffixPatternMatch(useSuffixPatternMatch());
    this.config.setTrailingSlashMatch(useTrailingSlashMatch());
    this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
    this.config.setContentNegotiationManager(getContentNegotiationManager());
	
    //调用父类方法
    super.afterPropertiesSet();
}

回调父类AbstractHandlerMethodMapping的afterPropertiesSet方法。
下面的方法都是在AbstractHandlerMethodMapping类中,这个要有印象,在下一篇DispacherServlet逻辑处理中要用到

public void afterPropertiesSet() {
    initHandlerMethods();
}

//1.扫描SpringMVC容器中所有bean,检测并注册Handler方法
protected void initHandlerMethods() {
    //1.1扫描所有bean
    for (String beanName : getCandidateBeanNames()) {
        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
            //1.2处理不是以"scopedTarget."开头的bean
            processCandidateBean(beanName);
        }
    }
    //不用管它
    handlerMethodsInitialized(getHandlerMethods());
}

进入父类processCandidateBean方法,从方法名字可以看出来这个是处理候选bean方法。
?

怎么处理呢?

在这个方法里只是判断下是不是Handler通过isHandler(beanType),判断的条件就是这个bean上有没有@Controller或者@RequestMapping注解。
如果有则开始从bean中发现url和方法的对应关系,实现这个方法的是detectHandlerMethods

protected void processCandidateBean(String beanName) {
    Class<?> beanType = null;
    try {
        beanType = obtainApplicationContext().getType(beanName);
    }
    if (beanType != null && isHandler(beanType)) {
        detectHandlerMethods(beanName);
    }
}

2.封装成一个RequestMappingInfo实例。

进入detectHandlerMethods方法,这个方法完成了两件事
1.得到一个Map<Method, T> methods变量,Method就是controller中的方法,T代表的是RequestMappingInfo
2.遍历methods进行注册

	protected void detectHandlerMethods(Object handler) {
        
		if (handlerType != null) {
			Class<?> userType = ClassUtils.getUserClass(handlerType);
            1.完成第一件事儿
			Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
					(MethodIntrospector.MetadataLookup<T>) method -> {
						try {
							return getMappingForMethod(method, userType);
						}
					});
			if (logger.isTraceEnabled()) {
				logger.trace(formatMappings(userType, methods));
			}
            //2.完成第二件事儿
			methods.forEach((method, mapping) -> {
				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                //开始注册
				registerHandlerMethod(handler, invocableMethod, mapping);
			});
		}
	}

我们看下methods的存储的数据
在这里插入图片描述
有了map结构之后开始注册,直接看registerHandlerMethod方法

3.注册就是向两个Map结构中存储数据

此时进入内部类MappingRegistry,根据类名可以推测出来是注册映射关系的。介绍两个成员变量

private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

mappingLookup:以RequestMappingInfo为key,HandlerMethod为value的Map结构
urlLookup:以url为key,RequestMapping集合为value的Map结构

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
    this.mappingRegistry.register(mapping, handler, method);
}

public void register(T mapping, Object handler, Method method) {
	//省略一下不重要代码
    
    //增加加锁性能,只加了写锁
    this.readWriteLock.writeLock().lock();
    try {
        
        //根据handler(就是Controller实例)和controller中的方法对象生成HandlerMethod
        HandlerMethod handlerMethod = createHandlerMethod(handler, method);
        validateMethodMapping(handlerMethod, mapping);
        
        1.向第一个Map存值
        this.mappingLookup.put(mapping, handlerMethod);

        List<String> directUrls = getDirectUrls(mapping);
        for (String url : directUrls) {
            //2.向第二个Map存值
            this.urlLookup.add(url, mapping);
        }

        String name = null;
        if (getNamingStrategy() != null) {
            name = getNamingStrategy().getName(handlerMethod, mapping);
            addMappingName(name, handlerMethod);
        }

        CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
        if (corsConfig != null) {
            this.corsLookup.put(handlerMethod, corsConfig);
        }

        this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
    }
    finally {
        this.readWriteLock.writeLock().unlock();
    }
}

看下mappingLookup存完值的图
在这里插入图片描述

urlLookup存完值的图
在这里插入图片描述
现在已经向Map中存储了数据,怎么根据一个请求找到对应的方法呢?就两个步骤
?

  • 1.根据请求的url,从urlLookup取出value(RequestMappingInfo实例)

  • 2.根据RequestMappingInfo实例从mappingLookup取出对应的HandlerMethod

上面的步骤就是下一篇DispacherServlet要写的根据url找到对应Controller中的方法。

三、初始化DispacherServlet

?
上面的过程描述了Tomcat是怎么初始化两个容器的,下面叙述下在初始化完SpringMVC容器后,如何给这个容器添加上web的全局变量,所谓的添加web的全局变量就是给DispacherServlet中的成员变量赋值,这个功能是onRefresh方法实现的

1.onRefresh方法

**_DispacherServlet类 _**重写FrameworkServlet的onRefresh方法,主要的刷新功能放到了initStrategies来做。

protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

/**
  * Initialize the strategy objects that this servlet uses.
  * <p>May be overridden in subclasses in order to initialize further strategy objects.
  */
protected void initStrategies(ApplicationContext context) {
    //1.初始化MultipartResolver,主要用于处理文件上传
    initMultipartResolver(context);
    //2.初始化LocaleResolver,实现国际化
    initLocaleResolver(context);
    //3.初始化主题,控制网页风格
    initThemeResolver(context);
    
    //4.初始化HandlerMappings
    initHandlerMappings(context);
    //5.初始化HandlerAdapters
    initHandlerAdapters(context);
    
    //6.初始化异常处理器
    initHandlerExceptionResolvers(context);
    //7.初始化请求解析视图翻译器,为请求找到一个合适的View
    initRequestToViewNameTranslator(context);
    //8.初始化视图解析器
    initViewResolvers(context);
    //9.初始化FlashMapManager,闪存管理器。可以缓存请求的属性值交给下一个请求,再清空缓存
    initFlashMapManager(context);
}

initHandlerMappings(context)

这个方法主要就是给DispacherServlet中 List handlerMappings;属性赋值。
怎么赋值呢?

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;

    if (this.detectAllHandlerMappings) {
        //1.找到IOC容器中所有HandlerMapping类型的bean,放到map中
        Map<String, HandlerMapping> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            //2.判空后就赋值给handlerMappings属性
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            // We keep HandlerMappings in sorted order.
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }
	//省略判断
}

在IOC容器中找到的bean如下所示
在这里插入图片描述
这些bean是在SpringMVC容器初始化时加载当前系统中所有实现了HandlerMapping接口的bean。
?

initHandlerAdapters(context)

?

该方法和初始化HandlerMapping方法如出一辙,都是从SpringMVC容器中找到 HandlerAdapter类型的bean然后给List handlerAdapters赋值

private void initHandlerAdapters(ApplicationContext context) {
    this.handlerAdapters = null;

    if (this.detectAllHandlerAdapters) {
         //1.找到IOC容器中所有HandlerAdapter类型的bean,放到map中
        Map<String, HandlerAdapter> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
        if (!matchingBeans.isEmpty()) {
            //2.判空后就赋值给handlerAdapters属性
            this.handlerAdapters = new ArrayList<>(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerAdapters);
        }
    }
    //不太重要省略掉
}

看下从SpringMVC容器找到的bean
在这里插入图片描述

2.总结给成员变量赋值方法

仔细看下DispacherServlet中这几个nitxxxxx方法里面有几个共同点
?

1.代码是有大量重复代码的,不变的是赋值的DispacherServlet成员变量不同,如this.handlerAdapters = null
?
2.而且有一个共同的逻辑就是如果SpringMVC容器没有成员变量对应的bean,那么就会加载DispacherServlet配置文件中的bean

?
DispacherServlet配置文件DispacherServlet.properties内容如下

//实现国际化
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

//主题
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

//HandlerMapping
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
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
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

//ViewResolver
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

//FlashMapManager
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

什么时候加载这个配置文件呢?在DispacherServlet类加载的时候,位于静态代码块中

static {
    try {
        ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
        //赋值给成员变量
        defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
}

而各个initxxxxx方法中都有一个判断逻辑

//如果没有从SpringMVC容器中找到各个bean,则从配置文件的成员变量中找
if (this.handlerMappings == null) {
    this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
    if (logger.isTraceEnabled()) {
        logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
                     "': using default strategies from DispatcherServlet.properties");
    }
}

如果在mvc的配置文件中有如下配置,则不会走上面的判断。

<mvc:annotation-driven></mvc:annotation-driven>

该配置的作用就是将配置文件的各个bean注册到SpringMVC容器中。
?

四、总结

上面最主要的点就是当一个项目启动时,会初始化两个容器(Spring容器和SpringMVC容器),为了完成web项目处理请求的功能,需要为SpringMVC容器添加web的全局变量,其实就是为DispacherServlet的成员变量赋值


五、扩展

本来写到上面就可以停笔了,出于好奇验证了下现在SpringBoot还采用父子容器形式了吗?发现不是。
下面源码基于SpringBoot 2.1.1.RELEASE 版本,可能版本间使用的IOC容器不相同,大家可以自行验证。
?

验证如下
从SpringBoot的main函数进去,一直到SpringApplication类的run方法,该方法中有一行去创建容器

context = createApplicationContext();

在该行代码打上断点运行
![image.png](https://img-blog.csdnimg.cn/img_convert/cceeeb385e8a313d8304117cd522d383.png#clientId=u42664c35-a64c-4&from=paste&height=302&id=ud94330e6&margin=[object Object]&name=image.png&originHeight=603&originWidth=1407&originalType=binary&ratio=1&size=107116&status=done&style=none&taskId=u3715c710-da72-400c-885a-7b6e21a885a&width=703.5)
此时的容器只有一个就是**AnnotationConfigServletWebServerApplicationContext**,继续向下走,去刷新容器。
?

查看该容器中的单例池(beanfactory属性中的singletonObjects)是否包含controller和service包下的类实例。
涉及到的对象有点多,只截取重要的信息
![image.png](https://img-blog.csdnimg.cn/img_convert/59b450587a377788df29a40e01e22844.png#clientId=u42664c35-a64c-4&from=paste&id=u512a4887&margin=[object Object]&name=image.png&originHeight=78&originWidth=653&originalType=binary&ratio=1&size=11877&status=done&style=none&taskId=uc2817c2b-a447-4ff8-b953-3789ff294e7)
由上图可以发现controller和service以及视图解析器都在单例池中。
可以证明SpringBoot只使用了一个容器
?


参考文章
mvc:annotation-driven的作用
Spring和SpringMVC父子容器概念
SpringMvc在SpringBoot环境和Web环境中上下文的关系
?


项目git地址及相关链接

项目git地址:https://gitee.com/shang_jun_shu/springmvc-analysis?
下篇:SpringMVC源码分析(三)—DispacherServlet逻辑处理及为什么要用适配器模式

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-03 11:01:36  更:2021-08-03 11:02:20 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/3 15:51:12-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码