Jetty 类加载问题处理

前几日使用 Jetty (9.2)部署公司一个 web 项目,这个项目原本部署在 Tomcat 服务器上,一切正常,但是部署到 Jetty 后,启动报错.关键错误信息为"java.lang.NoClassDefFoundError: Could not initialize class org.apache.tomcat.jdbc.pool.DataSource"


项目使用了 Tomcat jdbc connection pool 其中有两个 jar 包 tomcat-jdbc.jar 和 tomcat-juli.jar, 后者是前者的 maven 依赖,后者也是 tomcat 的日志抽象层,tomcat 服务器自带这个 jar 在 tomcat_home 的 bin 目录下.根据异常信息判断错误是由于加载和初始化org.apache.tomcat.jdbc.pool.DataSource导致的,为了更容易分析问题,我创建了一个简单的 web 项目只依赖于这两个 jar 包.部署到同一个 Jetty 服务器中,报错"java.util.ServiceConfigurationError: org.apache.juli.logging.Log: Provider org.eclipse.jetty.apache.jsp.JuliLog not a subtype"


查看源代码org.eclipse.jetty.apache.jsp.JuliLog很明确是org.apache.juli.logging.Log的子类,但为什么会报这样的错误呢,结合之前的java.lang.NoClassDefFoundErrorjava.util.ServiceConfigurationError可以确定问题是由于类加载引起的,根据对类加载的了解,同一个类被不同的类加载器实例加载得到的 Class 对象是不同的.所以我推断可能是由于 Jetty 服务器使用了不同的类加载器实例加载了两个累,导致继承关系不存在了.查看java.util.ServiceConfigurationError相关的API文档发现,这个 Error 是在 ServiceLoader加载 Service Provider 时发生的.查看 ServiceLoader 的源代码发现这样一段

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");//我遇到的错误信息刚好是这里.
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

为了验证我的推测,参考这段代码我写了个小程序来测试.主要代码如下

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        final CustomClassLoader customClassLoader
                = new CustomClassLoader("target/classes", "自定义加载器", systemClassLoader);//第一个参数的路径是类的编译路径

        //使用系统类加载器加载 Child
        //因为 Child 依赖 Parent 所以系统类加载器会自动加载 Parent,这个行为与 jetty 的 WebAppClassCloader 相同
        Class<?> child = Class.forName("Child", true, systemClassLoader);
        //因为之前加载过,不会重复加载直接返回 Parent 类实例
        Class<?> parent = Class.forName("Child", true, systemClassLoader);

        //使用自定义加载器加载 Child
        //自定义加载器会优先尝试自己加载,失败后使用父加载器
        Class<?> customChild = Class.forName("Child", true, customClassLoader);
        Class<?> customParent = Class.forName("Parent", true, customClassLoader);//同样不会重复加载

        //测试
        //同样使用系统加载器加载的两个类,继承关系正常
        System.out.println("parent.isAssignableFrom(child) = " + parent.isAssignableFrom(child));//true
        //使用自定义加载器加载的 Child 却不是系统类加载器加载的 Parent 的子类
        System.out.println("parent.isAssignableFrom(customChild) = " + parent.isAssignableFrom(customChild));//false
        //同样使用自定义加载器加载的两个类,集成关系正常
        System.out.println("customParent.isAssignableFrom(customChild) = " + customParent.isAssignableFrom(customChild));//true
    }
}

这段代码中 Child 是 Parent 的子类.这个简单的测试验证了我的推测.查看文档发现 Jettty 在9.2版本中的 jsp 引擎使用的是 tomcat 的.在 Jetty 的 lib 里面可以发现如下jar

org.eclipse.jetty.apache-jsp-9.2.3.v20140905.jar
org.eclipse.jetty.orbit.org.eclipse.jdt.core-3.8.2.v20130121.jar
org.mortbay.jasper.apache-el-8.0.9.M3.jar
org.mortbay.jasper.apache-jsp-8.0.9.M3.jar

org.mortbay开头的两个 jar 里面是 apache 的 jsp 实现类其中包含org.apache.juli.logging这个包,org.eclipse.jetty.apache-jsp-9.2.3.v20140905.jar这个 jar 中提供了 logging 的具体实现,最终通过 ServiceLoader 加载.问题就出在这里,因为我的项目中有 tomcat-juli.jar 其中也包含org.apache.juli.logging这个包.

查看了一下 Jetty 的文档中有关类加载的内容,发现 Jetty 对每个部署的 web 应用使用单独的WebAppClassLoader实例进行类加载.通常实现自定义类加载器的时候会优先委托给父加载器(一般为系统类加载器,可以通过 ClassLoader.getSystemClassLoader() 得到),然后再尝试自己加载类.但 Jetty 的这个 WebAppClassLoader 正相反,除了对于系统类和服务器类(什么是系统类和服务器类可以查看文档),会优先尝试自己加载,然后才委托父加载器.

根据这个行为基本可以确认了,服务器加载 jsp 引擎是会使用自己的类加载器加载服务器 lib 中上述的类(org.apache.juli.logging.Log及其实现org.eclipse.jetty.apache.jsp.JuliLog),应用部署时会使用WebAppClassLoader加载应用 lib 中的org.apache.juli.logging.Log.加载过程是这样的, WebAppClassLoader实例加载org.apache.tomcat.jdbc.pool.DataSource,其依赖org.apache.juli.logging.Log,优先尝试自己加载,所以会从应用的 lib 中加载到这个类,而尝试加载其实现的时候发现应用 lib 中没有,再委托给父类加载器,也就是 Jetty 服务器的加载器,成功加载到org.eclipse.jetty.apache.jsp.JuliLog,这样就是使用两个不同的加载器实例加载了子类和父类,根据之前的测试结果,两个类之间的继承关系是不成立的.所以导致错误发生.


清楚了问题的原因,怎么解决呢?

方案一,在 maven 配置中将 tomcat-juli 的依赖 scope 改为 provided,Jetty 服务器已经提供了. 这样在WebAppClassLoader 尝试自己加载org.eclipse.jetty.apache.jsp.JuliLog时会失败,进而委托父类加载器,这样org.apache.juli.logging.Log及其实现org.eclipse.jetty.apache.jsp.JuliLog两个类就是同一个加载器加载了.

方案二,更改 WebAppClassLoader 的父类加载器的优先级,使其优先使用父类加载器.具体配置方式可以参考文档.目标是调用 setParentLoaderPriority(true)


使用这两个方案,同样可以解决原始项目中的问题,但是为什么测试用的简单 web 项目和原始项目的错误信息不同呢?

回到原始项目的错误信息发现,"java.lang.NoClassDefFoundError: Could not initialize class org.apache.tomcat.jdbc.pool.DataSource"这个错误是由于在加载类的时候无法初始化,那么看org.apache.tomcat.jdbc.pool.DataSource中,在类加载后要做的初始化操作有什么,通过查看源码发现private static final Log log = LogFactory.getLog(DataSource.class);只有这句代码是需要在类加载后进行的初始化,跟踪这个语句发现最终会进入上文中提到的nextService方法,所以根源错误依然是上面描述的.

至此问题得到比较圆满的解释和解决.


总结:

本文涉及到的知识点

1.Java虚拟机的类加载机制

2.JavaServiceProvider 加载机制

3.Java 类的初始化过程

4.Jetty 服务器的配置方式


猜你喜欢

转载自blog.csdn.net/u012631045/article/details/40426455