Android Dagger2 + OkHttp + Retrofit 依赖循环错误

20

嘿,我正在使用 Dagger2RetrofitOkHttp,但遇到了依赖循环问题。

在提供 OkHttp 时:

@Provides
@ApplicationScope
OkHttpClient provideOkHttpClient(TokenAuthenticator auth,Dispatcher dispatcher){
    return new OkHttpClient.Builder()
            .connectTimeout(Constants.CONNECT_TIMEOUT, TimeUnit.SECONDS)
            .readTimeout(Constants.READ_TIMEOUT,TimeUnit.SECONDS)
            .writeTimeout(Constants.WRITE_TIMEOUT,TimeUnit.SECONDS)
            .authenticator(auth)
            .dispatcher(dispatcher)
            .build();
}

提供 Retrofit 时:

@Provides
@ApplicationScope
Retrofit provideRetrofit(Resources resources,Gson gson, OkHttpClient okHttpClient){
    return new Retrofit.Builder()
            .baseUrl(resources.getString(R.string.base_api_url))
            .addConverterFactory(GsonConverterFactory.create(gson))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .client(okHttpClient)
            .build();
}
提供 APIService 时:
@Provides
@ApplicationScope
APIService provideAPI(Retrofit retrofit) {
    return retrofit.create(APIService.class);
}

我的APIService接口:

public interface  APIService {
@FormUrlEncoded
@POST("token")
Observable<Response<UserTokenResponse>> refreshUserToken();

--- other methods like login, register ---

}

我的TokenAuthenticator类:

@Inject
public TokenAuthenticator(APIService mApi,@NonNull ImmediateSchedulerProvider mSchedulerProvider) {
    this.mApi= mApi;
    this.mSchedulerProvider=mSchedulerProvider;
    mDisposables=new CompositeDisposable();
}

@Override
public  Request authenticate(Route route, Response response) throws IOException {

    request = null;

    mApi.refreshUserToken(...)
            .subscribeOn(mSchedulerProvider.io())
            .observeOn(mSchedulerProvider.ui())
            .doOnSubscribe(d -> mDisposables.add(d))
            .subscribe(tokenResponse -> {
                if(tokenResponse.isSuccessful()) {
                    saveUserToken(tokenResponse.body());
                    request = response.request().newBuilder()
                            .header("Authorization", getUserAccessToken())
                            .build();
                } else {
                    logoutUser();
                }
            },error -> {

            },() -> {});

    mDisposables.clear();
    stop();
    return request;

}

我的日志:

Error:(55, 16) error: Found a dependency cycle:
com.yasinkacmaz.myapp.service.APIService is injected at com.yasinkacmaz.myapp.darkvane.modules.NetworkModule.provideTokenAuthenticator(…, mApi, …)
com.yasinkacmaz.myapp.service.token.TokenAuthenticator is injected at
com.yasinkacmaz.myapp.darkvane.modules.NetworkModule.provideOkHttpClient(…, tokenAuthenticator, …)
okhttp3.OkHttpClient is injected at
com.yasinkacmaz.myapp.darkvane.modules.NetworkModule.provideRetrofit(…, okHttpClient)
retrofit2.Retrofit is injected at
com.yasinkacmaz.myapp.darkvane.modules.NetworkModule.provideAPI(retrofit)
com.yasinkacmaz.myapp.service.APIService is provided at
com.yasinkacmaz.myapp.darkvane.components.ApplicationComponent.exposeAPI()

所以我的问题是:我的TokenAuthenticator类依赖于APIService,但是我需要在创建APIService时提供TokenAuthenticator。这会导致依赖循环错误。我该如何解决这个问题?有没有人遇到过这个问题?先谢谢。


因为它没有意义...使用TokenAuthenticator的OkHttpClient来获取TokenAuthenticator所需的身份验证令牌...即使在“纸上”,它也具有“循环依赖”...创建另一个服务以获取身份验证令牌,使用另一个HTTP客户端实例而不是认证器。 - Selvin
TokenAuthenticator 用于刷新用户令牌,我想在每个网络调用中使用同一个 OkHttp 实例,以便管理用户令牌。因此,在该 OkHttp 实例上有一个调度程序。 - Yasin Kaçmaz
1
再说一遍,它没有意义...即使你修复它,它也会导致堆栈溢出...你正在从authenticate中调用refreshUserToken...但是refreshUserToken需要调用authenticate - Selvin
1
refreshUserToken使用了OkHttpClient,它被设置为使用TokenAuthenticator,而TokenAuthenticator又使用refreshUserToken...那么refreshUserToken方法如何不需要调用authenticate是真的吗? - Selvin
@Selvin,我认为你的句子是几个小时结束的,得出了这个解决方案:在TokenAuthenticatorTokenInterceptor类中使用另一个OkHttp实例,因为它们仅在我们的通用OkHttp实例发起请求时才会触发。因此它们不受限制。 - Yasin Kaçmaz
显示剩余3条评论
4个回答

31

