Retrofit & retrofit-spring-boot-starter

2,339 阅读9分钟

什么是Retrofit?

Type-safe HTTP client for Android and Java by Square, Inc.

Retrofit是一个RESTful 的 HTTP 网络请求框架的封装,网络请求的工作本质上是 OkHttp 完成,而 Retrofit 仅负责 网络请求接口的封装。

Retrofit将 Http请求 抽象成 Java接口:采用 注解 描述网络请求参数 和配置网络请求参数。

比如下面的Java接口描述了使用GET请求发送网络请求,请求参数是一个QueryString name, 返回值是Call<DtoResult<BookQueryResult>>, 它是一个网络请求,可以调用它的execute方法获得执行结果DtoResult<BookQueryResult>

引入依赖

<dependency>
    <groupId>com.squareup.retrofit2</groupId>
    <artifactId>retrofit</artifactId>
    <version>2.9.0</version>
</dependency>
<dependency>
    <groupId>com.squareup.retrofit2</groupId>
    <artifactId>converter-scalars</artifactId>
    <version>2.9.0</version>
</dependency>
<dependency>
    <groupId>com.squareup.retrofit2</groupId>
    <artifactId>converter-jackson</artifactId>
    <version>2.9.0</version>
</dependency>

网络请求接口定义

public interface GetRequest_Interface {

    @GET("openapi.do?keyfrom=abc&key=2032414398&type=data&doctype=json&version=1.1&q=car")
    @FormUrlEncoded
    Call<DtoResult<BookQueryResult>> listBooks(@Field("name") String name);
}

发起网络请求

# 使用Retrofit.Builder创建Retrofit 客户端。
Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://fanyi.youdao.com/") //设置网络请求的Url地址
                .addConverterFactory(GsonConverterFactory.create()) //设置数据解析器
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();

创建Retrofit 客户端

// 使用Retrofit 客户端创建 网络请求接口 的实例
        GetRequest_Interface request = retrofit.create(GetRequest_Interface.class);
        //对 发送请求 进行封装
        Call<Reception> callListBook = request.listBooks("redis");

发送同步请求

        try {
            Response<DtoResult<BookQueryResult>> response = call.execute();
           if(response.isSuccessfully()){
               DtoResult<BookQueryResult> dtoResult = response.body();
               // ...
           }
           
        } catch (IOException e) {
            log.error(e);
        }

发送异步请求

call.enqueue(new Callback<DtoResult<BookQueryResult>>() {
            //请求成功时回调
            @Override
            public void onResponse(Call<DtoResult<BookQueryResult>> call, Response<DtoResult<BookQueryResult>> response) {
                //请求处理,输出结果
                if(response.isSuccessfully()){
                   DtoResult<BookQueryResult> dtoResult = response.body();
                   // ...
               }
            }
            //请求失败时候的回调
            @Override
            public void onFailure(Call<DtoResult<BookQueryResult>> call, Throwable throwable) {
                System.out.println("连接失败");
            }
        });

重点知识

  1. call.execute发起同步调用;call.enqueue发起异步调用,在异步回调中可以处理onResponse、onFailure.
  2. response.isSuccessfully(code >= 200 && code < 300)

image.png

3. 可以通过okhttp的interceptor进行全局拦截,然后统一处理所有可能的response code.注释在解析response可能会失败,比如期望的是json, 服务端网关层可能返回了html. 4. 如果网络请求失败,会产生IOException

