深入剖析Springboot内置容器Undertow初始化流程

由于种种原因,需要在springboot内置容器为undertow的环境下对servlet初始化做一些扩展工作。
我们都知道springMVC中的DispatcherServlet,而它是继承于FrameworkServlet这个类的。在FrameworkServlet这个类中,通过initServletBean这个方法去初始化一些servlet所需要的bean。观察一眼代码:

protected final void initServletBean() throws ServletException {
    this.getServletContext().log("Initializing Spring FrameworkServlet '" + this.getServletName() + "'");
    if (this.logger.isInfoEnabled()) {
        this.logger.info("FrameworkServlet '" + this.getServletName() + "': initialization started");
    }
 
    long startTime = System.currentTimeMillis();
 
    try {
        this.webApplicationContext = this.initWebApplicationContext();
        this.initFrameworkServlet();
    } catch (ServletException var5) {
        this.logger.error("Context initialization failed", var5);
        throw var5;
    } catch (RuntimeException var6) {
        this.logger.error("Context initialization failed", var6);
        throw var6;
    }
 
    if (this.logger.isInfoEnabled()) {
        long elapsedTime = System.currentTimeMillis() - startTime;
        this.logger.info("FrameworkServlet '" + this.getServletName() + "': initialization completed in " + elapsedTime + " ms");
    }
 
}

其中initFrameworkServlet是一个供我们扩展的抽象类,那么我们可以继承DispatcherServlet,并重写自定义的initFrameworkServlet方法,最后在springboot中将正牌的dispatcherServlet给替换掉即可。

但是改造完成之后,在内置容器分别为jetty和undertow的时候,却遭到了不同的境遇。在springboot使用jetty内置容器启动过程中会顺利调用FrameworkServlet的initServletBean这个方法,但是在undertow启动时候却不会去调用这个方法,反而会在第一次请求来临时候去调用这个方法,这是很奇怪的事情,难道undertow就这么标新立异么。

我们从springboot本身的初始化顺序开始看起,在SpringApplication.run方法中:

public ConfigurableApplicationContext run(String... args) {
   StopWatch stopWatch = new StopWatch();
   stopWatch.start();
   ConfigurableApplicationContext context = null;
   FailureAnalyzers analyzers = null;
   configureHeadlessProperty();
   SpringApplicationRunListeners listeners = getRunListeners(args);
   listeners.starting();
   try {
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(
            args);
      ConfigurableEnvironment environment = prepareEnvironment(listeners,
            applicationArguments);
      Banner printedBanner = printBanner(environment);
      context = createApplicationContext();
      analyzers = new FailureAnalyzers(context);
      prepareContext(context, environment, listeners, applicationArguments,
            printedBanner);
      refreshContext(context);
      afterRefresh(context, applicationArguments);
      listeners.finished(context, null);
      stopWatch.stop();
      if (this.logStartupInfo) {
         new StartupInfoLogger(this.mainApplicationClass)
               .logStarted(getApplicationLog(), stopWatch);
      }
      return context;
   }
   catch (Throwable ex) {
      handleRunFailure(context, listeners, analyzers, ex);
      throw new IllegalStateException(ex);
   }
}

可以清晰的看到整个过程,比较重要的步骤基本可以归纳为四个过程:

createApplicationContext -> refreshContext -> afterRefresh -> finishConext

其中refreshContext是一个很重要的过程,点进去瞅一眼:

public void refresh() throws BeansException, IllegalStateException {
    Object var1 = this.startupShutdownMonitor;
    synchronized(this.startupShutdownMonitor) {
        this.prepareRefresh();
        ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
        this.prepareBeanFactory(beanFactory);
 
        try {
            this.postProcessBeanFactory(beanFactory);
            this.invokeBeanFactoryPostProcessors(beanFactory);
            this.registerBeanPostProcessors(beanFactory);
            this.initMessageSource();
            this.initApplicationEventMulticaster();
            this.onRefresh();
            this.registerListeners();
            this.finishBeanFactoryInitialization(beanFactory);
            this.finishRefresh();
        } catch (BeansException var9) {
            if (this.logger.isWarnEnabled()) {
                this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
            }
 
            this.destroyBeans();
            this.cancelRefresh(var9);
            throw var9;
        } finally {
            this.resetCommonCaches();
        } 
    }
}

