Retrofit 响应体无body时解析EOFException

Posted 疯狂小芋头

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Retrofit 响应体无body时解析EOFException相关的知识,希望对你有一定的参考价值。

太长不看版

由于以下涉及到一些跟Retrofit使用有关的问题及部分Retrofit的代码,如果未使用过Retrofit可能看了很混乱.这里有简洁版.

异常

Caused by: java.io.EOFException: End of input at line 1 column

当出现以上异常时(这时指使用GsonConvertFactory进行解析json数据),并且请求结果无响应体body仅需要响应码时,很有可能就是因为gson解析出错导致的异常.


解决方案

针对这个问题,Retrofit是允许添加多个ConvertFactory的,也就是解析器.只需要自定义一个无响应体类型的解析器即可.操作如下:

  • 创建一个无响应体类型NoBodyEntity,这个是必须的,因为Retrofit中的定义接口时Call是必须填入一个类型的.(不能使用Object)
  • 创建一个NoBodyConvertFactory用于解析返回类型是NoBodyEntity的response,实际上这里就是自己处理无响应体的response.
  • 最后快乐地使用response里的statusCode就可以了,可以忽略你不需要的响应体(本来也没有)

附上NoBodyEntityNoBodyConverFactory的实现.后面是对异常的分析查找和解决方案的思考.

  • NoBodyEntity
public class NoBodyEntity 

是的,你没有看错,什么都没有!因为本来就没有响应体,只是Retrofit的接口定义必须要给一个类型.ConverFactory转换时也需要根据类型进行识别.

  • NoBodyConvertFactory
/**
 * 代替gson converter转换无响应体的response
 */
public static class NobodyConverterFactory extends Converter.Factory 

    public static final NobodyConverterFactory create() 
        return new NobodyConverterFactory();
    

    private NobodyConverterFactory() 
    

    //将响应对象responseBody转成目标类型对象(也就是Call里给定的类型)
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
                                                            Retrofit retrofit) 
        //判断当前的类型是否是我们需要处理的类型
        if (NoBodyEntity.class.equals(type)) 
            //是则创建一个Converter返回转换数据
            return new Converter<ResponseBody, NoBodyEntity>() 
                @Override
                public NoBodyEntity convert(ResponseBody value) throws IOException 
                    //这里直接返回null是因为我们不需要使用到响应体,本来也没有响应体.
                    //返回的对象会存到response.body()里.
                    return null;
                
            ;
        
        return null;
    

    //其它两个方法我们不需要使用到.所以不需要重写.
    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type,
                                                          Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) 
        return null;
    

    @Override
    public Converter<?, String> stringConverter(Type type, Annotation[] annotations,
                                                Retrofit retrofit) 
        return null;
    

具体分析版

异常原因

Caused by: java.io.EOFException: End of input at line 1 column

根据打印的异常栈信息可以知道这个其实是gson解析时出现的异常,无法解析数据.从调试进入Call.execute()方法查看问题
这里使用retrofit 2.0

return parseResponse(call.execute());

实际上call执行结果是正常的,也返回了正确的response对象,该对象与预计的结果是一致的(只有响应码不存在body).
那么问题出在parseResponse()这个方法中,进一步查看parseResponse()方法,确实异常就是在这个方法中抛出的.

try 
      T body = serviceMethod.toResponse(catchingBody);
      return Response.success(body, rawResponse);
     catch (RuntimeException e) 
      // If the underlying source threw an exception, propagate that rather than indicating it was
      // a runtime exception.
      catchingBody.throwIfCaught();
      //一旦有任何异常发生,就会在这里地方抛出,retrofit并没有处理
      throw e;
    

进入serviceMethod.toResponse()方法,查看代码如下

  T toResponse(ResponseBody body) throws IOException 
    return responseConverter.convert(body);
  

这里可以看到使用的是我们在创建retrofit时设置的ConverterFactory进行解析数据,由于只设置了GsonConverterFactory,所以只会使用gson进行解析,这就是出错的原因.

现在已经确定是由于GsonConverterFactory解析结果出错抛出的异常.这里会发生这个问题的原因应该是gsonConverter并没有处理如果不存在body数据的这种情况.
一般返回的数据都是JSON数据,但是可能某些情况下只需要一个响应码所以服务端并没有返回数据,此时并不需要解析body或者直接返回null都是没有问题的.但是gson的强制进行解析就造成了这个异常.


解决方案

现在需要解决这个问题.GsonConverterFactory的解析是必须保留的,因此需要创建新一个conveterFactory来解析这种特殊的情况.

在retrofix.builder中可以添加解析对象,但是问题是添加的解析对象是只能有一个还是多个?根据方法名addConverterFactory()分析应该是可以添加多个的,进一步查看方法可知

public Builder addConverterFactory(Converter.Factory factory) 
      converterFactories.add(checkNotNull(factory, "factory == null"));
      return this;
    

