主流web容器(jetty,tomcat,jboss)的classloader机制对比和相关问题分析

背景

     前段时间一直在做应用容器的迁移,将公司的应用容器从jboss,tomcat统一迁移到jetty。在整个迁移过程中遇到最多的潜在问题还是在classloader机制上,这里记录一下希望能对大家有所帮助,避免重复走弯路。

 

啥都不说,先来看下遇到的几个问题,比较纠结的问题。

问题1: (jar sealed问题)

Caused by: java.lang.SecurityException: sealing violation: package com.sun.media.jai.util is sealed
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:234)
        at java.net.URLClassLoader.access$000(URLClassLoader.java:58)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:197)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
        at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:419)
        at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:381)
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)

说明: jboss容器运行正常 , jetty容器运行出错

 

问题2:  (xml解析) 

Caused by: java.lang.NoSuchMethodError: javax.xml.parsers.SAXParserFactory.newInstance(Ljava/lang/String;Ljava/lang/ClassLoader;)Ljavax/xml/parsers/SAXParserFactory;

说明: jetty容器运行正常 , tomcat容器运行正常,jboss容器运行异常

 

问题3:  (xml解析) 

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
	at java.lang.reflect.Method.invoke(Method.java:597)
	at org.eclipse.jetty.start.Main.invokeMain(Main.java:490)
	at org.eclipse.jetty.start.Main.start(Main.java:634)
	at org.eclipse.jetty.start.Main.parseCommandLine(Main.java:280)
	at org.eclipse.jetty.start.Main.main(Main.java:82)
Caused by: javax.xml.parsers.FactoryConfigurationError: Provider org.apache.xerces.jaxp.SAXParserFactoryImpl not found
	at javax.xml.parsers.SAXParserFactory.newInstance(SAXParserFactory.java:134)
	at org.eclipse.jetty.xml.XmlParser.(XmlParser.java:68)
	at org.eclipse.jetty.xml.XmlConfiguration.initParser(XmlConfiguration.java:79)
	at org.eclipse.jetty.xml.XmlConfiguration.(XmlConfiguration.java:112)
	at org.eclipse.jetty.xml.XmlConfiguration$1.run(XmlConfiguration.java:1028)
	at java.security.AccessController.doPrivileged(Native Method)
	at org.eclipse.jetty.xml.XmlConfiguration.main(XmlConfiguration.java:983)
	... 8 more

说明: jboss容器运行正常 , jetty容器运行异常

 

问题4:(mail问题) 

Caused by: java.lang.ClassNotFoundException: javax.mail.event.TransportListener
        at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
        at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:419)
        at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:381)
        ... 78 more

说明: jboss容器运行正常 , jetty容器运行异常 

 

可以说基本都是对应的class , method等找不到,或者类冲突等问题,一看就是比较典型的classloader引发的问题。

下面就来看看对容器classloader机制的分析和对比,相信大家了解了相关classloader机制微妙的区别后,基本也能解析这一类问题了

jboss4.05 classloader机制

这里早期对jboss classloader几点配置做了下说明,可以参见: http://agapple.iteye.com/blog/791940

 

为了和tomcat,jetty有更明显的对比,这里就主要介绍三个参数代码层面上的实现:

 

 

 
   
   
   
......


      false
      
      true
      
      javax.servlet,org.apache.commons.logging

.....

 

 

相信这几个参数大家都应该比较熟知,配置项在 deploy/jbossweb-tomcat55.sar/META-INF/jboss-service.xml中

 

