4. 自定义 CallAdapter 用于埋点:返回类型设计、异步回调封装、错误统一捕获与兜底
4.1 本章概述
Retrofit 默认的 CallAdapter 仅支持 Call<T> 与 RxJava 等有限返回类型。在实际埋点场景中,我们希望网络请求返回时自动完成以下三件事:
- 返回类型统一:所有接口返回
ApiResult<T>包裹的数据或错误码。 - 异步回调封装:在
enqueue的 callback 中自动注入埋点上报逻辑,无需每个业务调用处手动调用。 - 错误统一捕获与兜底:网络异常、解析异常、HTTP 错误码等全部落入自定义错误处理器,返回默认值或错误对象,不抛未处理异常。
本章将带领读者实现一个专用于埋点的 CallAdapter.Factory 及其配套的 CallAdapter<R, T>,从头设计返回类型、封装异步回调、构造全局错误兜底。
4.2 返回类型设计
埋点接口通常不关心泛型实体,而是关注请求是否成功、耗时、状态码。但为了复用业务网络层,我们设计一个通用的 ApiResult<T> 封装:
public class ApiResult<T> {
public final int code;
public final String message;
public final T data;
public final boolean isSuccess;
public ApiResult(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.isSuccess = code >= 200 && code < 300;
}
// 快速创建失败实例
public static <T> ApiResult<T> error(int code, String msg) {
return new ApiResult<>(code, msg, null);
}
}
对于埋点专用接口(仅关注上报是否成功),我们额外定义 TrackingResult:
public class TrackingResult {
public final boolean success;
public final long durationMs;
public final String errorDetail;
private TrackingResult(boolean success, long durationMs, String errorDetail) {
this.success = success;
this.durationMs = durationMs;
this.errorDetail = errorDetail;
}
public static TrackingResult succeed(long durationMs) {
return new TrackingResult(true, durationMs, null);
}
public static TrackingResult failed(long durationMs, String err) {
return new TrackingResult(false, durationMs, err);
}
}
设计决策:
TrackingResult 不继承 ApiResult,因为埋点上报的返回值应当保持最小字段,避免污染业务数据。Custom CallAdapter 会将 HTTP 响应自动映射到 TrackingResult 或 ApiResult。
4.3 CallAdapter 核心结构
自定义 CallAdapter 需实现两个泛型:原始返回类型 R(通常为 Call<ApiResult<T>>)和 适配后类型 T(即开发者接口方法返回的类型)。这里我们仅针对返回类型为 Call<TrackingResult> 和 Call<ApiResult<?>> 的接口做适配。
public class TrackingCallAdapter<R> implements CallAdapter<R, Object> {
private final Type responseType;
public TrackingCallAdapter(Type responseType) {
this.responseType = responseType;
}
@Override
public Type responseType() {
return responseType;
}
@Override
public Object adapt(Call<R> call) {
// 返回一个自定义的代理 Call,在 enqueue 时注入埋点回调
return new TrackingCallProxy<>(call);
}
}
TrackingCallProxy 需要包装原始的 call,在 enqueue 时插入埋点逻辑:
public class TrackingCallProxy<R> implements Call<R> {
private final Call<R> delegate;
public TrackingCallProxy(Call<R> delegate) {
this.delegate = delegate;
}
@Override
public void enqueue(final Callback<R> callback) {
long startTime = System.currentTimeMillis();
delegate.enqueue(new Callback<R>() {
@Override
public void onResponse(Call<R> call, Response<R> response) {
long duration = System.currentTimeMillis() - startTime;
if (response.isSuccessful() && response.body() != null) {
// 埋点上报:成功
TrackingReporter.recordSuccess(duration);
callback.onResponse(call, response);
} else {
// 业务层处理非2xx
handleError(call, response, duration, callback);
}
}
@Override
public void onFailure(Call<R> call, Throwable t) {
long duration = System.currentTimeMillis() - startTime;
// 埋点上报:网络异常
TrackingReporter.recordError(t, duration);
// 兜底:构造一个默认的错误 Response 对象
R fallbackBody = createFallbackBody(t);
callback.onResponse(call, Response.success(fallbackBody));
}
private void handleError(Call<R> call, Response<R> response,
long duration, Callback<R> callback) {
TrackingReporter.recordHttpError(response.code(), duration);
R fallbackBody = createFallbackBodyForHttpError(response.code());
callback.onResponse(call, Response.success(fallbackBody));
}
});
}
@SuppressWarnings("unchecked")
private R createFallbackBody(Throwable t) {
// 根据实际返回类型构造默认值
if (responseType == TrackingResult.class) {
return (R) TrackingResult.failed(System.currentTimeMillis(), t.getMessage());
}
// 其他 ApiResult 返回失败对象
return (R) ApiResult.error(-1, t.getMessage());
}
@SuppressWarnings("unchecked")
private R createFallbackBodyForHttpError(int code) {
if (responseType == TrackingResult.class) {
return (R) TrackingResult.failed(System.currentTimeMillis(), "HTTP " + code);
}
return (R) ApiResult.error(code, "HTTP error");
}
// 其他 Call 接口方法直接委托
@Override public Response<R> execute() throws IOException { return delegate.execute(); }
@Override public boolean isExecuted() { return delegate.isExecuted(); }
@Override public void cancel() { delegate.cancel(); }
@Override public boolean isCanceled() { return delegate.isCanceled(); }
@Override public Call<R> clone() { return new TrackingCallProxy<>(delegate.clone()); }
@Override public Request request() { return delegate.request(); }
private Type responseType;
// 通过构造或 setter 注入 responseType
}
4.4 CallAdapter.Factory 实现
工厂类负责判断开发者接口方法的返回类型是否属于我们的适配范围,并创建对应的 CallAdapter。
public class TrackingCallAdapterFactory extends CallAdapter.Factory {
@Override
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
// 只处理 Call<...> 类型
if (getRawType(returnType) != Call.class) {
return null;
}
final Type callInnerType = getParameterUpperBound(0, (ParameterizedType) returnType);
// 如果内部类型是 TrackingResult 或 ApiResult,则使用我们的适配器
if (callInnerType == TrackingResult.class ||
(callInnerType instanceof ParameterizedType &&
getRawType(callInnerType) == ApiResult.class)) {
return new TrackingCallAdapter<>(callInnerType);
}
return null;
}
}
4.5 错误统一捕获与兜底机制
| 异常类型 | 捕获位置 | 兜底行为 |
|---|---|---|
| 网络异常(IOException、SocketTimeoutException) | onFailure | 构造 TrackingResult.failed 或 ApiResult.error(-1, ...) 并通过 callback.onResponse 返回,不抛异常。 |
| HTTP 错误(4xx/5xx) | onResponse 中 !response.isSuccessful() | 解析状态码,构造对应失败对象,同样走 onResponse 回调,业务层无感。 |
| 解析异常(JSON 解析失败) | Retrofit 底层抛 onFailure(包含 JsonSyntaxException) | 由 createFallbackBody 返回默认失败对象,并提供错误消息。 |
| 应用层业务错误(如 code != 200 但 HTTP 200) | 不在此层处理,保留给业务 Callback 自行判断 | 仍正常回调,但业务方可检查 ApiResult.isSuccess。 |
注意:兜底返回的对象并非真正的 HTTP 响应体,而是通过
Response.success(body) 构造的模拟成功响应。这会导致 response.isSuccessful() 始终为 true,业务层无法区分真实成功与兜底成功。解决办法是在 ApiResult 中增加 isFallback 字段,或要求业务层只依赖 ApiResult.code 来判断。
4.6 注册与使用示例
在 Retrofit.Builder 中添加自定义 Factory:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://tracking.example.com/")
.addCallAdapterFactory(new TrackingCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build();
定义埋点接口:
public interface TrackingService {
@POST("track/event")
Call<TrackingResult> sendEvent(@Body Event event);
@GET("track/config")
Call<ApiResult<TrackingConfig>> getConfig();
}
调用时无需任何额外埋点代码:
trackingService.sendEvent(event).enqueue(new Callback<TrackingResult>() {
@Override
public void onResponse(Call<TrackingResult> call, Response<TrackingResult> response) {
TrackingResult result = response.body();
if (result != null && result.success) {
// 业务处理
} else {
// 兜底失败也能进入此处
}
}
@Override
public void onFailure(Call<TrackingResult> call, Throwable t) {
// 此回调不会被调用,因为 onFailure 已被拦截并走兜底 onResponse
}
});
4.7 调试与验证
推荐使用 HttpLoggingInterceptor 观察实际行为:
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging)
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
// 模拟网络超时用于测试
// Thread.sleep(10000);
return chain.proceed(chain.request());
}
})
.build();
观察日志,确认 onFailure 不会被业务层收到,而是被转为 onResponse 并带有兜底对象。
调试技巧:在
TrackingCallProxy.createFallbackBody 中添加断点,可以观察所有异常是否被正确兜底。
4.8 性能与稳定性优化
- 避免反射调用:
createFallbackBody中判断类型尽量用==比较 Class 对象,不使用instanceof链。 - 最小化代理开销:
TrackingCallProxy只重写enqueue和clone,其他方法直接委托原生 Call,减少对象创建。 - 线程安全:
startTime在enqueue调用时记录,不会出现竞态。 - 兜底对象复用:对于无状态的失败对象(如
ApiResult.error(-1, "network")),可设置静态常量避免每次构造。
4.9 常见错误
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
onFailure 仍被业务层收到 | CallAdapter 未正确适配,或返回类型不在工厂处理范围内 | 检查 returnType 的泛型参数是否为 Call<TrackingResult> 等。 |
| 兜底对象字段为 null 导致 NPE | createFallbackBody 未正确处理泛型擦除 | 在工厂中通过 getParameterUpperBound 获取确切的运行时类型,并分别构造。 |
| 埋点上报重复 | 业务层自己也在 onResponse 中调用了埋点 | 约定业务层不再手动上报,由 CallAdapter 统一完成。 |
4.10 整体流程
4.11 总结
通过自定义 CallAdapter,我们实现了以下目标:
- 返回类型统一为
ApiResult或TrackingResult,业务层不必处理多个状态。 - 异步回调中自动注入埋点上报,业务代码零入侵。
- 所有异常均被捕获并返回兜底对象,避免上层收到
onFailure导致崩溃或遗漏处理。