这儿又出现了很多过程,那么与容器启动相关的我们需要关注的是onRefresh以及finishRefresh这两步,前者包含了初始化容器配置的过程,后者包含了容器启动的过程。

在不了解容器配置参数的构建意义的情况下,可以直接先看一下容器启动的过程,比较一下jetty与undertow的差异,看能不能找出原因。

@Override
protected void finishRefresh() {
   super.finishRefresh();
   EmbeddedServletContainer localContainer = startEmbeddedServletContainer();
   if (localContainer != null) {
      publishEvent(
            new EmbeddedServletContainerInitializedEvent(this, localContainer));
   }
}
  
private EmbeddedServletContainer startEmbeddedServletContainer() {
   EmbeddedServletContainer localContainer = this.embeddedServletContainer;
   if (localContainer != null) {
      localContainer.start();
   }
   return localContainer;
}

这里看到这个方法里完成了容器的启动,并且还会发布一个EmbeddedServletContainerInitializedEvent,之后如果想在springboot中容器启动好这个时机搞事情,可以监听这个event。

接着,springboot会直接调用localContainer.start()方法来启动容器,先看看jetty的实现:

@Override
public void start() throws EmbeddedServletContainerException {
   synchronized (this.monitor) {
      if (this.started) {
         return;
      }
      this.server.setConnectors(this.connectors);
      if (!this.autoStart) {
         return;
      }
      try {
         this.server.start();
         for (Handler handler : this.server.getHandlers()) {
            handleDeferredInitialize(handler);
         }
         Connector[] connectors = this.server.getConnectors();
         for (Connector connector : connectors) {
            try {
               connector.start();
            }
            catch (BindException ex) {
               if (connector instanceof NetworkConnector) {
                  throw new PortInUseException(
                        ((NetworkConnector) connector).getPort());
               }
               throw ex;
            }
         }
         this.started = true;
         JettyEmbeddedServletContainer.logger
               .info("Jetty started on port(s) " + getActualPortsDescription());
      }
      catch (EmbeddedServletContainerException ex) {
         stopSilently();
         throw ex;
      }
      catch (Exception ex) {
         stopSilently();
         throw new EmbeddedServletContainerException(
               "Unable to start embedded Jetty servlet container", ex);
      }
   }
}

jetty会在handleDeferredInitialize这个方法中会初始化servletHandler,初始化GenericServlet,最终调用到frameWorkServlet的initServletBean方法。

再看看undertow的,

public void start() throws EmbeddedServletContainerException {
   synchronized (this.monitor) {
      if (this.started) {
         return;
      }
      try {
         if (!this.autoStart) {
            return;
         }
         if (this.undertow == null) {
            this.undertow = createUndertowServer();
         }
         this.undertow.start();
         this.started = true;
         UndertowEmbeddedServletContainer.logger
               .info("Undertow started on port(s) " + getPortsDescription());
      }
      catch (Exception ex) {
         try {
            if (findBindException(ex) != null) {
               List<Port> failedPorts = getConfiguredPorts();
               List<Port> actualPorts = getActualPorts();
               failedPorts.removeAll(actualPorts);
               if (failedPorts.size() == 1) {
                  throw new PortInUseException(
                        failedPorts.iterator().next().getNumber());
               }
            }
            throw new EmbeddedServletContainerException(
                  "Unable to start embedded Undertow", ex);
         }
         finally {
            stopSilently();
         }
      }
   }
}

和jetty一对比,貌似恰好缺了初始化handler的地方。

通过测试得知,undertow初始化servlet的时机会放在第一次请求的时候,看一眼undertow处理请求的rootHandler:servletInitialHandler的dispatchRequest方法:

private void dispatchRequest(final HttpServerExchange exchange, final ServletRequestContext servletRequestContext, final ServletChain servletChain, final DispatcherType dispatcherType) throws Exception {    
        servletRequestContext.setDispatcherType(dispatcherType);            servletRequestContext.setCurrentServlet(servletChain);
    if (dispatcherType == DispatcherType.REQUEST || dispatcherType == DispatcherType.ASYNC) {
        firstRequestHandler.call(exchange, servletRequestContext);
    } else {
        next.handleRequest(exchange);
    }
}

可以看到一个醒目的名字firstRequestHandler,心想undertow果然还对第一个请求区别对待啊。

