背景
前段时间一直在做应用容器的迁移,将公司的应用容器从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.<init>(XmlParser.java:68)
- at org.eclipse.jetty.xml.XmlConfiguration.initParser(XmlConfiguration.java:79)
- at org.eclipse.jetty.xml.XmlConfiguration.<init>(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./blog/791940
为了和tomcat,jetty有更明显的对比,这里就主要介绍三个参数代码层面上的实现:
- <server>
-
-
- <mbean code="org.jboss.web.tomcat.tc5.Tomcat5"
- name="jboss.web:service=WebServer" xmbean-dd="META-INF/webserver-xmbean.xml">
- ......
-
- <!-- Get the flag indicating if the normal Java2 parent first class
- loading model should be used over the servlet 2.3 web container first
- model.
- -->
- <attribute name="Java2ClassLoadingCompliance">false</attribute>
- <!-- A flag indicating if the JBoss Loader should be used. This loader
- uses a unified class loader as the class loader rather than the tomcat
- specific class loader.
- The default is false to ensure that wars have isolated class loading
- for duplicate jars and jsp files.
- -->
- <attribute name="UseJBossWebLoader">true</attribute>
- <!-- The list of package prefixes that should not be loaded without
- delegating to the parent class loader before trying the web app
- class loader. The packages listed here are those tha are used by
- the web container implementation and cannot be overriden. The format
- is a comma separated list of the package names. There cannot be any
- whitespace between the package prefixes.
- This setting only applies when UseJBossWebLoader=false.
- -->
- <attribute name="FilteredPackages">javax.servlet,org.apache.commons.logging</attribute>
-
- .....
-
- </server>
相信这几个参数大家都应该比较熟知,配置项在 deploy/jbossweb-tomcat55.sar/META-INF/jboss-service.xml中
下面循着代码来看一下jboss的相关实现:
- 代码入口: org.jboss.web.tomcat.tc5.Tomcat5, 这里的三个配置都对应于Tomcat5类的属性,默认值就是当前配置的值。
- 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();
-
-
-
-
-
- Loader webLoader = null;
- if (config.isUseJBossWebLoader())
- {
- 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;
- }
- 最后通过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())));
说明:
- 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;
- }
-
-
- 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());
- 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());
- 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());
- }
- .....
- }
看到这里大家相信应该差不多了解了,总结一下:
- java2ClassLoadingCompliance是针对useJbossWebLoader=false时而言,是通过设置tomcat WebappClassloader的是否delegate进行控制classloader,实现child first/parent first。
- java2ClassLoadingCompliance在useJBossWebLoader=true时,不会生效,会被强制设置为false,具体可看WebCtxLoaders,实现了Loader接口,getClassLoader()返回的是一个ctxLoader对jboss WebLoader的一个包装。
- filteredPackages目前是通过tomcat的classloader机制上进行控制,所以只能是在useJbossWebLoader=false时有效,因为依赖了tomcat的实现。
不过需要注意两点:
- 目前在jboss4.05上不支持在在war包下WEB-INF/jboss-web.xml的配置,在jboss4.20以后才支持,可见https://jira./browse/JBAS-3047?subTaskView=all
- 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类,找不到才会去装载公用类。
下面就来看一下代码的一些实现:
- tomcat启动入口,通过分析StandardContext.start()
- public synchronized void start() {
- .....
-
- if (getLoader() == null) {
- WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
- webappLoader.setDelegate(getDelegate());
- setLoader(webappLoader);
- }
- .....
- }
- 如果getLoader为空,则创建一个新的WebappLoader,注意这也是jboss设置tomcat loader的入口,从而替换默认的tomcat classloader。
- WebappLoader通过在startInternal启动了一个新的WebappClassLoader
-
- 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]);
- }
- }<span style="white-space: normal;"> </span>
- 最后就是WebappClassLoader的loadClass方法了,具体的类装载策略。
下面来看一下,loadClass的相关逻辑:
- (0.1)先检查class是否已经被装载过,findLoadedClass
- (0.2)通过system classloader装载系统class,所以这里会优先装载jdk的相关代码,这个很重要,也很特别。
- 如果是delegate=true并且不是filterPackage列表,则采用parent first,否则采用child first。
相关代码:
-
- clazz = findLoadedClass(name);
-
-
- clazz = system.loadClass(name);
-
- boolean delegateLoad = delegate || filter(name);
-
- if (delegateLoad) {
- ClassLoader loader = parent;
- if (loader == null)
- loader = system;
- try {
- clazz = loader.loadClass(name);
- ....
- }
- .....
- }
-
-
- clazz = findClass(name);
-
-
- if (!delegateLoad) {
- ClassLoader loader = parent;
- if (loader == null)
- loader = system;
- try {
- clazz = loader.loadClass(name);
- ....
- }
- .....
- }
最后总结一下, 两个参数配置:
- delegate(child first/parent first),默认值为false,即为child first
- packageTriggers,执行child first时,排除的package列表,如果匹配了package,即时为delegate=false,也会优先执行parent first策略。
- tomcat的classloader机制在处理class装载时,会优先尝试使用system进行装载,这样可以优先使用jdk的相关类,这也是问题2的原因。
jetty7.1.20 classloader机制
jetty与tomcat相比,没有太大上的区别,也是有parent first/child first的概念,filter package等。但还是有些小区别,那先看一下代码:
- jetty启动入口org.eclipse.jetty.server.Server,通过DeploymentManager进行webapps目录的加载
- 具体war包加载是通过WebAppProvider,通过createContextHandler创建应用上下文WebAppContext。
- WebAppContext在初始化时,创建WebAppClassLoader
- public void preConfigure() throws Exception
- {
-
- loadConfigurations();
-
-
- loadSystemClasses();
-
-
- loadServerClasses();
-
-
- _ownClassLoader=false;
- if (getClassLoader()==null)
- {
- WebAppClassLoader classLoader = new WebAppClassLoader(this);
- setClassLoader(classLoader);
- _ownClassLoader=true;
- }
- .....
- }
- 最后就是WebAppClassLoader的loadclass方法了。
理解了这些基本概念后,jetty的loadclass方法,逻辑比较好理解:
- 如果属于system_class或者_parentLoaderPriority=true,并且不是server_class,优先采用parent first进行装载
- (_context.isParentLoaderPriority() || system_class) && !server_class)
- 否则采用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
- } ; <span style="white-space: normal;"> </span>
- __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
- <Call name="setAttribute">
- <Arg>org.eclipse.jetty.webapp.systemClasses</Arg>
- <Arg>
- <Array type="java.lang.String">
- <Item>java.</Item>
- ......
- </Array
- </Arg>
- </Call>
总结和对比一下(jboss,tomcat,jetty)容器的classloader机制
最后(相关问题分析)
问题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加载。
- <!--for jai_code.jar , jai_codec.jar -->
- <Item>com.sun.media.jai.</Item>
- <!--for jai_imageio.jar -->
- <Item>com.sun.media.imageio.</Item>
- <Item>com.sun.media.imageioimpl.</Item>
- <Item>jj2000.j2k.</Item>
问题2
xml加载的问题,原因也跟jetty的特殊性相关。大家都知道xml解析类有很多种
- xerces
- crimson
- jdk rt.jar
- j2ee.jar
真TMD令人纠结啊,应用容器中一旦同时依赖了这几个xml类库,那麻烦问题就来了。这个SAXParserFactory.newInstance(String factoryClassName, ClassLoader classLoader)在jdk1.6.18版本中有,而在其他的几个包中却没有该接口方法。
该问题的原因:
- 原先应用运行的是tomca,也因为tomcat容器的特殊性,会优先通过system classloader优先装载,正好SAXParserFactory类属于jdk的库,所以也正好装载了jdk的类
- jetty容器因为特殊systemclass配置,针对javax.打头的package采用parent first,所以这时装载了jdk SAXParserFactory
- 最后jboss运行时,因为我们使用的是UseJbossWebLoader=true,所以会优先装载应用中的lib,至于用哪个那就看概率了,一般按classpath装载lib的顺序。所以这时没有该方法,就抛出了对应的错误
解决方案:
处理方式也比较粗暴,如果该方法存在歧义,那干脆不用这方法不就得了,最后换用了无参的newInstance()方法。但这时得注意了,不同xml类库,对应的xml impl默认配置不一样。比如jdk的配置:就会以他自己的为默认值
- return (SAXParserFactory) FactoryFinder.find(
-
- "javax.xml.parsers.SAXParserFactory",
-
- "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl");
所以最后为了统一使用,通过设置环境变量:
- -Djavax.xml.parsers.SAXParserFactory=org.apache.xerces.jaxp.SAXParserFactoryImpl
问题3:
该问题是由问题2引出的,正因为设置了xml解析器为xerces,导致在jetty启动的时候出现了一些问题。这次主要的原因就跟共享lib库有关了。
最终原因分析:
- 因为通过环境变量设置了xml解析器的实现类,所以不同的xml类库在创建xml parse时,都会尝试去载入对应的lib库。
- jetty启动时,其自身需要解析xml文件,这时候就出现问题了,jetty默认是没有xerces xml的解析类的,所以启动就出错了。
解决方案:
- 参照jboss的共享lib的配置,在jetty的ext库里添加了xercesImpl-2.9.1.jar,xml-apis-1.3.04.jar,xml-resolver-1.2.jar
- 因为我使用的是外部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版本接口上还存在差异性(这个是别人告诉我的,个人没仔细验证)
看到这,各位看官需要多淡定一下,真是很让人纠结啊
最终原先分析:
- 原先jboss容器运行没问题,主要是和其默认的lib库有关,jboss 4.05在默认的server/default/lib下有个jboss-j2ee.jar,所以即使容器中没有javamail的包也能正常运行。
- 换成jetty以后,因默认在jetty下没有j2ee这个lib库,所以很悲惨,抛异常了。
解决方案:
1. 没必要参合j2ee.jar,直接在原先的工程里添加javamail的依赖,最后在应用中引入javamail的包。
结尾
在这次的jetty使用过程,还是遇到了比较多的问题,大部分还是在classloader机制上,希望通过这篇文章能对大家有所帮助,少走弯路。
写的有点小乱,大家将就看一下,如有问题,欢迎拍砖。
ps : 不知不觉已经到1点多了