问题描述
groovy 中使用http-builder-0.72.jar读取图片字节流,遇到 Attempted read from closed stream异常。
问题代码
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.Method
import groovyx.net.http.RESTClient
import sun.misc.BASE64Encoder
import static groovyx.net.http.ContentType.TEXT
import static groovyx.net.http.ContentType.ANY
import static groovyx.net.http.ContentType.HTML
def restclient = new RESTClient("http://www.xxxxx.com")
def params = [params: [frameNo : 'L65654748903789765',
engineNo : '458237',
license : '新车',
agentCode: '03515500']]
restclient.get(params) {
rsp,reader ->
rsp.entity.content.bytes
}
源码分析
RestClient是对HttpBuilder为满足festful调用风格的一层封装。
public Object get( Map<String,?> args ) throws ClientProtocolException,
IOException, URISyntaxException {
return doRequest( new RequestConfigDelegate( args, new HttpGet(), null ) );
}
实际调用HttpBuilder的doRequest方法
protected Object doRequest( URI uri, Method method, Object contentType, Closure configClosure )
throws ClientProtocolException, IOException {
HttpRequestBase reqMethod;
try { reqMethod = method.getRequestType().newInstance();
// this exception should reasonably never occur:
} catch ( Exception e ) { throw new RuntimeException( e ); }
reqMethod.setURI( uri );
RequestConfigDelegate delegate = new RequestConfigDelegate( reqMethod, contentType,
this.defaultRequestHeaders,
this.defaultResponseHandlers );
configClosure.setDelegate( delegate );
configClosure.setResolveStrategy( Closure.DELEGATE_FIRST );
configClosure.call( reqMethod );
return this.doRequest( delegate );
}
/**
* All <code>request</code> methods delegate to this method.
*/
protected Object doRequest( final RequestConfigDelegate delegate )
throws ClientProtocolException, IOException {
delegate.encodeBody();
final HttpRequestBase reqMethod = delegate.getRequest();
final Object contentType = delegate.getContentType();
if ( this.autoAcceptHeader ) {
String acceptContentTypes = contentType.toString();
if ( contentType instanceof ContentType )
acceptContentTypes = ((ContentType)contentType).getAcceptHeader();
reqMethod.setHeader( "Accept", acceptContentTypes );
}
reqMethod.setURI( delegate.getUri().toURI() );
if ( reqMethod.getURI() == null)
throw new IllegalStateException( "Request URI cannot be null" );
log.debug( reqMethod.getMethod() + " " + reqMethod.getURI() );
// set any request headers from the delegate
Map<?,?> headers = delegate.getHeaders();
for ( Object key : headers.keySet() ) {
Object val = headers.get( key );
if ( key == null ) continue;
if ( val == null ) reqMethod.removeHeaders( key.toString() );
else reqMethod.setHeader( key.toString(), val.toString() );
}
ResponseHandler<Object> responseHandler = new ResponseHandler<Object>() {
public Object handleResponse(HttpResponse response)
throws ClientProtocolException, IOException {
HttpResponseDecorator resp = new HttpResponseDecorator(
response, delegate.getContext(), null );
try {
int status = resp.getStatusLine().getStatusCode();
Closure responseClosure = delegate.findResponseHandler( status );
log.debug( "Response code: " + status + "; found handler: " + responseClosure );
Object[] closureArgs = null;
switch ( responseClosure.getMaximumNumberOfParameters() ) {
case 1 :
closureArgs = new Object[] { resp };
break;
case 2 : // parse the response entity if the response handler expects it:
HttpEntity entity = resp.getEntity();
try {
if ( entity == null || entity.getContentLength() == 0 )
closureArgs = new Object[] { resp, null };
else closureArgs = new Object[] { resp, parseResponse( resp, contentType ) };
}
catch ( Exception ex ) {
Header h = entity.getContentType();
String respContentType = h != null ? h.getValue() : null;
log.warn( "Error parsing '" + respContentType + "' response", ex );
throw new ResponseParseException( resp, ex );
}
break;
default:
throw new IllegalArgumentException(
"Response closure must accept one or two parameters" );
}
Object returnVal = responseClosure.call( closureArgs );
log.trace( "response handler result: " + returnVal );
return returnVal;
}
finally {
HttpEntity entity = resp.getEntity();
if ( entity != null ) entity.consumeContent();
}
}
};
return getClient().execute(reqMethod, responseHandler, delegate.getContext());
}
留意其中responseHandler处理逻辑中对针对参数个数不通处理不同的返回结果的逻辑。
switch ( responseClosure.getMaximumNumberOfParameters() ) {
case 1 :
closureArgs = new Object[] { resp };
break;
case 2 : // parse the response entity if the response handler expects it:
HttpEntity entity = resp.getEntity();
try {
if ( entity == null || entity.getContentLength() == 0 )
closureArgs = new Object[] { resp, null };
else closureArgs = new Object[] { resp, parseResponse( resp, contentType ) };
}
当处理闭包只有一个参数的时候,会直接返回resp也就是一个被封装后的httpResonse对象,我们可以从中拿到尚未被读取的字节流inputStream,进行字节流处理。
而如果是两个参数时,即问题中的形式,则会根据返回的contentType不同调用 parseResponse( resp, contentType ),此函数中会根据返回contentType选择对应的解析器进行返回结果的处理,即我们常见的json,xml ,html, ParserRegistry类中会根据不同contentType执行各自的解析过程。而当解析html,json ,xml的时候是会从输入流中读取数据进行封装。即轮到我们的闭包执行的时候,inputstream中的数据已经被读取过了,固此时会报错 Attempted read from closed stream。
问题解决
有以上可知,有两种问题解决此问题
方法一,将结果处理闭包中返回参数设置为一个,此时不会进行数据解析的过程,可以拿到对应的inputstream然后进行处理
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.Method
import groovyx.net.http.RESTClient
import sun.misc.BASE64Encoder
import static groovyx.net.http.ContentType.TEXT
import static groovyx.net.http.ContentType.ANY
import static groovyx.net.http.ContentType.HTML
def restclient = new RESTClient("http://www.xxxxx.com")
def params = [params: [frameNo : 'L65654748903789765',
engineNo : '458237',
license : '新车',
agentCode: '03515500']]
restclient.get(params) {
rsp->
rsp.entity.content.bytes
}
方法二 设置contentType 为TEXT
TEXT的parser过程不会读取流的字节,而只是直接返回一个封装后的InputStreamReader,所以此时依然可以拿到可读的inputStream,但是如果不设置contentType,则默认为ANY 会以返回报文头中的contentType来获取对应解析器。
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.Method
import groovyx.net.http.RESTClient
import sun.misc.BASE64Encoder
import static groovyx.net.http.ContentType.TEXT
import static groovyx.net.http.ContentType.ANY
import static groovyx.net.http.ContentType.HTML
def restclient = new RESTClient("http://www.xxxxx.com",TEXT)
def params = [params: [frameNo : 'L65654748903789765',
engineNo : '458237',
license : '新车',
agentCode: '03515500']]
restclient.get(params) {
rsp,reader->
rsp.entity.content.bytes
}
其实如果只是简单的要拿一个url返回的字节流可用最简单的方式
new URL('http://www.baidu.com').bytes
扩展
关于RestClient,和HttpBuilder的使用,RestClient本身是对HttpBuilder的一层Rest封装,所以如果使用RestClient,只使用他本身的get,post等方法即可,而其参数都是固定一个Map,会使用默认responseHandler进行处理进而返回一个HttpResponseDecorator对象,而例子中我们使用的方法其实是HttpBuilder的get(param,closure)方法,没有透彻理解restclient类的拓展目的。
对于HttpBuilder中params,query ,queryString参数的处理过程,先解析params以 ?key=value&key1=value1的方式拼接到uri,然后解析queryString,而如果有此参数的话,会更新uri,直接变为uri?qryString的形式,query参数是和params的一个合并,有则更新,没有则添加。
protected void setPropertiesFromMap( Map<String,?> args ) throws URISyntaxException {
if ( args == null ) return;
if ( args.containsKey( "url" ) ) throw new IllegalArgumentException(
"The 'url' parameter is deprecated; use 'uri' instead" );
Object uri = args.remove( "uri" );
if ( uri == null ) uri = defaultURI;
if ( uri == null ) throw new IllegalStateException(
"Default URI is null, and no 'uri' parameter was given" );
this.uri = new URIBuilder( convertToURI( uri ) );
Map query = (Map)args.remove( "params" );
if ( query != null ) {
log.warn( "'params' argument is deprecated; use 'query' instead." );
this.uri.setQuery( query );
}
String queryString = (String)args.remove("queryString");
if ( queryString != null ) this.uri.setRawQuery(queryString);
query = (Map)args.remove( "query" );
if ( query != null ) this.uri.addQueryParams( query );
Map headers = (Map)args.remove( "headers" );
if ( headers != null ) this.getHeaders().putAll( headers );
Object path = args.remove( "path" );
if ( path != null ) this.uri.setPath( path.toString() );
Object contentType = args.remove( "contentType" );
if ( contentType != null ) this.setContentType( contentType );
contentType = args.remove( "requestContentType" );
if ( contentType != null ) this.setRequestContentType( contentType );
Object body = args.remove("body");
if ( body != null ) this.setBody( body );
if ( args.size() > 0 ) {
String invalidArgs = "";
for ( String k : args.keySet() ) invalidArgs += k + ",";
throw new IllegalArgumentException("Unexpected keyword args: " + invalidArgs);
}
}