前言
在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拦截器,有以下几点优点:
- 完整覆盖 :通过
RealNameAuthInterceptor
统一处理所有网络请求的4406状态码 - 零侵入性:无需修改现有业务代码,在HttpsHelper中统一配置
- 避免重复处理:在HTTP层面统一拦截,避免在每个请求点重复处理
- 架构优雅 :所有Service都使用统一的OkHttpClient实例
具体实施流程图:
而完整实施这套方案,有以下几个技术细节:
(终于进入正文了)
关键技术细节
分几步走:
- 统一的OkHttpClient实例,添加应用拦截器
- 通过回调接口模式,解决循环依赖问题。
- ResponseBody流管理
- 重复Toast问题解决
- 优化弹窗管理,避免同时显示多个弹窗
添加应用拦截器
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()
方法只能调用一次,这是因为:
- 底层实现机制 :
ResponseBody
的string()
方法内部使用了BufferedSource
流。 - 流的特性:流是单向的,一旦读取完毕就会被关闭,无法重复读取。
- 内存管理:OkHttp为了内存效率,不会缓存整个响应体内容。
源码分析
- ResponseBody.string() 方法实现
java
public final String string() throws IOException {
return new String(bytes(), charset().name());
}
- ResponseBody.bytes() 方法实现
java
public final byte[] bytes() throws IOException {
// ...
BufferedSource source = source();
byte[] bytes;
try {
bytes = source.readByteArray();
} finally {
Util.closeQuietly(source); // 关键:默默关闭资源
}
// ...
return bytes;
}
- 资源关闭机制
Util.closeQuietly()
方法:
java
public static void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Exception ignored) {
}
}
}
- RealBufferedSource.close() 实现
java
@Override
public void close() throws IOException {
if (closed) return;
closed = true;
source.close();
buffer.clear();
}
- 第二次调用时的异常
当再次调用 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)**的原因:
- 内存优化:响应体可能很大,不会直接保存到内存中
- 资源管理:只持有数据流连接,需要时才从服务器获取
- 使用场景:实际开发中重复读取数据的可能性很小
解决方案:重新构建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
,导致:
- 重复错误处理:业务层继续按照错误流程处理
- 弹出错误Toast:用户看到不必要的错误提示
- 用户体验问题:实名认证弹窗和错误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()
}
这种设计模式的核心优势在于:
- 模块解耦:基建模块只定义接口,不依赖具体实现
- 灵活扩展:上层模块可以根据业务需求自定义实现
- 统一规范:为不同类型的统一处理定义一致的接口规范
架构设计原则
在实施统一错误处理方案时,应遵循以下架构设计原则:
- 单一职责原则:每个拦截器只负责一个特定功能
- 开闭原则:对扩展开放,对修改封闭
- 依赖倒置原则:高层模块不依赖低层模块,都依赖抽象
- 接口隔离原则:接口设计简洁,只包含必要的方法
基建规范
总结出以下基建规范,遵循这些规范,可提高代码质量、可扩展性、可维护性:
- 优先使用拦截器方案:在HTTP层面统一处理,覆盖所有网络请求
- 统一OkHttpClient实例:确保所有Service使用相同的网络配置
- 合理配置拦截器顺序:按照业务需求安排拦截器的执行顺序
统一OkHttpClient的重要性
通过HttpsHelper.getInstance().getCustomOkHttpClient()
为所有Service提供统一的OkHttpClient实例,这不仅仅是技术实现,更是架构设计的重要体现:
架构层面的价值:
- 配置一致性:确保所有网络请求使用相同的超时、SSL、代理配置
- 拦截器生效:只有使用统一实例,拦截器才能对所有请求生效
- 性能优化:连接池复用,减少资源消耗
- 维护简化:网络配置修改只需在一个地方进行
设计模式-分析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. 可测试性
- 每个拦截器可以独立测试
- 便于单元测试和集成测试
技术债务的解决思路
- 建立了统一处理模式:为其他状态码处理提供了参考
- 优化了网络架构:统一了网络配置管理
- 提升了代码质量:减少了重复代码和维护成本
参考资料与延伸阅读
- OkHttp官方文档 - Interceptors - 拦截器的官方权威文档,深入理解拦截器的工作原理和使用方法
- 为何 response.body().string() 只能调用一次? - 深入解析OkHttp响应体流管理机制,理解底层实现原理
- Retrofit官方文档 - 现代Android网络库设计理念,学习如何构建优雅的网络层
- Kotlin协程官方文档 - 异步编程的现代解决方案,掌握协程在网络请求中的应用