环境准备

相关文档

Spring Framework: https://spring.io/projects/spring-framework/ Reference: https://docs.spring.io/spring-framework/reference/web/webmvc.html

源码仓库 (Spring framework )

https://github.com/spring-projects/spring-framework 实例 v6.1.1版本源码: https://github.com/spring-projects/spring-framework/releases/tag/v6.1.1

环境搭建

入口webapp模块

新建一个子模块, 基于Spring Framework 的源码运行 new module > my-learn-spring-mvc > gradle项目

添加源码项目依赖

plugins {  
    id 'java'  
}  
  
group 'org.springframework'  
version '6.1.1'  
  
repositories {  
    mavenCentral()  
}  
  
dependencies {  
	implementation( project(':spring-context'))  
	implementation( project(':spring-web'))  
	implementation( project(':spring-webmvc'))
	    
	//配置某文件夹作为依赖项  
	implementation fileTree(dir: 'lib', include: ['*.jar'])
	
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'  
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'  
}  
  
test {  
    useJUnitPlatform()  
}

添加本地源码项目依赖

File Project Structure Artifacts

  • 添加本地源码项目编译的class, 选中项目右键: Put into WEB-INF/classes
  • 添加本地源码项目依赖的库, 选中项目的lib右键: Put into WEB-INF/lib
  • 添加本地源码项目web标记的资源: add copy of >Java EE Facet Resources
  • 添加本地项目资源: Directory Content > 添加上模块的 resources 目录
  • 在模型目录创建 lib文件夹, 复制Tomcat的 lib下面的jar包 到该目录, 缺失servlet-api的问题

最终

问题

  • The binary version of its metadata is 1.9.0, expected version is 1.7.1.

不是明面上的问题, 莫名其妙, 编译 spring-web 会编译其 kotlin 代码 File Project Structure Facets > kotlin 附加编译参数: -Xskip-metadata-version-check

  • java.lang.ClassCastException: class org.springframework.web.servlet.DispatcherServlet cannot be cast to class javax.servlet.Servlet (org.springframework.web.servlet.DispatcherServlet is in unnamed module of loader org.apache.catalina.loader.ParallelWebappClassLoader @76a47fdd; javax.servlet.Servlet is in unnamed module of loader java.net.URLClassLoader @4cf4d528) at org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1044) at org.apache.catalina.core.StandardWrapper.allocate(StandardWrapper.java:763)

Oracle 整活 从Java EE 迁移到 Jakarta EE的问题 参考 https://zh.wikipedia.org/wiki/Java_Servlet

