Dagger 2.38.1 注解详解:核心注解与使用规范
前言
本文旨在深入解析 Dagger 2.38.1 版本中的核心注解,重点涵盖 @MapKey、@Scope、@Qualifier、@IntoSet、@IntoMap 和 @ElementsIntoSet。这些注解是构建依赖注入图的基础,理解它们的交互机制对于正确使用 Dagger 至关重要。
其中 @MapKey、@Scope、@Qualifier 三个注解仅可用于修饰其他注解(即元注解)。@MapKey 是 Dagger 自定义的注解,而 @Scope 和 @Qualifier 源自 javax.inject 包。
本文章节顺序经过精心编排,确保在讲解后续关联注解前,先介绍其前置依赖注解,以便读者建立完整的知识体系。
@Scope
@Scope 用于定义依赖对象的生命周期和作用范围。它必须修饰一个注解,该注解随后被用于标记组件或绑定方法。
@Scope 修饰的注解使用规则
- 节点限制:在同一个作用域节点上,使用
@Scope修饰的注解最多只能出现一次。 - Module 限制:被
@Module或@ProducerModule修饰的 Module 节点本身不允许直接使用 Scope 修饰的注解。 - Binding Method 限制:
- 在 Module 中,使用
@Provides、@Binds、@Multibinds、@BindsOptionalOf修饰的绑定方法允许同时使用@Scope修饰的注解。 - 注意:
@Produces、@Multibinds和@BindsOptionalOf修饰的方法通常不建议或不允许同时使用@Scope修饰,具体取决于实现细节,标准实践中主要关注@Provides和@Binds。
- 在 Module 中,使用
- 构造函数限制:
- 被
@Inject或@AssistedInject修饰的构造函数不允许直接同时使用@Scope修饰的注解。 - 但是,包含
@Inject构造函数的类所在的父级节点(如 Component)允许使用@Scope修饰的注解。 - 包含
@AssistedInject构造函数的类所在的父级节点不允许使用@Scope修饰的注解。
- 被
- Instance 限制:
@BindsInstance修饰的方法或其参数不允许同时使用@Scope修饰。
Scope 的作用域传递
@Scope 表示一个作用域,当前 Scope 修饰的注解修饰的节点所属的 currentComponent 节点及其往下的 Subcomponent 节点都可以使用该节点提供的实例。
以 @Singleton 为例:
@Module(subcomponents = {Subcomponent1.class, Subcomponent2.class, Subcomponent3.class})
public class Module {
@Provides
@Singleton
public A bindingMethod(B b) {
return new A(b);
}
}
@Component(modules = Module.class)
public class Component {
void inject(MembersInjector m);
SubcomponentX getSubcomponentX(); // 这是一个 Subcomponent 节点
}
public class MembersInjector {
@Inject
public C c;
}
@Singleton
public class C {
@Inject
public C() {}
}
实例解析:
- 当前
@Singleton和@Provides修饰的bindingMethod方法返回的A的实例化既可以在当前Component作用域中使用,也可以在Subcomponent1、Subcomponent2、Subcomponent3和SubcomponentX中使用(前提是这些子组件需要A的实例化对象)。 - 当前
@Singleton修饰的C类(因为MembersInjector类是待实例化的对象,所以对象中@Inject修饰的变量类型或普通方法返回类型)实例化既可以在当前Component作用域中使用,也可以在各个Subcomponent中使用。 - 注意:如果在
Subcomponent1中定义了@Singleton修饰的节点,那么该节点的实例对象不能反向在父级Component中使用。作用域是单向向下传递的。
常见的 Scope 注解
Dagger 2.38.1 版本源码中包含几个 Scope 修饰的注解:
-
@Reusable:
- 当
@Reusable和@Binds同时修饰一个 Module 节点中的bindingMethod方法时:- 如果当前
bindingMethod方法参数类型A节点没有使用@Scope修饰的注解,则直接使用SingleCheck(单例获取)。 - 如果
A节点使用了@Scope修饰的注解,则使用DoubleCheck(双重校验单例)。
- 如果当前
SingleCheck与DoubleCheck的区别在于后者不仅获取对象,还校验当前对象的实例化始终是同一个实例,线程安全性更高。- 示例代码:
@Binds @Reusable CompilerOptions bindCompilerOptions( ProcessingEnvironmentCompilerOptions processingEnvironmentCompilerOptions ); - 在其他情况下,
@Reusable的用法与@Singleton类似,都在作用域中进行DoubleCheck双重校验。
- 当
-
@PerGeneratedFile:
- 表示将当前修饰的节点及其往下关联的节点和当前所属的
currentComponent生成的代码写在同一个文件中。在实际开发中较少使用,没有实际的业务意义。
- 表示将当前修饰的节点及其往下关联的节点和当前所属的
-
@PerComponentImplementation:
- 主要用于 Dagger 内部处理代码。相当于一个 A 类,但是这个 A 可能包含自己,此时需要另外开辟一个用于生成子类 A 的实例,需要用到此注解。
-
@ProductionScope:
- 如果使用了
@ProductionComponent或@ProductionSubcomponent修饰的 Component 节点,Dagger 会为我们生成一个@Module修饰的 Module 节点。 - 该 Module 节点中包含
monitor方法,用于监控整个 Production Component 的生命周期。 - 示例代码:
@Provides @ProductionScope static ProductionComponentMonitor monitor( Provider<component节点类型> component, Provider<Set<ProductionComponentMonitor.Factory>> factories ) { return Monitors.createMonitorForComponent(component, factories); } @ProductionScope的实际作用仍是当前 Component 的作用域,但增加了监控功能。
- 如果使用了
@Qualifier
@Qualifier(限定符)用于解决依赖注入过程中类型匹配歧义的问题。它必须修饰一个注解。
@Qualifier 修饰的注解使用规则
- 节点限制:节点上只允许出现一个
@Qualifier修饰注解的注解。 - Component Method 限制:Component Method 只有在返回类型是
Subcomponent节点或Subcomponent.Creator节点情况下才允许使用Qualifier修饰的注解修饰。 - Binding Method 限制:
bindingMethod方法可以被Qualifier修饰的注解修饰。 - 构造函数限制:
- 被
@Inject或@AssistedInject修饰的构造方法不允许使用Qualifier修饰的注解修饰。 - 但是,构造函数的参数可以使用
Qualifier修饰的注解。
- 被
- Assisted 限制:
@Assisted修饰的参数节点不能被Qualifier注解修饰的注解修饰。 - MembersInjector 限制:
MembersInjector的成员注入 T 类型不允许使用Qualifier修饰的注解修饰。 - AssistedInject 兼容性:一个节点允许使用
Qualifier修饰,那么当前节点类型(如果是方法,则表示方法返回类型)的构造函数允许使用@AssistedInject修饰。
使用场景
@Qualifier 修饰的注解主要是为了在实例化过程中成功匹配到正确的参数。当存在多个相同类型的依赖时,必须依靠限定符来精确匹配。
示例代码:
@Retention(RUNTIME)
@Qualifier
public @interface ProcessingOptions {}
public final class ExternalBindingGraphPlugins {
@Inject
ExternalBindingGraphPlugins(...,
@ProcessingOptions Map<String, String> processingOptions) {
}
}
@Module
interface ProcessingEnvironmentModule {
@Provides
@ProcessingOptions
static Map<String, String> processingOptions(XProcessingEnv xProcessingEnv) {
return xProcessingEnv.getOptions();
}
@Provides
static Map<String, String> getMap(String str) {
return new HashMap<>();
}
}
解析:
ExternalBindingGraphPlugins 对象的实例化需要 Map<String, String> 对象。这里有两个 Map<String, String> 提供者,如果不加区分,Dagger 将无法确定使用哪一个。通过 @ProcessingOptions 精确匹配,可以明确指定使用哪个提供者。
在容易产生类型混淆的情况下,务必使用 @Qualifier 修饰的注解。
@IntoSet、@IntoMap 和 @ElementsIntoSet
这三个注解用于多绑定(Multibinding),收集同一种类型的对象集合。
通用规则
- 互斥性:不允许同时使用,一个方法上仅仅可以使用
@IntoSet、@IntoMap和@ElementsIntoSet中的一个。 - 位置限制:必须在
bindingMethod绑定方法上使用,并且只支持@Provides、@Produces或@Binds三种类型的bindingMethod方法。 - 返回值限制:如果使用了
@ElementsIntoSet,那么当前bindingMethod方法返回类型必须是Set<T>。 - 成对出现:
@IntoMap和@MapKey修饰的注解一定是成对出现的。
@IntoSet
@IntoSet 用于收集同一种类型的对象到一个 Set 集合中。
示例代码:
@Module
public interface BindingMethodValidatorsModule {
// 下面的都是该方法参数
@Provides
static ImmutableMap<ClassName, BindingMethodValidator> indexValidators(
Set<BindingMethodValidator> validators) {
return uniqueIndex(validators, BindingMethodValidator::methodAnnotation);
}
@Binds
@IntoSet
BindingMethodValidator provides(ProvidesMethodValidator validator);
@Binds
@IntoSet
BindingMethodValidator produces(ProducesMethodValidator validator);
@Binds
@IntoSet
BindingMethodValidator binds(BindsMethodValidator validator);
@Binds
@IntoSet
BindingMethodValidator multibinds(MultibindsMethodValidator validator);
@Binds
@IntoSet
BindingMethodValidator bindsOptionalOf(BindsOptionalOfMethodValidator validator);
}
解析:
Set<BindingMethodValidator> validators 用于收集所有的 BindingMethodValidator 类型对象。@Binds 修饰的 bindingMethod 方法有且仅有一个参数,并且参数类型一定是方法返回类型或返回类型子类。
延伸:这里的 @Binds 当然可以改成 @Provides。
@ElementsIntoSet
和 @IntoSet 用法基本一致,但 @ElementsIntoSet 返回类型必须是 Set<T>。
示例代码:
@Module
public interface BindingMethodValidatorsModule {
@Provides
static ImmutableMap<ClassName, BindingMethodValidator> indexValidators(
Set<BindingMethodValidator> validators) {
return uniqueIndex(validators, BindingMethodValidator::methodAnnotation);
}
@Binds
@ElementsIntoSet
Set<BindingMethodValidator> provides(ProvidesMethodValidator validator);
@Binds
@ElementsIntoSet
Set<BindingMethodValidator> produces(ProducesMethodValidator validator);
@Binds
@ElementsIntoSet
Set<BindingMethodValidator> binds(BindsMethodValidator validator);
@Binds
@ElementsIntoSet
Set<BindingMethodValidator> multibinds(MultibindsMethodValidator validator);
@Binds
@ElementsIntoSet
Set<BindingMethodValidator> bindsOptionalOf(BindsOptionalOfMethodValidator validator);
}
区别:
还有一个关键区别,@ElementsIntoSet 可以返回空绑定,但是 @IntoSet 不会。基本 @IntoSet 用的比较多,@ElementsIntoSet 感觉像是一个附带产品,但在特定场景下(如可选的多绑定)非常有用。
@IntoMap
@IntoMap 也是收集同一类对象,但是 @IntoMap 和 @MapKey 修饰的注解是成双成对出现的。
原理: @IntoMap 是为了将当前类型 T 收集到 Map<K,T> 中,那么必须由 @MapKey 修饰的注解提供 K。
示例代码:
@Beta
@Module
public abstract class AndroidInjectionModule {
@Multibinds
abstract Map<Class<? extends Activity>, AndroidInjector.Factory<? extends Activity>>
activityInjectorFactories();
}
@Module(subcomponents = MainActivityModule_ContributeMainActivity.MainActivitySubcomponent.class)
public abstract class MainActivityModule_ContributeMainActivity {
private MainActivityModule_ContributeMainActivity() {}
@Binds
@IntoMap
@ActivityKey(MainActivity.class)
abstract AndroidInjector.Factory<? extends Activity> bindAndroidInjectorFactory(
MainActivitySubcomponent.Builder builder);
@Subcomponent(modules = FragmentBuildersModule.class)
public interface MainActivitySubcomponent extends AndroidInjector<MainActivity> {
@Subcomponent.Builder
abstract class Builder extends AndroidInjector.Builder<MainActivity> {}
}
}
解析:
AndroidInjectionModule 类的 activityInjectorFactories 方法用于收集 Map<Class<? extends Activity>, AndroidInjector.Factory<? extends Activity>>。
MainActivityModule_ContributeMainActivity 类中的 bindAndroidInjectorFactory 方法 @ActivityKey(MainActivity.class) 提供 K:MainActivity.class,V:MainActivitySubcomponent.Builder,符合被收集的条件。
@MapKey
当前使用 @MapKey 修饰的注解,满足如下规则:
-
unwrapValue = true (默认):
- 被修饰的注解有且仅有一个方法。
- 该方法返回类型不能是数组类型。
- 例如:
ClassKey注解。
-
unwrapValue = false:
- 被修饰的注解方法不但不限制,而且项目中必须引入了
com.google.auto.value.AutoAnnotation依赖。 - 引入 AutoAnnotation 的目的:对
MapKey修饰的注解的所有方法返回类型作为变量生成一个新的类 T,相当于@MapKey.unwrapValue() = true情况下的@MapKey(T.class)。 - 建议:
MapKey.unwrapValue() = false可以使用MapKey.unwrapValue() = true替代,而且true更容易理解、实现也更加稳健。非必要不要使用false。
- 被修饰的注解方法不但不限制,而且项目中必须引入了
-
关联性:
@MapKey和@IntoMap是情侣关系,必须在一起使用。
最佳实践与常见问题
1. 避免 Scope 冲突
在大型项目中,不同模块可能会定义不同的 Scope。请确保在合并 Module 时,Scope 的定义不会发生冲突。如果一个组件已经定义了 @Singleton,不要在子组件中再次定义同名的 Scope 注解,除非意图是覆盖父级行为。
2. Qualifier 命名规范
自定义 @Qualifier 注解时,建议遵循 @Name 或 @Type 的命名模式,使其语义清晰。例如 @ProcessingOptions 比 @Option 更明确。
3. Multibinding 的空值处理
在使用 @IntoSet 或 @IntoMap 时,如果某个绑定方法返回 null,可能会导致运行时错误。建议使用 @BindsOptionalOf 来处理可选的多绑定项。
4. MapKey 的性能
@MapKey 的 unwrapValue 为 false 时虽然灵活,但会增加编译期的复杂度。除非必要,优先选择 unwrapValue = true 的配置,以保证编译速度和稳定性。
总结
本文详细解析了 Dagger 2.38.1 中的核心注解机制。通过理解 @Scope 的生命周期管理、@Qualifier 的类型匹配、以及 @IntoSet/Map 的多绑定收集逻辑,开发者可以更高效地构建复杂的依赖注入图。在实际开发中,应严格遵守各注解的使用规则,避免常见的配置错误,以确保应用启动时的稳定性和运行时的正确性。
