在 Tomcat 中,Web 资源的访问和 Web 应用的类加载是两个核心且紧密耦合的子系统。本文基于 Tomcat 9.0.53 的源码,详细分析 WebResourceRoot、WebResourceSet 以及 WebappClassLoader 的实现原理,揭示 Tomcat 如何高效地管理 /WEB-INF/classes、/WEB-INF/lib 下的资源,并实现符合 Servlet 规范的双亲委派类加载模型。
1. 资源集合:allResources 与 WebResourceSet
Tomcat 将一个 Web 应用的资源划分为多个逻辑层,每一层由 WebResourceSet 实现。在 StandardRoot(WebResourceRoot 的标准实现)中,通过一个二维列表维护所有资源集:
java
private final List<List<WebResourceSet>> allResources = new ArrayList<>();
{
allResources.add(preResources);
allResources.add(mainResources);
allResources.add(classResources);
allResources.add(jarResources);
allResources.add(postResources);
}
-
preResources:最高优先级,通常用于覆盖后续资源。
-
mainResources :代表 Web 应用文档根目录(
docBase)。 -
classResources :对应
/WEB-INF/classes目录。 -
jarResources :对应
/WEB-INF/lib下每一个 JAR 文件,每个 JAR 会被包装成一个独立的JarResourceSet。 -
postResources:最低优先级,作为最后回退。
当需要获取某个路径的资源时,getResourceInternal 方法会按顺序遍历这五个层级,一旦找到存在的资源即返回:
java
for (List<WebResourceSet> list : allResources) {
for (WebResourceSet webResourceSet : list) {
result = webResourceSet.getResource(path);
if (result.exists()) {
return result;
}
}
}
这种设计实现了灵活的覆盖机制:例如开发者可以将静态资源放在 preResources 中覆盖 mainResources 的同名文件。
2. 从 JAR 中读取资源:JarResourceSet 的路径处理
JarResourceSet 负责从 /WEB-INF/lib/*.jar 中读取资源。其 getResource 方法需要处理两个关键点:挂载点(webAppMount) 和 JAR 内部路径转换。
java
@Override
public WebResource getResource(String path) {
String webAppMount = getWebAppMount(); // 通常为 "/WEB-INF/lib"
if (path.startsWith(webAppMount)) {
String pathInJar = getInternalPath() + path.substring(webAppMount.length());
// 去掉开头的 '/'
if (pathInJar.length() > 0 && pathInJar.charAt(0) == '/') {
pathInJar = pathInJar.substring(1);
}
// 查找 JarEntry...
}
}
例如,请求路径 /WEB-INF/lib/guava.jar/com/google/common/collect/Lists.class,经过转换后 pathInJar 变为 "com/google/common/collect/Lists.class",再通过 JarFile.getJarEntry() 获取条目。
值得注意的是,Tomcat 实现了对 多版本 JAR(Multi-Release) 的支持。如果 JAR 的 MANIFEST.MF 中包含 Multi-Release: true,则调用 getArchiveEntry(pathInJar) 时,底层会优先返回与当前 JRE 版本匹配的版本化条目。
获取 JarEntry 后,JarResourceSet 会创建 JarResource(或 JarResourceRoot 表示目录)并返回。对于不存在的资源,返回 EmptyResource 以统一处理空值。
3. 类加载器的双亲委派与本地查找
WebappClassLoaderBase 是 Tomcat Web 应用类加载器的基类,其 loadClass 方法实现了 Servlet 规范要求的类加载顺序(不同于 JVM 默认的双亲委派):
-
检查本地已加载类缓存 (
findLoadedClass0/findLoadedClass)。 -
尝试从 Java SE 类加载器加载 ------防止 Web 应用覆盖 JDK 核心类。通过
JavaseClassLoader.getResource(resourceName)试探资源是否存在,避免触发昂贵的ClassNotFoundException。 -
SecurityManager 包访问权限检查。
-
如果配置了
delegate=true或当前类属于包过滤列表,先委派给父类加载器。 -
从本地仓库查找 :调用
findClass(name),实际会从/WEB-INF/classes和/WEB-INF/lib中加载类。 -
最后再次委派给父类加载器(如果第4步未执行)。
这种顺序保证了 Web 应用优先加载自己提供的类(如 Spring、业务代码),同时防止覆盖 java.lang.String 等系统类。
4. findClassInternal:从字节码到 Class 对象
当 loadClass 进入本地查找阶段时,最终由 findClassInternal 完成实际加载。其核心步骤如下:
java
String path = binaryNameToPath(name, true); // "com.example.MyClass" -> "/com/example/MyClass.class"
ResourceEntry entry = resourceEntries.get(path);
if (entry == null) {
WebResource resource = resources.getClassLoaderResource(path);
if (!resource.exists()) return null;
// 创建 ResourceEntry 并缓存
}
resources.getClassLoaderResource(path) 实际上映射到 getResource("/WEB-INF/classes" + path, true, true),即优先从 classes 目录加载,若不存在则从 JAR 中加载。
获得 WebResource 后,调用 resource.getContent() 读取字节码。如果注册了 ClassFileTransformer(例如通过 Java Instrumentation 或 JSP 编译器),会对字节码进行转换。
接着处理 Package 定义与密封检查:
-
根据类名提取包名,若包未定义则调用
definePackage(从 JAR 的Manifest中获取规范信息)。 -
如果包被标记为 sealed,校验当前类的
CodeSource是否与包密封的 URL 匹配,否则抛出SecurityException。
最后通过 defineClass(name, binaryContent, ...) 完成类加载,并将结果存入 entry.loadedClass 以避免重复解析。
5. 动态处理 /WEB-INF/lib 下的 JAR
Tomcat 在启动时以及运行时(如果启用了 JAR 扫描)会调用 processWebInfLib 方法:
java
protected void processWebInfLib() throws LifecycleException {
WebResource[] possibleJars = listResources("/WEB-INF/lib", false);
for (WebResource possibleJar : possibleJars) {
if (possibleJar.isFile() && possibleJar.getName().endsWith(".jar")) {
createWebResourceSet(ResourceSetType.CLASSES_JAR,
"/WEB-INF/classes", possibleJar.getURL(), "/");
}
}
}
该方法遍历 /WEB-INF/lib 下的所有文件,对每个 .jar 文件创建一个 JarResourceSet 并添加到 jarResources 列表中。注意第二个参数 "/WEB-INF/classes" 表示这些 JAR 中的资源在虚拟路径上被挂载到 /WEB-INF/classes 下,从而使得 getClassLoaderResource("/com/example/MyClass.class") 能够从 JAR 中正确找到类文件。
Tomcat 还会记录每个 JAR 的最后修改时间(jarModificationTimes),用于后续的热部署检测。
6. 总结
通过上述源码分析,我们可以清晰看到 Tomcat 9.0.53 在资源管理和类加载方面的精巧设计:
-
分层资源集合 提供了灵活的覆盖和组合能力,支持从目录、JAR、外部位置等多种来源透明地获取资源。
-
统一的
WebResource抽象 屏蔽了文件系统、JAR、内存等不同存储方式的差异。 -
定制类加载器 既遵循 Servlet 规范的要求(允许 Web 应用覆盖部分类,同时保护 JDK 类),又通过缓存、字节码转换等特性提升了性能和可扩展性。
理解这些底层机制,不仅有助于诊断应用部署中的 ClassNotFoundException、资源找不到等问题,也为编写高性能、高可靠性的 Tomcat 应用打下坚实基础。如果你正在开发一个需要深入 Tomcat 集成的框架或工具,这些源码细节将是不可或缺的参考。
##源码
private final List<List<WebResourceSet>> allResources =
new ArrayList<>();
{
allResources.add(preResources);
allResources.add(mainResources);
allResources.add(classResources);
allResources.add(jarResources);
allResources.add(postResources);
}
@Override
public void start() throws LifecycleException {
state = LifecycleState.STARTING_PREP;
WebResource classes = resources.getResource("/WEB-INF/classes");
if (classes.isDirectory() && classes.canRead()) {
localRepositories.add(classes.getURL());
}
WebResource[] jars = resources.listResources("/WEB-INF/lib");
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
localRepositories.add(jar.getURL());
jarModificationTimes.put(
jar.getName(), Long.valueOf(jar.getLastModified()));
}
}
state = LifecycleState.STARTED;
}
@Override
public void start() throws LifecycleException {
state = LifecycleState.STARTING_PREP;
WebResource classes = resources.getResource("/WEB-INF/classes");
if (classes.isDirectory() && classes.canRead()) {
localRepositories.add(classes.getURL());
}
WebResource[] jars = resources.listResources("/WEB-INF/lib");
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
localRepositories.add(jar.getURL());
jarModificationTimes.put(
jar.getName(), Long.valueOf(jar.getLastModified()));
}
}
state = LifecycleState.STARTED;
}
@Override
public WebResource getResource(String path) {
checkPath(path);
String webAppMount = getWebAppMount();
WebResourceRoot root = getRoot();
if (path.startsWith(webAppMount)) {
File f = file(path.substring(webAppMount.length()), false);
if (f == null) {
return new EmptyResource(root, path);
}
if (!f.exists()) {
return new EmptyResource(root, path, f);
}
if (f.isDirectory() && path.charAt(path.length() - 1) != '/') {
path = path + '/';
}
return new FileResource(root, path, f, isReadOnly(), getManifest());
} else {
return new EmptyResource(root, path);
}
}
protected void processWebInfLib() throws LifecycleException {
WebResource[] possibleJars = listResources("/WEB-INF/lib", false);
for (WebResource possibleJar : possibleJars) {
if (possibleJar.isFile() && possibleJar.getName().endsWith(".jar")) {
createWebResourceSet(ResourceSetType.CLASSES_JAR,
"/WEB-INF/classes", possibleJar.getURL(), "/");
}
}
}
protected final WebResource getResourceInternal(String path,
boolean useClassLoaderResources) {
WebResource result = null;
WebResource virtual = null;
WebResource mainEmpty = null;
for (List<WebResourceSet> list : allResources) {
for (WebResourceSet webResourceSet : list) {
if (!useClassLoaderResources && !webResourceSet.getClassLoaderOnly() ||
useClassLoaderResources && !webResourceSet.getStaticOnly()) {
result = webResourceSet.getResource(path);
if (result.exists()) {
return result;
}
if (virtual == null) {
if (result.isVirtual()) {
virtual = result;
} else if (main.equals(webResourceSet)) {
mainEmpty = result;
}
}
}
}
}
@Override
public final WebResource getResource(String path) {
checkPath(path);
String webAppMount = getWebAppMount();
WebResourceRoot root = getRoot();
/*
* If jarContents reports that this resource definitely does not contain
* the path, we can end this method and move on to the next jar.
*/
if (jarContents != null && !jarContents.mightContainResource(path, webAppMount)) {
return new EmptyResource(root, path);
}
/*
* Implementation notes
*
* The path parameter passed into this method always starts with '/'.
*
* The path parameter passed into this method may or may not end with a
* '/'. JarFile.getEntry() will return a matching directory entry
* whether or not the name ends in a '/'. However, if the entry is
* requested without the '/' subsequent calls to JarEntry.isDirectory()
* will return false.
*
* Paths in JARs never start with '/'. Leading '/' need to be removed
* before any JarFile.getEntry() call.
*/
// If the JAR has been mounted below the web application root, return
// an empty resource for requests outside of the mount point.
if (path.startsWith(webAppMount)) {
String pathInJar = getInternalPath() + path.substring(
webAppMount.length());
// Always strip off the leading '/' to get the JAR path
if (pathInJar.length() > 0 && pathInJar.charAt(0) == '/') {
pathInJar = pathInJar.substring(1);
}
if (pathInJar.equals("")) {
// Special case
// This is a directory resource so the path must end with /
if (!path.endsWith("/")) {
path = path + "/";
}
return new JarResourceRoot(root, new File(getBase()),
baseUrlString, path);
} else {
JarEntry jarEntry = null;
if (isMultiRelease()) {
// Calls JarFile.getJarEntry() which is multi-release aware
jarEntry = getArchiveEntry(pathInJar);
} else {
Map<String,JarEntry> jarEntries = getArchiveEntries(true);
if (!(pathInJar.charAt(pathInJar.length() - 1) == '/')) {
if (jarEntries == null) {
jarEntry = getArchiveEntry(pathInJar + '/');
} else {
jarEntry = jarEntries.get(pathInJar + '/');
}
if (jarEntry != null) {
path = path + '/';
}
}
if (jarEntry == null) {
if (jarEntries == null) {
jarEntry = getArchiveEntry(pathInJar);
} else {
jarEntry = jarEntries.get(pathInJar);
}
}
}
if (jarEntry == null) {
return new EmptyResource(root, path);
} else {
return createArchiveResource(jarEntry, path, getManifest());
}
}
} else {
return new EmptyResource(root, path);
}
}
@Override
protected JarEntry getArchiveEntry(String pathInArchive) {
JarFile jarFile = null;
try {
jarFile = openJarFile();
return jarFile.getJarEntry(pathInArchive);
} catch (IOException ioe) {
// Should never happen
throw new IllegalStateException(ioe);
} finally {
if (jarFile != null) {
closeJarFile();
}
}
}
/**
* Returns the <code>JarEntry</code> for the given entry name or
* <code>null</code> if not found.
*
* @param name the jar file entry name
* @return the <code>JarEntry</code> for the given entry name or
* <code>null</code> if not found.
*
* @throws IllegalStateException
* may be thrown if the jar file has been closed
*
* @see java.util.jar.JarEntry
*/
public JarEntry getJarEntry(String name) {
return (JarEntry)getEntry(name);
}
public ZipEntry getEntry(String name) {
ZipEntry ze = super.getEntry(name);
if (ze != null) {
return new JarFileEntry(ze);
}
return null;
}
public ZipEntry getEntry(String name) {
if (name == null) {
throw new NullPointerException("name");
}
long jzentry = 0;
synchronized (this) {
ensureOpen();
jzentry = getEntry(jzfile, zc.getBytes(name), true);
if (jzentry != 0) {
// If no entry is found for the specified 'name' and
// the 'name' does not end with a forward slash '/',
// the implementation tries to find the entry with a
// slash '/' appended to the end of the 'name', before
// returning null. When such entry is found, the name
// that actually is found (with a slash '/' attached)
// is used
// (disabled if jdk.util.zip.ensureTrailingSlash=false)
ZipEntry ze = ensuretrailingslash ? getZipEntry(null, jzentry)
: getZipEntry(name, jzentry);
freeEntry(jzfile, jzentry);
return ze;
}
}
return null;
}
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
if (log.isDebugEnabled()) {
log.debug("loadClass(" + name + ", " + resolve + ")");
}
Class<?> clazz = null;
// Log access to stopped class loader
checkStateForClassLoading(name);
// (0) Check our previously loaded local class cache
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Returning class from cache");
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
// (0.1) Check our previously loaded class cache
clazz = JreCompat.isGraalAvailable() ? null : findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Returning class from cache");
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
// (0.2) Try loading the class with the system class loader, to prevent
// the webapp from overriding Java SE classes. This implements
// SRV.10.7.2
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
// Use getResource as it won't trigger an expensive
// ClassNotFoundException if the resource is not available from
// the Java SE class loader. However (see
// https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
// details) when running under a security manager in rare cases
// this call may trigger a ClassCircularityError.
// See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for
// details of how this may trigger a StackOverflowError
// Given these reported errors, catch Throwable to ensure any
// other edge cases are also caught
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
// Swallow all exceptions apart from those that must be re-thrown
ExceptionUtils.handleThrowable(t);
// The getResource() trick won't work for this class. We have to
// try loading it directly and accept that we might get a
// ClassNotFoundException.
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = sm.getString("webappClassLoader.restrictedPackage", name);
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name, true);
// (1) Delegate to our parent if requested
if (delegateLoad) {
if (log.isDebugEnabled()) {
log.debug(" Delegating to parent classloader1 " + parent);
}
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Loading class from parent");
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (2) Search local repositories
if (log.isDebugEnabled()) {
log.debug(" Searching local repositories");
}
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Loading class from local repository");
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) Delegate to parent unconditionally
if (!delegateLoad) {
if (log.isDebugEnabled()) {
log.debug(" Delegating to parent classloader at end: " + parent);
}
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Loading class from parent");
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
if (log.isDebugEnabled()) {
log.debug(" findClass(" + name + ")");
}
checkStateForClassLoading(name);
// (1) Permission to define this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
if (log.isTraceEnabled()) {
log.trace(" securityManager.checkPackageDefinition");
}
securityManager.checkPackageDefinition(name.substring(0,i));
} catch (Exception se) {
if (log.isTraceEnabled()) {
log.trace(" -->Exception-->ClassNotFoundException", se);
}
throw new ClassNotFoundException(name, se);
}
}
}
// Ask our superclass to locate this class, if possible
// (throws ClassNotFoundException if it is not found)
Class<?> clazz = null;
try {
if (log.isTraceEnabled()) {
log.trace(" findClassInternal(" + name + ")");
}
try {
if (securityManager != null) {
PrivilegedAction<Class<?>> dp =
new PrivilegedFindClassByName(name);
clazz = AccessController.doPrivileged(dp);
} else {
clazz = findClassInternal(name);
}
} catch(AccessControlException ace) {
log.warn(sm.getString("webappClassLoader.securityException", name,
ace.getMessage()), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled()) {
log.trace(" -->RuntimeException Rethrown", e);
}
throw e;
}
if ((clazz == null) && hasExternalRepositories) {
try {
clazz = super.findClass(name);
} catch(AccessControlException ace) {
log.warn(sm.getString("webappClassLoader.securityException", name,
ace.getMessage()), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled()) {
log.trace(" -->RuntimeException Rethrown", e);
}
throw e;
}
}
if (clazz == null) {
if (log.isDebugEnabled()) {
log.debug(" --> Returning ClassNotFoundException");
}
throw new ClassNotFoundException(name);
}
} catch (ClassNotFoundException e) {
if (log.isTraceEnabled()) {
log.trace(" --> Passing on ClassNotFoundException");
}
throw e;
}
// Return the class we have located
if (log.isTraceEnabled()) {
log.debug(" Returning class " + clazz);
}
if (log.isTraceEnabled()) {
ClassLoader cl;
if (Globals.IS_SECURITY_ENABLED){
cl = AccessController.doPrivileged(
new PrivilegedGetClassLoader(clazz));
} else {
cl = clazz.getClassLoader();
}
log.debug(" Loaded by " + cl.toString());
}
return clazz;
}
protected Class<?> findClassInternal(String name) {
checkStateForResourceLoading(name);
if (name == null) {
return null;
}
String path = binaryNameToPath(name, true);
ResourceEntry entry = resourceEntries.get(path);
WebResource resource = null;
if (entry == null) {
resource = resources.getClassLoaderResource(path);
if (!resource.exists()) {
return null;
}
entry = new ResourceEntry();
entry.lastModified = resource.getLastModified();
// Add the entry in the local resource repository
synchronized (resourceEntries) {
// Ensures that all the threads which may be in a race to load
// a particular class all end up with the same ResourceEntry
// instance
ResourceEntry entry2 = resourceEntries.get(path);
if (entry2 == null) {
resourceEntries.put(path, entry);
} else {
entry = entry2;
}
}
}
Class<?> clazz = entry.loadedClass;
if (clazz != null) {
return clazz;
}
synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
clazz = entry.loadedClass;
if (clazz != null) {
return clazz;
}
if (resource == null) {
resource = resources.getClassLoaderResource(path);
}
if (!resource.exists()) {
return null;
}
byte[] binaryContent = resource.getContent();
if (binaryContent == null) {
// Something went wrong reading the class bytes (and will have
// been logged at debug level).
return null;
}
Manifest manifest = resource.getManifest();
URL codeBase = resource.getCodeBase();
Certificate[] certificates = resource.getCertificates();
if (transformers.size() > 0) {
// If the resource is a class just being loaded, decorate it
// with any attached transformers
// Ignore leading '/' and trailing CLASS_FILE_SUFFIX
// Should be cheaper than replacing '.' by '/' in class name.
String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length());
for (ClassFileTransformer transformer : this.transformers) {
try {
byte[] transformed = transformer.transform(
this, internalName, null, null, binaryContent);
if (transformed != null) {
binaryContent = transformed;
}
} catch (IllegalClassFormatException e) {
log.error(sm.getString("webappClassLoader.transformError", name), e);
return null;
}
}
}
// Looking up the package
String packageName = null;
int pos = name.lastIndexOf('.');
if (pos != -1) {
packageName = name.substring(0, pos);
}
Package pkg = null;
if (packageName != null) {
pkg = getPackage(packageName);
// Define the package (if null)
if (pkg == null) {
try {
if (manifest == null) {
definePackage(packageName, null, null, null, null, null, null, null);
} else {
definePackage(packageName, manifest, codeBase);
}
} catch (IllegalArgumentException e) {
// Ignore: normal error due to dual definition of package
}
pkg = getPackage(packageName);
}
}
if (securityManager != null) {
// Checking sealing
if (pkg != null) {
boolean sealCheck = true;
if (pkg.isSealed()) {
sealCheck = pkg.isSealed(codeBase);
} else {
sealCheck = (manifest == null) || !isPackageSealed(packageName, manifest);
}
if (!sealCheck) {
throw new SecurityException
("Sealing violation loading " + name + " : Package "
+ packageName + " is sealed.");
}
}
}
try {
clazz = defineClass(name, binaryContent, 0,
binaryContent.length, new CodeSource(codeBase, certificates));
} catch (UnsupportedClassVersionError ucve) {
throw new UnsupportedClassVersionError(
ucve.getLocalizedMessage() + " " +
sm.getString("webappClassLoader.wrongVersion",
name));
}
entry.loadedClass = clazz;
}
return clazz;
}
@Override
public WebResource getClassLoaderResource(String path) {
return getResource("/WEB-INF/classes" + path, true, true);
}