Android网络层架构:统一错误处理的问题分析到解决方案与设计实现

前言

在Android项目开发中,我们经常遇到需要统一处理某些特定状态码的场景。

本文分享一个项目中遇到的 4406状态码(实名认证) 处理不统一问题,从问题分析到完整解决方案,提供一套可复用的架构设计模式。

目录

问题分析

在项目开发过程中,我发现4406状态码(实名认证)的处理存在以下核心问题:

问题类型 具体表现 影响程度
处理逻辑分散 在多个地方需要重复添加相同的处理逻辑 🔴 高
容易遗漏 新增接口时容易忘记添加4406处理 🔴 高
代码冗余 相同的处理逻辑在多处重复 🟡 中
网络框架复杂 项目中使用了多种网络请求方式,难以统一处理 🔴 高
循环依赖 网络层需要调用UI层,形成模块间循环依赖 🔴 高

原使用的解决方案:

针对不同的网络请求方式,单独添加不同框架的回调处理机制。

而项目经过了约10年的漫长历史,已集成了多套网络框架,导致处理逻辑分散、容易遗漏。

包括:传统HTTP请求、Retrofit回调、OkHttp实例、MVP模式、MVVM模式、特定业务组件独立封装网络请求等。

比如部分框架的回调处理逻辑,如:

不同框架的回调处理机制

复制代码
// MVVM模式
fun <T> ViewModel.request(
    block: suspend () -> ResultModel<T>,
    success: (T) -> Unit = {},
    error: (AppException) -> Unit = {}, // ⚠️ 需要在这里处理4406
    complete: () -> Unit = {}
)

// Retrofit传统回调
public abstract class BaseRetrofitResponseCallback<T> {
    public abstract void onSuccess(T response);
    public abstract void onFailure(AppException exception); // ⚠️ 需要在这里处理4406
}

// 原生OkHttp
public interface OnHttpRequestListener {
    void onSuccess(String response); // ⚠️ 需要解析JSON检查4406
    void onFailure(String error);
}

// MVP模式
public class YSPresenter {
    protected void onNetworkError(AppException exception) {
        // ⚠️ 需要在这里处理4406
    }
}

经过彻底排查,还定位到有4个接口绕过了标准的ResultModel解析流程,导致即便在统一处理逻辑中处理了4406,但实际请求中仍遗漏处理4406。

解决方案

采用响应拦截器的解决方案,通过采用添加OkHttp拦截器,有以下几点优点:

  1. 完整覆盖 :通过RealNameAuthInterceptor统一处理所有网络请求的4406状态码
  2. 零侵入性:无需修改现有业务代码,在HttpsHelper中统一配置
  3. 避免重复处理:在HTTP层面统一拦截,避免在每个请求点重复处理
  4. 架构优雅 :所有Service都使用统一的OkHttpClient实例
    具体实施流程图:

而完整实施这套方案,有以下几个技术细节:

(终于进入正文了)

关键技术细节

分几步走:

  1. 统一的OkHttpClient实例,添加应用拦截器
  2. 通过回调接口模式,解决循环依赖问题。
  3. ResponseBody流管理
  4. 重复Toast问题解决
  5. 优化弹窗管理,避免同时显示多个弹窗

添加应用拦截器

OkHttpClient.Builder.addInterceptor()是OkHttp框架中的核心方法,用于添加应用拦截器:

核心代码:

复制代码
OkHttpClient.Builder builder = new OkHttpClient.Builder();
        
// 1. 添加日志拦截器(调试时使用)
if (BuildConfig.DEBUG) {
    builder.addInterceptor(logInterceptor);
}

// 2. 添加签名拦截器(请求参数加密)
builder.addInterceptor(new Interceptor() {
    // 为POST请求添加签名头信息
});

// 3. 添加实名认证拦截器(统一处理4406状态码)
builder.addInterceptor(new RealNameAuthInterceptor());

// 4. 构建OkHttpClient实例
OkHttpClient mClient = builder.build();

然后通过HttpsHelper.getInstance().getCustomOkHttpClient()为所有Service提供统一的OkHttpClient实例。

以此,确保所有网络请求都会经过RealNameAuthInterceptor,网络配置修改只需在一个地方进行。

循环依赖问题与回调接口模式

问题分析

在Android项目架构中,遇到模块间循环依赖的问题:

  • 网络层位于基建模块(如common、base模块)
  • 认证弹窗是UI层(位于app模块)
  • 基础模块不能直接调用app模块代码,否则会形成循环依赖

架构依赖关系:

