Retrofit 2 - 在 API 级别上添加头部信息的简洁方式

18

我的Retrofit 2(当前版本为2.0.2)客户端需要向请求中添加自定义头信息。

我正在使用拦截器Interceptor来将这些头信息添加到所有的请求中:

OkHttpClient httpClient = new OkHttpClient();
httpClient.networkInterceptors().add(new Interceptor() {
    @Override
    public Response intercept(Chain chain) throws IOException {
        final Request request = chain.request().newBuilder()
                .addHeader("CUSTOM_HEADER_NAME_1", "CUSTOM_HEADER_VALUE_1")
                .addHeader("CUSTOM_HEADER_NAME_2", "CUSTOM_HEADER_VALUE_2")
                ...
                .addHeader("CUSTOM_HEADER_NAME_N", "CUSTOM_HEADER_VALUE_N")
                .build();

        return chain.proceed(request);
    }
});


Retrofit retrofitClient = new Retrofit.Builder()
        .baseUrl(baseUrl)
        .client(httpClient)
        .build();

有些标题我总是想添加的,但有些标题仅基于特定端点的要求需要添加,例如用户是否需要进行身份验证。

我希望能够在API层面上控制它,例如使用注解,类似于:

public interface MyApi {
    @NO_AUTH
    @POST("register")
    Call<RegisterResponse> register(@Body RegisterRequest data);

    @GET("user/{userId}")
    Call<GetUserResponse> getUser(@Path("userId") String userId);
}

当发送一个请求到register时,不需要添加身份验证令牌,但是没有@NO_AUTH注释的请求将具有标记头。

据我所知,Retrofit 2不支持自定义注释,并且尽管我找到了这个在Retrofit 2中使用自定义注释的解决方法,但这似乎有点过于繁琐。

我想避免每次请求都需要传递这些标头,例如:

public interface MyApi {
    @POST("register")
    Call<RegisterResponse> register(@Body RegisterRequest data);

    @GET("user/{userId}")
    Call<GetUserResponse> getUser(@Header("AuthToken") String token, @Path("userId") String userId);
}

每次调用该方法都执行相同的操作,感觉有些冗余,我希望能够在拦截器中执行(因为我可以静态地访问标头值)。
我需要在 Interceptor.intercept 方法中知道是否应为此特定请求添加特定的标题。

你有什么想法可以使其工作?
我希望有一个通用解决方案,而不仅仅是针对认证令牌情况的解决方案,但是如果有特定的解决方案也可以。谢谢。

3个回答

36

我为自己的问题提出了一个非常简单而优雅(在我看来)的解决方案,可能适用于其他情况。

我使用Headers注释来传递我的自定义注释,由于OkHttp要求它们遵循Name: Value格式,因此我决定我的格式将是:@:注释名称

所以基本上:

public interface MyApi {
    @POST("register")
    @HEADERS("@: NoAuth")
    Call<RegisterResponse> register(@Body RegisterRequest data);

    @GET("user/{userId}")
    Call<GetUserResponse> getUser(@Path("userId") String userId);
}

那么我可以拦截请求,检查是否存在名称为@的注释。如果是这样,我就获取该值并从请求中删除头。
即使您想要有多个“自定义注释”,这也能很好地工作:

@HEADERS({
    "@: NoAuth",
    "@: LogResponseCode"
})

以下是如何提取所有这些“自定义注释”并从请求中删除它们的方法:

new OkHttpClient.Builder().addNetworkInterceptor(new Interceptor() {
    @Override
    public okhttp3.Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        List<String> customAnnotations = request.headers().values("@");

        // do something with the "custom annotations"

        request = request.newBuilder().removeHeader("@").build();
        return chain.proceed(request);
    }
});

2
@panduka 不是的。那只是为了举例,你可以有一个客户端。 - Nitzan Tomer
这实际上是最干净的解决方案,谢谢分享 :) - MatPag
@NitzanTomer 很好的解决方案。它运行良好,但我想知道为什么 HttpLoggingInterceptor 在头部仍然显示 @: NoAuth。这是因为它在被移除之前被拦截了吗? - Poorya
@Poorya 也许你在这个拦截器之前添加了 HttpLoggingInterceptor?如果是这种情况,那么日志记录会在这个拦截器删除头部之前发生。 - Nitzan Tomer
1
@Poorya,你能在这个拦截器后面添加HttpLoggingInterceptor吗?还是会有问题? - Nitzan Tomer
显示剩余3条评论

7
也许您可以通过创建不同的Retrofit对象工厂方法来实现这一点,就像这样。
public class RestClient {
    public static <S> S createService(Class<S> serviceClass) {
        OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        OkHttpClient client = httpClient.build();

        Retrofit retrofit = new Retrofit.Builder().baseUrl(APIConfig.BASE_URL)
                .client(client)
                .build();
        return retrofit.create(serviceClass);
    }

    public static <S> S createServiceWithAuth(Class<S> serviceClass) {
        Interceptor interceptor = new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                final Request request = chain.request().newBuilder()
                        .addHeader("CUSTOM_HEADER_NAME_1", "CUSTOM_HEADER_VALUE_1")
                        .addHeader("CUSTOM_HEADER_NAME_2", "CUSTOM_HEADER_VALUE_2")
                        .build();

                return chain.proceed(request);
            }
        };
        OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        httpClient.addInterceptor(interceptor);
        OkHttpClient client = httpClient.build();

        Retrofit retrofit = new Retrofit.Builder().baseUrl(APIConfig.BASE_URL)
                .client(client)
                .build();
        return retrofit.create(serviceClass);
    }
}

如果您想在没有标头验证的情况下调用 API,只需调用 createService 方法:

YourApi api = RestClient.createService(YourApi.class);

如果您想使用身份验证调用API,请使用createServiceWithAuth方法:

YourApiWithAuth api = RestClient.createServiceWithAuth(YourApiWithAuth.class);

谢谢,那是一个好的解决方案,但它需要我根据请求是否需要身份验证将端点分组到不同的类中,这不是很方便。 - Nitzan Tomer
祝你好运,每次需要实例化资源密集型的 Retrofit 时。 - Farid
这就是正确的解决方案。认证和非认证请求应尽可能分开。 - Agent_L

0

你也可以像这篇文章中那样做一些事情

我添加了一个小的额外扩展来缩短所有内容

fun Request.isAnnotationPresent(annotation: Class<out Annotation>) =
    tag(Invocation::class.java)?.method()?.isAnnotationPresent(annotation) ?: false

private class CustomInterceptor(private val account: Account): Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {chain.request().tag(Invocation::class.java)?.method()
        if(chain.request().isAnnotationPresent(CustomAnnotation)) {
            // Do your stuff
        }
        return chain.proceed(chain.request())
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接