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