复制代码
app模块 → common模块 → network模块
   ↑         ↑
   └─────────┘ (不能形成循环依赖)
解决方案: 回调接口模式

通过在基建模块中定义回调接口,提供给上层模块自定义实现的方式,解决循环依赖问题:

复制代码
/**
 * 位于base模块定义,并在base模块使用
 */
interface RealNameAuthHandler {
    fun handleRealNameAuth()
}

/**
 * 全局4406处理器
 */
private var realNameAuthHandler: RealNameAuthHandler? = null

/**
 * 注册4406处理器 - 在Application中调用
 */
fun setRealNameAuthHandler(handler: RealNameAuthHandler) {
    realNameAuthHandler = handler
}

/**
 * 获取4406处理器
 */
fun getRealNameAuthHandler(): RealNameAuthHandler? {
    return realNameAuthHandler
}

// 在app模块中实现
override fun onCreate() {
    super.onCreate()

    // 注册4406处理器
    setRealNameAuthHandler(object : RealNameAuthHandler {
        override fun handleRealNameAuth() {
            // 处理实名认证逻辑
            val currentActivity = AppManager.getCurrentActivity()
            if (currentActivity is Activity && !currentActivity.isFinishing) {
                val decorView = currentActivity.window.decorView
                val extra = BannerAndModelBean().apply {
                    extraParam = ""
                }
                
                // 在主线程中显示弹窗
                Handler(Looper.getMainLooper()).post {
                    RealAuthenticationPop.showRealAuthenticationPop(
                        currentActivity, 
                        decorView, 
                        extra
                    )
                }
            }
        }
        })
}

ResponseBody流管理

问题现象

当调用 response.body().string()后原response再次调用,会抛出异常:

复制代码
java.lang.IllegalStateException: closed
原因总结

在OkHttp的拦截器中,response.body().string()方法只能调用一次,这是因为:

  1. 底层实现机制ResponseBodystring()方法内部使用了BufferedSource流。
  2. 流的特性:流是单向的,一旦读取完毕就会被关闭,无法重复读取。
  3. 内存管理:OkHttp为了内存效率,不会缓存整个响应体内容。
源码分析
  1. ResponseBody.string() 方法实现
java 复制代码
public final String string() throws IOException {
  return new String(bytes(), charset().name());
}
  1. ResponseBody.bytes() 方法实现
java 复制代码
public final byte[] bytes() throws IOException {
  // ...
  BufferedSource source = source();
  byte[] bytes;
  try {
    bytes = source.readByteArray();
  } finally {
    Util.closeQuietly(source);  // 关键:默默关闭资源
  }
  // ...
  return bytes;
}
  1. 资源关闭机制

Util.closeQuietly() 方法:

java 复制代码
public static void closeQuietly(Closeable closeable) {
  if (closeable != null) {
    try {
      closeable.close();
    } catch (RuntimeException rethrown) {
      throw rethrown;
    } catch (Exception ignored) {
    }
  }
}
  1. RealBufferedSource.close() 实现
java 复制代码
@Override
public void close() throws IOException {
  if (closed) return;
  closed = true;
  source.close();
  buffer.clear();
}
  1. 第二次调用时的异常

当再次调用 string() 时,会执行到 RealBufferedSource.readByteArray()

java 复制代码
@Override
public byte[] readByteArray() throws IOException {
  buffer.writeAll(source);
  return buffer.readByteArray();
}

writeAll() 方法中:

java 复制代码
@Override
public long writeAll(Source source) throws IOException {
  // ...
  for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) {
    totalBytesRead += readCount;
  }
  return totalBytesRead;
}

最终在 source.read() 方法中检查到资源已关闭:

java 复制代码
@Override
public long read(Buffer sink, long byteCount) throws IOException {
  // ...
  if (closed) throw new IllegalStateException("closed");
  // ...
  return buffer.read(sink, toRead);
}
总结其设计原理

OkHttp 将 ResponseBody 设计为**一次性流(one-shot)**的原因:

  1. 内存优化:响应体可能很大,不会直接保存到内存中
  2. 资源管理:只持有数据流连接,需要时才从服务器获取
  3. 使用场景:实际开发中重复读取数据的可能性很小

解决方案:重新构建ResponseBody

