在Gradle Transform中使用Javassist遇到broken jar file的解决方案

1,729 阅读3分钟

最近在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;
    }

}

仔细跟一下整个流程。

  1. 从factory中获取jarFile
  2. 尝试获取缓存中的connection
  3. 从jarFile中获取jarEntry
  4. 如果没有找到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的时候都会使用缓存中的文件。

找到原因后,解决方案就好确定了。

  1. 每次运行的时候,先杀死gradle进程,然后再开启。
  2. 每次处理jar之前,把这两个缓存Map清空。
  3. 每次处理完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()
}

希望对遇到同样问题的同学能够有帮助,也希望有更好解决方案的同学能够不吝赐教。谢谢!