这handler会执行一系列nextHandler.call方法,执行到ServletDispatchingHandler的时候,就是真正处理请求的时候:

private ServletChain(final HttpHandler originalHandler, final ManagedServlet managedServlet, final String servletPath, boolean defaultServletMapping, Map<DispatcherType, List<ManagedFilter>> filters, boolean wrapHandler) {
    if (wrapHandler) {
        this.handler = new HttpHandler() {
 
            private volatile boolean initDone = false;
 
            @Override
            public void handleRequest(HttpServerExchange exchange) throws Exception {
                if(!initDone) {
                    synchronized (this) {
                        if(!initDone) {
                            ServletRequestContext src = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
                            forceInit(src.getDispatcherType());
                        }
                    }
                }
                originalHandler.handleRequest(exchange);
            }
        };
    } else {
        this.handler = originalHandler;
    }
    this.managedServlet = managedServlet;
    this.servletPath = servletPath;
    this.defaultServletMapping = defaultServletMapping;
    this.executor = managedServlet.getServletInfo().getExecutor();
    this.filters = filters;
}

可以看到initDone这个属性默认就是为false的,并且会执行forceInit这个方法,正是在这个方法中完成了servlet的初始化。

其实到现在已经很明确缘由了,undertow默认就是会第一次请求的时候才会去初始化servlet的,那到底有没有地方去修改配置让undertow提前初始化servlet呢。

这里要回到最开始onRefresh()初始化容器配置的那部分代码观察了:

@Override
protected void onRefresh() {
   super.onRefresh();
   try {
      createEmbeddedServletContainer();
   }
   catch (Throwable ex) {
      throw new ApplicationContextException("Unable to start embedded container",
            ex);
   }
}
  
private void createEmbeddedServletContainer() {
   EmbeddedServletContainer localContainer = this.embeddedServletContainer;
   ServletContext localServletContext = getServletContext();
   if (localContainer == null && localServletContext == null) {
      EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
      this.embeddedServletContainer = containerFactory
            .getEmbeddedServletContainer(getSelfInitializer());
   }
   else if (localServletContext != null) {
      try {
         getSelfInitializer().onStartup(localServletContext);
      }
      catch (ServletException ex) {
         throw new ApplicationContextException("Cannot initialize servlet context",
               ex);
      }
   }
   initPropertySources();
}

可以看到获得embeddedServletContainer是通过getEmbeddedServletContainer这个方法获得,看源码发现:

@Override
public EmbeddedServletContainer getEmbeddedServletContainer(
      ServletContextInitializer... initializers) {
   DeploymentManager manager = createDeploymentManager(initializers);
   int port = getPort();
   Builder builder = createBuilder(port);
   return getUndertowEmbeddedServletContainer(builder, manager, port);
}

可以关注到DeploymentManager这个类,阅读undertow的文档可以知道,undertow的Deplotment首先会调用deploy方法,接着会调用start方法,而start方法返回的Httphandler将作为undertow启动时候的rootHandler,而这个handler会是undertow启动与初始化逻辑的关键。

private DeploymentManager createDeploymentManager(
      ServletContextInitializer... initializers) {
   DeploymentInfo deployment = Servlets.deployment();
   registerServletContainerInitializerToDriveServletContextInitializers(deployment,
         initializers);
   deployment.setClassLoader(getServletClassLoader());
   deployment.setContextPath(getContextPath());
   deployment.setDisplayName(getDisplayName());
   deployment.setDeploymentName("spring-boot");
   if (isRegisterDefaultServlet()) {
      deployment.addServlet(Servlets.servlet("default", DefaultServlet.class));
   }
   configureErrorPages(deployment);
   deployment.setServletStackTraces(ServletStackTraces.NONE);
   deployment.setResourceManager(getDocumentRootResourceManager());
   configureMimeMappings(deployment);
   for (UndertowDeploymentInfoCustomizer customizer : this.deploymentInfoCustomizers) {
      customizer.customize(deployment);
   }
   if (isAccessLogEnabled()) {
      configureAccessLog(deployment);
   }
   if (isPersistSession()) {
      File dir = getValidSessionStoreDir();
      deployment.setSessionPersistenceManager(new FileSessionPersistence(dir));
   }
   addLocaleMappings(deployment);
   DeploymentManager manager = Servlets.newContainer().addDeployment(deployment);
   manager.deploy();
   SessionManager sessionManager = manager.getDeployment().getSessionManager();
   int sessionTimeout = (getSessionTimeout() > 0 ? getSessionTimeout() : -1);
   sessionManager.setDefaultSessionTimeout(sessionTimeout);
   return manager;
}

