如何避免在使用Retrofit添加请求头时等待主线程?

4
我使用这个来配置我的Retrofit:
 RestAdapter restAdapter = new RestAdapter.Builder()

            //add headers to requests
            .setRequestInterceptor(getAuthenticatedRequestInterceptor())
            .setEndpoint(BASE_URL)
            .setConverter(new GsonConverter(getGson()))
            .build();

getAuthenticatedRequestInterceptor()方法会向请求添加头信息:

public AccountRequestInterceptor getAuthenticatedRequestInterceptor() {
    AccountRequestInterceptor interceptor = new AccountRequestInterceptor();
    Map<String, String> headers = new HashMap<>();
     String accessToken = null;
    try {
        accessToken = TokenProvider.getInstance(mContext).getToken();
    } catch (InterruptedException e) {

    }
    headers.put(HeadersContract.HEADER_AUTHONRIZATION, O_AUTH_AUTHENTICATION + accessToken);
    interceptor.setHeader(headers);
    return interceptor;
}

getToken()方法是:

private synchronized string getToken() throws InterruptedException {
    if (!isRefreshing()) {
        //This is very important to call notify() on the same object that we call wait();
        final TokenProvider myInstance = this;
        setRefreshing(true);

        MyApplication.getRestClient().getAccountService().getRefreshedToken(mLoginData.getRefreshToken())
                .subscribe(new Observer<LoginResponse>() {
                    @Override
                    public void onCompleted() {
                        synchronized (myInstance) {
                            setRefreshing(false);
                            myInstance.notifyAll();
                        }
                    }

                    @Override
                    public void onError(Throwable e) {
                        synchronized (myInstance) {
                            setRefreshing(false);
                            myInstance.notifyAll();
                        }
                    }

                    @Override
                    public void onNext(LoginResponse loginResponse) {
                        synchronized (myInstance) {
                            mLoginData = loginResponse;
                            mAccountProvider.saveLoginData(loginResponse);
                            myInstance.notifyAll();
                        }
                    }
                });
    }
    this.wait();
    return mLoginData.getToken();
}
TokenProvider.getInstance(mContext).getToken()方法在主线程上调用wait()以获取来自异步方法的响应。虽然这是一件不好的事情,但我需要在此处等待响应以获取令牌并返回令牌。如何在单独的线程中执行此操作以避免在主线程上等待?
注意:
1. 在使用Retrofit发出任何请求之前都会调用此方法。
2. 我阅读了这篇文章,知道可以在请求失败后刷新令牌,但出于业务原因,我希望避免使用无效令牌。
3. 我在我的Activity中调用MyApplication.getRestClient().getAccountService().login(loginRequest,callback...‌​),并且在添加令牌之前所有操作都在后台线程中进行。因此,我想使用我的令牌并且不阻塞主线程。
更新:我向新的OkHttp添加了以下Interceptor
public class RequestTokenInterceptor implements Interceptor {
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request request = chain.request();
        Request newRequest;
        try {
            Log.d("addHeader", "Before");
            String token = TokenProvider.getInstance(mContext).getToken();
            if (token != null) {
                newRequest = request.newBuilder()
                        .addHeader("Bearer", token)
                        .build();
            } else {
                // I want to cancel the request or raise an exception to catch it in onError method
                // of retrofit callback.
            }
        } catch (InterruptedException e) {
            Log.d("addHeader", "Error");
            e.printStackTrace();
            return chain.proceed(request);
        }
        Log.d("addHeader", "after");
        return chain.proceed(newRequest);
    }
}

如果令牌为空,我如何取消请求或引发异常以在Retrofit回调的onError方法中捕获它?


2
不要将您的请求发送到主线程? - user253751
我在我的活动中使用MyApplication.getRestClient().getAccountService().login(loginRequest,callback...),在添加token之前一切都发生在后台线程中。 - Morteza Rastgoo
@immibis所说的,也可以参考https://dev59.com/_GEh5IYBdhLWcg3wYSc2 - yiati
如果你在使用Kotlin的协程,可以使用runBlocking(Dispatchers.IO),这样就可以进行阻塞式的令牌更新,但同时也会阻塞后台线程。 - undefined
我在2015年8月24日提出了这个问题,当时还没有Kotlin这个东西:D - undefined
3个回答

2

这是一个有点奇怪的问题,但让我试着帮助你。 :)

如你所知,当使用Retrofit进行请求时,可以使用响应拦截器来在请求失败后刷新令牌。

现在我们尝试在请求之前使用拦截器。

public class RequestTokenInterceptor implements Interceptor {
   @Override
   public Response intercept(Chain chain) throws IOException {
      Request request = chain.request();
      // Here where we'll try to refresh token.
      // with an retrofit call
      // After we succeed we'll proceed our request
      Response response = chain.proceed(request);
      return response;
   }
}

