Java 载入Jar内资源问题的探究

       工作忙,有些许时间没有更新Blog了,这次在开发监控模块的时候遇到了这个问题,整个问题定位过程真是走了不少路,所以觉得有必要记录下来分享一下。在我看来很多时候结果也许就很简单一个原因,但是开发人员却要探究很久,也许在找到了其他可实现业务逻辑方法的情况下,就会放弃寻找原因,这期间我也是一样。

<o:p> </o:p>

问题初现:<o:p></o:p>

       在服务集成平台中需要新增一块写入数据库的逻辑,因此考虑最简便就是弄个SpringBeanFactory来搞定这一切,谁知道,问题就这么出现了。很简单,通过SpringClassPathXmlApplicationContext来构建BeanFactory,下面的语句大家应该很熟悉:

ctx = new ClassPathXmlApplicationContext("/spring/sip-*.xml");<o:p></o:p>

       通过通配符来载入ClassPath下的所有的符合规则的spring配置文件。然后在Eclipse中作完单元测试和集成测试,一切正常。然后用我们内部的打包部署,而这些配置文件都被打在Jar中作为lib库依赖。结果启动以后,在分析完日志需要写入到数据库的时候出现异常:

Could not resolve bean definition resource pattern [/spring/sip-*.xml]; nested exception is java.io.FileNotFoundException: class path resource [spring/] cannot be resolved to URL because it does not exist<o:p></o:p>

就提示来说,就是没有找到spring这个目录,也就是在ClassPath下面就没有找到资源。

<o:p> </o:p>

第一次试图解决问题:<o:p></o:p>

以前调整过Jboss关于ClassLoader的配置,即自上而下搜索还是自下而上搜索,以及是否采用Web容器的ClassLoader,开始怀疑是否是这种修改造成的问题。修改了没有问题,然后就设置断点跟踪SpringClassPathXmlApplicationContext的构造过程,发现Spring在分析此类通配类型的过程中,首先将前面的文件目录和后面的具体通配文件分开,先定位文件目录资源,也就是在定位文件目录资源的过程中,找不到spring目录,而出现了那个异常。看了代码中也有对Jar的处理,但是在处理之前就出现了问题。

自己做了尝试,将spring目录和其内容解压到WebClasses目录下运行正常,或者解压到war下面也是正常的,这些地方其实都是ClassPath可以找到的,但是lib目录下的jar也应该是可以找到的。在仔细跟踪了代码中最后载入这些资源的ClassLoader内的数据,所有的Jar都是包含在内的。

由于工作太多,因此将原有的打包模式作了修改,每次打包将这部分配置解压到war下面,这样就找到了可解决方案了,因此细致的缘由也就没有再去追究。(如果不是后面再次遇到,这个问题就会在此了结)

<o:p> </o:p>

问题再现:<o:p></o:p>

       监控模块中需要新增一块写入数据库的逻辑,在单元测试和集成测试通过的情况下出现了问题,由于此次是普通的J2SE的应用,所有的配置和依赖都打入在了Jar中,所以问题和前次一样。

       这次决定花一些时间好好找到问题所在,首先觉的Spring的资源载入应该不会不支持从Jar中载入,这是最基本的功能,因此再次打开了Spring的源码。

<o:p> </o:p>

<o:p> </o:p>

<o:p> </o:p>

问题二次定位:<o:p></o:p>

先看看ClassPathXmlApplicationContext的类图结构:

Java 载入Jar内资源问题的探究

<v:shapetype o:spt="75" coordsize="21600,21600" filled="f" stroked="f" id="_x0000_t75" path="[email protected]@[email protected]@[email protected]@[email protected]@5xe" o:preferrelative="t"><v:stroke joinstyle="miter"></v:stroke><v:formulas><v:f eqn="if lineDrawn pixelLineWidth 0"></v:f><v:f eqn="sum @0 1 0"></v:f><v:f eqn="sum 0 0 @1"></v:f><v:f eqn="prod @2 1 2"></v:f><v:f eqn="prod @3 21600 pixelWidth"></v:f><v:f eqn="prod @3 21600 pixelHeight"></v:f><v:f eqn="sum @0 0 1"></v:f><v:f eqn="prod @6 1 2"></v:f><v:f eqn="prod @7 21600 pixelWidth"></v:f><v:f eqn="sum @8 21600 0"></v:f><v:f eqn="prod @7 21600 pixelHeight"></v:f><v:f eqn="sum @10 21600 0"></v:f></v:formulas><v:path o:extrusionok="f" o:connecttype="rect" gradientshapeok="t"></v:path><o:lock v:ext="edit" aspectratio="t"></o:lock></v:shapetype><v:shape id="_x0000_i1025" type="#_x0000_t75" style="WIDTH: 414.75pt; HEIGHT: 264pt"><v:imagedata src="file:///C:\DOCUME~1\WENCHU~1.CEN\LOCALS~1\Temp\msohtml1\01\clip_image001.emz" o:title=""></v:imagedata></v:shape>

关键方法就是getResource方法,ClassPathXmlApplicationContext的资源定位就是采用了DefaultResourceLoadergetResource方法。内部也没有做太多的工作,其实就是如下的代码:

