昨天遇到一个诡异但是很有趣的类加载问题,虽然很快解决了,但是我还是打算剖根问底,分析内部问题出现的原因,毕竟类加载机制虽然说都知道怎么回事,但是还没在实战中实践过,也考虑到有个项目可能需要用到自定义类加载器,趁此机会先初步了解一下。 问题描述
以下tomcat:run...命令为tomcat7-maven-plugin的命令,scope为javax.servlet-api包在maven中的scope。
java.lang.ClassCastException: ch.qos.logback.classic.servlet.LogbackServletContainerInitializer cannot be cast to javax.servlet.ServletContainerInitializer
问题排查先看看这两个类的类加载器写个Servlet监听器,在启动时打出加载器和jar包信息 public class Callback implements ServletContextListener {
public void doCallback() {
System.out.println('查看看类加载器 ... ');
System.out.println('LogbackServletContainerInitializer = ' LogbackServletContainerInitializer.class.getClassLoader());
System.out.println('ServletContainerInitializer = ' ServletContainerInitializer.class.getClassLoader());
System.out.println('查看加载类所在jar包路径 ... ');
System.out.println('LogbackServletContainerInitializer = ' LogbackServletContainerInitializer.class.getProtectionDomain().getCodeSource().getLocation());
System.out.println('ServletContainerInitializer = ' ServletContainerInitializer.class.getProtectionDomain().getCodeSource().getLocation());
}
public void contextInitialized(ServletContextEvent servletContextEvent) {
doCallback();
}
public void contextDestroyed(ServletContextEvent servletContextEvent) {
doCallback();
}
}
在web.xml中配置好,启动,发现: 查看看类加载器 ...
LogbackServletContainerInitializer = WebappClassLoader
context:
delegate: false
repositories:
----------> Parent Classloader:
ClassRealm[plugin>org.apache.tomcat.maven:tomcat7-maven-plugin:2.2, parent: sun.misc.Launcher$AppClassLoader@18b4aac2]
ServletContainerInitializer = ClassRealm[plugin>org.apache.tomcat.maven:tomcat7-maven-plugin:2.2, parent: sun.misc.Launcher$AppClassLoader@18b4aac2]
查看加载类所在jar包路径 ...
LogbackServletContainerInitializer = file:/Users/coselding/.m2/repository-weidian/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar
ServletContainerInitializer = file:/Users/coselding/.m2/repository-weidian/org/apache/tomcat/embed/tomcat-embed-core/7.0.47/tomcat-embed-core-7.0.47.jar
![]() tomcat类加载器架构.jpg
结合Tomcat的类加载器架构,ServletContainerInitializer的类加载器ClassRealm应该就是对应的Common ClassLoader,而LogbackServletContainerInitializer就是WebappClassLoader,是Common ClassLoader的子加载器。和上面的场景结合起来就是,如果javax.servlet-api scope=compile,那么javax.servlet-api这个包就会在tomcat/lib下和应用WEB-INF/lib下各有一份,加载器分别是Common ClassLoader和WebappClassLoader。 ![]() WebAppClassLoader加载逻辑.jpg
因此你的WEB-INF/lib目录下的javax.servlet-api会被会在LogbackServletContainerInitializer加载时加载WebappClassLoader,而Tomcat启动自己加载自己lib目录下的那份WebappClassLoader,导致了ClassCastException。这个过程用图示如下: ![]() ServletContainerInitializer类加载.jpg
因此LogbackServletContainerInitializer实现的ServletContainerInitializer接口和tomcat识别的ServletContainerInitializer不是同一个类加载器加载的,故报错。
tomcat:run和tomcat:run-war的区别我们用
查看加载类所在jar包路径 ...
LogbackServletContainerInitializer = file:/Users/coselding/Projects/vweex/test/target/test-1.0.0-SNAPSHOT/WEB-INF/lib/logback-classic-1.2.3.jar
ServletContainerInitializer = file:/Users/coselding/.m2/repository-weidian/org/apache/tomcat/embed/tomcat-embed-core/7.0.47/tomcat-embed-core-7.0.47.jar
加载的确实是tomcat内部自带的javax.servlet-api,那我们放在WEB-INF/lib下的javax.servlet-api被忽略了?答案是的!!!我们看更完整的日志: 七月 24, 2018 3:36:57 下午 org.apache.catalina.loader.WebappClassLoader validateJarFile
信息: validateJarFile(/Users/coselding/Projects/vweex/test/target/test-1.0.0-SNAPSHOT/WEB-INF/lib/javax.servlet-api-3.0.1.jar) - jar not loaded. See Servlet Spec 2.3, section 9.7.2. Offending class: javax/servlet/Servlet.class
查看加载类所在jar包路径 ...
LogbackServletContainerInitializer = file:/Users/coselding/Projects/vweex/test/target/test-1.0.0-SNAPSHOT/WEB-INF/lib/logback-classic-1.2.3.jar
ServletContainerInitializer = file:/Users/coselding/.m2/repository-weidian/org/apache/tomcat/embed/tomcat-embed-core/7.0.47/tomcat-embed-core-7.0.47.jar
看见了吗?我们WEB-INF/lib目录下的jar被忽略了,WebappClassLoader在加载时做了校验,给出了警告,但是tomcat自己仍然会加载自身的javax.servlet-api,确保程序正常,这也是我们平时在项目中不太在意这个细节,但是程序仍然能正确执行的原因
他的好处是什么呢?这是一个开发时工具,你修改代码会自动进行热部署,避免每次改代码都需要重新启动!那么我们可以了解下热部署的原理:深入理解Java类加载器(2):线程上下文类加载器,这是为了开发方便而把类加载过程复杂化了,这个过程暂时不做了解,但是可以大致定位是这个复杂的类加载过程中有bug,导致了加载javax.servlet-api时没像正式部署时WebAppClassLoader正确过滤。
解决方式主要你保证你的项目依赖中 <dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
Servlet3.0依赖坐标 <dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
呵呵。。。我们的dubbo中这两个包都依赖了,需要全部exclude。。 |
|