maven wagon 唤起的一个 IOException

maven wagon 引起的一个 IOException使用 maven 进行 build 的时候,maven 会从 repository 中下载 artifac

maven wagon 引起的一个 IOException
使用 maven 进行 build 的时候,maven 会从 repository 中下载 artifact,maven 抽象了一个 Wagon 对象,由它来 download 和 deploy artifact。最近碰到了一个由它引起的一个灵异现象,花了一天才解决。

先说一下我们的应用场景吧,不理解我们的应用场景是很难理解为什么我们会碰到这个问题。现在我们的部分应用使用 maven build,我们有大量的遗留 java 类库,这些类库之前是放在 svn 中,如开发要使用只需 checkout,并使用我们自己开发的依赖管理工具来管理应用中的依赖。现在这些类库需要提供给使用 maven build 的应用使用,同时遗留应用还要能用以前的方式来使用这些类库。为此我们写了一个代理程序,简单地说就是一个 servlet,只要在 maven 的 settings 或 pom 中配置一下,maven 就能从这个代理中下载这些依赖。

代理的最近一次更新后频繁地抛 EofException(jetty 中的一个异常),引起这个异常的原因是 nio channel 中出现 broken pipe 的异常。一开始我以为是这次更新的代码中有 bug,但查了很久都没发现。后来我老板说,很有可能是我们返回给 maven 的某些信息,使 maven 提前关闭了 channel。在这句话的启发下,开始从 maven 入手。maven 中处理网络 i/o 就是 wagon,查看了 wagon 的代码后发现,果然是 wagon 提前关闭了输入流。maven 通过 WagonManager 下载依赖的时候,会首先尝试使用 Wagon 的 getIfNewer 方法,如果它失败,则会使用 Wagon 的 get 方法再尝试下载一次。下面是 StreamWagon 的 getIfNewer 方法的代码,这个类是个抽象类:

public boolean getIfNewer( String resourceName, File destination, long timestamp )        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException    {        boolean retValue = false;        Resource resource = new Resource( resourceName );        fireGetInitiated( resource, destination );        resource.setLastModified( timestamp );                InputStream is = getInputStream( resource );        // always get if timestamp is 0 (ie, target doesn't exist), otherwise only if older than the remote file        if ( timestamp == 0 || timestamp < resource.getLastModified() )        {            retValue = true;            checkInputStream( is, resource );            getTransfer( resource, destination, is );        }        else        {            IOUtil.close( is );        }        return retValue;    }


方法的三个参数分别表示远程资源的路径,下载到本地的目标文件,本地文件的时间戳。如果请求的资源在本地的 repository 中存在,它的值为本地文件的时间戳( File.lastModified,注:这种情况只有在下载 snapshot 的依赖的时候才会出现 ),否则为 0。从代码中可以看出,如果时间戳为 0 或小于远程资源的 last-modified 时才会下载,否则立即关闭输入流。

对于普通的使用 http 协议的 maven repository,resource.getLastModified() 返回的是 http header last-modified 的值( 参见 LightweheaderightHttpWagon 的 fillInputData 方法 )。由于我们代理的 servlet 中并没有设置这个 header,每次返回的都是默认值 0 ( 参见 HttpURLConnection 的 getLastModified 方法 ),所以如果本地文件存在,那么 wagon 就会立即关闭这个网络输入流。当 servlet 向已经关闭的 channel 中写入数据时,底层 channel 就会抛出 broken pipe 的异常。

ps:jetty 使用了 nio,向 servlet 的输出流中写数据的时候,如果数据量很小,它则会缓存在内存中,如果数据量很大,调用了多次写方法后,缓存中的数据就会被写到 channel 中,所有在下载一些很小的 jar 或是其他 maven metadata 时不会出现这个异常,从日志来看,这些异常都是在下载 jar 时出现的。

发现 EofException 异常的罪魁祸首后,只要简单地在 http response 中添加一个 last-modified header 就 ok 了。

哎,为了这个异常白白浪费了一天的时间!悲剧!!!