SpringMVC源码深度解析(上)

news2024/11/28 0:50:51

        今天,聊聊SpringMVC框架的原理。SpringMVC属于Web框架,它不能单独存在,需要依赖Servlet容器,常用的Servlet容器有Tomcat、Jetty等,这里以Tomcat为例进行讲解。老规矩,先看看本项目的层级结构:

        需要的依赖为:

plugins {
    id 'java'
    id 'war'
}

group 'org.springframework'
version '5.3.10-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    compile(project(":spring-web"))
    compile(project(":spring-webmvc"))
    testImplementation 'junit:junit:4.11'
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
    compile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
    compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.0'
    compile group: 'javax.servlet.jsp', name: 'javax.servlet.jsp-api' ,version: '2.3.1'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.4'
    compile group: 'commons-fileupload', name: 'commons-fileupload', version: '1.3.1'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.4'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.4'
    implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '9.0.33'
    implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper', version: '9.0.33'
}

test {
    useJUnitPlatform()
}

        启动类为Starter,代码如下:        

package com.szl;


import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;

public class Starter {

	private static int port = 9000;
	private static String contextPath = "/";

	public static void main(String[] args) throws Exception {
		Tomcat tomcat = new Tomcat();
		String baseDir = Thread.currentThread().getContextClassLoader().getResource("").getPath();
		tomcat.setBaseDir(baseDir);
		tomcat.setPort(port);
		Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
		connector.setPort(port);
		tomcat.setConnector(connector);

		tomcat.addWebapp(contextPath, baseDir);
		tomcat.enableNaming();
		try {
			tomcat.start();
		} catch (LifecycleException e) {
			System.err.println("tomcat 启动失败...");
		}
		tomcat.getServer().await();
	}
}

        在Starter类中,会启动Tomcat容器,这里面的代码属于固定写法,熟悉Spring Boot源码的朋友肯定知道,在Spring Boot中,启动Tomcat代码也是如此。然后在resources目录下,新建目录:META-INF/services,在该目录下,创建一个文件:javax.servlet.ServletContainerInitializer,这是一个接口的全限定名,里面内容为该接口的实现类的全限定名:

org.springframework.web.SpringServletContainerInitializer

        如果你看过其他的框架源代码,比如Dubbo、Spring Boot等,你就会知道,这属于SPI机制(Service Provider Interface),SpringServletContainerInitializer实现了ServletContainerInitializer接口。这属于J2EE的规范,因此,Servlet容器会实现。最终,SpringServletContainerInitializer会被实例化,并调用SpringServletContainerInitializer#onStartup()方法。这些操作不需要我们来做,是Tomcat在启动的时候帮我们做的,我们要做的就是在onStartup()方法中实现逻辑即可,而SpringServletContainerInitializer很明显是Spring提供的,看看该类的onStartup()方法,代码如下:

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

	@Override
	public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = Collections.emptyList();

		if (webAppInitializerClasses != null) {
			initializers = new ArrayList<>(webAppInitializerClasses.size());
			for (Class<?> waiClass : webAppInitializerClasses) {
				// 接口和抽象类servlet容器也会给我们,但是我们不要
				// 排除接口和容器
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						// 通过实例化并添加到集合中
						initializers.add((WebApplicationInitializer)
								ReflectionUtils.accessibleConstructor(waiClass).newInstance());
					} catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}

		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}

		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		AnnotationAwareOrderComparator.sort(initializers);
		// 调用initializer.onStartup  进行扩展
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}

}

        其中,@HandlesTypes注解会指定一个Class类型,也就是onStartup()方法的第一个入参类型。我还没研究过Tomcat的源码,我猜应该是Tomcat启动的时候,会从它自己的ClassLoader中获取到所有@HandlesTypes注解指定的Class,在调用onStartup()方法的时候传入。此时传入的就是所有实现了WebApplicationInitializer的类,也包括抽象类、接口等,因此需要过滤。当然也包括我自己写的MyWebApplicationInitializer类,通过反射实例化,放入到一个List中,最后遍历,调用WebApplicationInitializer#onStartup()方法。看看 MyWebApplicationInitializer类:

package com.szl.initialize;

import com.szl.config.RootConfig;
import com.szl.config.WebAppConfig;
import com.szl.listener.AppStartedApplicationListener;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class MyWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

	protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() {
		return new ApplicationContextInitializer[]{
				(applicationContext) -> {
					applicationContext.addApplicationListener(new AppStartedApplicationListener());
				}
		};
	}


    /**
	 * 返回父容器的配置类
	 * @return
	 */
	@Override
	protected Class<?>[] getRootConfigClasses() {
		return new Class[]{RootConfig.class};
	}

	/**
	 * 返回Web容器的配置类
	 * @return
	 */
	@Override
	protected Class<?>[] getServletConfigClasses() {
		return new Class[]{WebAppConfig.class};
	}

	/**
	 * 返回 DispatcherServlet的映射路径
	 * @return
	 */
	@Override
	protected String[] getServletMappings() {
		return new String[]{"/"};
	}
}

        最主要是实现 getRootConfigClasses()、getServletConfigClasses()、getServletMappings()方法等,其中前两个方法是实现SpringMVC父子容器的核心,分别返回的是RootConfig.class和WebAppConfig.class看看这两个类:

