技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 系统架构 --> 由一个问题到 Resin ClassLoader 的学习

由一个问题到 Resin ClassLoader 的学习

浏览:1543次  出处信息

    (感谢网友 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时候在标签下发现了这样一段:

    
            
             
    

        其中的compiling-loader很可能与之有关,遂将其注释掉,一切正常。担心是错觉,于是将compiling-loader的path属性改成:webapps/WEB-INF/classes1,然后运行pathServlet,b.txt位置如下图:

        确实与compiling-loader有关。

    结论

        终于通过将标签注释掉,同样可以在Resin中获取“预想”的路径。验证了的确是使用Resin的人出了问题。

    疑问

        但是没有这样就结束,我继续对getResource的源码进行了跟进,由于能力有限,没有弄清楚getResource的原理。

        最终留下了两个疑问:

        1、如果追踪到getResource方法的最底层(也许是JVM层面),它实现的原理是什么?

        2、为何Resin中的配置会对getResource产生影响,但是对getResourceAsStream毫无影响(getResourceAsStream可是将获取路径委托给getResource的啊)。还是这里我理解或者使用错误了?

        本来文章到这里就结束了,本来是想问问牛人的,但是这个问题引起了很多的好奇心,于是我又花了一两周做了下面的调查。

    Resin中类加载器

        在我了解的ClassLoader是在com.caucho.loader包下,结构请看下图:

         图1

         图1

         图2 (点击看大图)

         图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 loaders = _loaders;从各个loader中加载name,如果加载成功,即不为null,则返回,否则继续遍历,直至遍历完成。
  • EnvironmentClassLoader[web-app:http://localhost:8080]同4
  • EnvironmentClassLoader[web-app:http://localhost:8080/Test]同4
  •     OK,完事儿,后续还有,准备好好写几篇。

    建议继续学习:

    1. Apache、resin、rewrite泛域名、多域名设置    (阅读:3740)
    QQ技术交流群:445447336,欢迎加入!
    扫一扫订阅我的微信号:IT技术博客大学习
    © 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

    京ICP备15002552号-1