网络请求:Retrofit + OkHttp
使用 Retrofit 声明式接口配合 OkHttp 拦截器构建生产级网络层,涵盖注解、拦截器、超时、统一错误处理与 Callback/RxJava 集成。
学习目标
- 掌握 Retrofit 接口注解 @GET/@POST/@PUT/@DELETE 的写法
- 理解 @Path/@Query/@Field/@Body/@Header 参数绑定的差异
- 能够编写 OkHttp 日志、重试、缓存拦截器并配置超时
- 实现统一错误处理与 Callback/RxJava 两种异步集成方式
学习目标
- 理解 Retrofit 与 OkHttp 的分工:Retrofit 负责「接口声明」,OkHttp 负责「网络执行」。
- 能够独立定义 RESTful 接口,并区分路径参数、查询参数、表单字段、请求体的使用场景。
- 掌握 OkHttp 拦截器机制,编写日志、重试、缓存等通用拦截器。
- 学会配置超时、统一错误处理,并将其与 Callback、RxJava 两种异步风格集成。
Retrofit + OkHttp 网络层实战
1. 引入依赖
在 app/build.gradle 中添加 Retrofit、OkHttp、Gson 转换器与 RxJava 适配器(如需 RxJava 集成):
android {
compileSdk 34
defaultConfig {
minSdk 21
// ...
}
}
dependencies {
// Retrofit:声明式 HTTP 客户端
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
// Gson 转换器:将响应体自动反序列化为 Java 对象
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
// OkHttp:底层传输引擎
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
// 日志拦截器(Debug 友好)
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
// 可选:RxJava3 适配器
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.11.0'
implementation 'io.reactivex.rxjava3:rxjava:3.1.8'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
}
注意:Retrofit 2.11+ 要求 Java 8+。在
android{}中确认compileOptions已开启sourceCompatibility与targetCompatibility为JavaVersion.VERSION_1_8。
2. 数据模型
package com.example.network.model;
import com.google.gson.annotations.SerializedName;
public class User {
@SerializedName("id")
public int id;
@SerializedName("login")
public String login;
@SerializedName("avatar_url")
public String avatarUrl;
@SerializedName("bio")
public String bio;
}
3. 定义 Retrofit 接口
Retrofit 的核心思想:把 HTTP API 描述成一个 Java 接口,框架在运行时生成实现类。
package com.example.network.api;
import com.example.network.model.User;
import java.util.List;
import java.util.Map;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Part;
import retrofit2.http.Path;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;
public interface UserService {
// @Path:将路径占位符替换为参数值
@GET("users/{login}")
Call<User> getUser(@Path("login") String login);
// @Query:拼接为 ?since=10&per_page=30
@GET("users")
Call<List<User>> listUsers(@Query("since") int since,
@Query("per_page") int perPage);
// @QueryMap:批量查询参数
@GET("search/users")
Call<UserSearchResult> search(@QueryMap Map<String, String> options);
// @Body:发送 JSON 请求体(配合 Gson 转换器)
@POST("users")
Call<User> createUser(@Body User user);
// @FormUrlEncoded + @Field:传统表单提交
@FormUrlEncoded
@POST("login")
Call<AuthToken> login(@Field("username") String username,
@Field("password") String password);
// @FieldMap:批量表单字段
@FormUrlEncoded
@POST("profile/update")
Call<ResponseBody> updateProfile(@FieldMap Map<String, String> fields);
// @PUT:更新资源
@PUT("users/{login}")
Call<User> updateUser(@Path("login") String login, @Body User user);
// @DELETE:删除资源
@DELETE("users/{login}")
Call<Void> deleteUser(@Path("login") String login);
// 动态 Header:每个请求单独添加
@GET("me")
Call<User> me(@Header("Authorization") String token);
// 静态 Header:固定值
@Headers({"Accept: application/json", "X-Client: android/1.0"})
@GET("config")
Call<Config> fetchConfig();
// @Multipart:文件上传
@Multipart
@POST("upload")
Call<UploadResult> upload(@Part MultipartBody.Part file,
@Part("description") RequestBody description);
}
参数注解速查:
| 注解 | 用途 | 示例 |
|---|---|---|
@Path |
替换 URL 中的 {xx} |
users/{login} |
@Query |
拼接查询字符串 ?k=v |
?since=10 |
@QueryMap |
批量查询参数 | Map<String,String> |
@Field |
表单字段(需 @FormUrlEncoded) |
username=xxx |
@FieldMap |
批量表单字段 | Map<String,String> |
@Body |
JSON/请求体对象 | 配合转换器 |
@Part |
Multipart 分片 | 文件上传 |
@Header / @Headers |
动态/静态请求头 | Token |
4. 配置 OkHttp 与拦截器
package com.example.network;
import android.content.Context;
import android.util.Log;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.Cache;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
public class OkHttpFactory {
private static final String TAG = "OkHttp";
private static volatile OkHttpClient sClient;
private OkHttpFactory() {}
public static OkHttpClient client(Context context) {
if (sClient == null) {
synchronized (OkHttpFactory.class) {
if (sClient == null) {
sClient = build(context.getApplicationContext());
}
}
}
return sClient;
}
private static OkHttpClient build(Context context) {
// 10 MiB 磁盘缓存
Cache cache = new Cache(context.getCacheDir(), 10 * 1024 * 1024);
// 日志拦截器:Release 包仅打印基本信息
HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
Log.d(TAG, message);
}
});
logging.setLevel(BuildConfig.DEBUG
? HttpLoggingInterceptor.Level.BODY
: HttpLoggingInterceptor.Level.BASIC);
return new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(new RetryInterceptor(3))
.addInterceptor(new CacheStrategyInterceptor())
.addInterceptor(logging)
// 网络层拦截器:在重定向后执行,能拿到最终响应
.addNetworkInterceptor(new CacheStrategyInterceptor())
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
}
/** 失败重试拦截器:仅对网络错误重试,HTTP 4xx/5xx 不重试。 */
static class RetryInterceptor implements Interceptor {
private final int maxRetry;
RetryInterceptor(int maxRetry) { this.maxRetry = maxRetry; }
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
IOException last = null;
for (int i = 0; i <= maxRetry; i++) {
try {
return chain.proceed(request);
} catch (IOException e) {
last = e;
if (i == maxRetry) break;
}
}
throw last;
}
}
/** 缓存策略:在线时缓存最多 60 秒;离线时使用 7 天前的缓存。 */
static class CacheStrategyInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request.Builder rb = chain.request().newBuilder()
.header("Cache-Control", "max-age=60");
if (!NetworkUtil.isOnline(chain)) {
rb.header("Cache-Control", "only-if-cached, max-stale=" + (60 * 60 * 24 * 7));
}
return chain.proceed(rb.build());
}
}
}
应用拦截器 vs 网络拦截器:
addInterceptor注册的是应用拦截器,只调用一次,能看到原始请求;addNetworkInterceptor在重定向/缓存命中后调用,能拿到真实网络响应。缓存策略常同时在两处注册。
5. 构建 Retrofit 实例
package com.example.network;
import android.content.Context;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitClient {
private static final String BASE_URL = "https://api.example.com/";
private static volatile Retrofit sRetrofit;
public static Retrofit retrofit(Context context) {
if (sRetrofit == null) {
synchronized (RetrofitClient.class) {
if (sRetrofit == null) {
OkHttpClient ok = OkHttpFactory.client(context);
sRetrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(ok)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build();
}
}
}
return sRetrofit;
}
@SuppressWarnings("unchecked")
public static <T> T service(Context context, Class<T> clazz) {
return retrofit(context).create(clazz);
}
}
6. 同步 / 异步 Callback 调用
// 同步:必须在子线程
new Thread(() -> {
try {
retrofit2.Response<User> resp = RetrofitClient.service(this, UserService.class)
.getUser("octocat")
.execute();
if (resp.isSuccessful() && resp.body() != null) {
Log.i("NET", "user=" + resp.body().login);
} else {
Log.e("NET", "code=" + resp.code());
}
} catch (IOException e) {
Log.e("NET", "network error", e);
}
}).start();
// 异步 Callback:回调在主线程执行
RetrofitClient.service(this, UserService.class)
.getUser("octocat")
.enqueue(new retrofit2.Callback<User>() {
@Override
public void onResponse(retrofit2.Call<User> call, retrofit2.Response<User> response) {
if (response.isSuccessful()) {
showUser(response.body());
} else {
showError(parseError(response));
}
}
@Override
public void onFailure(retrofit2.Call<User> call, Throwable t) {
showError(t.getMessage());
}
});
7. 统一错误处理
服务端通常返回统一错误结构,如 {"code": 1001, "message": "invalid token"}。我们封装一个工具类,把非 2xx 响应解析为 ApiException:
public class ApiException extends RuntimeException {
public final int code;
public ApiException(int code, String message) { super(message); this.code = code; }
}
public final class ErrorUtils {
public static <T> ApiException parse(retrofit2.Response<T> response) {
String raw = null;
try (ResponseBody body = response.errorBody()) {
if (body != null) raw = body.string();
} catch (IOException ignored) {}
try {
ApiError err = new Gson().fromJson(raw, ApiError.class);
return new ApiException(err.code, err.message);
} catch (Exception e) {
return new ApiException(response.code(), "HTTP " + response.code());
}
}
public static class ApiError {
int code;
String message;
}
}
8. 与 RxJava 集成
加上 adapter-rxjava3 后,接口可直接返回 Observable / Single / Flowable:
public interface UserService {
@GET("users/{login")
io.reactivex.rxjava3.core.Single<User> rxUser(@Path("login") String login);
}
调用示例:
RetrofitClient.service(this, UserService.class)
.rxUser("octocat")
.subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.io())
.observeOn(io.reactivex.rxjava3.android.schedulers.AndroidSchedulers.mainThread())
.subscribe(user -> showUser(user), this::showError);
RxJava 适合复杂流式场景;Callback 简单轻量。新项目还可考虑用 Kotlin 协程(见 Part 8)。
常见坑与最佳实践
baseUrl必须以/结尾,否则启动时抛IllegalArgumentException。@GET("users")的相对路径不得以/开头,二者拼合规则是 baseUrl 末尾/之后的内容被相对路径替换。errorBody()只能读一次。parse()中使用 try-with-resources 关闭后即不可再读,所以务必在一次性读取中完成解析。- 拦截器顺序很重要:日志拦截器放在应用拦截器链末端更易阅读;重试拦截器放在最外层避免重复日志;缓存拦截器同时挂网络层才生效。
- 生产环境不要用
Level.BODY:会打印敏感 Token、用户隐私字段,应改用BASIC或NONE,并配合BuildConfig.DEBUG。 - OkHttpClient 应全局单例。每 new 一个 Client 都会新建连接池、线程池、缓存,导致内存泄漏与性能下降。
execute()严禁在主线程调用,否则抛NetworkOnMainThreadException。enqueue()内部已切到工作线程,回调通过主线程Handler切回。- 取消请求:Activity 销毁时调用
Call.cancel(),避免回调更新已销毁的 UI 造成崩溃。 - HTTPS 证书 pinning:可使用
CertificatePinner防止中间人攻击,但需配套完善的证书更新策略。 - 避免对 5xx 也无脑重试:可能加重服务端雪崩;推荐只重试网络异常或 429 Too Many Requests。
章节小结
- Retrofit 把 HTTP API 描述成接口,OkHttp 在底层执行请求;二者配合既声明式又可观测。
- 通过
@Path/@Query/@Field/@Body/@Part/@Header完成所有参数绑定场景。 - OkHttp 拦截器分应用层和网络层,分别适合日志、重试、缓存、鉴权。
- 通过
errorBody()解析 + 自定义ApiException实现统一错误处理。 - 异步既可用 Callback,也可通过 RxJava 适配器返回响应式流。
下一章预告
网络拿到的是文本流,需要解析成 Java 对象才能使用。下一章《JSON 解析:Gson 与 org.json》将系统讲解 Gson 反序列化、@SerializedName、TypeToken 泛型解析、嵌套与集合,以及 org.json 手动解析的适用场景。