Servlet API 版本发布日期平台重要变化
Servlet 5.02020年6月12日 (页面存档备份,存于互联网档案馆Jakarta EE 9移到包名到“jakarta.servlet
Servlet 4.0.32019年3月13日 (页面存档备份,存于互联网档案馆Jakarta EE 8去除“Java”商标
Servlet 4.02017年9月 (页面存档备份,存于互联网档案馆Java EE 8https://zh.wikipedia.org/wiki/HTTP/2 “HTTP/2”

Tomcat 需要 10.x 版本之后才支持 Servlet 4.0 The Apache Tomcat Project is proud to announce the release of version 10.1.20 of Apache Tomcat. This release implements specifications that are part of the Jakarta EE 10 platform.

  • 找到多个名为[org_apache_tomcat_websocket]的片段。这是不合法的相对排序。有关详细信息,请参阅Servlet规范的第8.2.2 2c节 错误的含义是在打包的WAR文件中存在多个同名但内容不同的文件,这些文件可能是由于不同的库被打包进了WAR,导致了相同的资源名称冲突。

归档

Transclude of my-learn-spring-mvc.zip

前置知识

两个容器(Context)

在Spring MVC 中有两个容器(Context):

1. 父容器 (Root WebApplicationContext)

web.xml 它的配置

<!-- spring Context, 配置文件名 applicationContext.xml 约定为父容器配置-->
<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>classpath:applicationContext.xml</param-value>
</context-param>
 
...
<!-- spring mvc 监听器-->
<listener>
	<listener-class>
		org.springframework.web.context.ContextLoaderListener
	</listener-class>
</listener>
</web-app>

相关源码

它的源码入口点是Servlet监听器 contextInitialized, 由Servlet容器调用(即 Tomcat 在容器启动时便会初始化)

org.springframework.web.context.ContextLoaderListener#contextInitialized Spring Servlet 监听器入口

创建 Context

org.springframework.web.context.ContextLoader#initWebApplicationContext

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
		if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
			throw new IllegalStateException(
					"Cannot initialize context because there is already a root application context present - " +
					"check whether you have multiple ContextLoader* definitions in your web.xml!");
		}
 
		servletContext.log("Initializing Spring root WebApplicationContext");
		Log logger = LogFactory.getLog(ContextLoader.class);
		if (logger.isInfoEnabled()) {
			logger.info("Root WebApplicationContext: initialization started");
		}
		long startTime = System.currentTimeMillis();
 
		try {
			// Store context in local instance variable, to guarantee that
			// it is available on ServletContext shutdown.
			/**
			 * 如果当前 context 为 null 则创建它
			 */
			if (this.context == null) {
				/**
				 *  * 1. 推断应用上下文的class
				 * 	 (例如: XML的是 ClassPathXmlApplicationContext, 注解的是: AnnotationConfigApplicationContext)
				 * 	 这里是读取 web模块的{@link org.springframework.web.context.ContextLoader#DEFAULT_STRATEGIES_PATH}
				 * 	 定义的配置文件名`ContextLoader.properties` 指向的 {@link org.springframework.web.context.support.XmlWebApplicationContext}
				 * 	 * 2. 验证下如果Context 不是 ConfigurableWebApplicationContext 则抛异常;
				 * 	 因为用户可以自定义配置Context的实现类
				 * 	 * 3. 反射 拿到构造函数实例化它
				 * 	 注意第一次获取的是 Root (父容器)级别的context //TODO?
				 */
				this.context = createWebApplicationContext(servletContext);
			}
			if (this.context instanceof ConfigurableWebApplicationContext cwac && !cwac.isActive()) {
				// The context has not yet been refreshed -> provide services such as
				// setting the parent context, setting the application context id, etc
				/**
				 * 如果父容器为null, 则加载父容器
				 */
				if (cwac.getParent() == null) {
					// The context instance was injected without an explicit parent ->
					// determine parent for root web application context, if any.
					ApplicationContext parent = loadParentContext(servletContext);
					cwac.setParent(parent);
				}
				/**
				 * 关键方法:
				 * 1. 它会读取到 applicationContext.xml 即 <context-param>  (名称为contextConfigLocation)标签的参数
				 * 		 * 	<context-param>
				 * 		 * 		<param-name>contextConfigLocation</param-name>
				 * 		 * 		<param-value>classpath:applicationContext.xml</param-value>
				 * 		 * 	</context-param>
				 * 		 * 设置这个配置文件的路径到 Context中, 逻辑跟IOC一样, [[见 IOC 的笔记]]
				 * 2. 获取环境对象
				 * 3. 定制化 context
				 * 4. 最终调用到 {@link org.springframework.context.support.AbstractApplicationContext#refresh()} 
				 * 创建 BeanFactory 刷新上下文
				 * 
				 */
				configureAndRefreshWebApplicationContext(cwac, servletContext);
			}
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
 
			ClassLoader ccl = Thread.currentThread().getContextClassLoader();
			if (ccl == ContextLoader.class.getClassLoader()) {
				currentContext = this.context;
			}
			else if (ccl != null) {
				currentContextPerThread.put(ccl, this.context);
			}
 
			if (logger.isInfoEnabled()) {
				long elapsedTime = System.currentTimeMillis() - startTime;
				logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
			}
 
			return this.context;
		}
		catch (RuntimeException | Error ex) {
			logger.error("Context initialization failed", ex);
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
			throw ex;
		}
	}
创建 BeanFactory

org.springframework.web.context.ContextLoader#configureAndRefreshWebApplicationContext

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
		/**
		 * 设置一下 context的唯一ID标识
		 * 如果有配置参数 contextId(CONTEXT_ID_PARAM) 则用配置的, 没有就生成一个
		 */
		if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
			// The application context id is still set to its original default value
			// -> assign a more useful id based on available information
			String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
			if (idParam != null) {
				wac.setId(idParam);
			}
			else {
				// Generate default id...
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(sc.getContextPath()));
			}
		}
 
		wac.setServletContext(sc);
		/**
		 * 注意 这个就是配置的 applicationContext.xml (contextConfigLocation) <context-param> 标签的参数
		 * 	<context-param>
		 * 		<param-name>contextConfigLocation</param-name>
		 * 		<param-value>classpath:applicationContext.xml</param-value>
		 * 	</context-param>
		 *
		 * 设置这个配置文件的路径到 Context中, 逻辑跟IOC一样, [[见 IOC 的笔记]]
		 */
		String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
		if (configLocationParam != null) {
			wac.setConfigLocation(configLocationParam);
		}
 
		/**
		 * 获取环境对象, 如果是可配置的(ConfigurableWebEnvironment) 则
		 *  替换环境对象中的 servletContextInitParams, servletConfigInitParams
		 *
		 * 	 @see org.springframework.web.context.support.WebApplicationContextUtils#initServletPropertySources(org.springframework.core.env.MutablePropertySources, jakarta.servlet.ServletContext, jakarta.servlet.ServletConfig)
		 */
		// The wac environment's #initPropertySources will be called in any case when the context
		// is refreshed; do it eagerly here to ensure servlet property sources are in place for
		// use in any post-processing or initialization that occurs below prior to #refresh
		ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment cwe) {
			cwe.initPropertySources(sc, null);
		}
		/**
		 * context 定制化
		 */
		customizeContext(sc, wac);
		/**
		 * context refresh, 注意当前的 是根(父)容器, 它对应的配置文件是 `applicationContext.xml`
		 * 
		 * 调用到 {@link org.springframework.context.support.AbstractApplicationContext#refresh()} 
		 * [[见IOC的笔记]]
		 */
		wac.refresh();
	}

