一个Tomcat实例上同时跑着多个Web应用应用A依赖Spring 4应用B依赖Spring 5。如果类加载走标准的双亲委派模型同一个全限定名的类只会被加载一次。Spring 4和Spring 5里都有org.springframework.context.ApplicationContext这个类版本不同、实现不同双亲委派会把它交给上层的公共类加载器去加载结果只加载了一份两个应用里必定有一个会出问题。下面从问题场景、层级设计、源码实现三个层面拆分来看一下。先回忆一下双亲委派标准的双亲委派规则很简单一个类加载器收到加载请求后不自己动手先把请求往上传给父加载器。父加载器也一样继续往上传。一直传到最顶层的启动类加载器它加载不了再一层一层退回来轮到自己试。这套机制的好处是保证了类的唯一性。不管你在哪个地方触发加载java.lang.String最终都会由启动类加载器来加载不会出现两份String。单应用场景下这没问题到了Web容器就不够用了。标准双亲委派在Web容器里会出什么问题有三个。应用之间的隔离问题。前面说的Spring版本冲突就是最典型的例子。两个应用用了同一个类库的不同版本双亲委派会让父加载器统一加载两个应用共享同一份Class对象。版本不兼容的类混在一起用运行时报NoSuchMethodError或者ClassNotFoundException。应用和容器之间的隔离问题。Tomcat自身运行也需要依赖一些第三方库。如果Tomcat内部用的库版本和某个应用的库版本冲突了按双亲委派的规则Tomcat的类先被加载应用就只能被迫用Tomcat的版本。这不合理容器不应该影响应用的类库选择。应用之间的共享问题。隔离也并不一定就是一刀切的。有些类库确实应该被所有应用共享比如Servlet API。如果每个应用各自加载一份javax.servlet.http.HttpServlet不同应用加载出来的是不同的Class对象跨应用传参数会出现类型转换异常。这些规范级别的类必须保证全局只有一份。三个问题归结成一句话就是Web容器需要一套既能「隔离」又能「共享」的类加载机制标准双亲委派只能做到共享做不到隔离。Tomcat的类加载器层级设计Tomcat的解决思路是设计一套多层的类加载器结构每一层有明确的职责边界。最上面是JVM自带的启动类加载器和系统类加载器跟标准JVM一样没有变化。往下是Tomcat自己创建的Common类加载器。它负责加载Tomcat和所有Web应用都能看到的公共类比如Servlet API、Tomcat自身的核心类库。这些类放在Tomcat安装目录的lib文件夹下。Common类加载器再往下就是每个Web应用各自独立的WebappClassLoader。应用A有自己的WebappClassLoader实例应用B也有自己的。它们之间是平级关系互相看不到对方加载的类。这就实现了应用间的隔离。在Tomcat的Bootstrap类里能看到这个层级是怎么创建的private void initClassLoaders() { // 创建Common类加载器父加载器为系统类加载器 commonLoader createClassLoader(common, null); if (commonLoader null) { commonLoader this.getClass().getClassLoader(); } // Server类加载器父加载器为Common catalinaLoader createClassLoader(server, commonLoader); // Shared类加载器父加载器为Common sharedLoader createClassLoader(shared, commonLoader); }这里除了Common之外还有Server和Shared两个加载器。Server加载器只对Tomcat容器内部可见Web应用看不到。Shared加载器对所有Web应用可见可以放多个应用需要共享的类库。不过默认配置下这两个加载器都没有单独配置加载路径等于直接复用了Common加载器。它们的加载路径在catalina.properties里配置common.loader${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar server.loader shared.loaderserver.loader和shared.loader为空时createClassLoader方法会直接返回传入的父加载器即Common加载器。只有你手动填上路径Tomcat才会真正创建独立的Server和Shared加载器实例。看一下createClassLoader里的这段逻辑就清楚了private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { String value CatalinaProperties.getProperty(name .loader); // 配置为空直接返回父加载器 if ((value null) || (value.equals())) { return parent; } // 配置不为空才真正创建新的类加载器实例 // ... return ClassLoaderFactory.createClassLoader(repositories, parent); }因此公共的东西放上面共享私有的东西放下面隔离。层级本身就能解决共享和隔离的需求分层但光有层级还不够还得改类加载的查找顺序。loadClass源码Tomcat到底怎么打破的标准双亲委派是「先问父加载器父加载器找不到再自己找」。Tomcat的WebappClassLoader把这个顺序反过来了先在自己的Web应用目录下找找不到再问父加载器。这个逻辑在WebappClassLoaderBase的loadClass方法里。去掉日志和异常处理后核心流程是这样的public Class? loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class? clazz null; // 检查本地缓存这个类是否已经被自己加载过 clazz findLoadedClass0(name); if (clazz ! null) { return clazz; } // 检查JVM级别的缓存 clazz findLoadedClass(name); if (clazz ! null) { return clazz; } // 尝试用Java SE的类加载器加载 // 这一步是安全防线防止Web应用覆盖JDK核心类 ClassLoader javaseLoader getJavaseClassLoader(); URL url javaseLoader.getResource(resourceName); if (url ! null) { clazz javaseLoader.loadClass(name); if (clazz ! null) { return clazz; } } // 判断是否需要委派给父加载器 boolean delegateLoad delegate || filter(name, true); // 如果delegatetrue或者命中了filter规则先交给父加载器 if (delegateLoad) { clazz Class.forName(name, false, parent); if (clazz ! null) { return clazz; } } // 在自己的/WEB-INF/classes和/WEB-INF/lib下查找 clazz findClass(name); if (clazz ! null) { return clazz; } // 前面没委派过的话现在兜底委派给父加载器 if (!delegateLoad) { clazz Class.forName(name, false, parent); if (clazz ! null) { return clazz; } } } throw new ClassNotFoundException(name); }这段代码里有几个值得注意的设计。Java SE类加载器那一步是一个硬性的安全保底。不管delegate怎么配JDK核心类java.lang.String、java.util.List这些永远由Java SE的类加载器来加载。Web应用就算在自己的lib下放了一个java.lang.String.class也加载不进去。Tomcat在这里先用getResource探测一下这个类是否属于Java SE如果是就直接交给Java SE加载器避免抛出不必要的ClassNotFoundException。delegate属性默认是false。false的意思是不走标准委派跳过父加载器直接去自己的Web应用目录下找。只有当delegate为true或者filter()方法返回true时才会先委派给父加载器。最后两步的顺序是整个方法的关键。默认情况下delegatefalse会先执行findClass在本地查找找不到了才兜底委派给父加载器。这就是打破双亲委派的核心动作把「先父后子」变成了「先子后父」。对比一下标准双亲委派和Tomcat的加载顺序步骤标准双亲委派Tomcat WebappClassLoader1检查缓存检查缓存2委派给父加载器用Java SE加载器保护JDK核心类3父加载器找不到自己找在/WEB-INF/classes和/WEB-INF/lib下找4找不到抛异常自己找不到再委派给父加载器哪些类不能被打破Tomcat打破双亲委派不是无条件的。有些类必须由父加载器统一加载不允许Web应用覆盖。这个过滤逻辑在filter()方法里。它检查类名的包前缀对特定包名下的类强制返回true让这些类走委派路径protected boolean filter(String name, boolean isClassName) { if (name.startsWith(javax)) { // javax.servlet.*、javax.el.*、javax.websocket.*等 // Servlet规范相关的类强制委派 if (name.startsWith(servlet., 6) || name.startsWith(el., 6) || name.startsWith(websocket., 6)) { return true; } } else if (name.startsWith(org)) { if (name.startsWith(apache., 4)) { // org.apache.catalina.*、org.apache.tomcat.* // org.apache.jasper.*、org.apache.coyote.*等 // Tomcat内部的核心类强制委派 if (name.startsWith(catalina., 11) || name.startsWith(tomcat., 11) || name.startsWith(jasper., 11) || name.startsWith(coyote., 11)) { return true; } } } return false; }被filter()命中的类会在loadClass里被直接委派给父加载器不会走到本地查找的那一步。这意味着你不能在Web应用里放一个自己改过的javax.servlet.http.HttpServlet来替换Tomcat提供的版本。Servlet规范的类和Tomcat自身的类必须全局唯一这是安全底线。值得注意的是filter()里有一个例外javax.servlet.jsp.jstl.*JSTL相关的类没有被强制委派Web应用可以自带JSTL实现。同样org.apache.tomcat.jdbc.*也没有被强制委派应用可以用自己的数据库连接池版本。Tomcat在这些细节上的处理是很精确的只锁住必须锁的其他的放开。打破双亲委派也需要控制好要做到在安全边界内给应用更多自主权。面试怎么答这个问题面试出现频率不低给一个可以直接拿去用的回答Tomcat作为Web容器一个实例上可以部署多个Web应用。如果用标准双亲委派同名类只会被父加载器加载一份不同应用用了同一个类库的不同版本就会冲突。Tomcat的做法是为每个Web应用创建独立的WebappClassLoader并且重写了loadClass方法把加载顺序从「先父后子」改成了「先子后父」。默认情况下WebappClassLoader会先在自己的/WEB-INF/classes和/WEB-INF/lib下查找类找不到才委派给父加载器。这样不同应用就可以各自加载自己版本的类库互不干扰。同时Tomcat通过filter()方法保证了JDK核心类、Servlet API、Tomcat内部类不会被Web应用覆盖。面试官如果追问细节可以补充两点Tomcat的类加载器层级是启动类加载器 → 系统类加载器 → Common类加载器 → WebappClassLoaderCommon负责加载所有应用共享的类库delegate属性可以把WebappClassLoader切回标准委派模式filter()方法控制哪些包名下的类强制走委派。