【值得收藏】深入剖析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 启动时候,之中的关键顺序如下图:


作者:fredalxin

来源链接: https://fredal.xin/dig-into-springboot-undertow-init

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值