在编程的某些情况下,我们需要读取jar包中的文件,这种情况要区别于我们平时使用类加载器读取配置文件,这个时候配置在jar包中,就能读取到,但是配置文件也可以不在jar包中,只要放在Class-Path下就行了,所以这种情况下,我更愿意把它称之为:读取Class-Path下的配置文件。而我今天描述的比较明确,就是要读取jar包中的文件。这种需求可能不多,但是我碰见了,并且发现了几种,今天全部罗列分享一下。
目前有3种:
因为有好几种方式,那就直接定义个接口:
public interface JarReader {
/**
* 读取jar包中的文件
* @param jarPath jar包路径
* @param file jar包中的文件路径
* @return 文件内容,转换成字符串了,其它需求也可以转换成输入流。
* @throws IOException
*/
String readFromJar(String jarPath,String file) throws IOException;
}
jar包读取器,jar包中的文件读取出来。
JarFile是java自带的一种读取jar包的API,很多人应该用过,我就直接贴代码了。
public class JarFileJarReader implements JarReader {
@Override
public String readFromJar(String jarPath,String file) throws IOException {
JarFile jarFile=null;
try {
jarFile=new JarFile(jarPath);
JarEntry jarEntry=jarFile.getJarEntry(file);
InputStream input=jarFile.getInputStream(jarEntry);
return IOUtils.toString(input,"UTF-8");
} catch (IOException e) {
throw e;
} finally {
IOUtils.closeQuietly(jarFile);
}
}
}
代码也比较简单,重点就是最后一定要把jarFile这个对象关闭一下,中间的输入流都可以不用关闭。
不过我在写这段代码之前,从我的个人经验上来说,JarFile好像更多是用来读取清单文件(MANIFEST.MF)的,可能是见这种情况比较多,当然它的用途肯定远不止如此。
因此我顺便写了一下读取清单文件的代码:
public void getManiFest(String jarPath) throws IOException {
JarFile jarFile=null;
try {
jarFile=new JarFile(jarPath);
Manifest manifest=jarFile.getManifest();
if (manifest!=null){
//获取Class-Path
String classPaths = (String) manifest.getMainAttributes().get(new Attributes.Name("Class-Path"));
if (classPaths != null && !classPaths.isEmpty()) {
String[] classPathArray = classPaths.split(" ");
}
//获取JDK版本
String jdkVersion = (String) manifest.getMainAttributes().get(new Attributes.Name("Build-Jdk"));
//还可以获取其它内容,比如Main-Class等等
}
} catch (IOException e) {
throw e;
} finally {
IOUtils.closeQuietly(jarFile);
}
}
java自带的URL是支持读取jar中的文件的,协议是jar,表示方式的话是用"/!"把jar包和文件区分一下。代码如下:
public class URLJarReader implements JarReader {
@Override
public String readFromJar(String jarPath, String file) throws IOException {
JarURLConnection jarURLConnection=null;
try {
URL fileUrl=ParseUtil.fileToEncodedURL(new File(jarPath));
URL jarUrl=new URL("jar", "", -1, fileUrl + "!/");
URL moduleUrl = new URL(jarUrl, ParseUtil.encodePath(file, false));
jarURLConnection = (JarURLConnection)moduleUrl.openConnection();
return IOUtils.toString(jarURLConnection.getInputStream(),"UTF-8");
} catch (IOException e) {
throw e;
} finally {
if (jarURLConnection!=null){
try {
jarURLConnection.getJarFile().close();
} catch (IOException ignore) {
}
}
}
}
}
ParseUtil的几个方法是我在看java源码的时候看见的,用来处理一些不规则的文件路径。
我刚开始用URL的时候,就出现了一个内存泄漏的文件,读取完了以后,jar包被占用,死活不能删除,刚才开始把输入流给关闭了,也没有用。然后想到了类加载器里面有close方法,然后去看了一下,找到了上述代码中的finally块的代码。这样就可以把占用问题解决了,仔细看的话,会发现,getJarFile.close(),因此本质上还是关闭了JarFile,和上面是一样的。
这个也是借鉴了我们平时读取配置文件的方式,借用一下ClassLoader来读取。
public class ClassLoaderJarReader implements JarReader {
@Override
public String readFromJar(String jarPath, String file) throws IOException{
URLClassLoader urlClassLoader=null;
try {
URL fileUrl=ParseUtil.fileToEncodedURL(new File(jarPath));
urlClassLoader=new URLClassLoader(new URL[]{
fileUrl},null);
InputStream inputStream=urlClassLoader.getResourceAsStream(file);
if (inputStream==null){
throw new FileNotFoundException("not find file:"+file+" in jar:"+jarPath);
}else{
return IOUtils.toString(inputStream,"UTF-8");
}
} catch (IOException e) {
throw e;
} finally {
IOUtils.closeQuietly(urlClassLoader);
}
}
}
代码也是比较简单,最后把ClassLoader关闭一下就行了。
关于类加载器的读取方式,我很早之前就看过了,它本质上用的就是上面两种方式结合起来读取文件的。
这几种方式的话,其实没有什么区别,从开发角度来说的话,比较推荐第三种,因为是java自带的功能,也是比较完善,也简单,也不容易出错,而且它内部用的就是前面两种。不过从资源消耗上面来说,我猜测,前面两种应该占优,不过我也不纠结这个,没去研究。
有时候我们或许有另外一种需求,读取jar中的jar中的文件,这个在一些场景下,会使用到。最起码spring-boot确实是用到了,很早之前我看过它的实现,它就是把URL重写了一下,支持了一下多个"/!"表达式,就能够支持这种情况了。