当您创建 API 时,请创建一个新的 HttpClient:

OkHttpClient client = new OkHttpClient();
client.interceptors().add(new RequestTokenInterceptor());

并将您的HTTP客户端添加到适配器中,如下所示:

.setClient(new OkClient(client))

如果这样做成功了,在每个请求之前,您将首先尝试刷新令牌,然后再进行API请求。因此,在UI中,您的常规API调用没有任何区别。
编辑:
我也在编辑我的答案。如果您想在令牌为空的else情况下返回错误,则可以在else情况下创建自定义响应。
private Response(Builder builder) {
    this.request = builder.request;
    this.protocol = builder.protocol;
    this.code = builder.code;
    this.message = builder.message;
    this.handshake = builder.handshake;
    this.headers = builder.headers.build();
    this.body = builder.body;
    this.networkResponse = builder.networkResponse;
    this.cacheResponse = builder.cacheResponse;
    this.priorResponse = builder.priorResponse;
  }

或者,你可以返回一个空响应。如果你构建了自定义响应并将代码设置为401或400+等非200状态码,则会在Retrofit的回调失败方法中收到该响应。然后你就可以随心所欲地做任何事情。

如果你返回 null,我认为你会收到 RuntimeException,但仍可以在回调的失败方法中捕获响应。

在 else 中创建自己的响应之后,可以创建自定义回调并捕获你的空响应,并像下面一样自定义你的错误:

public abstract class DefaultRequestCallback<T> implements Callback<T> {

    public abstract void failure(YourCustomException ex);

    public abstract void success(T responseBean);

    @Override
    public void success(T baseResponseBean, Response response) {
        if (response == null) {
            // Here we catch null response and transform it to our custom     Exception
            failure(new YourCustomException());
        }
        } else {
            success(baseResponseBean);
        }
    }

    @Override
    public void failure(RetrofitError error) {
        // Here's your failure method.
        // Also you can transform default retrofit errors to your customerrors
        YourCustomException ex = new YourCustomException();
        failure(ex);
    }
}

我认为这可以帮助你。

编辑2:

你可以像下面这样构建一个新的响应。Retrofit的Response类中有一个构建器模式,你可以从那里检查它。

Response response = new Response.Builder().setCode(401).setMessage("Error Message").build();

这是一个应用拦截器。 - savepopulation
我需要在这里添加更多内容:在拦截方法中将令牌添加到请求之前,如果令牌为空,我需要取消请求或引发自定义异常。我该怎么做? - Morteza Rastgoo
我不确定,但在我的示例中,我试图解释。在调用chain.proceed(request)之前,您可以进行检查,如果为null,则不会继续请求。只需使用应用程序拦截器,您就可以向网络请求添加代码块,在网络请求之前或之后工作。您可以做任何想做的事情。 - savepopulation
在else语句中只需键入return null并检查结果即可。您的代码将不会继续请求,因此您可以通过返回null来取消API调用。 - savepopulation
让我们在聊天中继续这个讨论 - Morteza Rastgoo
显示剩余5条评论

1

好的,我认为如果您在主线程上调用getAuthenticatedRequestInterceptor(),并且在其中调用getInstance(),那么我觉得您将创建一个Type TokenProvider对象,因此当您在主线程中创建此对象时,您的object.wait()运行在主线程上,因此要在后台线程上运行此操作,可能需要修改getAuthenticatedRequestInterceptor()方法以在新线程中执行以下行。

try {
    accessToken = TokenProvider.getInstance(mContext).getToken();
} catch (InterruptedException e) {

}
headers.put(HeadersContract.HEADER_AUTHONRIZATION, O_AUTH_AUTHENTICATION + accessToken);
interceptor.setHeader(headers);
return interceptor;

但是这样会存在问题,因为主线程会继续执行,无法通知您的RestAdapter,因此我建议您在新线程中首先调用getAuthenticatedRequestInterceptor()方法,然后通知主线程构建RestAdapter。这将释放您的主线程,但使用您采用的策略,您必须等待收到令牌才能进行任何调用。

我不想这样做,因为我将不得不改变我在应用程序中迄今为止所做的每个请求的调用方式。 - Morteza Rastgoo
这只需要更改您创建RestAdapter的方式。然而更重要的是,我试图解释为什么您的对象正在等待主线程。 - harshitpthk

1
你可以在AsyncTask的doInBackground方法中执行所有长时间操作,而在onPre和onPostExecute中,当用户等待时,你可以显示/隐藏一些进度条。

避免像瘟疫一样使用AsyncTask。相反,使用IntentServices等。以下是一个例子,说明为什么AsyncTask很糟糕:http://simonvt.net/2014/04/17/asynctask-is-bad-and-you-should-feel-bad/ - dazito
是的,@dazito是对的,这会让所有事情变得更加困难。 - Morteza Rastgoo

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