2. 子容器 (WebApplicationContext for namespace ‘spring-mvc-servlet’)

web.xml 它的配置

<!-- spring mvc servlet, 配置文件 spring-mvc.xml 约定为子容器配置 -->
<servlet>
	<servlet-name>spring-mvc</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:spring-mvc.xml</param-value>
	</init-param>
</servlet>
<servlet-mapping>
	<servlet-name>spring-mvc</servlet-name>
	<url-pattern>/*</url-pattern>
</servlet-mapping>

相关源码

本质上它是Servlet 所以入口方法时 Servlet#init, 走生命周期流程, 当有请求 servlet-mapping 匹配到该Servlet容器才会初始化

org.springframework.web.servlet.HttpServletBean#init 入口

创建 Context 和 BeanFactory

org.springframework.web.servlet.FrameworkServlet#createWebApplicationContext(org.springframework.context.ApplicationContext)

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
		/**
		 * 1. 获取到 context 的 class,这里同样是 org.springframework.web.context.support.XmlWebApplicationContext
		 */
		Class<?> contextClass = getContextClass();
		if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
			throw new ApplicationContextException(
					"Fatal initialization error in servlet with name '" + getServletName() +
					"': custom WebApplicationContext class [" + contextClass.getName() +
					"] is not of type ConfigurableWebApplicationContext");
		}
		/**
		 * 2. 实例化 context
		 */
		ConfigurableWebApplicationContext wac =
				(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
 
		/**
		 * 3. 设置环境对象
		 */
		wac.setEnvironment(getEnvironment());
		/**
		 * 4.设置父容器, 就是此前由监听器创建的(Root WebApplicationContext)
		 */
		wac.setParent(parent);
		String configLocation = getContextConfigLocation();
		if (configLocation != null) {
			wac.setConfigLocation(configLocation);
		}
		/**
		 * 5. 创建BeanFactory, refresh ...
		 */
		configureAndRefreshWebApplicationContext(wac);
		return wac;
	}

