前段时间做Jetty迁移项目过程中,遇到的与ClassLoading有关的两个问题的总结与分析,不对的地方,请指正,欢迎拍砖。
问题:Jar包信赖冲突,即项目中依赖的一个类有两个实现版本,我们的JVM、Web容器到底载入的是哪一个呢?
现象:这种问题是很难发现的,也是比较随机的,可能本地编译、运行都没有问题,可是部署到服务器上就有问题,经常:MethodNotFound exception.有时你在eclipse中去查看这个方法时,又是存在的,这时候就感觉比较诡异.
解析:解决方法非常简单,你可能一下就想到了,只要装载的是我们需要的那个版本就可以了,因此你可能想到的有两种方法:
方法一:只保留一份实现,这个不用解释,只有一份当然不会冲突了。
方法二:你可能想到Jvm的classloader的特性,即:同一个类只载入一次.只要保证我们需要的类先载入不就行啦,这种想法是没错的,你可能想到:在lib下的载入顺序是按照文件名的字母顺序,通过修改jar包的名字的方式来更改载入的顺序,我也在测试环境下试过确实好用.但是依然不能下定论,我们要看看Jvm到底是如何载入的,它的载入顺序是怎样的?
我们都知道ClassLoader委托体系,我们看一下URLClassLoader中是如何载入一类的:
1.classLoader在URLClassPath下查找资源文件:
public URL findResource(String name, boolean check) {
Loader loader;
for (int i = 0; (loader =getLoader(i)) != null; i++) {
URL url =loader.findResource(name, check);
if (url !=null) {
return url;
}
}
return null;
}
其中getLoader(i)函数是按照classPath的定义顺序来获取每一个资源的Loader,这个时候我们的问题都归结到了classPath的构建上来了。
先看看java2中的classLoader的体系是如何构建的:
ExtClassLoader的classPath构建过程:
1.获取extDirs,即系统属性:“java.ext.dirs”
2.遍历extDirs下的每一个文件,并生成对应的URL,存入urls数组。
3.用urls构建ucp(URLClassPath)。
注:在ucp中的顺序即为载入类时的顺序。这个顺序是严格按照urls中的顺序。因此,对于由ExtClassLoader来装载的类是顺序是由上面第2部来决定的,即这个URL的数组的构建,下面我们来看一下源码:
private static URL[] getExtURLs(File[] dirs) throwsIOException {
Vector urls = new Vector();
for (int i = 0; i < dirs.length; i++) {
String[] files = dirs[i].list();
if (files != null) {
for (int j = 0; j < files.length; j++) {
if (!files[j].equals("meta-index")){
File f = new File(dirs[i], files[j]);
urls.add(getFileURL(f));
}
}
}
}
URL[] ua = new URL[urls.size()];
urls.copyInto(ua);
return ua;
}
我们看到extURL的构建是dirs[i].list()产生的,但是看到这个File.list()函数,我们就要吐血了,它不保证列出的文件顺序,尤其是不保证它们按照fileName字母顺序出现。 也就是说对于在extdirs下放置的类的载入顺序是依赖于系统实现的(即File.list()),因此在ExtClassLoader中,如果我们采用方法二是行不通的。
AppClassLoader的classPath构建过程:
1.获取classPath,即系统属性:“java.class.path”
2.将classPath按分分隔符来拆分构建URL,并存储到:urls数组
3.用urls构建ucp(URLClassPath)。
注意:这里面与ext稍有不同的是第二步,这里的没有对classPath的目录进行扩展。因此在“java.class.path”中的顺序即定义了载入的顺序,可以利用这个来控制载入类顺序。
在jetty容器中的WebAppClassLoader我们的web-inf/lib目录的构建webAppClassLoader时采用的方式与ExtClassLoader相同的方式:
public void addJars(Resource lib){
if (lib.exists() &&lib.isDirectory()) {
String[]files=lib.list();
for (int f=0;files!=null&& f<files.length;f++){
try {
Resource fn=lib.addPath(files[f]);
String fnlc=fn.getName().toLowerCase();
if (!fn.isDirectory() && isFileSupported(fnlc)) {
String jar=fn.toString();
jar=StringUtil.replace(jar, ",", "%2C");
jar=StringUtil.replace(jar, ";", "%3B");
addClassPath(jar);
}
}
catch (Exception ex) {
Log.warn(Log.EXCEPTION,ex);
}
}
}
}
对于文件FileResource中的定义:
public String[] list(){
String[] list =_file.list();
if (list==null)
return null;
for (int i=list.length;i-->0;){
if (newFile(_file,list[i]).isDirectory() && !list[i].endsWith("/"))
list[i]+="/";
}
return list;
}
因此在jetty应用的lib中jar包在classPath顺序也是依赖于File.list()的实现。同样因为这个File的list()方法,不保证任何顺序。
综上,在我们应用中要保证所有依赖仅保留一个版本,就不会出问题啦,否则不同的运行环境可能就会有问题。
问题:sealingviolation
现象:sealingviolation: packag is sealed
解析:这个涉及到jetty的加载机制 Jetty Classloading
Jetty做为Servlet容器,为了遵守Servlet的规范,类加载体系中重写了loadClass()方法,改变了类加载机制不再是java2中的父委托机制,先把这个loaderClass()方法贴出来:
protected synchronized Class<?> loadClass(Stringname, boolean resolve) throws ClassNotFoundException{
Class<?> c= findLoadedClass(name);
ClassNotFoundException ex= null;
boolean tried_parent= false;
booleansystem_class=_context.isSystemClass(name);
boolean server_class=_context.isServerClass(name);
if (system_class && server_class){
return null;
}
if(c == null && _parent!=null &&(_context.isParentLoaderPriority() || system_class) && !server_class){
tried_parent= true;
try {
c= _parent.loadClass(name);
if (Log.isDebugEnabled())
Log.debug("loaded " + c);
}
catch (ClassNotFoundException e) {
ex= e;
}
}
if (c == null) {
try {
c= this.findClass(name);
}
catch (ClassNotFoundException e) {
ex= e;
}
}
if (c == null && _parent!=null && !tried_parent &&!server_class )
c= _parent.loadClass(name);
if (c == null)
throw ex;
if (resolve)
resolveClass(c);
if (Log.isDebugEnabled())
Log.debug("loaded " + c+ " from "+c.getClassLoader());
return c;
}
对于这段代码,在jetty的官网关于classLoading有这样一段解读,说明了为什么jetty中的要重写这个方法:web容器的classLoader的结构与普通java应用有所不同,每一个web应用应该有自己的一个WebAppClassloader,Parent为systemClassloader,层次结构也遵守java2中的结构,但是Servlet规范对于servlet容器是有规定的,即它不是完全采用parent first机制装载Class,它是这样规定的:
1.对于WEB-INF/lib 和 WEB-INF/classes下的类优先于ParentClassLoader被加载,即child first机制.
2.系统类如:java.lang.String 在子优先的方式中是不允许被WebAppClassLoader加载的,避免了在WEB-INF/lib or WEB-INF/ 替换了系统类.但不幸的是,这个规范并没有明确定义哪些是系统类
3.Server的实现类不允许在任何的ClassLoader中访问.但也规范中也没有明确给出哪些是Server实现类.
对于上面的1,2,3都是可以进行配置的,比如:classloader的优先级child first 还是parent first;哪些是System Class,哪些是Server Class,都可以配置的,例如通过jetty-web.xml。
Jetty的这种ClassLoading结构可能会出现一种问题就是:如果应用的web-inf/lib 与 jdk下ext目录有相同的类的Jar包,并且这些jar中的类有被定义为Sealed的,则会出现jar Sealed问题。因为在同一package下有些类是在WebAppClassLoader中加载,有些是在ExtClassLoader中加载。对于这个的解决方法,就是可以利用上面的第2点,我们可以把这些类指定为System Class,采用parent first来加载,这样就不会被WebAppClassLoader加载,也就不会出现sealed violation。