最近在Gradle Transform中使用Javassist处理注解的时候,遇到了broken jar file的问题。当在一个Library中添加类之后进行编译必然会出现这个问题。
解决问题的过程如下。
在Javassist的源码中查找broken jar file这个关键词,可以在ClassPoolTail这个类中找到抛出异常的地方。
ClassPoolTail
@Override
public InputStream openClassfile(String classname)
throws NotFoundException
{
URL jarURL = find(classname);
if (null != jarURL)
try {
return jarURL.openConnection().getInputStream();
}
catch (IOException e) {
throw new NotFoundException("broken jar file?: "
+ classname);
}
return null;
}
可以看到,是在通过jarURL打开连接的时候出的问题,大概率是在调用openConnection()方法的时候没有找到文件所致。继续跟进去看这个方法。
URL
public URLConnection openConnection() throws java.io.IOException {
return handler.openConnection(this);
}
可以看到是调用了一个名为handler的成员变量的同名方法。继续跟进去看这个同名方法。
URLStreamHandler
abstract protected URLConnection openConnection(URL u) throws IOException;
是一个抽象方法,接下来看看这个抽象方法的实现,有很多。但是由于这里处理的是Jar文件,可以定位到sun.net.www.protocol.jar.Handler这个类
sun.net.www.protocol.jar.Handler
protected URLConnection openConnection(URL var1) throws IOException {
return new JarURLConnection(var1, this);
}
可以看到这里是返回了JarURLConnection的实例。继续跟进去看这个类的实现。可以看到有一个connect()方法,这个方法会抛出FileNotFoundException的异常,有可能是这里了。
打个断点验证一下,最后是可以发现当遇到出错文件的时候,确实是到了这里,抛出了异常。这就很奇怪了,是哪里出了问题?我在Gradle 的Transform中打断点,查看了出问题的类所在的jarFile,可以看到这个jarFile中是存在出问题的类的。排除了Transform中jar文件的问题后,回到刚才看到的connect()方法。
JarURLConnection
public void connect() throws IOException {
if (!this.connected) {
this.jarFile = factory.get(this.getJarFileURL(), this.getUseCaches());
if (this.getUseCaches()) {
this.jarFileURLConnection = factory.getConnection(this.jarFile);
}
if (this.entryName != null) {
this.jarEntry = (JarEntry)this.jarFile.getEntry(this.entryName);
if (this.jarEntry == null) {
try {
if (!this.getUseCaches()) {
this.jarFile.close();
}
} catch (Exception var2) {
}
throw new FileNotFoundException("JAR entry " + this.entryName + " not found in " + this.jarFile.getName());
}
}
this.connected = true;
}
}
仔细跟一下整个流程。
- 从factory中获取jarFile
- 尝试获取缓存中的
connection - 从jarFile中获取jarEntry
- 如果没有找到jarEntry,则抛出异常。
在第一步获取jarFile的时候,factory的方法第二个参数是是否使用缓存。也许问题就在这里,javassist处理jarFile的时候,由于存在Library修改前的jar的缓存,直接从缓存中读取了这个修改前的jarFile,而没有使用修改后的jar文件。
看看JarFileFactory这个类验证下猜想。果然。这个Factory是一个单例,里面保存了两个静态的Map。
private static final HashMap<String, JarFile> fileCache = new HashMap();
private static final HashMap<JarFile, URL> urlCache = new HashMap();
private static final JarFileFactory instance = new JarFileFactory();
当Gradle的守护进程一直运行的时候,其JVM虚拟机会一直运行,这也就意味着在不关闭当前运行的Gradle进程的情况下,这两个Map中的缓存文件会一直存在。后续处理jarFile的时候都会使用缓存中的文件。
找到原因后,解决方案就好确定了。
- 每次运行的时候,先杀死gradle进程,然后再开启。
- 每次处理jar之前,把这两个缓存Map清空。
- 每次处理完jar之后,把这个jar所对应的缓存清理掉。
1这个方式太麻烦了;2这个方式比较粗暴;3这个方式更精细一些。根据我的需求我这里使用了2这个方式。由于
JarFactory这个类不是public的,那么需要借助反射来处理了。
private void clearJarFactoryCache() {
Class clazz = Class.forName("sun.net.www.protocol.jar.JarFileFactory")
Field fileCacheField = clazz.getDeclaredField("fileCache")
Field urlCacheField = clazz.getDeclaredField("urlCache")
fileCacheField.accessible = true
urlCacheField.accessible = true
Map fileCache = fileCacheField.get(null)
Map urlCache = urlCacheField.get(null)
fileCache.clear()
urlCache.clear()
}
希望对遇到同样问题的同学能够有帮助,也希望有更好解决方案的同学能够不吝赐教。谢谢!