下面循着代码来看一下jboss的相关实现:

  1. 代码入口: org.jboss.web.tomcat.tc5.Tomcat5, 这里的三个配置都对应于Tomcat5类的属性,默认值就是当前配置的值。
  2. Tomcat5会创建一个Deploy进行war包的装载,TomcatDeployer(继承于AbstractWebDeployer)
    if (ctxPath.equals("/") || ctxPath.equals("/ROOT") || ctxPath.equals(""))
          {
             log.debug("deploy root context=" + ctxPath);
             ctxPath = "/";
             metaData.setContextRoot(ctxPath);
          }
    
          URL url = new URL(warUrl);
    
          ClassLoader loader = Thread.currentThread().getContextClassLoader();
          /* If we are using the jboss class loader we need to augment its path
          to include the WEB-INF/{lib,classes} dirs or else scoped class loading
          does not see the war level overrides. The call to setWarURL adds these
          paths to the deployment UCL.
          */
          Loader webLoader = null;
          if (config.isUseJBossWebLoader())  // 这里对useJbossWebLoader进行判断,进行不同的classloader处理
          {
             WebCtxLoader jbossLoader = new WebCtxLoader(loader);
             jbossLoader.setWarURL(url);
             webLoader = jbossLoader;
          }
          else
          {
             String[] pkgs = config.getFilteredPackages();
             WebAppLoader jbossLoader = new WebAppLoader(loader, pkgs);
             jbossLoader.setDelegate(getJava2ClassLoadingCompliance());
             webLoader = jbossLoader;
          }
     
  3. 最后通过MBean调用,将classloader设置给对应的tomcat上下文对象: "org.apache.catalina.core.StandardContext";
    if (webLoader != null)
          {
             server.setAttribute(objectName, new Attribute("loader", webLoader));
          }
          else
          {
             server.setAttribute(objectName, new Attribute("parentClassLoader", loader));
          }
    
          server.setAttribute(objectName, new Attribute("delegate", new Boolean(getJava2ClassLoadingCompliance()))); // 设置deletegate属性

说明:

  • WebCtxLoader: 一个对jboss UCL classloader的一个代理而已,setWarUrl也只是将war资源加入到当前jboss的UCL classloader去装载
    WebCtxLoader(ClassLoader encLoader)
       	{
    	      this.encLoader = encLoader;
    	      this.ctxLoader = new ENCLoader(encLoader);
    	      ClassLoader parent = encLoader;
    	      while ((parent instanceof RepositoryClassLoader) == false && parent != null)
    		 parent = parent.getParent();
    	      this.delegate = (RepositoryClassLoader) parent; //delegate对象设置
    	}
    
    
    public void setWarURL(URL warURL) throws MalformedURLException
       	{
    	      this.warURL = warURL;
    	      String path = warURL.getFile();
    	      File classesDir = new File(path, "WEB-INF/classes");
    	      if (classesDir.exists())
    	      {
    		 delegate.addURL(classesDir.toURL()); //无非都是委托给delegate loader
    		 ctxLoader.addURLInternal(classesDir.toURL());
    	      }
    	      File libDir = new File(path, "WEB-INF/lib");
    	      if (libDir.exists())
    	      {
    		 File[] jars = libDir.listFiles();
    		 int length = jars != null ? jars.length : 0;
    		 for (int j = 0; j < length; j++)
    		 {
    		    delegate.addURL(jars[j].toURL()); //无非都是委托给delegate loader
    		    ctxLoader.addURLInternal(jars[j].toURL());
    		 }
    	      }
    	   }
  • WebAppLoader: 对tomcat WebappLoader的一个封装, 同时设置filteredPackages给tomcat WebappLoader进行class控制。
    public class WebAppLoader extends org.apache.catalina.loader.WebappLoader
    {
       private String[] filteredPackages = {
          "org.apache.commons.logging"
       };
    
       public WebAppLoader()
       {
          super();
          setLoaderClass(WebAppClassLoader.class.getName());
       }
    .....
    }
     