你的问题是:

  1. 你的 OKHttpClient 依赖于你的 Authenticator
  2. 你的 Authenticator 依赖于 Retrofit Service
  3. Retrofit 依赖于 OKHttpClient(如第1点所述)

因此出现了循环依赖。

这里有一种可能的解决方案,就是让你的 TokenAuthenticator 依赖于 APIServiceHolder 而不是 APIService。然后,当配置 OKHttpClient 时,可以提供作为依赖项的 TokenAuthenticator,而不管对象图中是否已经实例化了 APIService(在更深层次上)。

一个非常简单的 APIServiceHolder:

public class APIServiceHolder {

    private APIService apiService;

    @Nullable
    APIService apiService() {
        return apiService;
    }

    void setAPIService(APIService apiService) {
        this.apiService = apiService;
    }
}

然后重构您的TokenAuthenticator:

@Inject
public TokenAuthenticator(@NonNull APIServiceHolder apiServiceHolder, @NonNull ImmediateSchedulerProvider schedulerProvider) {
    this.apiServiceHolder = apiServiceHolder;
    this.schedulerProvider = schedulerProvider;
    this.disposables = new CompositeDisposable();
}

@Override
public  Request authenticate(Route route, Response response) throws IOException {

    if (apiServiceHolder.get() == null) {
         //we cannot answer the challenge as no token service is available

         return null //as per contract of Retrofit Authenticator interface for when unable to contest a challenge
    }    

    request = null;            

    TokenResponse tokenResponse = apiServiceHolder.get().blockingGet()

    if (tokenResponse.isSuccessful()) {
        saveUserToken(tokenResponse.body());
        request = response.request().newBuilder()
                     .header("Authorization", getUserAccessToken())
                     .build();
    } else {
       logoutUser();
    }

    return request;
}

请注意,检索令牌的代码应当是同步的。这是Authenticator合约的一部分。 Authenticator中的代码将在主线程之外运行

当然,您还需要编写相应的@Provides方法:

@Provides
@ApplicationScope
apiServiceHolder() {
    return new APIServiceHolder();
}

并重构提供程序的方法:

@Provides
@ApplicationScope
APIService provideAPI(Retrofit retrofit, APIServiceHolder apiServiceHolder) {
    APIService apiService = retrofit.create(APIService.class);
    apiServiceHolder.setAPIService(apiService);
    return apiService;
}

请注意,可变的全局状态通常不是一个好主意。然而,如果您的包组织得很好,您可以适当地使用访问修饰符来避免对持有者的意外使用。


我有两种方法。其中一种是这样的,另一种是:为TokenAuthenticator和TokenInterceptor类使用另一个OkHttp实例,因为它们只在任何APIService请求调用时触发,所以它们没有绑定。此外,通过这种方式,我将从我的其他请求中分离令牌处理,因此我可以轻松地进行维护。你建议哪一个? - Yasin Kaçmaz
@ Yasin,如果你查看Authenticator的文档,似乎它是设计为与单个OkHttpClient一起使用的。authenticate中的代码在与原始请求相同的线程上执行。 - David Rawson
感谢您的回复。我只是想确保这一点:由于它在同一个线程上,我可以使用任何我想要的东西。我的意思是OkHttpRetrofit甚至是HttpUrlConnection,因为我正在Authenticator类中进行同步请求,这意味着它们会阻塞该线程直到工作完成。因此,将OkHttp实例分离为令牌和常规API调用是一个好方法,或者也许不是? - Yasin Kaçmaz
4
在我的情况下,APIServiceHolder始终返回null。 - thalissonestrela
1
这不还是一个循环依赖吗?你正在使用包装器隐藏服务并应用一种延迟初始化,但TokenAuthenticator仍然依赖于该服务。 - Tartar
显示剩余16条评论

10
使用Dagger 2的 Lazy 接口是解决方法。在您的 TokenAuthenticator 中,将 APIService mApi 替换为 Lazy<APIService> mApiLazyWrapper
@Inject
public TokenAuthenticator(Lazy<APIService> mApiLazyWrapper,@NonNull ImmediateSchedulerProvider mSchedulerProvider) {
    this.mApiLazyWrapper= mApiLazyWrapper;
    this.mSchedulerProvider=mSchedulerProvider;
    mDisposables=new CompositeDisposable();
}

从包装器中获取APIService实例,请使用mApiLazyWrapper.get()

如果mApiLazyWrapper.get()返回null,则也应该从TokenAuthenticatorauthenticate方法中返回null。


1
亲爱的 @Tartar,这个问题来自2017年,大约3年前。这可能也有效,但是我们的APIService仍然持有TokenAuthenticator。唯一的区别是我们延迟注入它。所以在我看来,我更喜欢拥有一个简单的客户端进行身份验证流程,可以与多个拦截器隔离开来。 - Yasin Kaçmaz
4
这是Dagger解决循环依赖错误的方法,因此对于希望以这种方式解决问题而不使用多个HTTP客户端的人来说,这个答案会很有用。 - Tartar
是的,这将是新观众的指南。此外,@max也有关于Lazy的答案。解决依赖循环问题有几种方法,他们可以选择适合他们应用程序的解决方案。 - Yasin Kaçmaz
1
我认为这是目前最好的解决方案。我不相信当我写下最初的答案时,Lazy已经存在了。 - David Rawson
如何使用Hilt实现这个? - Torima
你真是个救命恩人!谢谢,这很有效。 - mrzbn

