tomcat通过重写webappclassloader的loadclass方法打破双亲委派:先查本应用web-inf/classes与lib,未找到再委托父加载器,实现类隔离与共享库统一。

Java 中实现类似 Tomcat 的 Web 类加载器,核心在于打破双亲委派模型(Parent-Delegation Model),让每个 Web 应用拥有独立的类加载路径和隔离能力。Tomcat 的 WebAppClassLoader 正是这样做的:它优先从当前应用的 WEB-INF/classes 和 WEB-INF/lib 加载类,仅在必要时才委托给父类加载器(如共享的 CommonClassLoader)。
关键设计原则
要模拟 Tomcat 行为,需明确以下几点:
-
打破双亲委派:重写
loadClass(String name, boolean resolve),不直接调用super.loadClass(),而是先尝试本地资源加载 -
资源路径隔离:每个 WebApp 类加载器绑定专属目录(如
/app1/WEB-INF/classes、/app1/WEB-INF/lib/*.jar) -
委托例外规则:对
java.*、javax.*(部分)、org.w3c.*等系统类,仍强制走 Bootstrap/AppClassLoader;对日志、JDBC 驱动等常见“共享但易冲突”类,可配置白名单交由父加载器处理 - 热部署支持基础:类加载器本身可被整体替换(而非单个类卸载),因此需避免静态引用、线程上下文泄露等问题
一个简化版 WebAppClassLoader 示例
以下是一个最小可行实现(基于 URLClassLoader 扩展):
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
public class WebAppClassLoader extends URLClassLoader {
private final ClassLoader parent;
private final Set<string> systemPackages = Set.of(
"java.", "javax.", "sun.", "oracle.", "jdk."
);
private final Set<string> delegateFirstPackages = Set.of(
"org.slf4j.", "ch.qos.logback.", "org.apache.logging.log4j."
);
public WebAppClassLoader(File webInfClasses, File webInfLib, ClassLoader parent) {
super(new URL[0], null); // parent 传 null,切断默认委派链
this.parent = parent;
try {
addURL(webInfClasses.toURI().toURL());
Arrays.stream(webInfLib.listFiles((d, n) -> n.endsWith(".jar")))
.map(this::toUrl)
.forEach(this::addURL);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 已加载?先查缓存
Class> loaded = findLoadedClass(name);
if (loaded != null) return resolveClass(loaded, resolve);
// 2. 系统包 → 委托给启动类加载器或扩展类加载器(通过 parent)
if (isSystemPackage(name)) {
return parent.loadClass(name);
}
// 3. 白名单包(如日志)→ 也走 parent,避免多个 App 各自打包冲突
if (isDelegateFirst(name)) {
return parent.loadClass(name);
}
// 4. 尝试本应用加载
try {
Class> clazz = findClass(name);
return resolveClass(clazz, resolve);
} catch (ClassNotFoundException ignored) {
// 5. 失败则委托 parent(如 Servlet API、JSP API 等共享接口)
return parent.loadClass(name);
}
}
private boolean isSystemPackage(String name) {
return systemPackages.stream().anyMatch(name::startsWith);
}
private boolean isDelegateFirst(String name) {
return delegateFirstPackages.stream().anyMatch(name::startsWith);
}
private Class> resolveClass(Class> clazz, boolean resolve) {
if (resolve) resolveClass(clazz);
return clazz;
}
private URL toUrl(File jar) {
try { return jar.toURI().toURL(); } catch (Exception e) { return null; }
}
}
</string></string>
配套要点:如何使用与管理
光有类加载器不够,还需配合运行时结构:
-
每个 WebApp 实例持有唯一类加载器:例如
WebAppContext app1 = new WebAppContext(...); app1.setClassLoader(new WebAppClassLoader(...)); -
线程上下文类加载器(TCCL)需正确设置:在请求进入时(如 Servlet 容器分发前)执行
Thread.currentThread().setContextClassLoader(appClassLoader),确保框架代码(如 JDBC DriverManager、JAXP)能感知当前应用环境 -
资源查找统一走
getResourceAsStream():不要硬编码getClass().getClassLoader().getResource(),而应始终使用当前 TCCL 或显式传入的 appClassLoader - 避免静态跨应用引用:例如在 common 模块中缓存某个 WebApp 特有的类实例,会导致内存泄漏和 ClassCastException
注意边界与陷阱
真实场景比示例复杂得多,需警惕:
- JDBC 驱动注册:现代驱动通过
ServiceLoader自动注册,但若应用自带mysql-connector-java.jar,又依赖容器统一管理连接池,容易因类加载器不同导致DriverManager找不到驱动 - JNDI 查找:上下文环境(
InitialContext)依赖 TCCL,若未设好,会找不到java:comp/env/jdbc/xxx - JSP 编译:生成的 servlet 类必须由 WebAppClassLoader 加载,且其父类(如
HttpServlet)由共享类加载器提供,否则编译失败 - 序列化/反序列化:跨应用传递对象时,若类定义在不同类加载器下,即使全限定名相同也会视为不同类型
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