ServletContext 和 ServletConfig

在Java EE中,ServletContextServletConfig 都是与Servlet相关的重要接口,它们提供了访问Web应用程序环境和配置信息的能力,但它们的作用和使用方式略有不同。

ServletContext

jakarta.servlet.ServletContext 接口代表了整个Web应用程序的上下文环境,可以在整个应用程序范围内共享数据。每个Web应用程序都有一个对应的 ServletContext 实例。可以通过 getServletContext() 方法在任何Servlet中获取 ServletContext 实例。

作用:

  • 提供了一种在整个Web应用程序范围内共享信息的机制。
  • 允许Servlet与Web容器进行通信,获取应用程序级别的配置信息、资源和日志等。

常见用途:

  • 获取Web应用程序范围内的初始化参数。
  • 存储和获取全局数据,如数据库连接池、计数器等。
  • 访问Web应用程序的资源,如文件、图片等。

常用方法:

  • getInitParameter(String name):获取初始化参数。
  • getInitParameterNames():获取所有初始化参数的名称。
  • setAttribute(String name, Object object):设置属性。
  • getAttribute(String name):获取属性。
  • removeAttribute(String name):移除属性。
  • getRequestDispatcher(String path):获取请求分派器,用于转发请求。

ServletConfig

jakarta.servlet.ServletConfig 接口用于在Servlet初始化阶段获取Servlet的配置信息。每个Servlet实例都有一个对应的 ServletConfig 实例。ServletConfig 对象由Servlet容器在Servlet初始化时传递给Servlet。

作用:

  • 允许Servlet获取自身的初始化参数和配置信息。
  • 使得Servlet在运行时可以根据配置进行适当的行为。

常见用途:

  • 获取Servlet的初始化参数,如数据库连接信息、文件路径等。
  • 通过Servlet容器传递的配置信息初始化Servlet。

常用方法:

  • getInitParameter(String name):获取初始化参数。
  • getInitParameterNames():获取所有初始化参数的名称。
  • getServletName():获取Servlet的名称。

区别

  • ServletContext 是整个Web应用程序的上下文环境,提供了在整个应用程序范围内共享数据的机制,而 ServletConfig 是Servlet的配置信息,提供了在Servlet初始化阶段获取配置信息的能力。
  • ServletContext 是由Web容器创建和管理的,而 ServletConfig 是由Servlet容器在初始化Servlet时传递给Servlet的。
  • 在使用上,ServletContext 通常用于存储和获取整个应用程序范围内的数据,而 ServletConfig 用于获取Servlet的初始化参数和配置信息。

源码流程

Spring MVC 中的 两个容器和九大组件初始化过程

  1. Spring MVC 父容器 Spring MVC 父容器的加载入口: org.springframework.web.context.ContextLoaderListener 本质上它是一个 Servlet 的监听器

  2. Spring MVC 子容器 Spring MVC 子容器的加载入口: org.springframework.web.servlet.DispatcherServlet 本质上它是一个Servlet

Spring MVC 中九大组件

Spring MVC中的九大组件

Spring MVC 中请求处理流程

Spring MVC中的参数解析&响应处理

Spring MVC中的参数解析&响应处理

TODO 继续

https://www.mashibing.com/study?courseNo=49&sectionNo=30992&courseVersionId=1098