spring mvc作为优秀的web框架,从2003年问世(根据changelog)到现在已经经历了21年。springframework框架里,web相关的类从1.0版本的25个,发展到现在6.1版本,已经有103个。还不包括spring-boot里web相关的代码。初学者使用spring-boot-starter-web
能很快启动一个web服务,但是要理清内部的运行逻辑和理解作者的设计思路,就要花费很大力气。
下面我尝试模仿spring mvc,从0开始搭建web服务,剖析作者的设计意图。
tomcat作为开源轻量级web服务器,支持java servlet,是spring boot默认的web服务器。通过内嵌的tomcat,我们可以很快速的开发web应用。我们通过一个demo,看一下开发web应用需要的最小配置。
引入内嵌tomcat的pom文件:
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>10.1.18</version>
<scope>compile</scope>
</dependency>
然后注册一个servlet就可以对外提供服务。
public static void main(String[] args) throws LifecycleException {
Tomcat tomcat = new Tomcat();
String path = "C:\\\\Users\\\\admin\\\\AppData\\\\Local\\\\Temp\\\\tomcat.default.9999";
tomcat.setBaseDir(path);
Context context = tomcat.addContext("", path);
tomcat.addServlet(context.getPath(), "defaultServlet", new HttpServlet() {
@Override
public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("get request " + req.getRequestURL());
resp.setStatus(200);
PrintWriter writer = resp.getWriter();
writer.println(System.currentTimeMillis());
}
});
context.addServletMappingDecoded("/*", "defaultServlet");
Connector connector = new Connector("HTTP/1.1");
connector.setPort(9999);
tomcat.setConnector(connector);
tomcat.start();
tomcat.getServer().await();
}
这里做了几件事情:
访问http://localhost:9999就能看到正常返回了结果:
> curl -i <http://localhost:9999/abc/dd?xx=1>
HTTP/1.1 200
Content-Length: 15
Date: Wed, 24 Jan 2024 04:05:39 GMT
1706069139905
了解完tomcat的基本使用方式,再对比spring boot里tomcat的用法。下面是spring boot初始化tomcat的逻辑:
// org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
for (LifecycleListener listener : this.serverLifecycleListeners) {
tomcat.getServer().addLifecycleListener(listener);
}
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}
它这里干了几个事:
backgroundProcessorDelay
属性和添加自定义engineValves
TomcatWebServer
对象在不考虑内部细节的情况下,spring boot初始化tomcat的步骤基本和demo里的步骤是一样的。主要增加了很多扩展点,可以添加Server容器、自定义Connector、添加Engine的Valve(管道处理类)。还封装了tomcat的Context,在自己的Context里也加了很多扩展点。
另外,有一点很大的不同,就是spring boot的初始流程里没用看到注册Servlet的地方。我们都知道spring mvc核心的Servlet是DispatcherServlet,它会代理所有请求。下面分析DispatcherServlet是怎么注册到tomcat的Context。
spring boot是通过ServletContextInitializer来注册tomcat的Servlet、Filter、Listener等对象到ServletContext里。所以第一步要先将DispatcherServlet转成ServletContextInitializer对象。
DispatcherServlet是一个Servlet类型,要转成ServletContextInitializer,需要一个包装类,DispatcherServletRegistrationBean就是这个包装类。 DispatcherServletRegistrationBean是ServletContextInitializer的子类,在spring boot启动时,通过自动装配机制,注册了DispatcherServletRegistrationBean。并且将DispatcherServlet对象放到了DispatcherServletRegistrationBean对象里。 DispatcherServletRegistrationBean的注册流程:
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
在得到ServletContextInitializer后就要考虑什么时候去执行它,并将DispatcherServlet注册到tomcat容器。这时先看一下tomcat的初始化过程。
SpringApplication.run()方法在执行refreshContext(context)时,会调用AnnotationConfigServletWebServerApplicationContext.refresh()方法,一直会调用到web容器的父类ServletWebServerApplicationContext的createWebServer()。
createWebServer()会调用TomcatServletWebServerFactory.getWebServer()来初始化tomcat对象。
初始化tomcat对象需要设置一个tomcat上下文,对应类型是StandardContext。这里spring自定义了StandardContext的子类TomcatEmbeddedContext作为tomcat上下文。
这时spring会预定义3个ServletContextInitializer,并封装到TomcatStarter里。TomcatStarter是ServletContainerInitializer的子类,ServletContainerInitializer是tomcat的对象和ServletContextInitializer不一样,后者是spring的对象。 然后调用TomcatEmbeddedContext.addServletContainerInitializer(TomcatStarter),把TomcatStarter添加到tomcat上下文的initializers属性里。initializers属性在启动tomcat时会用到。
tomcat初始化后,会被包装成TomcatWebServer对象,然后在构造函数里启动tomcat。之后tomcat就会从上下文对象里拿到ServletContextInitializer进行初始化。
private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}
核心逻辑在getServletContextInitializerBeans()里,方法返回ServletContextInitializerBeans对象。 ServletContextInitializerBeans是ServletContextInitializer的集合,它会把beanFactory里的ServletContextInitializer对象加进来,并且还把Servlet、Fileter、Listerner等spring bean包装成RegistrationBean(RegistrationBean是ServletContextInitializer的子类)也加进来。这样就得到一个ServletContextInitializer列表,默认会加载的对象有:
然后调用每个ServletContextInitializer的onStartup(servletContext)方法。
注册DispatcherServlet要执行这两行代码:
// 添加servlet
servletContext.addServlet(servletName, servlet);
// 添加映射关系
context.addServletMappingDecoded(urlPattern, servletName);
DispatcherServletRegistrationBean里onStratup方法会调用register()方法。register()方法会做两个事情:
执行addRegistration()方法,会把里面的DispatcherServlet对象注册到ServletContext,并返回ServletRegistration对象。
protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
String name = getServletName();
return servletContext.addServlet(name, this.servlet);
}
执行configure()方法,注册url到DispatcherServlet对象的映射,逻辑在registration.addMapping(urlMapping)里
// org.springframework.boot.web.servlet.ServletRegistrationBean#configure
protected void configure(ServletRegistration.Dynamic registration) {
// ...
if (!ObjectUtils.isEmpty(urlMapping)) {
registration.addMapping(urlMapping);
}
// ...
}
// org.apache.catalina.core.ApplicationServletRegistration#addMapping
public Set<String> addMapping(String... urlPatterns) {
// ...
for (String urlPattern : urlPatterns) {
context.addServletMappingDecoded(UDecoder.URLDecode(urlPattern, StandardCharsets.UTF_8), wrapper.getName());
}
return Collections.emptySet();
}
到这里,DispatcherServlet就成功注册到了toncat的上下文上,并且和url建立了映射关系,默认url是"/"。
spring boot的基础是tomcat,就要遵循tomcat的servlet规范。它通过ServletContextInitializer实现了Servlet的自动注册机制;用DispatcherServlet代理所有请求,内部实现了请求的路由、类型转换等。将开发者和tomcat解耦,也方便框架去替换不同的web容器。