1
感谢@Selvin和@David的帮助。我有两种方法,其中一种是David的答案,另一种是:

创建另一个OkHttpRetrofit库,它将在TokenAuthenticator类中处理我们的操作。

如果您想使用另一个OkHttpRetrofit实例,则必须使用Qualifier注释。

例如:

@Qualifier
public @interface ApiClient {}


@Qualifier
public @interface RefreshTokenClient {}

然后提供:
@Provides
@ApplicationScope
@ApiClient
OkHttpClient provideOkHttpClientForApi(TokenAuthenticator tokenAuthenticator, TokenInterceptor tokenInterceptor, Dispatcher dispatcher){
    return new OkHttpClient.Builder()
            .connectTimeout(Constants.CONNECT_TIMEOUT, TimeUnit.SECONDS)
            .readTimeout(Constants.READ_TIMEOUT,TimeUnit.SECONDS)
            .writeTimeout(Constants.WRITE_TIMEOUT,TimeUnit.SECONDS)
            .authenticator(tokenAuthenticator)
            .addInterceptor(tokenInterceptor)
            .dispatcher(dispatcher)
            .build();
}

@Provides
@ApplicationScope
@RefreshTokenClient
OkHttpClient provideOkHttpClientForRefreshToken(Dispatcher dispatcher){
    return new OkHttpClient.Builder()
            .connectTimeout(Constants.CONNECT_TIMEOUT, TimeUnit.SECONDS)
            .readTimeout(Constants.READ_TIMEOUT,TimeUnit.SECONDS)
            .writeTimeout(Constants.WRITE_TIMEOUT,TimeUnit.SECONDS)
            .dispatcher(dispatcher)
            .build();
}

@Provides
@ApplicationScope
@ApiClient
Retrofit provideRetrofitForApi(Resources resources, Gson gson,@ApiClient OkHttpClient okHttpClient){
    return new Retrofit.Builder()
            .baseUrl(resources.getString(R.string.base_api_url))
            .addConverterFactory(GsonConverterFactory.create(gson))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .client(okHttpClient)
            .build();
}

@Provides
@ApplicationScope
@RefreshTokenClient
Retrofit provideRetrofitForRefreshToken(Resources resources, Gson gson,@RefreshTokenClient OkHttpClient okHttpClient){
    return new Retrofit.Builder()
            .baseUrl(resources.getString(R.string.base_api_url))
            .addConverterFactory(GsonConverterFactory.create(gson))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .client(okHttpClient)
            .build();
}

然后我们可以提供我们分离的接口:
@Provides
@ApplicationScope
public APIService provideApi(@ApiClient Retrofit retrofit) {
    return retrofit.create(APIService.class);
}

@Provides
@ApplicationScope
public RefreshTokenApi provideRefreshApi(@RefreshTokenClient Retrofit retrofit) {
    return retrofit.create(RefreshTokenApi.class);
}

当提供我们的 TokenAuthenticator 时:
@Provides
@ApplicationScope
TokenAuthenticator provideTokenAuthenticator(RefreshTokenApi mApi){
    return new TokenAuthenticator(mApi);
}

优点:您有两个分离的API接口,这意味着您可以独立维护它们。此外,您可以使用纯OkHttpHttpUrlConnection或其他库。
缺点:您将拥有两个不同的OkHttp和Retrofit实例。
附注:确保在Authenticator类中进行同步调用。

亲爱的@BajrangHudda,如果您不展示您的代码,我无法帮助您。我的意思是您的组件、模块。此外,如果您是Dagger的初学者,我强烈建议您阅读Dagger示例并尝试理解什么是依赖注入。 - Yasin Kaçmaz
我使用了David的答案,有什么缺点吗? - Bajrang Hudda
1
@BajrangHudda,我认为David的回答没有任何问题;但是你知道,在软件开发中有时候会有多种解决方案可供选择。而且我个人不喜欢在依赖注入模块中使用可空类型。此外,这个问题已经很老了,我现在正在使用Kotlin和Dagger。Dagger已经更新了很多,让我看看这个问题是否有其他解决方法。 - Yasin Kaçmaz
@BajrangHudda 我猜我们可以使用lateinit而不是null,因为你也在使用kotlin。另外,在我的答案中,我将更新限定符,请也尝试使用它。 - Yasin Kaçmaz
1
@BajrangHudda 还有 Dagger 中的 Lazy。我没有使用过它,但你可以看一下。 - Yasin Kaçmaz
显示剩余6条评论

0

你可以通过 Lazy 类型将服务依赖注入到你的身份验证器中。这样,你就可以避免在实例化时出现循环依赖。

查看此 链接 了解 Lazy 的工作原理。


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