今天,聊聊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 {@Overridepublic 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*/@Overrideprotected Class<?>[] getRootConfigClasses() {return new Class[]{RootConfig.class};}/*** 返回Web容器的配置类* @return*/@Overrideprotected Class<?>[] getServletConfigClasses() {return new Class[]{WebAppConfig.class};}/*** 返回 DispatcherServlet的映射路径* @return*/@Overrideprotected 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 {@Beanpublic MyInterceptor interceptor() {return new MyInterceptor();}@Beanpublic MultipartResolver multipartResolver() {CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();multipartResolver.setDefaultEncoding("UTF-8");multipartResolver.setMaxUploadSize(1024 * 1024 * 10);return multipartResolver;}/* @Beanpublic AcceptHeaderLocaleResolver localeResolver() {AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();return acceptHeaderLocaleResolver;}*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(interceptor()).addPathPatterns("/*");}@Beanpublic InternalResourceViewResolver internalResourceViewResolver() {InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();viewResolver.setSuffix(".jsp");viewResolver.setPrefix("/WEB-INF/jsp/");return viewResolver;}/*@Overridepublic 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源码深度解析(中)》中讲,敬请期待~