由一个问题到 Resin ClassLoader 的学习
(感谢网友 liuxiaori 分享其经历)
目录
背景
某日临近下班,一个同事欲取任何类中获取项目绝对路径,不通过Request方式获取,可是始终获取不到预想的路径。于是晚上回家google了一下,误以为是System.getProperty(“java.class.path”)-未实际进行测试,早上来和同事沟通,提出了使用这个内置方法,结果人家早已验证过,该方法是打印出CLASSPATH环境变量的值。
于是乎,继续google,找到了Class的getResource与getResourceAsStream两个方法。这两个方法会委托给ClassLoader对应的同名方法。以为这样就可以搞定(实际上确实可以搞定),但验证过程中却发生了奇怪的事情。
软件环境:Windows XP、Resin 3、Tomcat6.0、Myeclipse、JDK1.5
发展
我的验证思路是这样的:
- 定义一个Servlet,然后在该Servlet中调用Path类的getPath方法,getPath方法返回工程classpath的绝对路径,显示在jsp中。
- 另外在Path类中,通过Class的getResourceAsStream读取当前工程classpath路径中的a.txt文件,写入到getResource路径下的b.txt。
由于时间匆忙,代码没有好好去组织。大致能看出上述两个功能,很简单不做解释。
public class Path { public String getPath() throws IOException { InputStream is = this.getClass().getResourceAsStream("/a.txt"); File file = new File(Path.class.getResource("/").getPath()+"/b.txt"); OutputStream os = new FileOutputStream(file); int bytesRead = 0; byte[] buffer = new byte[8192]; while ((bytesRead = is.read(buffer, 0, 8192)) != -1) { os.write(buffer, 0, bytesRead); } os.close(); is.close(); return this.getClass().getResource("/").getPath(); } }
public class PathServlet extends HttpServlet { private static final long serialVersionUID = 4443655831011903288L; public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Path path = new Path(); request.setAttribute("path", path.getPath()); PrintWriter out = response.getWriter(); out.println("Class.getResource('/').getPath():" + path.getPath()); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
在此之前使用main函数测试Path.class.getResource(“/”).getPath()打印出预想的路径为:/D:/work/project/EhCacheTestAnnotation/WebRoot/WEB-INF/classes/
于是将WEB应用部署到Resin下,运行定义好的Servlet,出乎意料的结果是:/D:/work/resin-3.0.23/webapps/WEB-INF/classes/ 。特别奇怪,怎么会丢掉项目名称:EhCacheTestAnnotation呢?
还有一点值得注意,getPath方法中使用getResourceAsStream(“/a.txt”)却正常的读到了位于下图的a.txt。
然后写到了如下图的b.txt中。代码中是这样实现的:File file = new File(Path.class.getResource(“/”).getPath()+”/b.txt”);本意是想在a.txt文件目录下入b.txt。结果却和料想的不一样。
请注意,区别还是丢掉了项目名称。
写的比较乱,稍微总结下:
程序中使用ClassLoader的两个方法:getResourceAsStream和getResource。但是事实证明在WEB应用的场景下却得到了不同的结果。大家别误会啊,看名字他们两个方法肯定不一样,这个我知道,但是getResourceAsStream总会获取指定路径下的文件吧,示例中的参数为”/a.txt”,正确读取“/D:/work/resin-3.0.23/webapps/EhCacheTestAnnotation/WEB-INF/classes/ ”下的a.txt,可是将文件写到getResource方法的getPath返回路径的b.txt文件。两个位置的差别在项目名称(EhCacheTestAnnotation)。
这样我暂且得出一个结论:通过getResourceAsStream和getResource两个方法获取的路径是不同的。但是为什么呢?
于是查看了ClassLoader的源码,贴出getResource和getResourceAsStream的源码。
public URL getResource(String name) { URL url; if (parent != null) { url = parent.getResource(name); } else { url = getBootstrapResource(name); } if (url == null) { url = findResource(name); } return url; } public InputStream getResourceAsStream(String name) { URL url = getResource(name); try { return url != null ? url.openStream() : null; } catch (IOException e) { return null; } }
从代码中看,getResourceAsStream将获取URL委托给了getResource方法。天啊,这是怎么回事儿?由此我彻底迷茫了,百思不得其解。
但是没有因此就放弃,继续回想了一遍整个过程:
- 在main函数中,测试getResource与getResourceAsStream是完全相同的,正确的。
- 将其部署到Resin下,导致了getResource与getResourceAsStream获取的路径不一致。
一个闪光点,是不是与web容器有关啊,于是换成Tomcat6.0。OMG,“奇迹”出现了,真的是这样子啊,换成Tomcat就一样了啊!和预想的一致。
在Tomcat下运行结果如下图:
对,这就是我想要的。
因此我对Resin产生了厌恶感,之前也因为在Resin下程序报错,在Tomcat下正常运行而纠结了好久。记得看《松本行弘的程序世界》中对C++中的多继承是这样评价的(大概意思):多重继承带来的负面影响多数是由于使用不当造成的。是不是因为对Resin使用不得当才使得和Tomcat下得到不同的结果。
最终,在查阅Resin配置文件resin.conf时候在<host-default>标签下发现了这样一段:
<class-loader> <compiling-loader path="webapps/WEB-INF/classes"/> <library-loader path="webapps/WEB-INF/lib"/> </class-loader>
其中的compiling-loader很可能与之有关,遂将其注释掉,一切正常。担心是错觉,于是将compiling-loader的path属性改成:webapps/WEB-INF/classes1,然后运行pathServlet,b.txt位置如下图:
确实与compiling-loader有关。
结论
终于通过将<class-loader>标签注释掉,同样可以在Resin中获取“预想”的路径。验证了的确是使用Resin的人出了问题。
疑问
但是没有这样就结束,我继续对getResource的源码进行了跟进,由于能力有限,没有弄清楚getResource的原理。
最终留下了两个疑问:
1、如果追踪到getResource方法的最底层(也许是JVM层面),它实现的原理是什么?
2、为何Resin中<class-loader>的配置会对getResource产生影响,但是对getResourceAsStream毫无影响(getResourceAsStream可是将获取路径委托给getResource的啊)。还是这里我理解或者使用错误了?
本来文章到这里就结束了,本来是想问问牛人的,但是这个问题引起了很多的好奇心,于是我又花了一两周做了下面的调查。
Resin中类加载器
在我了解的ClassLoader是在com.caucho.loader包下,结构请看下图:
图1
图2
从上面两幅图中可以看出,图1是与Jdk有关联的,继承自java.net.URLClassLoader。DynamicClassLoader的注释是这样的:
/** * Class loader which checks for changes in class files and automatically * picks up new jars. * * DynamicClassLoaders can be chained creating one virtual class loader. * From the perspective of the JDK, it's all one classloader. Internally, * the class loader chain searches like a classpath. */
EnvironmentClassLoader又继承了DynamicClassLoader。
图2应该是Resin本身的ClassLoader,其中Loader是一个抽象类,包含了各种子类类加载器。
从两幅图中是看不出Resin自身的Loader体系与继承自JVM的类加载器存在关系,那是不是他们就不存在某种关联呢?其实不是这样子的。请看下面DynamicClassLoader源码的片段:
// List of resource loaders private ArrayList _loaders = new ArrayList(); private JarLoader _jarLoader; private PathLoader _pathLoader;
清楚了吧,这两个Loader分支通过组合的方式协作。
类加载器顺序
既然Resin标准的Loader及其子类以组合的方式嵌入到DynamicClassLoader中,那么在加载一个“资源”时,Loader分支和java.net.URLClassLoader分支的先后顺序是什么样子的呢?
首先使用下面这段代码,将类加载器名称打印到控制台:
ClassLoader loader = PathServlet.class.getClassLoader(); while (loader != null) { System.out.println(loader.toString()); loader = loader.getParent(); }
输出的结果为:
EnvironmentClassLoader[web-app:http://localhost:8080/Test]
EnvironmentClassLoader[web-app:http://localhost:8080]
EnvironmentClassLoader[cluster ]
EnvironmentClassLoader[]
sun.misc.Launcher$AppClassLoader@cac268
sun.misc.Launcher$ExtClassLoader@1a16869
额,没有任何一个Resin的Loader被打印出来啊,对头,有就错了。下面就让我们看看DynamicClassLoader中getResource的源码来解答。
/** * Gets the named resource * * @param name name of the resource */ public URL getResource(String name) { if (_resourceCache == null) { long expireInterval = getDependencyCheckInterval(); _resourceCache = new TimedCache(256, expireInterval); } URL url = _resourceCache.get(name); if (url == NULL_URL) return null; else if (url != null) return url; boolean isNormalJdkOrder = isNormalJdkOrder(name); if (isNormalJdkOrder) { url = getParentResource(name); if (url != null) return url; } ArrayList loaders = _loaders; for (int i = 0; loaders != null && i < loaders.size(); i++) { Loader loader = loaders.get(i); url = loader.getResource(name); if (url != null) { _resourceCache.put(name, url); return url; } } if (! isNormalJdkOrder) { url = getParentResource(name); if (url != null) return url; } _resourceCache.put(name, NULL_URL); return null; }
代码不难懂,我画了一张流程图,不规范,凑合看下。
总结
boolean isNormalJdkOrder = isNormalJdkOrder(name);
这行代码控制着Resin类加载的顺序,如果是常规的类加载顺序(向上代理,原文:Returns true if the class loader should use the normal order, i.e. looking at the parents first.),则先url = getParentResource(name),后遍历_loaders。否则是按照先遍历_loaders再url = getParentResource(name)向上代理。
在我的调试经历中,一直都是先向上代理,后遍历_loaders的顺序,未遇到第二种方式。
文字对先向上代理,后遍历的顺序做点儿说明:
- 首先使用“最上层”的sun.misc.Launcher$ExtClassLoader@1a16869加载name资源,如果找到就返回URL否则返回null
- 程序返回到sun.misc.Launcher$AppClassLoader@cac268,首先判断父类加载器返回的url是否为null,如果不为null则返回url,返回null。
- EnvironmentClassLoader[]
- 程序返回到EnvironmentClassLoader[cluster ]的getParentResource,再返回到getResource,如果url不为null,则直接返回,否则遍历ArrayList<Loader> loaders = _loaders;从各个loader中加载name,如果加载成功,即不为null,则返回,否则继续遍历,直至遍历完成。
- EnvironmentClassLoader[web-app:http://localhost:8080]同4
- EnvironmentClassLoader[web-app:http://localhost:8080/Test]同4
OK,完事儿,后续还有,准备好好写几篇。
本文同时发布于:
(全文完)
(转载本站文章请注明作者和出处 宝酷 – sou-ip ,请勿用于任何商业用途)
《由一个问题到 Resin ClassLoader 的学习》的相关评论
虽然这些我还不能看懂,但学习的精神值得欣赏。
不断深挖,这才是真正的学习。
很专业啊!!
像深度历险,喜欢看也喜欢做这样的事。
这个类加载规则叫做双亲委派模型,JVM使用类加载器加载类的时候,不会首先去尝试自己去加载这个类,而是先委派给父类加载器去加载,每一层次都是向上委派,只有当父加载器反馈自己无法加载这个类的时候,子加载器才会自己去加载,因为JVM中标识类是用类的全限定名跟加载器一起标识的,双亲委派模型的工作过程保证了Java类和加载它的类加载器之间的层次关系,要不就会出现多个类加载器加载的java.lang.Object了。。。
建议他了解下类加载的机制就好了
谢谢,会得。嘿嘿。见谅。
@chihz
谢谢了,以后注意了,用词不专业。惭愧。
这篇不像sou-ip的风格啊,怎么谈起这么细节未节的东西了
作者从发现问题,发掘问题,再到解决问题,由浅入深,由点及面的学习思路才是整篇文章的精华所在,有时候一个细微的问题往往能够提升自己对底层原理更深入的认知能力。这也是成为优秀程序员所应当具备的优秀素质之一,底层知识扎实,对整个流程实现原理有清晰的认识,才能够写出高质量的程序。
xprogrammer 飘过
精神可嘉 称职的程序员必须得有往底层挖到本能
今天在项目里想着用这个东东获取绝对路径呢。结果发现hibernate里提供了一个很好用的方法。ConfigHelper.getConfigStream(String path)。后来看了看源码,发现其首先是利用URL机制去处理,在发生异常MalformedURLException的时候会采用ClassLoader的机制去做。
恩,我查了下你说的方式,先new URL(path),在这里包含了很多验证,还有异常处理,抛出异常再走ClassLoader。不过还没动手实践。谢谢了。nice!
另外,有网友提供了这种方式:T.class.getProtectionDomain().getCodeSource().getLocation().getPath(),经验证,不会出现文中的问题。
直接调试下不就知道代码走到哪里了. 需要这么麻烦?
这就是调试后总结的,敢问你调试的时候遇到URLPath的时候怎么处理的?直接跳过?
sorry,是URLClassPath。真心求教。
LZ 解决了这个问题了吗:为何Resin中的配置会对getResource产生影响,但是对getResourceAsStream毫无影响(getResourceAsStream可是将获取路径委托给getResource的啊)。还是这里我理解或者使用错误了?
为何我看了这么久,还是没有头绪啊,恕我愚昧,类是向上代理的,但是getResourceAsStream里边回去的URL确实是
getResource里边的url,难道是url.openStream()这里的问题?
我尝试着这么理解: url.openStream() 也是类似的相同的加载机制,在父类中直接get到stream?
求解释
嘿嘿,被你看破了,兄弟莫着急,已经着手再写一篇来解释这个问题。
证据我已找到,要花点儿时间写出来。元旦应该可以搞定。
最后,谢谢你细致的阅读。
我以前是这样取绝对路径的:
我改造了一下,测试了。代码:
URL url = Path.class.getResource(“/test”);
return url.getFile();
输出:/D:/work_other/project/EhCacheTestAnnotation/WebRoot/WEB-INF/classes/test
和文中的做法的原理没有什么不同,这种方式获取的是包test的绝对路径。
如果有说的不对的地方,麻烦指出。谢谢啦。
@liuxiaori :
是的,和文中的做法原理是一样。只是以前看了api就这么用了,并没有像作者这样深入研究过。
在Servlet中获取某资源文件(比如数据库信息)个人一般在初始化的时候(init(ServletConfig config))调用
config.getServletContext().getRealPath(“/WEB-INF/classes/jdbc.properties”);
然后用util包中的Properties去load这个文件然后读这个文件
这样与web服务器无关
后来我试着用util包了 代码变短了 赞一个JDK作者的强悍 我对比了sina neteasy renren的资源加载办法 最后还是sun的代码写得简单高效美观
import java.util.ResourceBundle;
public class Cherry_Note_Book {
private static final ResourceBundle BUNDLE_RESOURCES = ResourceBundle.getBundle(“MY_RESOURCES.config_ch”); //src.zip下有更好的api说明
public static String getValure(String key){
StringBuffer op = new StringBuffer(BUNDLE_RESOURCES.getString(key));
String foo = “\n===================================”;
//optimize it
for(int i = 0 ; (i+=foo.length()/1.5) < op.length(); )
op.insert(i, "\n–");
op.append(foo);
op.insert(0,foo+"\n");
return op.toString();
}
}
sina是这样写的:
private static Properties props = new Properties();
props.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("config.properties"));
测试一下看看能不能看见头像。。
@hosea
为什么一楼、三楼有头像?