SpringMVC源码深度解析(上)

        今天,聊聊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源码深度解析(中)》中讲,敬请期待~

  • 19
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值