由于种种原因,需要在 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);
}
}
仔细看源码发现几点:
讲道理在这个方法中应该完成 servlet 的初始化,可以看到种种相关的方法 servlet.createServlet(),state = State.STARTED 等等
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