MVC 运行入口及嵌入式Tomcat的使用

警告
本文最后更新于 2020-12-15,文中内容可能已过时。

spring源码系列文章,示例代码的中文注释,均是 copy 自 https://gitee.com/wlizhi/spring-framework

链接中源码是作者从 github 下载,并以自身理解对核心流程及主要节点做了详细的中文注释。


1 入口

根据文章 web容器启动时ServletContainerInitializer的加载 中所述,创建一个 WebApplicationInitializer 的实现类,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
	/** 根容器 父容器 */
	@Override
	protected Class<?>[] getRootConfigClasses() {
		logger.info("===========spring根容器加载成功============");
		return new Class[]{RootConfig.class};
	}

	/** web容器 子容器 */
	@Override
	protected Class<?>[] getServletConfigClasses() {
		logger.info("===========springweb子容器加载成功============");
		return new Class[]{WebConfig.class};
	}

	/** 获取前端控制器映射信息 dispatcherServlet */
	@Override
	protected String[] getServletMappings() {
		return new String[]{"/"};
	}
}

上面代码中,共重写了三个方法,分别是spring父容器的配置类、mvc子容器的配置类、DispatcherServlet 的拦截路径的设置。

在 mvc 子容器中,使用注解 @EnableWebMvc 来激活mvc功能。

mvc 子容器配置类代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@EnableWebMvc
@ComponentScan(value = {"top.wlz922.controller", "top.wlz922.interceptor", "top.wlz922.resolver"}
//		, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})}
//		, useDefaultFilters = true
)
@EnableAspectJAutoProxy
public class WebConfig implements WebMvcConfigurer {
	private static final Logger logger = LoggerFactory.getLogger(WebConfig.class);
	@Autowired(required = false)
	private List<HandlerInterceptor> interceptors;

	/**
	 * 配置springmvc视图
	 */
	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		logger.info("============jsp解析器开始加载===============");
//        registry.jsp();
		registry.jsp("/WEB-INF/views/", ".jsp");

		logger.info("============jsp解析器开始成功,开始渲染把===============");
	}

	@Override
	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
		ArrayList<MediaType> list = new ArrayList<>();
		list.add(MediaType.APPLICATION_JSON);
		list.add(MediaType.TEXT_PLAIN);
		FastJsonHttpMessageConverter c = new FastJsonHttpMessageConverter();
		c.setSupportedMediaTypes(list);
		converters.add(c);
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		if (interceptors != null) {
			for (HandlerInterceptor interceptor : interceptors) {
				InterceptorRegistration registration = registry.addInterceptor(interceptor);
				if (interceptor instanceof HaldlerInterceptorPathPattern) {
					String[] pathPatterns = ((HaldlerInterceptorPathPattern) interceptor).getPathPatterns();
					registration.addPathPatterns(pathPatterns);
				}
			}
		}
	}
}

根据 Servlet3.0 规范,tomcat 容器在启动时,会根据 SPI 机制加载配置的 ServletContainerInitializer 实现类。springmvc中使用的 SpringServletContainerInitializer,在这个类声明中,引入了 WebApplicationInitializer。

在 WebApplicationInitializer 的 onStartup() 中,会在创建 DispatcherServlet 时,创建一个 WebApplicationContext,将其设置到 DispatcherServlet 的成员变量中。

而在创建这个 WebApplicationContext 时,会调用 WebApplicationInitializer 实现类的回调方法 getServletConfigClasses(),获取到 mvc 子容器的配置类。

接着,在 mvc 子容器核心方法 refresh() 调用的时候,就会扫描加载这个配置类中的信息。

大致流程是:

  1. SPI机制加载 ServletContainerInitializer 实现类。
  2. 执行 HandlerTypes 中声明的 WebApplicationInitializer 实现类。
  3. 调用 WebApplicationInitializer.onStartup()。
  4. 创建父容器 ApplicationContext,调用钩子方法 getRootConfigClasses(),将其设置为父容器 refresh() 的配置类,并将父容器缓存到 ServletContext 中。
  5. 创建 DispatcherServlet,并创建 WebApplicationContext 子容器,在创建时,调用钩子方法 getServletConfigClasses() 作为子容器 refresh() 的配置类,将子容器设置到 DispatcherServlet 成员变量中。
  6. 在父容器、子容器 refresh() 执行的时候,就会加载扫描各自配置类中的注解。

@EnableWebMvc 注解是对 mvc功能的支持,内部通过 @Bean 向 ioc 容器中注入了许多功能支持的类实例。这里是 mvc 功能支持的入口。

2 嵌入式 Tomcat

首先,引入依赖包:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!-- 嵌入式tomcat依赖 -->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>${tomcat.embed.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <version>${tomcat.embed.version}</version>
    <!--<scope>provided</scope>-->
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-logging-juli</artifactId>
    <version>${tomcat.embed.version}</version>
</dependency>

编写启动类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class SpringSourceMvcAnoApplication {
	private static final int PORT = 8080;
	private static final String CONTEXT_PATH = "";
	private static final Logger LOG = LoggerFactory.getLogger(SpringSourceMvcAnoApplication.class);

	public static void main(String[] args) throws Exception {
		SpringSourceMvcAnoApplication starter = new SpringSourceMvcAnoApplication();
		starter.start();
	}

	public void start() throws Exception {
		LOG.info("=================开始加载内嵌tomcat=====================");
		String userDir = System.getProperty("user.dir");
		// 这种方式设置System.setProperty("tomcat.util.scan.StandardJarScanFilter.jarsToSkip", "\\,*");
		// 可以加快启动速度,但是由于读取的字节码文件是maven编译的(maven编译使用的本地仓库的jar包),会导致gradle项目中的代码断点无效。
		// 这里设置为直接读取src/main/webapp下的文件,自动扫描baseDir下的java代码。(启动慢,可在gradle项目代码中打断点调试。)
		// String webappDir = userDir + File.separator + "/target/spring-source-mvc-ano-1.0-SNAPSHOT";
		String webappDir = userDir + File.separator + "/src/main/webapp";

		Tomcat tomcat = new Tomcat();
		tomcat.setBaseDir(userDir);
		tomcat.setPort(PORT);

		tomcat.addWebapp(CONTEXT_PATH, webappDir);

		tomcat.enableNaming();
		LOG.info("BaseDir:{}", userDir);
		LOG.info("port:{}", PORT);
		LOG.info("context_path:{}", webappDir);

		// System.setProperty("tomcat.util.scan.StandardJarScanFilter.jarsToSkip", "\\,*");
		tomcat.start();
		LOG.info("==================tomcat加载成功==========================");
		LOG.info("测试地址:http://localhost:8080/index");
		tomcat.getServer().await();
	}
}

通过运行 main() 方法来启动一个嵌入式 tomcat,它会根据 上面 的步骤,加载对应的实现类。