tomcat使用简单示范
简单回顾下内嵌tomcat使用,新建一个maven项目,导入如下依赖
复制代码 javax.annotation javax.annotation-api 1.3.2 compile org.apache.tomcat.embed tomcat-embed-core 9.0.12 compile tomcat-annotations-api org.apache.tomcat org.apache.tomcat.embed tomcat-embed-el 9.0.12 compile
新建一个servlet类,实现对应的方法。
public class HomeServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("request scheme: " + req.getScheme()); resp.getWriter().print("hello tomcat"); }}复制代码
在main函数中添加如下代码
public static void main(String[] args) throws Exception { Tomcat tomcat = new Tomcat(); //设置路径 tomcat.setBaseDir("d:tomcat/dir"); tomcat.getHost().setAutoDeploy(false); Connector connector = new Connector(); //设置端口 connector.setPort(10086); tomcat.getService().addConnector(connector); Context context = new StandardContext(); //设置context路径 context.setPath(""); context.addLifecycleListener(new Tomcat.FixContextListener()); tomcat.getHost().addChild(context); //添加servlet tomcat.addServlet("", "homeServlet", new HomeServlet()); //设置servlet路径 context.addServletMappingDecoded("/", "homeServlet"); tomcat.start(); tomcat.getServer().await(); }复制代码
这样的话一个简单的tomcat服务器就启动了,打开浏览器输入localhost:10086,就可以看到servlet中的返回值。
springboot中tomcat容器的启动
还记得前两节讲到里面的配置文件么,配置文件中有一个类,org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration ,进入这个类。
@Configuration@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@ConditionalOnClass(ServletRequest.class)@ConditionalOnWebApplication(type = Type.SERVLET)@EnableConfigurationProperties(ServerProperties.class)@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, ServletWebServerFactoryConfiguration.EmbeddedTomcat.class, ServletWebServerFactoryConfiguration.EmbeddedJetty.class, ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })public class ServletWebServerFactoryAutoConfiguration {复制代码
发现上面有一个import注解,进入import注解导入的类
@Configuration @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) public static class EmbeddedTomcat { @Bean public TomcatServletWebServerFactory tomcatServletWebServerFactory() { return new TomcatServletWebServerFactory(); } }复制代码
根据上一节学习的判断条件可以知道,import注解向spring容器中注入了一个TomcatServletWebServerFactory类,这个类我们先标记着。
回到main函数中,顺着SpringApplication.run(Application.class, args);方法进入AbstractApplicationContext的refresh方法
// Initialize other special beans in specific context subclasses. onRefresh();复制代码
在onRefresh方法上发现一行注释,在子类方法中初始化特殊的bean。tomcat容器应该算是一个特殊的bean了,所以我们进入子类的onRefresh方法。在子类ServletWebServerApplicationContext发现了这样的代码。
@Override protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } }复制代码
猜也能猜到,createWebServer方法就是tomcat初始化的地方了。所以进入方法一探究竟。
private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); //初始化进来,webServer和servletContext两个对象都是null,所以进入if if (webServer == null && servletContext == null) { ServletWebServerFactory factory = getWebServerFactory(); this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { ... } initPropertySources(); }复制代码
首先看一下getWebServerFactory方法。
protected ServletWebServerFactory getWebServerFactory() { // Use bean names so that we don't consider the hierarchy String[] beanNames = getBeanFactory() .getBeanNamesForType(ServletWebServerFactory.class); if (beanNames.length == 0) { ... } if (beanNames.length > 1) { ... } return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class); }复制代码
方法逻辑比较简单,获取容器中ServletWebServerFactory类型的实例,并校验其数量,多了或者少了都不行,必须是正好1个。这个时候看一下上面通过自动化配置那边导入spring容器的TomcatServletWebServerFactory类,这个类就是ServletWebServerFactory的子类。所以在没有其他配置的情况下,getWebServerFactory方法,获取到的就是TomcatServletWebServerFactory类。
获取到factory实例后,就来看一下factory的getWebServer方法。
Tomcat tomcat = new Tomcat(); File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat"); tomcat.setBaseDir(baseDir.getAbsolutePath()); //设置端口 Connector connector = new Connector(this.protocol); 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); } 配置context prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat);复制代码
虽然比我们一开始那个示范要复杂许多,但是大致的逻辑还是很清晰的,不难看懂。(这个地方如果不理解的话,你需要补充一下tomcat的知识)
进入getTomcatWebServer方法。
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { return new TomcatWebServer(tomcat, getPort() >= 0); } public TomcatWebServer(Tomcat tomcat, boolean autoStart) { Assert.notNull(tomcat, "Tomcat Server must not be null"); this.tomcat = tomcat; this.autoStart = autoStart; initialize(); } private void initialize() throws WebServerException { synchronized (this.monitor) { try { addInstanceIdToEngineName(); Context context = findContext(); context.addLifecycleListener((event) -> { if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) { removeServiceConnectors(); } }); this.tomcat.start(); ... startDaemonAwaitThread(); } ... } }复制代码
在getTomcatWebServer方法中,发现了tomcat启动相关的代码,所以这个地方就是tomcat容器启动的地方啦。不过如果你用debug的话,你会发现这个地方即使tomcat启动过后,依然无法访问。因为在启动前spring框架还做了一件事。
Context context = findContext(); context.addLifecycleListener((event) -> { if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) { //移除tomcat容器的连接器connector removeServiceConnectors(); } });复制代码
因为这个时候作为一个特殊的bean,tomcat容器需要优先初始化,但是此时其他bean还没有初始化完成,连接进来后是无法处理的。所以spring框架在这个地方移除了连接器。
那么被移除的连接器在那个地方启动的呢?在AbstractApplicationContext的refresh方法中,onRefresh方法后面还有一个方法finishRefresh方法。进入子类的这个方法(进入这个方法之前,所有的非lazy属性的bean已经全部完成了初始化)
@Override protected void finishRefresh() { super.finishRefresh(); WebServer webServer = startWebServer(); if (webServer != null) { publishEvent(new ServletWebServerInitializedEvent(webServer, this)); } } private WebServer startWebServer() { WebServer webServer = this.webServer; if (webServer != null) { webServer.start(); } return webServer; } public void start() throws WebServerException { ... addPreviouslyRemovedConnectors(); Connector connector = this.tomcat.getConnector(); if (connector != null && this.autoStart) { performDeferredLoadOnStartup(); } ... } }复制代码
在这个方法中,我们找到了被移除的connector。spring框架将刚刚移除得到连接器又放到tomcat容器中,并且启用了他,这样的话tomcat就可以被访问到了。
tomcat的启动到这儿我们已经了解了,不知道大家有没有发现一个问题,就是我们并没有看到类似示例中添加servlet和设置servlet路径相关的代码。那这部分代码在哪里呢?
回到刚刚factory的getWebServer方法。这个方法中传入了一个参数getSelfInitializer()我们看一下这个参数是啥。
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() { return this::selfInitialize; } private void selfInitialize(ServletContext servletContext) throws ServletException { prepareWebApplicationContext(servletContext); registerApplicationScope(servletContext); WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext); for (ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); } }复制代码
这个lambda表达式应该还很好理解吧,返回了一个ServletContextInitializer实例,该实例的onStartup方法就是调用了这边的selfInitialize方法。这个selfInitialize方法里,最关键的就是getServletContextInitializerBeans方法了。但是我们从这边分析代码的话,其实不太看得出来getServletContextInitializerBeans到底获取到了那些类,所以可以取巧一下,使用IDEA的debug功能。借助debug我们看到了这边获取到的几个类,关键的是DispatcherServletRegistrationBean。也就是这个地方会调用DispatcherServletRegistrationBean的onStartup方法。
那么他的onStartup到底干了那些事呢?
@Override public final void onStartup(ServletContext servletContext) throws ServletException { String description = getDescription(); if (!isEnabled()) { logger.info(StringUtils.capitalize(description) + " was not registered (disabled)"); return; } register(description, servletContext); } @Override protected final void register(String description, ServletContext servletContext) { D registration = addRegistration(description, servletContext); if (registration == null) { logger.info(StringUtils.capitalize(description) + " was not registered " + "(possibly already registered?)"); return; } configure(registration); } @Override protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) { String name = getServletName(); //这个地方将servlet添加进了context return servletContext.addServlet(name, this.servlet); } @Override protected void configure(ServletRegistration.Dynamic registration) { super.configure(registration); String[] urlMapping = StringUtils.toStringArray(this.urlMappings); if (urlMapping.length == 0 && this.alwaysMapUrl) { urlMapping = DEFAULT_MAPPINGS; } if (!ObjectUtils.isEmpty(urlMapping)) { //这个方法则对servlet的路径进行了配置 registration.addMapping(urlMapping); } registration.setLoadOnStartup(this.loadOnStartup); if (this.multipartConfig != null) { registration.setMultipartConfig(this.multipartConfig); } }复制代码
既然知道了ServletContextInitializer的作用,那么我们就追踪一下这个ServletContextInitializer被放置到了什么地方,何时调用他的方法。
@Override public WebServer getWebServer(ServletContextInitializer... initializers) { ... prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); } protected void prepareContext(Host host, ServletContextInitializer[] initializers) { ... ServletContextInitializer[] initializersToUse = mergeInitializers(initializers); ... configureContext(context, initializersToUse); ... } protected void configureContext(Context context, ServletContextInitializer[] initializers) { TomcatStarter starter = new TomcatStarter(initializers); if (context instanceof TomcatEmbeddedContext) { TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context; embeddedContext.setStarter(starter); embeddedContext.setFailCtxIfServletStartFails(true); } context.addServletContainerInitializer(starter, NO_CLASSES); ... }复制代码
可以看到ServletContextInitializer被包装成了一个TomcatStarter放入了context中。在context的start方法里,我们就可以看到initializers的启动(这个地方涉及到tomcat容器的启动,如果不熟悉的话可以回顾下)。
@Override protected synchronized void startInternal() throws LifecycleException { ... // Call ServletContainerInitializers for (Map.Entry>> entry : initializers.entrySet()) { try { entry.getKey().onStartup(entry.getValue(), getServletContext()); } catch (ServletException e) { log.error(sm.getString("standardContext.sciFail"), e); ok = false; break; } } ... }复制代码
总结
经过这几轮的分析,从SpringApplication的启动,到自动化配置,再到今天的tomcat容器的启动。我们已经窥探到了整个springboot框架的全貌。所以后面就需要对常用功能定点学习了。