看到createDeploymentManager这个方法中只有manager.deploy()方法,那么manger.start()方法去哪了呢。仔细观察之前undertow的start方法,发现一句比较可疑的话
this.undertow = createUndertowServer();

点进去看源码:

private Undertow createUndertowServer() throws ServletException {
   HttpHandler httpHandler = this.manager.start();
   httpHandler = getContextHandler(httpHandler);
   if (this.useForwardHeaders) {
      httpHandler = Handlers.proxyPeerAddress(httpHandler);
   }
   if (StringUtils.hasText(this.serverHeader)) {
      httpHandler = Handlers.header(httpHandler, "Server", this.serverHeader);
   }
   this.builder.setHandler(httpHandler);
   return this.builder.build();
}

果然发现了manager.start方法,并且可以看到它确实返回了httpHandler,并设置到了undertow的builder中去。观察start方法源码

@Override
public HttpHandler start() throws ServletException {
    try {
        return deployment.createThreadSetupAction(new ThreadSetupHandler.Action<HttpHandler, Object>() {
            @Override
            public HttpHandler call(HttpServerExchange exchange, Object ignore) throws ServletException {
                deployment.getSessionManager().start();
 
                //we need to copy before iterating
                //because listeners can add other listeners
                ArrayList<Lifecycle> lifecycles = new ArrayList<>(deployment.getLifecycleObjects());
                for (Lifecycle object : lifecycles) {
                    object.start();
                }
                HttpHandler root = deployment.getHandler();
                final TreeMap<Integer, List<ManagedServlet>> loadOnStartup = new TreeMap<>();
                for (Map.Entry<String, ServletHandler> entry : deployment.getServlets().getServletHandlers().entrySet()) {
                    ManagedServlet servlet = entry.getValue().getManagedServlet();
                    Integer loadOnStartupNumber = servlet.getServletInfo().getLoadOnStartup();
                    if (loadOnStartupNumber != null) {
                        if (loadOnStartupNumber < 0) {
                            continue;
                        }
                        List<ManagedServlet> list = loadOnStartup.get(loadOnStartupNumber);
                        if (list == null) {
                            loadOnStartup.put(loadOnStartupNumber, list = new ArrayList<>());
                        }
                        list.add(servlet);
                    }
                }
                for (Map.Entry<Integer, List<ManagedServlet>> load : loadOnStartup.entrySet()) {
                    for (ManagedServlet servlet : load.getValue()) {
                        servlet.createServlet();
                    }
                }
 
                if (deployment.getDeploymentInfo().isEagerFilterInit()) {
                    for (ManagedFilter filter : deployment.getFilters().getFilters().values()) {
                        filter.createFilter();
                    }
                }
 
                state = State.STARTED;
                return root;
            }
        }).call(null, null);
    } catch (ServletException|RuntimeException e) {
        throw e;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

仔细看源码发现几点:

  1. 讲道理在这个方法中应该完成servlet的初始化,可以看到种种相关的方法 servlet.createServlet(),state = State.STARTED等等

  2. loadOnStartUp这个属性很可疑,在小于0的时候直接跳出了逻辑

Debug可知loadOnStartUp这个属性为-1,确实跳出了后面的逻辑,失去了初始化servlet的机会。那么这个属性在哪被设置呢,可以看到Integer loadOnStartupNumber = servlet.getServletInfo().getLoadOnStartup(),这个属性是从managedServlet中来的,而managedServlet其实就是类似dispatcherServlet的servlet,它将在manager.deploy方法中设定。那么现在解决办法已经很清晰了,只要将自己定义的dispatcherServlet的loadOnStartUp属性设置为1即可。这样undertow会在启动的时候就会初始化servlet,最终调用到FrameworkServlet的initServletBean方法。

好了最后梳理一下可知道springboot内置容器为undertow启动时候,之中的关键顺序如下图:


标题:深入剖析Springboot内置容器Undertow初始化流程
作者:fredalxin
地址:https://fredal.xin/dig-into-springboot-undertow-init