开发需求期间引入友军的二方包,导致服务启动失败,失败日志如下
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)
查看创建容器代码
@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);
}
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 null
.
*/
@Override
public Container getParent() {
return parent;
}
也就是说最终的ServletContext接口以及实现都是内嵌Tomcat自给自足。那么为什么会报错NoSuchMethod呢?运行时的ApplicationContextFacade对象并不是我们所看到的ApplicationContextFacade?怎么确认该信息呢?
首先,异常何时抛出?
在跑出异常前我们将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/