A
第 20 章JAVA45 分钟

网络请求: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)。

常见坑与最佳实践

  1. baseUrl 必须以 / 结尾,否则启动时抛 IllegalArgumentException。@GET("users") 的相对路径不得以 / 开头,二者拼合规则是 baseUrl 末尾 / 之后的内容被相对路径替换。
  2. errorBody() 只能读一次。parse() 中使用 try-with-resources 关闭后即不可再读,所以务必在一次性读取中完成解析。
  3. 拦截器顺序很重要:日志拦截器放在应用拦截器链末端更易阅读;重试拦截器放在最外层避免重复日志;缓存拦截器同时挂网络层才生效。
  4. 生产环境不要用 Level.BODY:会打印敏感 Token、用户隐私字段,应改用 BASIC 或 NONE,并配合 BuildConfig.DEBUG。
  5. OkHttpClient 应全局单例。每 new 一个 Client 都会新建连接池、线程池、缓存,导致内存泄漏与性能下降。
  6. execute() 严禁在主线程调用,否则抛 NetworkOnMainThreadException。enqueue() 内部已切到工作线程,回调通过主线程 Handler 切回。
  7. 取消请求:Activity 销毁时调用 Call.cancel(),避免回调更新已销毁的 UI 造成崩溃。
  8. HTTPS 证书 pinning:可使用 CertificatePinner 防止中间人攻击,但需配套完善的证书更新策略。
  9. 避免对 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 手动解析的适用场景。