所以确实是保存了多个解析对象在list中.因此可以添加多个解析对象.那么在存在多个解析对象时到底使用的是哪个解析对象呢?
根据之前serviceMethod.toResponse()可以发现,解析对象是由serviceMethod提供的,那么查找serviceMethod类查看解析对象是如何获取的.在serviceMethod.Builder中可以发现

responseConverter = createResponseConverter();

该方法实际上是使用retrofit中的方法去获取解析对象,来源也就是我们最早设置的addConverterFactory()

private Converter<ResponseBody, T> createResponseConverter() 
      Annotation[] annotations = method.getAnnotations();
      try 
        //获取converter
        return retrofit.responseBodyConverter(responseType, annotations);
       catch (RuntimeException e)  // Wide exception range because factories are user code.
        throw methodError(e, "Unable to create converter for %s", responseType);
      
    

查看Converter中内部的Factory抽象类,可以发现实际数据的类型转换都是通过这个Factory来实现的.
Factory中分别包含了

  • responseBodyConverter,获取响应体的转换类converter
  • requestBodyConverter,获取请求体的转换类converter
  • stringConverter,获取将类型数据转换成string的转换类converter
abstract class Factory 
  //提供一个将ResponseBody转成当前type类型数据的Converter
  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
      Retrofit retrofit) 
    return null;
  

  //提供一个将当前type类型数据转成http request body类型的Converter
  public Converter<?, RequestBody> requestBodyConverter(Type type,
      Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) 
    return null;
  

  //提供一个将当前type类型的数据转成String的Converter
  public Converter<?, String> stringConverter(Type type, Annotation[] annotations,
      Retrofit retrofit) 
    return null;
  

由于我们是在response的解析数据时出错,所以我们只需要重写responseBodyConverter()方法就够了
重写该方法需要确定的是我们需要处理的类型type是哪种类型.
以下以String类型为例,假设我们需要处理的数据类型是String

//响应体转换类converter
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
                                                                Retrofit retrofit) 
    if (String.class.equals(type)) 
        //如果需要处理的类型是String(即我们需要处理的类型)
        //返回一个转换的Converter对象
        return new Converter<ResponseBody, String>() 
            //converter对象实现其抽象方法,将当前类型数据转换成目标类型的convert方法
            @Override
            public String convert(ResponseBody value)     throws IOException 
                return value==null?null:value.toString();
            
        ;
    
return null;

自定义处理类型

由于Converter是通过判断响应体提供的类型(由Call<T>决定)与我们需要处理的类型类进行转换的,所以我们必须提供一个自定义的类型去处理数据,否则的话就无法识别出当前响应体返回的类型是否我们需要处理的了.

对于仅有响应头无响应体的情况,我们定义一个类NoBodyEntity来处理,实际上该类只是一个空类,并没有任何内容,但是这里是需要的.

除了在Conterter.Factory中需要使用到类型进行判断,实际上retrofit的Call<T>创建时也是要求必须给定一个类型的,不能创建一个无类型的Call

//创建retrofit的caller,必须指定一个返回类型
Call<NoBodyEntity> caller=service.noBodyRequest();

//Converter类中的处理方式
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,Retrofit retrofit) 
    //匹配response对象需要返回的类型是否与我们想自定义处理的类型一致
    if (NoBodyEntity.class.equals(type)) 
        return new Converter<ResponseBody, NoBodyEntity>() 
            @Override
            public NoBodyEntity convert(ResponseBody value) throws IOException 
                //这里返回null是因为本来就是无响应实体
                //所以也不会使用到该类的实例.
                return null;
            
        ;
    
    return null;

以上就是通过自定义的方式解决无响应体时的response数据解析,使用其它的ConverterFactory可能会出现异常错误(至少GsonConverterFactory肯定会).

最后一个重要的点:由于添加converterFactory时是保存到list中,而解析时也是从list按顺序取出converter进行解析,所以

务必确定先添加自定义的ConverterFactory再添加第三方的converter,否则要不出现异常,要不会使数据被错误解析掉

//对于以上创建的NoBodyConverterFactory
//retrofit的添加方式
Retrofit retrofit = new Retrofit.Builder()
            //一定要在gsonConverter前面,否则gson会拦截所有的解析方式
            .addConverterFactory(NobodyConverterFactory.create())
            .addConverterFactory(GsonConverterFactory.create())
            .client("your http client")
            .baseUrl("your base url")
            .build();

以上是关于Retrofit 响应体无body时解析EOFException的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Retrofit 库中将 YAML 响应正文解析为 POJO?

Retrofit-2.0 - 解析在 xml 中包含 json 的响应

无法通过 @POST 请求使用 Retrofit 解析 JSON 响应和参数

如何让我的 retrofit2 模型解析这个?

使用 Moshi 和 Retrofit 将响应包装在另一个对象中

如何使用 Retrofit 2 处理空响应体?