springboot内嵌Tomcat启动失败

问题描述

开发需求期间引入友军的二方包,导致服务启动失败,失败日志如下

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.taobao.pandora.boot.loader.LaunchRunner.run(LaunchRunner.java:38)
	at java.lang.Thread.run(Thread.java:748)
Caused by: org.springframework.context.ApplicationContextException: Unable to start embedded container; nested exception is org.springframework.boot.context.embedded.EmbeddedServletContainerException: Unable to start embedded Tomcat
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.onRefresh(EmbeddedWebApplicationContext.java:138)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:536)
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:123)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:666)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:353)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:300)
	at com.....order.fundamental.boot.WirelessSpringApplication.run(WirelessSpringApplication.java:20)
	at com.....order.callback.Application.main(Application.java:22)
	... 6 more
Caused by: org.springframework.boot.context.embedded.EmbeddedServletContainerException: Unable to start embedded Tomcat
	at org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer.initialize(TomcatEmbeddedServletContainer.java:135)
	at org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer.<init>(TomcatEmbeddedServletContainer.java:87)
	at org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory.getTomcatEmbeddedServletContainer(TomcatEmbeddedServletContainerFactory.java:535)
	at org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory.getEmbeddedServletContainer(TomcatEmbeddedServletContainerFactory.java:177)
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.createEmbeddedServletContainer(EmbeddedWebApplicationContext.java:162)
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.onRefresh(EmbeddedWebApplicationContext.java:135)
	... 13 more
Caused by: org.apache.catalina.LifecycleException: Failed to start component [StandardServer[-1]]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:167)
	at org.apache.catalina.startup.Tomcat.start(Tomcat.java:427)
	at org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer.initialize(TomcatEmbeddedServletContainer.java:112)
	... 18 more
Caused by: org.apache.catalina.LifecycleException: Failed to start component [StandardService[Tomcat]]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:167)
	at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:770)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
	... 20 more
Caused by: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Tomcat]]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:167)
	at org.apache.catalina.core.StandardService.startInternal(StandardService.java:422)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
	... 22 more
Caused by: org.apache.catalina.LifecycleException: A child container failed during start
	at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:942)
	at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:258)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
	... 24 more
Caused by: java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Tomcat].StandardHost[localhost]]
	at java.util.concurrent.FutureTask.report(FutureTask.java:122)
	at java.util.concurrent.FutureTask.get(FutureTask.java:192)
	at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:931)
	... 26 more
Caused by: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Tomcat].StandardHost[localhost]]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:167)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1412)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1402)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	... 1 more
Caused by: org.apache.catalina.LifecycleException: A child container failed during start
	at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:942)
	at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:850)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
	... 6 more
Caused by: java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbeddedContext[]]
	at java.util.concurrent.FutureTask.report(FutureTask.java:122)
	at java.util.concurrent.FutureTask.get(FutureTask.java:192)
	at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:931)
	... 8 more
Caused by: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbeddedContext[]]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:167)
	... 6 more
Caused by: org.apache.catalina.LifecycleException: Failed to start component [Pipeline[StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbeddedContext[]]]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:167)
	at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5075)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
	... 6 more
Caused by: org.apache.catalina.LifecycleException: Failed to start component [org.apache.catalina.authenticator.NonLoginAuthenticator[]]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:167)
	at org.apache.catalina.core.StandardPipeline.startInternal(StandardPipeline.java:178)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
	... 8 more
Caused by: java.lang.NoSuchMethodError: javax.servlet.ServletContext.getVirtualServerName()Ljava/lang/String;
	at org.apache.catalina.authenticator.AuthenticatorBase.startInternal(AuthenticatorBase.java:1181)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
	... 10 more
Stopping available components

Process finished with exit code 137 (interrupted by signal 9: SIGKILL)

问题分析