package com.szl.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;

import static org.springframework.context.annotation.FilterType.ASSIGNABLE_TYPE;

@Configuration
@ComponentScan(basePackages = "com.szl", excludeFilters = {
		@ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Controller.class}),
		@ComponentScan.Filter(type = ASSIGNABLE_TYPE, value = WebAppConfig.class),
})
public class RootConfig {

}
package com.szl.config;

import com.szl.interceptor.MyInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@ComponentScan(basePackages = {"com.szl"}, includeFilters = {
		@ComponentScan.Filter(type = FilterType.ANNOTATION, value = {RestController.class, Controller.class})
}, useDefaultFilters = false)
@EnableWebMvc   // = <mvc:annotation-driven/>
public class WebAppConfig implements WebMvcConfigurer {

	@Bean
	public MyInterceptor interceptor() {
		return new MyInterceptor();
	}

	@Bean
	public MultipartResolver multipartResolver() {
		CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
		multipartResolver.setDefaultEncoding("UTF-8");
		multipartResolver.setMaxUploadSize(1024 * 1024 * 10);
		return multipartResolver;
	}

/*	@Bean
	public AcceptHeaderLocaleResolver localeResolver() {
		AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
		return acceptHeaderLocaleResolver;
	}*/

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(interceptor()).addPathPatterns("/*");
	}


	@Bean
	public InternalResourceViewResolver internalResourceViewResolver() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
		viewResolver.setSuffix(".jsp");
		viewResolver.setPrefix("/WEB-INF/jsp/");
		return viewResolver;
	}

/*
	@Override
	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
		converters.add(new MappingJackson2HttpMessageConverter());
	}*/

}

        主要就是这两个类上的@ComponentScan注解,熟悉Springd的朋友应该知道,该注解用于指定要扫描的包路径。RootConfig上的@ComponentScan注解表示扫描的是:com.szl下的所有类,但是排除被@Controller注解修饰的类以及WebAppConfig类

        WebAppConfig的上@ComponentScan注解表示扫描的是:com.szl下的所有类,但是这些类必须是被@Controller或者@RestController注所修饰

        因此可以知道,RootConfig扫描的是非Web相关类,WebAppConfig扫描的是Web相关类。同时,MyWebApplicationInitializer#onStartup()方法,但是该方法是在其父类中实现的,代码如下:

        这里会调用AbstractAnnotationConfigDispatcherServletInitializer#getRootConfigClasses()方法,返回的数组只有RootConfig.class一个元素,创建AnnotationConfigWebApplicationContext对象,调用AnnotationConfigWebApplicationContext#register()方法,传入RootConfig.class,进行注册。创建ContextLoaderListener,调有参构造,传入AnnotationConfigWebApplicationContext对象,代码如下:

        并将ContextLoaderListener对象添加到ServletContext的监听器中,Tomcat启动的是会调用,最终调用到 ContextLoaderListener#contextInitialized()方法,代码如下:

        到这里为止,父容器已经完成了初始化,并且可以通过servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)方法获取到父容器。OK,再回到AbstractDispatcherServletInitializer#onStartup()方法中,代码如下:

        重点看看AbstractDispatcherServletInitializer#createServletApplicationContext()方法,代码如下:

        可知,会创建 AnnotationConfigWebApplicationContext对象,并将WebAppConfig注册到Web容器中,并创建DispatcherServlet,调用其有参构造,传入Web容器。并调用AbstractDispatcherServletInitializer#getServletApplicationContextInitializers()方法(该方法为抽象方法,在子类中实现,我在MyWebApplicationInitializer中实现了),返回的是ApplicationContextInitializer的集合,并将其设置到DispatcherServlet对象中的 contextInitializers属性中,这是SpringMVC的扩展点。调用ServletContext#addServlet(),传入DispatcherServlet对象和servletName,即"dispatcher",调用AbstractDispatcherServletInitializer#registerServletFilter()方法,代码如下:

        顺便看看MyWebApplicationInitializer#getServletApplicationContextInitializers(),代码如下:

        到这里为止,就聊完了MyWebApplicationInitializer是如何将DispatcherServlet对象注册到Web服务(Tomcat)的。而DispatcherServlet是SpringMVC的核心,它是一个Servlet对象。如果有请求到了DispatcherServlet这里,再通过它进行请求的分发,由它决定将具体调用哪个 Controller。

        先看看DispatcherServlet的继承图,如下:

        如果对Servlet熟悉的话,会知道Tomcat会自动调用GenericServlet#init(ServletConfig config)方法,代码如下:

        重点看看HttpServletBean#initServletBean()方法,这里会做初始化处理,以及调用 AbstractApplicationContext#refresh(方法等,代码如下:

        再看看FrameworkServlet#onRefresh()方法,代码如下:

        随便看几个,如下所示:

        剩下的几个组件:包括国际化、主题等等,这些有兴趣自己看看,我就不说了。以上几个组件,后面用到的时候都会讲。到现在为止,SpringMVC与Tomcat的整合以及SpringMVC的初始化讲完了。

        至于SpringMVC的父子容器,我多说两句:我觉得这没什么特殊的,就是创建两个AnnotationConfigWebApplicationContext对象,其中一个存储非Web相关的类(没有被@Controller、@RestController),他是父容器;另一个当然就是存储Web相关的类,它是子容器,并且将父容器赋值给子容器的parent属性。如果要获取某个Bean对象,首先调用子容器的getBean()方法,如果获取不到Bean对象,就调用父容器的getBean()方法获取Bean对象。我觉得不分父子容器,把所有的Bean对象都存储在一个容器中,也是可以的。

        剩下的内容讲DispatcherServler的流程,这将在下一篇博客《SpringMVC源码深度解析(中)》中讲,敬请期待~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1938054.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ETAS RTM配置及使用-CPU Load测量/task时间测量/Isr时间测量

