本章我们讨论命名注解(@Named)和限定符注解(@Qualifier),这两个注解都属于 JSR330 特性,命名注解实际上由限定符注解标记,因此最终讨论的是限定符注解,它可以让任何人去定义一个注解,用来限定依赖的不同实例。
查看 @Named
注解的源代码:
/**
* String-based {@linkplain Qualifier qualifier}.
*
* Example usage:
*
*
* public class Car {
* @Inject @Named("driver") Seat driverSeat;
* @Inject @Named("passenger") Seat passengerSeat;
* ...
* }
*/
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
/** The name. */
String value() default "";
}
从 API 描述可以看到,当注入的类型相同时,使用 @Named
注解区分不同的实例来源。
6.1 @Named
考虑到这样一个开发任务:
打印日志时,为了方便观察,需要美化的 json 格式。
数据持久化时,为了节省空间,通常需要紧凑的 json 格式。
这两种格式无法由同一个 Gson
实例序列化出来,我们需要提供不同的 Gson
实例,但 Dagger2 不能理解我们到底需要哪一个实例,必须通过 @Named
注解进行标记,定义相应的命名,Dagger2 才能为我们注入准确的实例。
6.1.1 美化格式
修改 AccountModule
类:
@Module
final class AccountModule {
// ...
// @ActivityScoped
@Provides
Gson provideGson() {
return new Gson();
}
@Named("pretty")
@Provides
Gson providePrettyGson() {
return new GsonBuilder()
.setPrettyPrinting()
.create();
}
}
修改 ActivityComponent
接口:
@ActivityScoped
@Component(modules = {AccountModule.class})
public interface ActivityComponent {
Account account();
void inject(MainActivity activity);
void inject(AccountActivity activity);
}
修改 AccountActivity
类:
public class AccountActivity extends AppCompatActivity {
@Named("pretty")
@Inject
Gson gson;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_account);
ActivityComponent component = DaggerApplication.ofComponent(this);
component.inject(this);
Account firstAccount = component.account();
((TextView) findViewById(R.id.first_account)).setText(firstAccount.toString());
Account secondAccount = component.account();
((TextView) findViewById(R.id.second_account)).setText(secondAccount.toString());
Log.i("Account", "first equals second: " + firstAccount.equals(secondAccount));
Log.i("Account", "first account: " + gson.toJson(firstAccount));
Log.i("Account", "second account: " + gson.toJson(secondAccount));
}
}
查看编译生成的代码:
-
DaggerActivityComponent.java
新增内容:
private AccountActivity injectAccountActivity(AccountActivity instance) {
AccountActivity_MembersInjector.injectGson(instance, AccountModule_ProvidePrettyGsonFactory.providePrettyGson(accountModule));
return instance;
}
-
AccountModule_ProvidePrettyGsonFactory.java
核心内容:
public static Gson providePrettyGson(AccountModule instance) {
return Preconditions.checkNotNull(instance.providePrettyGson(), "Cannot return null from a non-@Nullable @Provides method");
}
可以看到,使用 @Named
注解标记依赖字段,Dagger2 会根据 pretty
参数选择调用对应的实例。
同时,默认实例也没有受到干扰,非常方便开发者切换不同的依赖注入。
6.2 多数据源
思考一下:
对于网络数据和本地数据,是否可以建立一个 DataSource
接口,包含一些通用的操作,随后用 @Name("remote")
注解标记从网络获取数据的实现,用 @Name("local")
注解标记从本地获取数据的实现?
的确可以这样做,并且业务代码基本不需要改动,就可以选择从网络或从本地获取数据。
甚至可以实现简单的本地缓存功能,比如包装一个 DataSource
接口,同时依赖 remote
实例和 local
实例,在本地有数据时,直接返回数据,否则将从网络获取数据并存储到本地。
6.2.1 数据源接口
创建 DataSource
接口:
public interface DataSource {
List findAll();
Optional findBy(I id);
boolean save(D data);
boolean deleteAll();
boolean deleteBy(I id);
}
简单起见,数据源仅包含 CURD 操作,同时创建和更新合并为保存方法。
6.2.2 远端数据源
网络框架将在讨论多模块(@Module)时详细介绍,本节我们使用 LinkedHashMap
类,它被设计为按照插入顺序进行迭代,因此比较适合模拟远端数据源。
public class AccountRemoteDataSource implements DataSource {
private static final long DELAY = TimeUnit.SECONDS.toMillis(2);
private final Map remoteAccount = new LinkedHashMap<>();
@Override
public List findAll() {
delay();
return new ArrayList<>(remoteAccount.values());
}
@Override
public Optional findBy(String id) {
delay();
return Optional.ofNullable(remoteAccount.get(id));
}
@Override
public boolean save(Account data) {
delay();
remoteAccount.put(data.username, data);
return true;
}
@Override
public boolean deleteAll() {
delay();
remoteAccount.clear();
return true;
}
@Override
public boolean deleteBy(String id) {
delay();
remoteAccount.remove(id);
return true;
}
private void delay() {
try {
Thread.sleep(DELAY);
} catch (InterruptedException ignore) {
}
}
}
6.2.3 本地数据源
数据库框架将在讨论多模块(@Module)时详细介绍,本节我们使用 ConcurrentHashMap
类,它支持在并发环境下使用,适合模拟本地数据。
public class AccountLocalDataSource implements DataSource {
private final Map local = new ConcurrentHashMap<>();
@Override
public List findAll() {
return new ArrayList<>(local.values());
}
@Override
public Optional findBy(String id) {
return Optional.ofNullable(local.get(id));
}
@Override
public boolean save(Account data) {
local.put(data.username, data);
return true;
}
@Override
public boolean deleteAll() {
local.clear();
return true;
}
@Override
public boolean deleteBy(String id) {
local.remove(id);
return true;
}
}
6.2.4 账号数据源
创建 AccountDataSource
包装类:
public class AccountDataSource implements DataSource {
@Named("local")
@Inject
DataSource local;
@Named("remote")
@Inject
DataSource remote;
@Inject
public AccountDataSource() {
}
@Override
public List findAll() {
return findAll(false);
}
public List findAll(boolean forceRemote) {
List accounts = local.findAll();
if (forceRemote || accounts.isEmpty()) {
local.deleteAll();
accounts.clear();
remote.findAll().forEach(account -> {
local.save(account);
accounts.add(account);
});
}
return accounts;
}
@Override
public Optional findBy(String id) {
Optional optional = local.findBy(id);
if (!optional.isPresent()) {
optional = remote.findBy(id);
optional.ifPresent(local::save);
}
return optional;
}
@Override
public boolean save(Account data) {
// 如果保存到远端成功,但保存到本地失败,这没有关系
// 在 findById 时,发现本地不存在会自动从远端获取,然后存到本地
// 但 findAll 无法甄别异常,必须调用 findAll(true) 来强制刷新本地数据
return remote.save(data) && local.save(data);
}
@Override
public boolean deleteAll() {
return remote.deleteAll() && local.deleteAll();
}
@Override
public boolean deleteBy(String id) {
return remote.deleteBy(id) && local.deleteBy(id);
}
}
6.2.5 依赖注入
修改 AccountModule
类:
@Module
final class AccountModule {
// ...
@Named("remote")
@Provides
DataSource provideRemoteAccountDataSource() {
AccountRemoteDataSource source = new AccountRemoteDataSource();
source.save(new Account());
source.save(new Account());
source.save(new Account());
source.save(new Account());
source.save(new Account());
return source;
}
@Named("local")
@Provides
DataSource provideLocalAccountDataSource() {
return new AccountLocalDataSource();
}
}
修改 AccountActivity
类:
public class AccountActivity extends AppCompatActivity {
@Inject
AccountDataSource dataSource;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_account);
ActivityComponent component = DaggerApplication.ofComponent(this);
component.inject(this);
List all = dataSource.findAll();
((TextView) findViewById(R.id.first_account)).setText(all.get(0).toString());
((TextView) findViewById(R.id.second_account)).setText(all.get(1).toString());
Log.i("Account", "all account: " + all);
}
}
6.2.6 打开账号页面
运行应用,打开账号页面看看效果:
查看运行日志:
通过账号数据源获取全部数据,正好是远端数据源初始化的 5 条。
从用户角度来说,无需关心数据来自远端还是本地。
利用 @Named
注解和面向接口编程,可以在不破坏业务代码的前提下,随心切换数据源。
6.3 @Qualifier
事实上,使用字符串命名定义不同的依赖,有可能因为单词拼写错误而导致依赖注入失败。
为此我们使用 @Qualifier
注解自定义 @RemoteDataSource
注解:
@Qualifier
@Retention(RUNTIME)
public @interface RemoteDataSource {
}
还有 @LocalDataSource
注解:
@Qualifier
@Retention(RUNTIME)
public @interface LocalDataSource {
}
随后修改 @Named
注解的相关标记:
public class AccountDataSource implements DataSource {
@LocalDataSource
@Inject
DataSource local;
@RemoteDataSource
@Inject
DataSource remote;
// ...
}
@Module
final class AccountModule {
// ...
@RemoteDataSource
@Provides
DataSource provideRemoteAccountDataSource() {
AccountRemoteDataSource source = new AccountRemoteDataSource();
source.save(new Account());
source.save(new Account());
source.save(new Account());
source.save(new Account());
source.save(new Account());
return source;
}
@LocalDataSource
@Provides
DataSource provideLocalAccountDataSource() {
return new AccountLocalDataSource();
}
}
再次运行应用:
效果和 @Named
注解一样,但自定义注解明显是更合理的方式。
6.4 总结
-
@Named
注解使用字符串区分不同的实例 -
@Qualifier
注解使用自定义注解区分不同的实例 - 使用这两种方式都没问题,
@Named
注解需要确保单词拼写无误
可以预见的是,仅在提供者方法的参数中,使用 @Named
注解最合适,因为不会有其他地方需要拼写单词,失误的情况完全可以避免。
但在其他地方,比如注入的字段上,则使用 @Qualifier
标记的自定义注解最为合适。如果你没有提供默认实例,那么在编译期你就会收到遗漏了自定义注解的警告,这对多人开发来说非常重要。