springboot内嵌容器

  1. 创建容器
    org.springframework.boot.context.embedded.EmbeddedWebApplicationContext#onRefresh->org.springframework.boot.context.embedded.EmbeddedWebApplicationContext#createEmbeddedServletContainer->org.springframework.boot.context.embedded.EmbeddedServletContainerFactory#getEmbeddedServletContainer
  2. 启动容器

查看创建容器代码

@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
	Tomcat tomcat = new Tomcat();
	...
    // 如果Host不存在则创建
    tomcat.getHost().setAutoDeploy(false);
    ...
	prepareContext(tomcat.getHost(), initializers);
	return getTomcatEmbeddedServletContainer(tomcat);
}

查看准备上下文代码

protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
	File docBase = getValidDocumentRoot();
	docBase = (docBase != null) ? docBase : createTempDir("tomcat-docbase");
	final TomcatEmbeddedContext context = new TomcatEmbeddedContext();
	context.setName(getContextPath());
	...
    // 各种兼容问题-_-!!
	try {
		context.setUseRelativeRedirects(false);
	}
	catch (NoSuchMethodError ex) {
		// Tomcat is < 8.0.30. Continue
	}
	try {
		context.setCreateUploadTargets(true);
	}
	catch (NoSuchMethodError ex) {
		// Tomcat is < 8.5.39. Continue.
	}
	...
    // 配置上下文
	ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
	host.addChild(context);
	configureContext(context, initializersToUse);
    // 上下文后置动作
	postProcessContext(context);
}

Tomcat上下文与Servlet上下文

TomcatEmbeddedContext上下文继承父类StandardContext类实现org.apache.catalina.Context接口的getServletContext方法,该方法返回ServletContext上下文。StandardContext方法中直接创建上下文org.apache.catalina.core.ApplicationContext#ApplicationContext

@Override
public ServletContext getServletContext() {
    if (context == null) {
        context = new ApplicationContext(this);
        if (altDDName != null)
            context.setAttribute(Globals.ALT_DD_ATTR,altDDName);
    }
    return context.getFacade();
}

通过ApplicationContext构建并返回上下文门面对象

/**
 * @return the facade associated with this ApplicationContext.
 */
protected ServletContext getFacade() {
    return this.facade;
}
/**
 * The facade around this object.
 */
private final ServletContext facade = new ApplicationContextFacade(this);

ApplicationContextFacade实现了ServletContext接口,门面对象提供了getVirtualServerName方法实现,实际委派调用org.apache.catalina.core.ApplicationContext#getVirtualServerName

@Override
public String getVirtualServerName() {
    if (SecurityUtil.isPackageProtectionEnabled()) {
        return (String) doPrivileged("getVirtualServerName", null);
    } else  {
        return context.getVirtualServerName();
    }
}
@Override
public String getVirtualServerName() {
    // Constructor will fail if context or its parent is null
    Container host = context.getParent();
    Container engine = host.getParent();
    return engine.getName() + "/" + host.getName();
}

调用context.getParent方法,context即TomcatEmbeddedContext,实际调用父类方法:org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedContext->StandardContext->org.apache.catalina.core.ContainerBase#getParent。返回容器对象

/**
 * Return the Container for which this Container is a child, if there is
 * one.  If there is no defined parent, return <code>null</code>.
 */
@Override
public Container getParent() {
    return parent;
}

也就是说最终的ServletContext接口以及实现都是内嵌Tomcat自给自足。那么为什么会报错NoSuchMethod呢?运行时的ApplicationContextFacade对象并不是我们所看到的ApplicationContextFacade?怎么确认该信息呢?
首先,异常何时抛出?

  1. 准备上下文prepareContext,创建完上下文第一步则添加了FixContextListener该监听器
  2. 创建容器TomcatEmbeddedServletContainer
  3. 初始化容器
  4. 启动tomcat服务器server
  5. 启动tomcat服务器server,第一步发送配置启动事件:fireLifecycleEvent(CONFIGURE_START_EVENT, null)
  6. FixContextListener监听到事件将NonLoginAuthenticator注册至上下文并等待启动