try <o:p></o:p>

{<o:p></o:p>

              // Try to parse the location as a URL...<o:p></o:p>

              URL url = new URL(location);<o:p></o:p>

              return new UrlResource(url);<o:p></o:p>

       }<o:p></o:p>

       catch (MalformedURLException ex) <o:p></o:p>

{<o:p></o:p>

           // No URL -> resolve as resource path.<o:p></o:p>

           return getResourceByPath(location);<o:p></o:p>

    }<o:p></o:p>

上面的代码都是标准的j2se的代码.作为URL通过字符串来构造,通常需要能够首先获得URL的资源全路径,而在当前情况下发现到获取资源的时候location还是保持了spring/的状态,而没有被替换成为所在jar的资源全路径,那么就先作以下测试:<o:p></o:p>

    新建简单的项目,然后在项目中加入包含spring配置的jar,然后作单元测试,测试代码如下:<o:p></o:p>

URL url = Thread.currentThread().getClass().getResource("/spring/");<o:p></o:p>

    未获取到URL,出现异常。<o:p></o:p>

URL url = Thread.currentThread().getClass().getResource("/spring/sip-analyzer-dataSource.xml");<o:p></o:p>

       正常获取到了URL<o:p></o:p>

       <o:p></o:p>

<o:p> </o:p>

由此看来应该是在获取Jar中的目录资源路径的时候出现问题导致后续载入出现问题,尝试直接传入具体的文件名:<o:p></o:p>

ctx = new ClassPathXmlApplicationContext("/spring/sip-analyzer-dataSource.xml");<o:p></o:p>

发现还是出现问题,在new URL的时候传入的是没有翻译过的文件名,考虑在传入的过程中就直接替换成为资源路径,因此写了一个简单的方法:

public static String[] getRealClassPath(String[] locationfile)<o:p></o:p>

    {<o:p></o:p>

       String[] result = locationfile;<o:p></o:p>

           for(int i = 0 ; i < locationfile.length; i++)<o:p></o:p>

           {<o:p></o:p>

              try<o:p></o:p>

              {<o:p></o:p>

                  URL url = Thread.currentThread().getClass().getResource(locationfile[i]);<o:p></o:p>

                  String file = url.getFile();<o:p></o:p>

                  if (file.indexOf(".jar!") > 0)<o:p></o:p>

                     result[i] = new StringBuffer("jar:").append(file.substring(0,file.indexOf(".jar!")+".jar!".length()))<o:p></o:p>

                            .append(locationfile[i]).toString();<o:p></o:p>

              }<o:p></o:p>

              catch(Exception ex)<o:p></o:p>

              {}<o:p></o:p>

           }<o:p></o:p>

       <o:p></o:p>

       return result;<o:p></o:p>

}<o:p></o:p>

在将构造工厂类修改为:<o:p></o:p>

ctx = new ClassPathXmlApplicationContext(BaseUtil.getRealClassPath(new String[]{"/spring/sip-analyzer-dataSource.xml"}));<o:p></o:p>

<o:p> </o:p>

运行测试,正常启动,这也就是又变成最原始的文件罗列的模式。问题虽然找到了解决方案,但是始终觉得很别扭,同时对于无法在Jar中载入配置资源的情况我一直都还是觉得应该不是Spring的问题。<o:p></o:p>

<o:p> </o:p>

峰回路转:<o:p></o:p>

晚上到家还是有点不死心,就直接建了个项目作单元测试,然后将一个自己建立的Jar加入到Classpath下面,作单元测试,结果大吃一惊。<o:p></o:p>

URL url = Thread.currentThread().getClass().getResource("/test/");<o:p></o:p>

URL url = Thread.currentThread().getClass().getResource("/test/test.txt");<o:p></o:p>

都正常获取到了资源,这完全推翻了我早先认为在Jar中无法获得目录作为资源的问题。然后把公司里面的项目重新打包然后加入到ClassPath下,验证spring的目录,出错,目录无法获取,此时我确定看来应该不是应用的问题,而是环境问题。检查了两个Jar,看似没有什么区别,将公司项目的Jar中的spring目录拷贝到测试的jar中,然后作测试,可以找到目录。那么问题完全定位到了Jar本身。通过RAR的压缩工具看了一下两个Jar的信息,除了显示所谓的压缩平台不同(一个是DOS,一个是Unix)其他没有任何区别。然后自己用RAR打了一个Jar以及在linux下打了一个Jar做了测试,两个Jar内的目录都是正常可以被获取。<o:p></o:p>

无意中我换了一下需要获取的目录名称,也就是说在公司项目中有多个目录在jar中,这次换成为ibatis目录,正常获取,看来不是Jar的格式。回想了一下,公司的打包工具是自己人写的,其中提供了一个特性,如果一个项目内部的一些配置信息是需要让调用它的第三方在编译期配置,那么可以通过在第三方项目构建的过程中,动态的生成配置文件然后植入到被依赖的jar中。而spring这个目录中由于那些数据库的配置都是需要动态配置的,因此spring的那个目录是后期被写入的,而ibatis是早先就固化在项目中的。<o:p></o:p>

由于我们的JarMETA-INF中都有INDEX.LIST文件,过去遇到过在JAR