文章目录 前言RTM配置RtmControlRtmGeneralRtmMonitorOS配置RTE配置集成与测试初始化主函数函数执行测量测试CPU LoadTask MonitorISR Monitor函数监控总结前言 一般对CPU Load的测量,task及runnable的监控等有两种方案: 1.需要使用带trace功能的调试器,且硬件也需要支持对…

鸿蒙开发 01 实现骰子布局

鸿蒙开发 01 鸿蒙开发 01 实现骰子布局 1、效果2、代码 1、效果 2、代码 Entry Component struct Index {State message: string Hello Worldbuild() {Column() {Row() {Radio({ value: Radio1, group: radioGroup }).checked(false).height(100)Radio({ value: Radio1, grou…

【Linux线程】线程的认识

目录 线程的概念及一些基本理论 线程异常 线程与进程的关系 线程ID、线程控制块 线程的概念及一些基本理论 在一个程序里的一个执行路线就叫做线程&#xff08;thread&#xff09;。更准确的定义是&#xff1a;线程是“一个进程内部的控制序列” 。 Linux没有真正意义上的线…

【每日刷题】Day83

【每日刷题】Day83 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. LCR 192. 把字符串转换成整数 (atoi) - 力扣&#xff08;LeetCode&#xff09; 2. 415. 字符串相加…

ubuntu安装显卡驱动,anaconda,cuda,cudnn,pytorch

安装显卡驱动&#xff1a;主要参考第一篇。 Ubuntu22.04安装显卡驱动(高速、避错版)-CSDN博客 [超级详细系列]ubuntu22.04配置深度学习环境(显卡驱动CUDAcuDNNPytorch)--[2]安装Anaconda与CUDA_ubuntu22.04配置cuda环境变量-CSDN博客 安装anaconda和cuda [超级详细系列]ubun…

从0开始的informer代码解读

股价预测源码原版来源 github https://github.com/zhouhaoyi/Informer2020 数据 工欲善其事必先利其器&#xff0c;这是我经常说的。所以了解我们的数据处理很重要&#xff0c;毕竟要的就是地地地地地地道。 源码中的date数据很重要。但是我们要知道我们下载的代码具有可拓…

python-NLP:1中文分词

文章目录 规则分词正向最大匹配法逆向最大匹配法双向最大匹配法 统计分词语言模型HMM模型 jieba分词分词关键词提取词性标注 规则分词 基于规则的分词是一种机械分词方法&#xff0c;主要是通过维护词典&#xff0c;在切分语句时&#xff0c;将语句的每个字符串与词表中的词进行…

pytorch学习(十二)c++调用minist训练的onnx模型

在实际使用过程中&#xff0c;使用python速度不够快&#xff0c;并且不太好嵌入到c程序中&#xff0c;因此可以把pytorch训练的模型转成onnx模型&#xff0c;然后使用opencv进行调用。 所需要用到的库有&#xff1a; opencv 1.完整的程序如下 import torch from torch impo…

06. 截断文本 选择任何链接 :root 和 html 有什么区别

截断文本 对超过一行的文本进行截断,在末尾添加省略号(…)。 使用 overflow: hidden 防止文本超出其尺寸。使用 white-space: nowrap 防止文本超过一行高度。使用 text-overflow: ellipsis 使得如果文本超出其尺寸,将以省略号结尾。为元素指定固定的 width,以确定何时显示省略…

韩顺平0基础学Java——第35天

p689-714 格式化语句 gpt说的&#xff1a; System.out.println 方法不支持像 printf 一样的格式化字符串。要使用格式化字符串&#xff0c;你可以使用 System.out.printf 方法或将格式化后的字符串传递给 System.out.println。下面是两种修正的方法&#xff1a; ### 方法一…

科研绘图系列:R语言circos图(circos plot)

介绍 Circos图是一种数据可视化工具,它以圆形布局展示数据,通常用于显示数据之间的关系和模式。这种图表特别适合于展示分层数据或网络关系。Circos图的一些关键特点包括: 圆形布局:数据被组织在一个或多个同心圆中,每个圆可以代表不同的数据维度或层次。扇区:每个圆被划…

昇思25天学习打卡营第25天|MindNLP ChatGLM-6B StreamChat

配置环节 %%capture captured_output !pip uninstall mindspore -y !pip install -i https://pypi.mirrors.ustc.edu.cn/simple mindspore2.2.14 !pip install mindnlp !pip install mdtex2html配置国内镜像 !export HF_ENDPOINThttps://hf-mirror.com下载与加载模型 from m…

【safari】react在safari浏览器中,遇到异步时间差的问题,导致状态没有及时更新到state,引起传参错误。如何解决

在safari浏览器中&#xff0c;可能会遇到异步时间差的问题&#xff0c;导致状态没有及时更新到state&#xff0c;引起传参错误。 PS&#xff1a;由于useState是一个普通的函数&#xff0c; 定义为() > void;因此此处不能用await/async替代setTimeout&#xff0c;只能用在返…

Vue3 composition api计算属性活学活用(作业题1 - 计算扁平化树树节点的索引)

本示例节选自vue3最新开源组件实战教程大纲&#xff08;持续更新中&#xff09;的tree组件开发部分。在学习了tree组件实现折叠与展开功能&#xff08;方式2 - visible计算属性&#xff09;后&#xff0c;给读者朋友留的一道编程作业题。 作业要求 合理的设计和实现树节点的计…

【C#】计算两条直线的交点坐标

问题描述 计算两条直线的交点坐标&#xff0c;可以理解为给定坐标P1、P2、P3、P4&#xff0c;形成两条线&#xff0c;返回这两条直线的交点坐标&#xff1f; 注意区分&#xff1a;这两条线是否垂直、是否平行。 代码实现 斜率解释 斜率是数学中的一个概念&#xff0c;特别是…

HTML开发笔记:1.环境、标签和属性、CSS语法

一、环境与新建 在VSCODE里&#xff0c;加载插件&#xff1a;“open in browser” 然后新建一个文件夹&#xff0c;再在VSCODE中打开该文件夹&#xff0c;在右上角图标新建文档&#xff0c;一定要是加.html&#xff0c;不要忘了文件后缀 复制任意一个代码比如&#xff1a; <…

reserve和resize

void test_vector4() {vector<int> v1;//cout << v1.max_size() << endl;//v1.reserve(10);v1.resize(10);for (size_t i 0; i < 10; i){v1[i] i;}for (auto e : v1){cout << e << " ";}cout << endl;} 在上面这段代码中对…

数学建模--国赛备赛---TOPSIS算法

目录 1.准备部分 1.1提交材料 1.2MD5码相关要求 2.TOPSIS算法 2.1算法概述 2.2基本概念 2.3算法核心思想 2.4拓展思考 3.适用赛题 3.1适用赛题说明 3.2适用赛题举例 4.赛题分析 4.1指标的分类 4.2数据预处理 4.2.1区间型属性的变换 4.2.2向量规范化 4.3数据加…

vue 侧边锚点外圆角

环境&#xff1a;uniapp、vue3、unocss、vant4 效果&#xff1a; 代码 主要是&#xff1a;pointTop 、pointCentent 、pointBottom&#xff0c;这三个样式 html <div v-show"!showPoint" class"fixedLeftDiv"><div><div class"pointT…

RPG素材Unity7月20闪促限时4折游戏开发资产兽人角色模型动画休闲放置模板物理交互流体水下焦散VR界面UI2D模板场景20240720

今天这个是RPG素材比较多&#xff0c;还有一些休闲放置模板、FPS场景素材、角色模型、动画、特效。 详细内容展示&#xff1a;www.bilibili.com/video/BV1Tx4y1s7vm 闪促限时4折&#xff1a;https://prf.hn/l/0eEOG1P 半价促销&#xff1a;https://prf.hn/l/RlDmDeQ 7月闪促…