call.enqueue(new Callback<List<GitHubRepo>>() {  
    @Override
    public void onResponse(Call<List<GitHubRepo>> call, Response<List<GitHubRepo>> response) {
        if (response.isSuccessful()) {
            Toast.makeText(ErrorHandlingActivity.this, "server returned data", Toast.LENGTH_SHORT).show();
            // todo display the data instead of just a toast
        }
        else {
            Toast.makeText(ErrorHandlingActivity.this, "Server returned an error", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void onFailure(Call<List<GitHubRepo>> call, Throwable t) {
if (t instanceof IOException) {
        Toast.makeText(ErrorHandlingActivity.this, "this is an actual network failure :( inform the user and possibly retry", Toast.LENGTH_SHORT).show();
        // logging probably not necessary
    }
    else {
        Toast.makeText(ErrorHandlingActivity.this, "conversion issue! big problems :(", Toast.LENGTH_SHORT).show();
        // todo log to some central bug tracking service
    }
 }
});

Retrofit的注解详细说明

我们可以将Retrofit的注解分为两类

  • 接口方法上的注解
  • 接口参数上的注解 Retrofit的注解分类
  1. @HTTP
public interface GetRequest_Interface {
    /**
     * method:网络请求的方法(区分大小写)
     * path:网络请求地址路径
     * hasBody:是否有请求体
     */
    @HTTP(method = "GET", path = "blog/{id}", hasBody = false)
    Call<ResponseBody> getCall(@Path("id") int id);
    // {id} 表示是一个变量
    // method 的值 retrofit 不会做处理,所以要自行保证准确
}

@FormUrlEncoded@Multipart

/**
         *表明是一个表单格式的请求(Content-Type:application/x-www-form-urlencoded)
         * <code>Field("username")</code> 表示将后面的 <code>String name</code> 中name的取值作为 username 的值
         */
        @POST("/form")
        @FormUrlEncoded
        Call<ResponseBody> testFormUrlEncoded1(@Field("username") String name, @Field("age") int age);
         
        /**
         * {@link Part} 后面支持三种类型,{@link RequestBody}、{@link okhttp3.MultipartBody.Part} 、任意类型
         * 除 {@link okhttp3.MultipartBody.Part} 以外,其它类型都必须带上表单字段({@link okhttp3.MultipartBody.Part} 中已经包含了表单字段的信息),
         */
        @POST("/form")
        @Multipart
        Call<ResponseBody> testFileUpload1(@Part("name") RequestBody name, @Part("age") RequestBody age, @Part MultipartBody.Part file);
  1. 添加请求头@Header@Headers
// @Header
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

// @Headers
@Headers("Authorization: authorization")
@GET("user")
Call<User> getUser()

// 以上的效果是一致的。
// 区别在于使用场景和使用方式
// 1. 使用场景:@Header用于添加不固定的请求头,@Headers用于添加固定的请求头
// 2. 使用方式:@Header作用于方法的参数;@Headers作用于方法
  1. 表单参数: @Field@FieldMap
    发送 Post请求 时提交请求的表单字段,与 @FormUrlEncoded 注解配合使用
public interface GetRequest_Interface {
        /**
         *表明是一个表单格式的请求(Content-Type:application/x-www-form-urlencoded)
         * <code>Field("username")</code> 表示将后面的 <code>String name</code> 中name的取值作为 username 的值
         */
        @POST("/form")
        @FormUrlEncoded
        Call<ResponseBody> testFormUrlEncoded1(@Field("username") String name, @Field("age") int age);

/**
         * Map的key作为表单的键
         */
        @POST("/form")
        @FormUrlEncoded
        Call<ResponseBody> testFormUrlEncoded2(@FieldMap Map<String, Object> map);

}
  1. URL查询参数 @Query,@QueryMap
    用于 @GET 方法的查询参数(Query = Url 中 ‘?’ 后面的 key-value)
    如:url = www.println.net/?cate=andro…,其中,Query = cate
    配置时只需要在接口方法中增加一个参数即可:
public interface GetRequest_Interface {
   @GET("/")    
   Call<String> cate(@Query("cate") String cate);
   
   @GET("/")    
   Call<String> cate2(@QueryMap Map<String, Object> cate);
}
  1. URL路径参数 @Path
public interface GetRequest_Interface {

        @GET("users/{user}/repos")
        Call<ResponseBody>  getBlog(@Path("user") String user );
    }
  1. 上传表单参数@Part, @PartMap
    发送 Post请求 时提交请求的表单字段,与@Field的区别:功能相同,但携带的参数类型更加丰富,包括数据流,所以适用于 有文件上传 的场景,与 @Multipart 注解配合使用
public interface GetRequest_Interface {

          /**
         * {@link Part} 后面支持三种类型,{@link RequestBody}、{@link okhttp3.MultipartBody.Part} 、任意类型
         * 除 {@link okhttp3.MultipartBody.Part} 以外,其它类型都必须带上表单字段({@link okhttp3.MultipartBody.Part} 中已经包含了表单字段的信息),
         */
        @POST("/form")
        @Multipart
        Call<ResponseBody> testFileUpload1(@Part("name") RequestBody name, @Part("age") RequestBody age, @Part MultipartBody.Part file);

        /**
         * PartMap 注解支持一个Map作为参数,支持 {@link RequestBody } 类型,
         * 如果有其它的类型,会被{@link retrofit2.Converter}转换,如后面会介绍的 使用{@link com.google.gson.Gson} 的 {@link retrofit2.converter.gson.GsonRequestBodyConverter}
         * 所以{@link MultipartBody.Part} 就不适用了,所以文件只能用<b> @Part MultipartBody.Part </b>
         */
        @POST("/form")
        @Multipart
        Call<ResponseBody> testFileUpload2(@PartMap Map<String, RequestBody> args, @Part MultipartBody.Part file);

        @POST("/form")
        @Multipart
        Call<ResponseBody> testFileUpload3(@PartMap Map<String, RequestBody> args);
}
  1. 设置请求URL @URL
public interface GetRequest_Interface {

        @GET
        Call<ResponseBody> testUrlAndQuery(@Url String url, @Query("showAll") boolean showAll);
       // 当有URL注解时,@GET传入的URL就可以省略
       // 当GET、POST...HTTP等方法中没有设置Url时,则必须使用 {@link Url}提供

}

Retrofit内部实现原理

Retrofit使用JDK动态代理机制为接口创建实现类(通过解析接口方法上的注解、参数注解和返回值动态实现OkHttp请求)。

public interface MyInterface {
  void fun1();
  void fun2();
}
 
public static void main(String[] args) {
  Class[] cs = {MyInterface.class};
  ClassLoader loader = MyInterface.class.getClassLoader();
  InvocationHandler h = new InvocationHandler() {
    public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
        Annotation[] annotations = method.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println("annotation " + annotation.toString());
        }

      System.out.println("无论你调用代理对象的什么方法,其实都是在调用invoke()...");
      return null;
    }
  };
  MyInterface mi = (MyInterface)Proxy.newProxyInstance(loader, cs, h);
  mi.fun1();
  mi.fun2();
}

### Proxy.newProxyInstance生成的类
class X implements MyInterface {
  private InvocationHandler h;
  public X(InvocationHandler h) {
    this.h = h;
  }
  public void fun1() {
    h.invoke();
  }
  public void fun2() {
    h.invoke();
  }
}

扩展点有哪些?

  • Request/Response Converter
    • converter-scalars: Scalars (primitives, boxed, and String)
    • converter-jackson
    • converter-simplexml
    • converter-gson
    • converter-moshi
    • converter-protobuf
    • converter-wire
    • converter-jaxb
  • Call Adapter
    • adapter-rxjava3
    • adapter-rxjava2
    • adapter-rxjava
    • adapter-java8
    • adapter-guava
    • adapter-scala

使用了哪些设置模式

  • Builder:建造者模式

image.png

  • Abstract Factory: 抽象工厂模式

image.png

  • Adapter: 适配器模式

image.png

如何将Retrofit集成到SpringBoot项目中

使用retrofit-spring-boot-starter将Retrofit集成到SpringBoot项目中

引入依赖

<dependency>
    <groupId>com.github.lianjiatech</groupId>
    <artifactId>retrofit-spring-boot-starter</artifactId>
    <version>2.3.6</version>
</dependency>

添加Java配置类型

@Configuration
@RetrofitScan("cn.xxx.gymy.app.openapi.service")
public class RetrofitConfiguration {
}

在JavaConfig指向的包(cn.xxx.gymy.app.openapi.service)下面创建@RetrofitClient接口

/**
 * 工业码用户认证接口
 */
@RetrofitClient(baseUrl = "${gymy.biz.baseUrl}")
public interface GymyAuthHttpClient {
    @FormUrlEncoded
    @POST("openApi/v1/user/login")
    Call<GymyResultDto<GymyTokenDto>> login(@Field("username") String username, @Field("password") String password);
}

将服务中使用@RetrofitClient接口

@Service
@Slf4j
public class GymyAuthServiceImpl implements GymyAuthService {
    @Resource
    private GymyAuthHttpClient gymyAuthHttpClient;

    @Override
    public GymyTokenDto login(String username, String password) throws IOException {
        Call<GymyResultDto<GymyTokenDto>> loginCall = gymyAuthHttpClient.login(username, password);
        retrofit2.Response<GymyResultDto<GymyTokenDto>> gymyTokenDtoResponse = loginCall.execute();
        if (gymyTokenDtoResponse.isSuccessful()
                && gymyTokenDtoResponse.body() != null
                && gymyTokenDtoResponse.body().getData() != null) {

            return gymyTokenDtoResponse.body().getData();
        }

        return null;
    }
}

上传文件示例

  • RetrofitClient定义
/**
 * 企业接口
 *
 * @author James.H.Fu
 * @date 2022/6/10 16:53
 */
@RetrofitClient(baseUrl = "${gymy.biz.erjiBaseUrl}")
public interface ErjiFileHttpClient {
    /**
     * 文件上传接口
     *
     * @param token 登录令牌
     * @param bodyPart
     * @return
     */
    @POST("/snms/api/file/upload")
    @Multipart
    Call<ErjiDtoResult<ErjiFileUploadResultDto>> fileUpload(@Header("Authorization") String token, @Part MultipartBody.Part bodyPart);
}
  • 上传服务实现
@Service
@Slf4j
public class ErjiUploadServiceImpl implements ErjiUploadService {

    @Resource
    private ErjiFileHttpClient erjiFileHttpClient;

    @Override
    @SneakyThrows
    public ErjiFileUploadResultDto upload(String fileName, String fileUrl) {
        String token = ErjiTokenContext.getToken();
        if (!StringUtils.hasText(fileName)) {
            fileName = "file.png";
        }
        // 下载文件
        byte[] bytes = HttpUtil.downloadBytes(fileUrl);

        // 上传到二级节点
        String encodeFileName = URLEncodeUtil.encode(fileName, StandardCharsets.UTF_8);
        okhttp3.RequestBody requestBody = okhttp3.RequestBody.create(MediaType.parse("multipart/form-data"), bytes);
        MultipartBody.Part bodyPart = MultipartBody.Part.createFormData("file", encodeFileName, requestBody);
        Call<ErjiDtoResult<ErjiFileUploadResultDto>> callFileUpload = erjiFileHttpClient.fileUpload(token, bodyPart);
        Response<ErjiDtoResult<ErjiFileUploadResultDto>> responseFileUpload = callFileUpload.execute();
        ErjiDtoResult<ErjiFileUploadResultDto> result = responseFileUpload.body();
        if (result != null) {
            if (!result.isSuccessful()) {
                log.error("erji file upload result {}", result);
            }
            return result.getData();
        } else {
            log.error("erji file upload response {}, errorBody {}", responseFileUpload, responseFileUpload.errorBody());
            return null;
        }
    }
}

下载文件示例

  • 定义接口:如果是较大的文件,建议使用@Streaming注解来实现流式读取
@RetrofitClient(baseUrl = "https://img.ljcdn.com/hc-picture/")
public interface DownloadApi {

    @GET("{fileKey}")
    @Streaming
    Response<ResponseBody> download(@Path("fileKey") String fileKey);
}
  • 使用ResponseBody.byteStream()读取数据流
@SpringBootTest(classes = RetrofitTestApplication.class)
@RunWith(SpringRunner.class)
public class DownloadTest {
    @Autowired
    DownloadApi downLoadApi;

    @Test
    public void download() throws Exception {
        String fileKey = "6302d742-ebc8-4649-95cf-62ccf57a1add";
        Response<ResponseBody> response = downLoadApi.download(fileKey);
        ResponseBody responseBody = response.body();
        // 二进制流
        InputStream is = responseBody.byteStream();

        // 具体如何处理二进制流,由业务自行控制。这里以写入文件为例
        File tempDirectory = new File("temp");
        if (!tempDirectory.exists()) {
            tempDirectory.mkdir();
        }
        File file = new File(tempDirectory, UUID.randomUUID().toString());
        if (!file.exists()) {
            file.createNewFile();
        }
        FileOutputStream fos = new FileOutputStream(file);
        byte[] b = new byte[1024];
        int length;
        while ((length = is.read(b)) > 0) {
            fos.write(b, 0, length);
        }
        is.close();
        fos.close();
    }
}

使用ThreadLocal 在线程间传递Token

  • 定义TokenContext
/**
 * 授权上下文
 *
 */
public class ErjiTokenContext implements AutoCloseable {
    private static final ThreadLocal<String> ctx = new ThreadLocal<>();

    public ErjiTokenContext(String token) {
        ctx.set(token);
    }

    @Override
    public void close() {
        ctx.remove();
    }

    /**
     * 获取授权上下文
     *
     * @return 授权上下文
     */
    public static String getToken() {
        return ctx.get();
    }
}
  • 在调用服务前获取Token并初始化一个TokenContext
@Test
public void testEntConfig() {
    // 登录,获取token
    String token = erjiAuthService.getToken("admin", "***");

    // 将token注入ThreadLocal线程上下文
    try (ErjiTokenContext erjiTokenContext = new ErjiTokenContext(token)) {
        List<String> entPrefixList = Collections.singletonList("88.***.***");
        erjiEntService.entConfig(entPrefixList);
    }
}

注意:如果在内部实现中新开了线程,一定要记得在新的线程中重新创建TokenContext
String token = ErjiTokenContext.getToken();
ThreadUtil.execAsync(()->{
try (ErjiTokenContext erjiTokenContext = new ErjiTokenContext(token)) {
这里可以调用其他需要token的http接口
}
});

使用TokenInterceptor动态往请求中注入token

  • 继承BasePathMatchInterceptor编写拦截处理器
@Component
public class TimeStampInterceptor extends BasePathMatchInterceptor {

    @Override
    public Response doIntercept(Chain chain) throws IOException {
        Request request = chain.request();
        HttpUrl url = request.url();
        long timestamp = System.currentTimeMillis();
        HttpUrl newUrl = url.newBuilder()
                .addQueryParameter("timestamp", String.valueOf(timestamp))
                .build();
        Request newRequest = request.newBuilder()
                .url(newUrl)
                .build();
        return chain.proceed(newRequest);
    }
}
  • 使用@Intercept注解指定要使用的拦截器
@RetrofitClient(baseUrl = "${test.baseUrl}")
@Intercept(handler = TimeStampInterceptor.class, include = {"/api/**"}, exclude = "/api/test/savePerson")
@Intercept(handler = TimeStamp2Interceptor.class) // 需要多个,直接添加即可
public interface HttpApi {

    @GET("person")
    Result<Person> getPerson(@Query("id") Long id);

    @POST("savePerson")
    Result<Person> savePerson(@Body Person person);
}

retrofit-spring-boot-starter的其它特性

自定义httpClient
  • 注入okHttpClient:实现接口SourceOkHttpClientRegistrar并将类注册为Bean。
@Slf4j
@Component
public class CustomSourceOkHttpClientRegistrar implements SourceOkHttpClientRegistrar {
    @Override
    public void register(SourceOkHttpClientRegistry registry) {
        OkHttpClient defaultOkHttpClient = new OkHttpClient.Builder()
                .connectTimeout(0, TimeUnit.SECONDS)
                .readTimeout(0, TimeUnit.SECONDS)
                .writeTimeout(0, TimeUnit.SECONDS)
                .callTimeout(0, TimeUnit.SECONDS)
                .build();
        // 替换默认的SourceOkHttpClient
        registry.register(Constants.DEFAULT_SOURCE_OK_HTTP_CLIENT, defaultOkHttpClient);
        
        
         // 添加新的SourceOkHttpClient
         registry.register("testSourceOkHttpClient", new OkHttpClient.Builder()
                 .addInterceptor(chain -> {
                    log.info("============使用testSourceOkHttpClient=============");
                    return chain.proceed(chain.request());
                 })
                 .build());

    }
}
  • 通过@RetrofitClient.sourceOkHttpClient指定当前接口要使用的OkHttpClient
@RetrofitClient(baseUrl = "${test.baseUrl}", sourceOkHttpClient = "testSourceOkHttpClient")
public interface CustomOkHttpTestApi {

    @GET("person")
    Result<Person> getPerson(@Query("id") Long id);
}
方法上的扩展注解
  • @Logging
  • @Retry
  • @SentinelDegrade

需要在配置中启用sentinel熔断降级策略并在pom.xml中引入依赖

启用sentinel熔断降级策略并在全局关闭,在需要的方法上使用注解@SentinelDegrade

retrofit:
  # 熔断降级配置
  degrade:
    # 熔断降级类型。默认none,表示不启用熔断降级
    degrade-type: sentinel
    # 全局sentinel降级配置
    global-sentinel-degrade:
      # 是否开启
      enable: false
      # ...其他sentinel全局配置

引入pom依赖

<dependency>
   <groupId>com.alibaba.csp</groupId>
   <artifactId>sentinel-core</artifactId>
   <version>1.6.3</version>
</dependency>
  • @InterceptMark: 可用于实现自定义拦截器注解
可扩展的接口
  • 通过ErrorDecoder接口/@RetrofitClient#errorDecoder实现自定义错误解码器
  • 通过集成ServiceInstanceChooser/@RetrofitClient#serviceId/@RetrofitClient#path实现微服务之间的http调用

用户可以自行实现ServiceInstanceChooser接口,完成服务实例的选取逻辑,并将其配置成Spring Bean。对于Spring Cloud 应用,组件提供了SpringCloudServiceInstanceChooser实现,用户只需将其配置成Spring Bean即可。

@Bean
@Autowired
public ServiceInstanceChooser serviceInstanceChooser(LoadBalancerClient loadBalancerClient) {
    return new SpringCloudServiceInstanceChooser(loadBalancerClient);
}
  • 通过SourceOkHttpClientRegistrar接口/@RetrofitClient#sourceHttpClient注入自定义 ok http client
  • 通过集成BasePathMatchInterceptor类/@Intercept实现拦截器
  • 继承LoggingInterceptor实现日志自定义功能
@Bean
public LoggingInterceptor loggingInterceptor(RetrofitProperties retrofitProperties){
    return new AggregateLoggingInterceptor(retrofitProperties.getGlobalLog());
}
  • 通过GlobalInterceptor实现全局拦截
  • 通过NetworkInterceptor实现全局网络拦截
  • 通过CallAdapter.Factory/@RetrofitClient.callAdapterFactories实现自定义调用适配器
  • 通过Converter.Factory/@RetrofitClient.converterFactories实现自定义数据适配器

Retrofit调用适配器

  • String:将Response Body适配成String返回。
  • 基础类型(Long/Integer/Boolean/Float/Double):将Response Body适配成上述基础类型
  • 任意Java类型: 将Response Body适配成对应的Java对象返回
  • CompletableFuture<T>: 将Response Body适配成CompletableFuture<T>对象返回
  • Void: 不关注返回类型可以使用Void
  • Response<T>: 将Response适配成Response<T>对象返回
  • Call<T>: 不执行适配处理,直接返回Call<T>对象
  • Mono<T>Project Reactor响应式返回类型
  • Single<T>Rxjava响应式返回类型(支持Rxjava2/Rxjava3
  • CompletableRxjava响应式返回类型,HTTP请求没有响应体(支持Rxjava2/Rxjava3
@RetrofitClient(baseUrl = "${test.baseUrl}")
public interface HttpApi {

   @POST("getString")
   String getString(@Body Person person);

   @GET("person")
   Result<Person> getPerson(@Query("id") Long id);

   @GET("person")
   CompletableFuture<Result<Person>> getPersonCompletableFuture(@Query("id") Long id);

   @POST("savePerson")
   Void savePersonVoid(@Body Person person);

   @GET("person")
   Response<Result<Person>> getPersonResponse(@Query("id") Long id);

   @GET("person")
   Call<Result<Person>> getPersonCall(@Query("id") Long id);

   @GET("person")
   Mono<Result<Person>> monoPerson(@Query("id") Long id);
   
   @GET("person")
   Single<Result<Person>> singlePerson(@Query("id") Long id);
   
   @GET("ping")
   Completable ping();
}

Retrofit数据适配器

通常情况下我们选择的是Scalars和Jackson,其中Scalars代表的是基础数据类型的解析,而Jackson代表的是JSON格式的解析。

遇到过的问题

  • Jackson ObjectMapper序列化问题 由于第三方接口返回值和我们通常的约定不一样,导致ReponseBody序列化为Java对象时出现以下问题
com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot coerce empty String ("") to element of `java.util.ArrayList<cn.xxx.gymy.app.openapi.model.dto.gymy.code.GymyCodeDto>` (but could if coercion was enabled using `CoercionConfig`)\
at [Source: (okhttp3.ResponseBody$BomAwareReader); line: 3, column: 9] (through reference chain: cn.xxx.gymy.app.openapi.model.dto.gymy.GymyResultDto["data"])

输入值为

{"code":0,"message":"error","success":false,"data":""}

解决方案:
第一步配置ObjectMapper

@Configuration
public class ObjectMapperConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return objectMapper;
    }
}

第二步在RetrofitScan配置类中增加自定义BeanJacksonConverterFactory

@Configuration
@RetrofitScan("cn.yzw.gymy.app.openapi.service")
public class RetrofitConfiguration {
    @Resource
    private ObjectMapper objectMapper;

    @Bean
    public customizeJacksonConverterFactory() {
        return JacksonConverterFactory.create(objectMapper);
    }
}
  • 注意ResponseBody,比如errorBody()只能读一次
Service service = retrofit.create(Service.class);

Response<User> response = service.getUser().execute();
log.info("reponse code={}, message={}, errorBody={}, body={}",
        response.code(),
        response.message(),
        // 第一次读取errorBody,正常
        response.errorBody() != null ? response.errorBody().string() : "null",
        response.body() != null ? "notNull" : "null");

if (response.errorBody() != null) {
    // Look up a converter for the Error type on the Retrofit instance.
    Converter<ResponseBody, ErrorBody> errorConverter =
            retrofit.responseBodyConverter(ErrorBody.class, new Annotation[0]);
    // Convert the error body into our Error type.
    // 第二次再读取就会报错了
    ErrorBody errorBody = errorConverter.convert(response.errorBody());
    if (errorBody != null) {
        System.out.println("ERROR: " + errorBody.message);
    }
}
  • 接口不能返回void, 因为始终是有http reponse返回,如果响应中没值,可能写返回Void值
java.lang.IllegalArgumentException: Service methods cannot return void.
    for method GitHubServiceRetrofitClient.getContributorsVoid

扩展知识点

参考资料