在跑出异常前我们将TomcatContextCustomizer自定义配置器注册至容器,来判断上下文的元数据。通过自定义BeanPostProcessor后置动作将自定义配置器注入至tomcat容器

package com.....order.callback.hsf;

import javax.servlet.ServletContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.stereotype.Component;

/**
 * @author : 会灰翔的灰机
 * @date : 2021/3/12
 */
@Component
public class MyEmbeddedServletContainerCustomizerBeanPostProcessor implements
    BeanPostProcessor {

  @Override
  public Object postProcessBeforeInitialization(Object bean, String s) throws BeansException {
    if (bean instanceof TomcatEmbeddedServletContainerFactory) {
      TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory = (TomcatEmbeddedServletContainerFactory) bean;
      tomcatEmbeddedServletContainerFactory.addContextCustomizers((context -> {
        System.out.println("context:"+context);
        ServletContext servletContext = context.getServletContext();
        String cp = servletContext.getClass().getResource("").getPath();
        System.out.println("cp:"+cp);
        System.out.println("cl:"+servletContext.getClass().getClassLoader());
        System.out.println("SecurityUtil.isPackageProtectionEnabled():"+ SecurityUtil.isPackageProtectionEnabled());
      }));
    }
    return bean;
  }

  @Override
  public Object postProcessAfterInitialization(Object o, String s) throws BeansException {
    return o;
  }
}

输入日志,当前的ServletContext与跑出异常时的实例是同一个,但是断点也走不进org.apache.catalina.core.ApplicationContextFacade#getVirtualServerName方法

cp:file:/Users/.../.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/8.5.43/tomcat-embed-core-8.5.43.jar!/org/apache/catalina/core/
cl:com.....pandora.boot.loader.ReLaunchURLClassLoader@1d0a61c8
SecurityUtil.isPackageProtectionEnabled():false

删除新引入的pom依赖后重启应用,流程完全一致,唯一不同的是断点走进了org.apache.catalina.core.ApplicationContextFacade#getVirtualServerName方法,这也正是我们所期望的流程,引入的依赖如果造成上面的诡异现象?比对mvn dependency树发现友军的client包引入了org.mortbay.jetty:servlet-api,里面同样包含ServletContext类并且是同包路径

问题基本定位为jar包冲突,根据堆栈查看源码如下

/**
 * Get the primary name of the virtual host on which this context is
 * deployed. The name may or may not be a valid host name.
 *
 * @return The primary name of the virtual host on which this context is
 *         deployed
 * @since Servlet 3.1
 */
public String getVirtualServerName();

可以看到注释中有描述Servlet 3.1版本之后提供的该方法。那么检查当前服务中servlet api版本即可。发现汤姆猫上下文对接Servlet的门面对象ApplicationContextFacade实现的ServletContext原本是由tomcat-embed-core-8.5.43.jar内嵌包提供,是支持getVirtualServerName方法的,引入的依赖中导入了org.mortbay.jetty:servlet-api-2.5:jar:6.1.14:compile。该版本使用的servlet 2.5版本,不支持getVirtualServerName方法,因此产生了上面的问题

验证

增加代码打印接口源头

printAllInterface(servletContext.getClass().getInterfaces());
private static void printAllInterface(Class<?>[] classes) {
  if (classes != null && classes.length > 0) {
    for (Class<?> anInterface : classes) {
      System.out.println("anInterface:" + anInterface.getResource("").getPath());
      Class<?>[] parentInters = anInterface.getInterfaces();
      if (parentInters.length > 0) {
        printAllInterface(parentInters);
      }
    }
  }
}

输出结果

anInterface:file:/Users/.../.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/8.5.43/tomcat-embed-core-8.5.43.jar!/org/apache/catalina/servlet4preview/
anInterface:file:/Users/.../.m2/repository/org/mortbay/jetty/jsp-api-2.1/6.1.14/jsp-api-2.1-6.1.14.jar!/javax/servlet/

总结

温习下Tomcat架构
1

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值