笔者最近在一次上线过程中,遇到服务器迁移部署失败的问题,根据定位是jar中A.class.getClassloader().getResourceAsStream("/request.xml")。一开始笔者没把这个当回事,觉得是正常的。
先说解决方案:1.升级tomcat8以上版本2.修改应用代码A.class.getClassloader().getResourceAsStream("/request.xml");中request.xml去掉/。
至于原因,下面我们来具体谈谈:
在应用中该request.xml在jar中的根目录下,而类也在该jar中,纵所周知tomcat的应用是有WebappClassLoaderBase去加载,于是去复现该问题时相对虚拟路径在tomcat8之前都是null值,导致了应用的资源获取失败。
至于为什么tomcat8前是null值,我们来对比下tomcat8前的实现,以tomcat7.0.65为例。相较于tomcat A.class.getClassloader()获取的当前classloader为WebappClassLoaderBase,而对于为什么是WebappClassLoaderBase加载的后续在整关于tomcat的classloader树结构来表述。我们直接看tomcat7.0.65的代码:
根据代码:配置delegate为true时才会执行(1)方法,用于将委托给父类去加载资源。delegate默认值为false,即子优先,纵使配置了delegate为true,他能加载到jar中的资源文件,parent.getResourceAsStream(name)也是调用jdk原生的ClassLoader,原生的ClassLoader.class实现参考本篇博客最下面的代码原理一样。那么显然执行的是(2)中的findResource(name);由于该xml是在jar中,在所以调用了 entry = findResourceInternal(name, name, false);最后调试可见 JarEntry jarEntry = jarFiles[i].getJarEntry(name);而问题就是出现在这里jdk的getJarEntry(name);该方法不支持/request.xml资源获取。因此返回了null,如果是request.xml则可以获取jar中对应的资源。因此问题查明。建议升级tomcat至8以上版本,或者getResourceAsStream("request.xml")不要带有/即可。
Tomcat 7.0.65 WebappClassLoader.class
public InputStream getResourceAsStream(String name) {
if (log.isDebugEnabled())
log.debug("getResourceAsStream(" + name + ")");
InputStream stream = null;
// (0) Check for a cached copy of this resource
stream = findLoadedResource(name);
if (stream != null) {
if (log.isDebugEnabled())
log.debug(" --> Returning stream from cache");
return (stream);
}
// (1) Delegate to parent if requested
if (delegate) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader " + parent);
stream = parent.getResourceAsStream(name);
if (stream != null) {
// FIXME - cache???
if (log.isDebugEnabled())
log.debug(" --> Returning stream from parent");
return (stream);
}
}
// (2) Search local repositories
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
URL url = findResource(name);
if (url != null) {
// FIXME - cache???
if (log.isDebugEnabled())
log.debug(" --> Returning stream from local");
stream = findLoadedResource(name);
try {
if (hasExternalRepositories && (stream == null))
stream = url.openStream();
} catch (IOException e) {
// Ignore
}
if (stream != null)
return (stream);
}
public URL findResource(final String name) {
if (log.isDebugEnabled())
log.debug(" findResource(" + name + ")");
URL url = null;
if (hasExternalRepositories && searchExternalFirst)
url = super.findResource(name);
if (url == null) {
ResourceEntry entry = resourceEntries.get(name);
if (entry == null) {
if (securityManager != null) {
PrivilegedAction<ResourceEntry> dp =
new PrivilegedFindResourceByName(name, name, false);
entry = AccessController.doPrivileged(dp);
} else {
entry = findResourceInternal(name, name, false);
}
}
if (entry != null) {
url = entry.source;
}
}
if ((url == null) && hasExternalRepositories && !searchExternalFirst)
url = super.findResource(name);
if (log.isDebugEnabled()) {
if (url != null)
log.debug(" --> Returning '" + url.toString() + "'");
else
log.debug(" --> Resource not found, returning null");
}
return (url);
}
至于tomcat8后为什么jar中A.class.getClassloader().getResourceAsStream("/request.xml");同理我们来查看tomcat8之后的webappClassLoaderBase.class的实现。主要是tomcat8增加了WebResource的接口,判断和实现机制发生了改变,因此可以正常获取到输入流。
Tomcat 8.0.50 WebappClassLoaderBase.class
// (2) Search local repositories
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
String path = nameToPath(name);
WebResource resource = resources.getClassLoaderResource(path);
if (resource.exists()) {
stream = resource.getInputStream();
trackLastModified(path, resource);
}
Tomcat 8.0.50 StandardRoot.class
@Override
public WebResource getClassLoaderResource(String path) {
return getResource("/WEB-INF/classes" + path, true, true);
}
protected WebResource getResource(String path, boolean validate,
boolean useClassLoaderResources) {
if (validate) {
path = validate(path);
}
if (isCachingAllowed()) {
return cache.getResource(path, useClassLoaderResources);
} else {
return getResourceInternal(path, useClassLoaderResources);
}
}
应用部署在服务器上,一般服务器的自己的classloader都会重写getResourceAsStream()方法,前面我们也讲到了要想直接调用jdk的classloader,在tomcat中配置delegate为true即可具体配置在tomcat的context.xml中配置<Loader delegate="true" />即可,即向上委托,所有的类优先交由上层的classloader去加载。
那么这个案例直接在idea中写测试demo的调用jdk自身的classloader会咋样呢,调用JDK自己的classloader也可发现差异。
InputStream classInputStream = HashMapDemo.class.getResourceAsStream("/request.xml"); //能获取到资源
InputStream classloaderInputStream = HashMapDemo.class.getClassLoader().getResourceAsStream("/request.xml"); //null
原生JDK的ClassLoader.class 和Class.class的实现
对比下面2块可以清晰的看出问题所在,在Class的getResourceAsStream方法中,调用的resolveName(name)的方法,对name以/开头的相对虚拟路径进行了截取操作,得到的name就变成了request.xml,其实后面Class.class的执行逻辑就和Classloader.class一致了,直接看下面的代码可知,调用了ClassLoader的getResourceAsStream(name)方法。
InputStream classInputStream = HashMapDemo.class.getResourceAsStream("/request.xml");
JDK1.8.0_121 Class.class源码
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResourceAsStream(name);
}
return cl.getResourceAsStream(name);
}
private String resolveName(String name) {
if (name == null) {
return name;
}
if (!name.startsWith("/")) {
Class<?> c = this;
while (c.isArray()) {
c = c.getComponentType();
}
String baseName = c.getName();
int index = baseName.lastIndexOf('.');
if (index != -1) {
name = baseName.substring(0, index).replace('.', '/')
+"/"+name;
}
} else {
name = name.substring(1);
}
return name;
}
InputStream classloaderInputStream = HashMapDemo.class.getClassLoader().getResourceAsStream("/request.xml");
JDK1.8.0_121 ClassLoader.class
public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}