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 了。
哎,为了这个异常白白浪费了一天的时间!悲剧!!!