看到这里大家相信应该差不多了解了,总结一下: 

  1. java2ClassLoadingCompliance是针对useJbossWebLoader=false时而言,是通过设置tomcat WebappClassloader的是否delegate进行控制classloader,实现child first/parent first。
  2. java2ClassLoadingCompliance在useJBossWebLoader=true时,不会生效,会被强制设置为false,具体可看WebCtxLoaders,实现了Loader接口,getClassLoader()返回的是一个ctxLoader对jboss WebLoader的一个包装。
  3. filteredPackages目前是通过tomcat的classloader机制上进行控制,所以只能是在useJbossWebLoader=false时有效,因为依赖了tomcat的实现。

不过需要注意两点:

  1. 目前在jboss4.05上不支持在在war包下WEB-INF/jboss-web.xml的配置,在jboss4.20以后才支持,可见https://jira.jboss.org/browse/JBAS-3047?subTaskView=all
  2. jboss 5.0以后,就没有UseJbossWebLoader这一配置项了,实现方式也不是delegate到tomcat,而是独立的一个classloader,统一处理。
jboss classloader机制大家也不必太刻意的去学习,只要适当的了解基本使用。只能说一切都是浮云浮云,随时都可能被改变。

 

tomcat6.0.30 classloader机制 

tomcat相比于jboss4.05概念上简介了很多,不过tomcat 6版本相比于tomcat 5有一些变化,少了一些shared lib库的概念。

 

      Bootstrap
          |
       System
          |
       Common
       /     \
  Webapp1   Webapp2 ... 

 一个树状结构,相信大家差不多都知道tomcat默认是以child first装载class,优先载入web中的class类,找不到才会去装载公用类。

 

下面就来看一下代码的一些实现: 

 

  1. tomcat启动入口,通过分析StandardContext.start()
    public synchronized void start() {
    .....
    
    if (getLoader() == null) {
                WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
                webappLoader.setDelegate(getDelegate());
                setLoader(webappLoader);
            }
    .....
    }
  2. 如果getLoader为空,则创建一个新的WebappLoader,注意这也是jboss设置tomcat loader的入口,从而替换默认的tomcat classloader。
  3. WebappLoader通过在startInternal启动了一个新的WebappClassLoader
    // Construct a class loader based on our current repositories list
            try {
    
                classLoader = createClassLoader();
                classLoader.setResources(container.getResources());
                classLoader.setDelegate(this.delegate);
                classLoader.setSearchExternalFirst(searchExternalFirst);
                .....
                for (int i = 0; i < repositories.length; i++) {
                    classLoader.addRepository(repositories[i]);
                }
    } 
  4. 最后就是WebappClassLoader的loadClass方法了,具体的类装载策略。
下面来看一下,loadClass的相关逻辑: 
  1. (0.1)先检查class是否已经被装载过,findLoadedClass
  2. (0.2)通过system classloader装载系统class,所以这里会优先装载jdk的相关代码,这个很重要,也很特别
  3. 如果是delegate=true并且不是filterPackage列表,则采用parent first,否则采用child first。
相关代码:
// (0.1) Check our previously loaded class cache
	clazz = findLoadedClass(name);

// (0.2) Try loading the class with the system class loader, to prevent the webapp from overriding J2SE classes
	clazz = system.loadClass(name);

 boolean delegateLoad = delegate || filter(name);
// (1) Delegate to our parent if requested
if (delegateLoad) {
	ClassLoader loader = parent;
            if (loader == null)
                loader = system;
            try {
                clazz = loader.loadClass(name);
	    ....
 	    }
     .....
}

// (2) Search local repositories
clazz = findClass(name);

// (3) Delegate to parent unconditionally
if (!delegateLoad) {
	ClassLoader loader = parent;
            if (loader == null)
                loader = system;
            try {
                clazz = loader.loadClass(name);
	    ....
 	    }
     .....
}
 

最后总结一下, 两个参数配置:

  1. delegate(child first/parent first),默认值为false,即为child first
  2. packageTriggers,执行child first时,排除的package列表,如果匹配了package,即时为delegate=false,也会优先执行parent first策略。
  3. tomcat的classloader机制在处理class装载时,会优先尝试使用system进行装载,这样可以优先使用jdk的相关类,这也是问题2的原因。

 