复制代码
override fun intercept(chain: Interceptor.Chain): Response {
    val response = chain.proceed(chain.request())
   
    if (response.isSuccessful) {
        try {
            val responseBody = response.body
            if (responseBody != null) {
                // 读取原始响应体内容
                val originalContent = responseBody.string()
                
                // 解析JSON检查4406状态码
                val jsonObject = JSONObject(originalContent)
                val code = jsonObject.optInt("code", -1)
                
                if (code == 4406) {
                   ...
                    
                    return response.newBuilder()
                        .body(newResponseBody)
                        .build()
                } else {
                    // 重新构建原始响应体
                    val newResponseBody = ResponseBody.create(
                        responseBody.contentType(),
                        originalContent
                    )
                    
                    return response.newBuilder()
                        .body(newResponseBody)
                        .build()
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    
    return response
}

重复Toast问题解决

问题

当拦截器处理了4406状态码后,如果不修改响应体内容,后续的业务逻辑仍然会检测到code != 200,导致:

  1. 重复错误处理:业务层继续按照错误流程处理
  2. 弹出错误Toast:用户看到不必要的错误提示
  3. 用户体验问题:实名认证弹窗和错误Toast同时出现
解决方案:修改响应体内容
kotlin 复制代码
/**
 * 修改4406响应,避免后续错误处理
 * @param originalContent 原始响应内容
 * @return 修改后的响应内容
 */
private fun modifyResponse4406(originalContent: String): String {
    return try {
        val jsonObject = JSONObject(originalContent)
        // msg 改为空,就不会弹出toast提示了
        jsonObject.put("msg", "")
        jsonObject.toString()
    } catch (e: JSONException) {
        // 如果JSON解析失败,返回原始字符串
        originalContent
    }
}

总结与延伸思考

扩展接口设计

基于回调接口模式的解决方案,我们可以将其扩展到其他类似的统一处理场景:

kotlin 复制代码
/**
 * 登录状态处理接口
 * 用于处理token过期、登录失效等场景
 */
interface LoginStateHandler {
    fun handleTokenExpired()
    fun handleLoginRequired()
}

/**
 * 网络异常处理接口
 * 用于处理网络错误、服务器维护等场景
 */
interface NetworkErrorHandler {
    fun handleNetworkError(errorCode: Int, message: String)
    fun handleServerMaintenance()
}

这种设计模式的核心优势在于:

  • 模块解耦:基建模块只定义接口,不依赖具体实现
  • 灵活扩展:上层模块可以根据业务需求自定义实现
  • 统一规范:为不同类型的统一处理定义一致的接口规范

架构设计原则

在实施统一错误处理方案时,应遵循以下架构设计原则:

  1. 单一职责原则:每个拦截器只负责一个特定功能
  2. 开闭原则:对扩展开放,对修改封闭
  3. 依赖倒置原则:高层模块不依赖低层模块,都依赖抽象
  4. 接口隔离原则:接口设计简洁,只包含必要的方法

基建规范

总结出以下基建规范,遵循这些规范,可提高代码质量、可扩展性、可维护性:

  1. 优先使用拦截器方案:在HTTP层面统一处理,覆盖所有网络请求
  2. 统一OkHttpClient实例:确保所有Service使用相同的网络配置
  3. 合理配置拦截器顺序:按照业务需求安排拦截器的执行顺序

统一OkHttpClient的重要性

通过HttpsHelper.getInstance().getCustomOkHttpClient()为所有Service提供统一的OkHttpClient实例,这不仅仅是技术实现,更是架构设计的重要体现:

架构层面的价值:

  1. 配置一致性:确保所有网络请求使用相同的超时、SSL、代理配置
  2. 拦截器生效:只有使用统一实例,拦截器才能对所有请求生效
  3. 性能优化:连接池复用,减少资源消耗
  4. 维护简化:网络配置修改只需在一个地方进行

设计模式-分析OKHttp拦截器的责任链模式

OkHttp的拦截器机制采用了责任链模式(Chain of Responsibility Pattern),这是一种行为型设计模式。在该模式中,多个处理器对象组成一条链,请求沿着链传递,直到被某个处理器处理。

复制代码
// 拦截器执行顺序的设计思考
builder.addInterceptor(logInterceptor)        // 1. 日志记录
      .addInterceptor(signInterceptor)        // 2. 签名加密
      .addInterceptor(realNameAuthInterceptor) // 3. 实名认证处理

官方文档

A call to chain.proceed(request) is a critical part of each interceptor's implementation. This simple-looking method is where all the HTTP work happens, producing a response to satisfy the request. If chain.proceed(request) is being called more than once previous response bodies must be closed.

Interceptors can be chained. Suppose you have both a compressing interceptor and a checksumming interceptor: you'll need to decide whether data is compressed and then checksummed, or checksummed and then compressed. OkHttp uses lists to track interceptors, and interceptors are called in order.
对 chain.proceed(request) 的调用是每个拦截器实现的关键部分。这个看起来简单的方法是所有 HTTP 工作发生的地方,生成响应以满足请求。如果 chain.proceed(request) 被多次调用,则必须关闭之前的响应正文。

拦截器都可以被链接。假设您同时有一个压缩拦截器和一个校验和拦截器:您需要决定是压缩数据然后进行校验和计算,还是校验和计算然后压缩数据。OkHttp 使用列表来跟踪拦截器,拦截器是按顺序调用的

核心接口定义

1. Interceptor 接口

java 复制代码
public interface Interceptor {
  Response intercept(Chain chain) throws IOException;
}

2. Chain 接口

java 复制代码
public interface Chain {
  Request request();
  Response proceed(Request request) throws IOException;
}
责任链模式的核心实现

拦截器执行顺序

拦截器的执行顺序遵循**先进后出(FILO)**的原则:

复制代码
请求发送:Interceptor1 → Interceptor2 → Interceptor3 → 网络层
响应接收:Interceptor3 ← Interceptor2 ← Interceptor1 ← 网络层
责任链模式的关键机制

chain.proceed() 方法

这是责任链模式的核心,每个拦截器通过调用chain.proceed()将请求传递给下一个拦截器:

java 复制代码
// 伪代码展示责任链的执行流程
public Response intercept(Chain chain) throws IOException {
    // 前置处理
    Request request = chain.request();
    // 可以修改请求
    
    // 关键:调用下一个拦截器
    Response response = chain.proceed(request);
    
    // 后置处理
    // 可以修改响应
    
    return response;
}

请求和响应的传递

java 复制代码
// 请求传递:每个拦截器都可以修改请求
Request modifiedRequest = request.newBuilder()
    .addHeader("Authorization", "Bearer token")
    .build();

// 响应传递:每个拦截器都可以修改响应
Response modifiedResponse = response.newBuilder()
    .body(newResponseBody)
    .build();
责任链模式的执行流程

请求阶段

复制代码
1. 应用拦截器1(日志记录)
   ↓
2. 应用拦截器2(添加签名)
   ↓
3. 应用拦截器3(实名认证检查)
   ↓
4. 网络拦截器(缓存处理)
   ↓
5. 实际网络请求

响应阶段

复制代码
5. 实际网络响应
   ↑
4. 网络拦截器(缓存处理)
   ↑
3. 应用拦截器3(实名认证处理)
   ↑
2. 应用拦截器2(响应处理)
   ↑
1. 应用拦截器1(日志记录)
责任链模式的优势

1. 解耦合

  • 每个拦截器只负责自己的职责
  • 拦截器之间相互独立,易于维护

2. 可扩展性

  • 可以轻松添加新的拦截器
  • 不需要修改现有代码

3. 灵活性

  • 可以动态调整拦截器顺序
  • 可以根据条件启用/禁用拦截器

4. 可测试性

  • 每个拦截器可以独立测试
  • 便于单元测试和集成测试

技术债务的解决思路

  1. 建立了统一处理模式:为其他状态码处理提供了参考
  2. 优化了网络架构:统一了网络配置管理
  3. 提升了代码质量:减少了重复代码和维护成本

参考资料与延伸阅读

相关推荐
hdsoft_huge4 小时前
SpringBoot 与 JPA 整合全解析:架构优势、应用场景、集成指南与最佳实践
java·spring boot·架构
爬虫程序猿5 小时前
利用爬虫按关键字搜索淘宝商品实战指南
android·爬虫
顾北川_野6 小时前
Android ttyS2无法打开该如何配置 + ttyS0和ttyS1可以
android·fpga开发
DoraBigHead6 小时前
你写前端按钮,他们扛服务器压力:搞懂后端那些“黑话”!
前端·javascript·架构
isNotNullX7 小时前
数据中台架构解析:湖仓一体的实战设计
java·大数据·数据库·架构·spark
Kookoos8 小时前
ABP VNext + .NET Minimal API:极简微服务快速开发
后端·微服务·架构·.net·abp vnext
码字的字节9 小时前
深入理解Transformer架构:从理论到实践
深度学习·架构·transformer
bug攻城狮9 小时前
Alloy VS Promtail:基于 Loki 的日志采集架构对比与选型指南
运维·架构·grafana·数据可视化
代码改变世界ctw9 小时前
ARM汇编编程(AArch64架构)课程 - 第5章函数调用规范
汇编·arm开发·架构