jetty7.1.20 classloader机制

jetty与tomcat相比,没有太大上的区别,也是有parent first/child first的概念,filter package等。但还是有些小区别,那先看一下代码:

  1. jetty启动入口org.eclipse.jetty.server.Server,通过DeploymentManager进行webapps目录的加载
  2. 具体war包加载是通过WebAppProvider,通过createContextHandler创建应用上下文WebAppContext。
  3. WebAppContext在初始化时,创建WebAppClassLoader
    public void preConfigure() throws Exception
        {
            // Setup configurations
            loadConfigurations();
    
            // Setup system classes
            loadSystemClasses();
            
            // Setup server classes
            loadServerClasses();
    
            // Configure classloader
            _ownClassLoader=false;
            if (getClassLoader()==null)
            {
                WebAppClassLoader classLoader = new WebAppClassLoader(this);
                setClassLoader(classLoader);
                _ownClassLoader=true;
            }
    .....
    }
  4. 最后就是WebAppClassLoader的loadclass方法了。
理解了这些基本概念后,jetty的loadclass方法,逻辑比较好理解:
  1. 如果属于system_class或者_parentLoaderPriority=true,并且不是server_class,优先采用parent first进行装载
    (_context.isParentLoaderPriority() || system_class) && !server_class)
  2. 否则采用child first,先由WebAppClassLoader进行装载,没找到class再交由父CL装载

里面的有几个概念:

  •  _parentLoaderPriority: 对应child first/parent first。
  • _serverClasses : jetty系统实现类,jetty认为服务实现类,必须在WEB-INF/lib , WEB-INF/classes优先加载。
    public final static String[] __dftServerClasses = 
        {
            "-org.eclipse.jetty.continuation.", // don't hide continuation classes
            "-org.eclipse.jetty.jndi.",         // don't hide naming classes
            "-org.eclipse.jetty.plus.jaas.",    // don't hide jaas classes
            "-org.eclipse.jetty.websocket.",    // don't hide websocket extension
            "-org.eclipse.jetty.servlet.DefaultServlet", // don't hide default servlet
            "org.eclipse.jetty."                // hide other jetty classes
        } ;  
  • __dftSystemClasses :  系统类,类似于tomcat的filterPackage,优先采取parent first进行装载。
    public final static String[] __dftSystemClasses = 
        {
            "java.",                            // Java SE classes (per servlet spec v2.5 / SRV.9.7.2) 
            "javax.",                           // Java SE classes (per servlet spec v2.5 / SRV.9.7.2)
            "org.xml.",                         // needed by javax.xml
            "org.w3c.",                         // needed by javax.xml
            "org.apache.commons.logging.",      // TODO: review if special case still needed 
            "org.eclipse.jetty.continuation.",  // webapp cannot change continuation classes
            "org.eclipse.jetty.jndi.",          // webapp cannot change naming classes
            "org.eclipse.jetty.plus.jaas.",     // webapp cannot change jaas classes
            "org.eclipse.jetty.websocket.",     // WebSocket is a jetty extension
            "org.eclipse.jetty.servlet.DefaultServlet"  // webapp cannot change default servlets
        } ;

相关配置: 

  • _parentLoaderPriority的变量,可以通过环境变量org.eclipse.jetty.server.webapp.parentLoaderPriority=false/true
  • server_class和system_class,可以通过设置Server attributes
    
          org.eclipse.jetty.webapp.systemClasses
          
              
                  java.
    	      ......
    	  
    

 

总结和对比一下(jboss,tomcat,jetty)容器的classloader机制

容器 jboss(4.05) tomcat(6.0.30) jetty(7.1.20)
支持child/parent first设置(默认值) Java2ClassLoadingCompliance=false delegate=false _parentLoaderPriority=false
过滤package配置 FilteredPackages
默认值: javax.servlet,org.apache.commons.logging
packageTriggers
默认配置:org.apache.commons.logging
systemClasses
默认配置:java. 
javax.
org.xml.
org.w3c.
org.apache.commons.logging.
org.eclipse.jetty.continuation.
org.eclipse.jetty.jndi.
org.eclipse.jetty.plus.jaas.
org.eclipse.jetty.websocket.
org.eclipse.jetty.servlet.DefaultServlet.
特殊性

1. UseJBossWebLoader=false时,过滤packages才能生效

2. UseJBossWebLoader=true时,不支持过滤packages

3. jboss 5.0以后UseJBossWebLoader参数将不支持

1. 在执行child/parent判断之前,会委托system classloader装载系统class,比如jdk的lib库

1. 多了一个serverclass配置,如果是serverclass优先采用child first

2. systemclass默认的配置,多了javax,org.xml,org.w3c配置。

相关文档 svn url : http://anonsvn.jboss.org/repos/jbossas/tags/JBoss_4_0_5_GA_CP18
jboss社区classloader文档: http://community.jboss.org/wiki/ClassLoadingConfiguration

svn url : http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk

官方classloader机制: http://tomcat.apache.org/tomcat-6.0-doc/class-loader-howto.html

svn url : http://dev.eclipse.org/svnroot/rt/org.eclipse.jetty/jetty/tags/jetty-7.2.0.v20101020/

classloader 官方文档: http://docs.codehaus.org/display/JETTY/Classloading

 
 

 

最后(相关问题分析)

 

问题1:

  是一个jar sealed问题, 官方说明: http://download.oracle.com/javase/tutorial/deployment/jar/sealman.html

 

Package Sealing: A package within a JAR file can be optionally sealed, which means that all classes defined in that package must be archived in the same JAR file. 
A package might be sealed to ensure version consistency among the classes in your software or as a security measure.
To seal a package, a Name header needs to be added for the package, followed by a Sealed header, similar to this:
Name: myCompany/myPackage/
Sealed: true

The Name header's value is the package's relative pathname. Note that it ends with a '/' to distinguish it from a filename. 
Any headers following a Name header, without any intervening blank lines, apply to the file or package specified in the Name header. 
In the above example, because the Sealed header occurs after the Name: myCompany/myPackage header, 
with no blank lines between, the Sealed header will be interpreted as applying (only) to the package myCompany/myPackage. 

This code doesn't work.

 

说白了就是jdk在jar层面上做的一层控制,避免出现两个不同版本的实现类同时在应用中被装载。

理清了sealed的含义,再看一下出错的堆栈:com.sun.media.jai.util,这个类是jai相关处理

  • jai_core.jar
  • jai_codec.jar
  • jai_imageio.jar

几个jar包的META-INF/MANIFEST.MF中都定义了sealed :true。而我们的应用中刚好有两个jar包,那为什么在jboss运行中没问题,jetty运行却出了问题。

 

最后的原因:

1. 我们使用的是javax.media.jai.JAI.create(String opName,ParameterBlock args),该类刚好依赖了com.sun.media.jai.util.ImagingListenerImpl等相关类。

2. 意看jetty的特殊性,针对javax.media.jai.JAI是属于systemclass,所以会优先使用jdk去装载,而com.sun则通过应用容器装载,很巧的是公司的jdk下的jre/lib/ext下刚好增加了jai的相关jar,最终导致了sealed冲突。

 

解决方案:

   处理起来相对暴力,增加配置systemclass配置,添加jai的相关package,所有的jai都采取parent first加载。


com.sun.media.jai.

com.sun.media.imageio.
com.sun.media.imageioimpl.
jj2000.j2k.


问题2

xml加载的问题,原因也跟jetty的特殊性相关。大家都知道xml解析类有很多种

  • xerces
  • crimson
  • jdk rt.jar
  • j2ee.jar

真TMD令人纠结啊,应用容器中一旦同时依赖了这几个xml类库,那麻烦问题就来了。这个SAXParserFactory.newInstance(String factoryClassName, ClassLoader classLoader)在jdk1.6.18版本中有,而在其他的几个包中却没有该接口方法。

 

该问题的原因:

  1. 原先应用运行的是tomca,也因为tomcat容器的特殊性,会优先通过system classloader优先装载,正好SAXParserFactory类属于jdk的库,所以也正好装载了jdk的类
  2. jetty容器因为特殊systemclass配置,针对javax.打头的package采用parent first,所以这时装载了jdk SAXParserFactory
  3. 最后jboss运行时,因为我们使用的是UseJbossWebLoader=true,所以会优先装载应用中的lib,至于用哪个那就看概率了,一般按classpath装载lib的顺序。所以这时没有该方法,就抛出了对应的错误

解决方案:

处理方式也比较粗暴,如果该方法存在歧义,那干脆不用这方法不就得了,最后换用了无参的newInstance()方法。但这时得注意了,不同xml类库,对应的xml impl默认配置不一样。比如jdk的配置:就会以他自己的为默认值

 

return (SAXParserFactory) FactoryFinder.find(
                /* The default property name according to the JAXP spec */
                "javax.xml.parsers.SAXParserFactory",
                /* The fallback implementation class name */
                "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl");

 

所以最后为了统一使用,通过设置环境变量:

-Djavax.xml.parsers.SAXParserFactory=org.apache.xerces.jaxp.SAXParserFactoryImpl

 

问题3:

 该问题是由问题2引出的,正因为设置了xml解析器为xerces,导致在jetty启动的时候出现了一些问题。这次主要的原因就跟共享lib库有关了。

 

最终原因分析:

  1. 因为通过环境变量设置了xml解析器的实现类,所以不同的xml类库在创建xml parse时,都会尝试去载入对应的lib库。
  2. jetty启动时,其自身需要解析xml文件,这时候就出现问题了,jetty默认是没有xerces xml的解析类的,所以启动就出错了。

 

解决方案:

  1. 参照jboss的共享lib的配置,在jetty的ext库里添加了xercesImpl-2.9.1.jar,xml-apis-1.3.04.jar,xml-resolver-1.2.jar
  2. 因为我使用的是外部ext库,不想放到jetty软件的lib库下,所以需要通过手工指定,在start.ini中添加:
    lib=${jettyserverhome}/ext


问题4:

是一个mail邮件发送时发现的问题,从堆栈信息描述看也很见到,对应的javax.mail.event.TransportListener没找到

mail的lib库也是挺让人纠结的

1. xml一样有多个lib库:j2ee.jar,javamail。

2. 但有一点又不同的是j2ee.jar中只有接口申明没有对应的实现类

3. 更纠结的是j2ee-1.4新版本和老的javamail版本接口上还存在差异性(这个是别人告诉我的,个人没仔细验证)

 

看到这,各位看官需要多淡定一下,真是很让人纠结啊

 

最终原先分析:

  1. 原先jboss容器运行没问题,主要是和其默认的lib库有关,jboss 4.05在默认的server/default/lib下有个jboss-j2ee.jar,所以即使容器中没有javamail的包也能正常运行。
  2. 换成jetty以后,因默认在jetty下没有j2ee这个lib库,所以很悲惨,抛异常了。

 

解决方案:

1. 没必要参合j2ee.jar,直接在原先的工程里添加javamail的依赖,最后在应用中引入javamail的包。

 

 

 

 

  结尾

在这次的jetty使用过程,还是遇到了比较多的问题,大部分还是在classloader机制上,希望通过这篇文章能对大家有所帮助,少走弯路。

写的有点小乱,大家将就看一下,如有问题,欢迎拍砖。

ps : 不知不觉已经到1点多了

你可能感兴趣的:(java)