参考地址: https://dagger.dev/tutorial/01-setup
代码仓库地址:https://gitee.com/guchuanhang/dagger-tutorial
完成指令行ATM应用,演示dagger2使用。可以跟踪账户余额,在控制台接收指令。
> deposit 20
> withdraw 10
这里使用的 Intellij IDEA社区版,使用Maven进行构建。Maven内容如下:
4.0.0
dagger.example.atm
Dagger2ATMApp
1.0-SNAPSHOT
8
8
UTF-8
8
alimaven
https://maven.aliyun.com/repository/public
alimaven
https://maven.aliyun.com/repository/public
com.google.dagger
dagger
2.41
com.google.dagger
dagger-compiler
2.41
true
org.apache.maven.plugins
maven-compiler-plugin
3.8.1
11
default-compile
none
default-testCompile
none
java-compile
compile
compile
java-test-compile
test-compile
testCompile
在pom.xml右键,Maven->Reload Project. 下载依赖。
编写一些应用的基本框架代码。刚开始不使用Dagger,当应用变得复杂时,我们再使用Dagger,凸显Dagger优越性。
创建统一的ATM可以处理的指令接口.
/** Logic to process some user input. */
interface Command {
/**
* String token that signifies this command should be selected (e.g.:
* "deposit", "withdraw")
*/
String key();
/** Process the rest of the command's words and do something. */
Result handleInput(List input);
/**
* This wrapper class is introduced to make a future change easier
* even though it looks unnecessary right now.
*/
final class Result {
private final Status status;
private Result(Status status) {
this.status = status;
}
static Result invalid() {
return new Result(Status.INVALID);
}
static Result handled() {
return new Result(Status.HANDLED);
}
Status status() {
return status;
}
}
enum Status {
INVALID,
HANDLED
}
}
创建CommandRouter记录ATM可以处理的所有指令,并将指令交给具体的实现类进行处理。
final class CommandRouter {
private final Map commands = new HashMap<>();
Result route(String input) {
List splitInput = split(input);
if (splitInput.isEmpty()) {
return invalidCommand(input);
}
String commandKey = splitInput.get(0);
Command command = commands.get(commandKey);
if (command == null) {
return invalidCommand(input);
}
List args = splitInput.subList(1, splitInput.size());
Result result = command.handleInput(args);
return result.status().equals(Status.INVALID) ?
invalidCommand(input) : result;
}
private Result invalidCommand(String input) {
System.out.println(
String.format("couldn't understand \"%s\". please try again.", input));
return Result.invalid();
}
// Split on whitespace
private static List split(String input) {
return Arrays.asList(input.trim().split("\\s+"));
}
}
创建Main函数实现上述类的交互。
class CommandLineAtm {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
CommandRouter commandRouter = new CommandRouter();
while (scanner.hasNextLine()) {
Command.Result unused = commandRouter.route(scanner.nextLine());
}
}
}
至此,完成了项目的准备工作。
feature/prepare
创建接口添加@Component,函数返回CommandRouter实例。
@Component
interface CommandRouterFactory {
CommandRouter router();
}
对项目进行编译 Build->Rebuild Project
java: 警告: 源发行版 11 需要目标发行版 11
“java: 警告: 源发行版 11 需要目标发行版 11”错误解决
E:\IdeaProjects\DeaggerDemo\Dagger2ATMApp\src\main\java\dagger\example\atm\CommandRouterFactory.java:6
java: [Dagger/MissingBinding] dagger.example.atm.CommandRouter cannot be provided without an @Inject constructor or an @Provides-annotated method.
dagger.example.atm.CommandRouter is requested at
dagger.example.atm.CommandRouterFactory.router()
毕竟,有地方注入才能进行返回。
给CommandRouter添加构造函数,并用@Inject标注。类似声明,需要CommandRouter的时候,可以调用我进行创建。
@Inject
public CommandRouter(){
}
对项目进行编译 Build->Rebuild Project
生成的CommandRouterFactory实现类DaggerCommandRouterFactory,内部调用new CommandRouter()
package dagger.example.atm;
import dagger.internal.DaggerGenerated;
@DaggerGenerated
final class DaggerCommandRouterFactory implements CommandRouterFactory {
...
public CommandRouter router() {
return new CommandRouter();
}
...
class CommandLineAtm {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
CommandRouterFactory commandRouterFactory =
DaggerCommandRouterFactory.create();
CommandRouter commandRouter = commandRouterFactory.router();
while (scanner.hasNextLine()) {
Command.Result unused = commandRouter.route(scanner.nextLine());
}
}
}
此致,完成mini Dagger2使用。
概念:
@Component 告诉Dagger实现一个接口或抽象类返回一个或多个对象。Dagger生成的Component实现类命名方式是DaggerYourType(DaggerYourType_NestedType对于内部类)
@Inject 修饰构造函数,告诉Dagger如何去实例化类的对象。
feature/init
让我们创建第一个指令,改变例子不能处理任何指令的的状况。
final class HelloWorldCommand implements Command {
@Inject
HelloWorldCommand() {
}
@Override
public String key() {
return "hello";
}
@Override
public Result handleInput(List input) {
if (!input.isEmpty()) {
return Result.invalid();
}
System.out.println("world!");
return Result.handled();
}
}
final class CommandRouter {
private final Map commands = new HashMap<>();
@Inject
CommandRouter(HelloWorldCommand helloWorldCommand) {
commands.put(helloWorldCommand.key(), helloWorldCommand);
}
...
}
对项目进行编译 Build->Rebuild Project
在控制台输入hello,就会返回world!
hello
world!
feature/first_command
修改CommandRouter构造函数依赖Comand接口而不是具体的类,方便以后进行扩展.eg. DepositCommand.
@Inject
CommandRouter(Command command) {
commands.put(command.key(), command);
}
...
对项目进行编译 Build->Rebuild Project
E:\IdeaProjects\DeaggerDemo\Dagger2ATMApp\src\main\java\dagger\example\atm\CommandRouterFactory.java:6
java: [Dagger/MissingBinding] dagger.example.atm.Command cannot be provided without an @Provides-annotated method.
dagger.example.atm.Command is injected at
dagger.example.atm.CommandRouter(command)
dagger.example.atm.CommandRouter is requested at
dagger.example.atm.CommandRouterFactory.router()
找不到Command实现类,需要给Dagger更多的提示信息。
创建Module,提示Dagger如何创建Command
@Module
abstract class HelloWorldModule {
@Binds
abstract Command helloWorldCommand(HelloWorldCommand command);
}
@Component(modules = HelloWorldModule.class)
interface CommandRouterFactory {
CommandRouter router();
}
对项目进行编译 Build->Rebuild Project
ok,恢复正常了。
Dagger需要Command但不知道如何创建时,发现@Binds返回Command,就会将helloWorldCommand()参数转化为Command进行注入。
概念
@Binds 只能修饰抽象函数,有些仅有一个参数与返回值是同一种类型。只能出现在@Module修饰的接口或抽象类中
@Module修饰的类是绑定方法的集合(被@Binds、@Providers等)
@Inject修饰的构造函数不需要声明在@Module修饰的类中
depending_interface
现在,HelloWorldCommand直接使用System.out.println()输出。为了方便扩展,添加一层抽象Outputter,避免直接调用System.out。这样,以后更换输出函数避免修改HelloWorldCommand。
interface Outputter {
void output(String output);
}
private final Outputter outputter;
@Inject
HelloWorldCommand(Outputter outputter) {
this.outputter = outputter;
}
...
@Override
public Result handleInput(List input) {
if (!input.isEmpty()) {
return Result.invalid();
}
outputter.output("world!");
return Result.handled();
}
通过前面我们可以知道,对于接口的注入需要进行下面三个步骤:
public class SystemOutOutputter implements Outputter{
@Inject
public SystemOutOutputter(){
}
@Override
public void output(String output) {
System.out.println(output);
}
}
@Module
public abstract class OutputterModule {
@Binds
abstract Outputter getOutputer(SystemOutOutputter outputter);
}
@Component(modules = {HelloWorldModule.class, OutputterModule.class})
interface CommandRouterFactory {
CommandRouter router();
}
由于SystemOutOutputter实现过于简单,我们也可以直接在@Module中进行实现
@Module
public abstract class OutputterModule {
@Provides
static Outputter getOutputer() {
return System.out::println;
}
}
正常的写法是下面的样式,上面是lambda简化后的效果。
@Module
public abstract class OutputterModule {
@Provides
static Outputter getOutputer() {
return new Outputter() {
@Override
public void output(String output) {
System.out.println(output);
}
};
}
}
完成了Outputter抽象。加入以后需要进行记录什么的,仅仅在一处修改就可以了。
概念
@Provides 相对于@Binds,@Providers修饰普通方法(非抽象函数)。函数返回值就是其提供的对象,参数就是其依赖。
@Providers 修饰的函数可以包含复杂的逻辑,只要可以返回特定的对象。不一定需要每一次都返回新的实例。这正是Dagger(依赖注入框架)的优点,
当请求一个对象时,是否创建新实例是框架的实现细节。以后,将使用"提供"而不是"创建",更为精确。
feature/ouputter
/** Abstract command that accepts a single argument. */
abstract class SingleArgCommand implements Command {
@Override
public final Result handleInput(List input) {
return input.size() == 1 ? handleArg(input.get(0)) : Result.invalid();
}
/** Handles the single argument to the command. */
protected abstract Result handleArg(String arg);
}
final class LoginCommand extends SingleArgCommand {
private final Outputter outputter;
@Inject
LoginCommand(Outputter outputter) {
this.outputter = outputter;
}
@Override
public String key() {
return "login";
}
@Override
public Result handleArg(String username) {
outputter.output(username + " is logged in.");
return Result.handled();
}
}
@Module
abstract class LoginCommandModule {
@Binds
abstract Command loginCommand(LoginCommand command);
}
@Component(modules = {LoginCommandModule.class, SystemOutModule.class})
interface CommandRouterFactory {
CommandRouter router();
}
对项目进行编译 Build->Rebuild Project Run
login gch
gch is logged in.
feature/login_command
目前为止 CommandRouter一次仅仅支持一个指令.如何让他一次支持多个指令?如果将LoginCommandModule.class、HelloWorldModule.class 同时加入@Component
对项目进行编译 Build->Rebuild Project
E:\IdeaProjects\DeaggerDemo\Dagger2ATMApp\src\main\java\dagger\example\atm\CommandRouterFactory.java:6
java: [Dagger/DuplicateBindings] dagger.example.atm.Command is bound multiple times:
@Binds dagger.example.atm.Command dagger.example.atm.HelloWorldModule.helloWorldCommand(dagger.example.atm.HelloWorldCommand)
@Binds dagger.example.atm.Command dagger.example.atm.LoginCommandModule.loginCommand(dagger.example.atm.LoginCommand)
dagger.example.atm.Command is injected at
dagger.example.atm.CommandRouter(command)
dagger.example.atm.CommandRouter is requested at
dagger.example.atm.CommandRouterFactory.router()
把Dagger整迷糊了,仅仅需要一个Command,却提供了两个。
下面使用@IntoMap支持多Command注入。
@Module
abstract class LoginCommandModule {
@Binds
@IntoMap
@StringKey("login")
abstract Command loginCommand(LoginCommand command);
}
@Module
abstract class HelloWorldModule {
@Binds
@IntoMap
@StringKey("hello")
abstract Command helloWorldCommand(HelloWorldCommand command);
}
@StringKey + @IntoMap,告诉Dagger如何创建Map
注意:
现在@StringKey指定了指令的key,Command接口key()可以删掉了。
final class CommandRouter {
private Map commands ;
@Inject
public CommandRouter(Map map){
this.commands = map;
}
...
}
对项目进行编译 Build->Rebuild Project Run
login gch
gch is logged in.
hello
world!
概念
@IntoMap 创建特定类型的Map,其key使用@StringKey或@IntKey指定,dagger确保没有重复的key
@IntoSet 创建Set集合.可以和@Binds、@Providers联合创建Set集合
@IntoMap、@IntoSet都是引入多绑定(集合包含多个元素并且元素来自不同的绑定方法)的方式.
feature/multibinding
既然用户可以进行登录了,让我们添加一些只有登录用户才能进行的操作。
跟踪每一个用户和他们的账户余额。
class Database {
private final Map accounts = new HashMap<>();
@Inject
Database() {}
Account getAccount(String username) {
return accounts.computeIfAbsent(username, Account::new);
}
static final class Account {
private final String username;
private BigDecimal balance = BigDecimal.ZERO;
Account(String username) {
this.username = username;
}
String username() {
return username;
}
BigDecimal balance() {
return balance;
}
void deposit(BigDecimal amount) {
balance = balance.add(amount);
}
}
}
将database注入LoginCommand, 用户登录后,打印账户余额。
final class LoginCommand extends SingleArgCommand {
private final Database database;
private final Outputter outputter;
@Inject
LoginCommand(Database database, Outputter outputter) {
this.database = database;
this.outputter = outputter;
}
@Override
public String key() {
return "login";
}
@Override
public Result handleArg(String username) {
Database.Account account = database.getAccount(username);
outputter.output(
username + " is logged in with balance: " + account.balance());
return Result.handled();
}
}
对项目进行编译 Build->Rebuild Project Run
login gch
gch is logged in with balance: 0
login gch
gch is logged in with balance: 0
现在登录可以打印余额了,不过可以重复登录。这个问题,我们接下来会进行解决。
feature/login_print
代码里面有点小问题,你发现了吗? 为了方便找到他,我们添加一些代码。
final class DepositCommand implements Command {
private final Database database;
private final Outputter outputter;
@Inject
DepositCommand(Database database, Outputter outputter) {
this.outputter = outputter;
this.database = database;
}
@Override
public Result handleInput(List input) {
if (input.size() != 2) {
return Result.invalid();
}
Database.Account account = database.getAccount(input.get(0));
account.deposit(new BigDecimal(input.get(1)));
outputter.output(account.username() + " now has: " + account.balance());
return Result.handled();
}
}
@Module
abstract class UserCommandsModule {
@Binds
@IntoMap
@StringKey("deposit")
abstract Command depositCommand(DepositCommand command);
}
@Component(modules = {LoginCommandModule.class, HelloWorldModule.class,UserCommandsModule.class, OutputterModule.class})
interface CommandRouterFactory {
CommandRouter router();
}
对项目进行编译 Build->Rebuild Project Run
deposit gch 2
gch now has: 2
login gch
gch is logged in with balance: 0
明明刚存入2,怎么登录显示余额0?
为了让情况更明显一些,在Database、LoginCommand、DepositCommand构造函数添加System.out.println("Creating a new " + this);
,为了让打印更清晰,在LoginCommand、DepositCommand构造函数再添加System.out.println("database:" + database);
对项目进行编译 Build->Rebuild Project Run
Creating a new org.Database@71e7a66b
Creating a new org.LoginCommand@246ae04d
database:org.Database@71e7a66b
Creating a new org.Database@2ef9b8bc
Creating a new org.DepositCommand@5d624da6
database:org.Database@2ef9b8bc
Dagger为LoinCommand、DepositCommand分别创建了Database对象。为了告诉Dagger,这两个使用同一个Database对象,我们需要添加@Singleton注解。
@Singleton
final class Database { ... }
@Singleton
@Component
interface CommandRouterFactory {
...
}
对项目进行编译 Build->Rebuild Project Run
Creating a new org.Database@591f989e
Creating a new org.LoginCommand@4cb2c100
database:org.Database@591f989e
Creating a new org.DepositCommand@77b52d12
database:org.Database@591f989e
现在是同一个Database实例啦。
概念
@Singleton 对于每一个component仅仅创建一个对象实例,适用于@Inject修饰构造函数的类、@Binds修饰的方法、@Providers修饰的方法
为什么将compoment添加@Singleton修饰,现在还不是挺清楚,接下来就更清晰啦。
feature/singleton
为了实现先登录后存款的宏大目标,让我们先对代码进行重构。
引入CommandProcessor,包含若干CommandRouters,将CommandRouter添加到CommandProcessor上就支持里面的指令集,删除就返回到之前支持的指令集。
@Singleton
final class CommandProcessor {
private final Deque commandRouterStack = new ArrayDeque<>();
@Inject
CommandProcessor(CommandRouter firstCommandRouter) {
commandRouterStack.push(firstCommandRouter);
}
Command.Status process(String input) {
Command.Result result = commandRouterStack.peek().route(input);
if (result.status().equals(Command.Status.INPUT_COMPLETED)) {
commandRouterStack.pop();
return commandRouterStack.isEmpty()
? Command.Status.INPUT_COMPLETED : Command.Status.HANDLED;
}
result.nestedCommandRouter().ifPresent(commandRouterStack::push);
return result.status();
}
}
interface Command {
/**
* Process the rest of the command's words and do something.
*/
Result handleInput(List input);
/**
* This wrapper class is introduced to make a future change easier
* even though it looks unnecessary right now.
*/
final class Result {
private final Status status;
private final Optional nestedCommandRouter;
private Result(Status status, Optional nestedCommandRouter) {
this.status = status;
this.nestedCommandRouter = nestedCommandRouter;
}
static Result invalid() {
return new Result(Status.INVALID, Optional.empty());
}
static Result handled() {
return new Result(Status.HANDLED, Optional.empty());
}
Status status() {
return status;
}
public Optional nestedCommandRouter() {
return nestedCommandRouter;
}
static Result enterNestedCommandSet(CommandRouter nestedCommandRouter) {
return new Result(Status.HANDLED, Optional.of(nestedCommandRouter));
}
}
enum Status {
INVALID,
HANDLED,
INPUT_COMPLETED
}
}
删除CommandRouterFactory,创建CommandProcessorFactory,其内容如下:
@Singleton
@Component(modules = {LoginCommandModule.class, HelloWorldModule.class, UserCommandsModule.class, OutputterModule.class})
interface CommandProcessorFactory {
CommandProcessor commandProcessor();
}
class CommandLineAtm {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
CommandProcessorFactory commandProcessorFactory =
DaggerCommandProcessorFactory.create();
CommandProcessor commandProcessor =
commandProcessorFactory.commandProcessor();
while (scanner.hasNextLine()) {
Command.Status unused = commandProcessor.process(scanner.nextLine());
}
}
}
feature/refactor
Dagger 已经可以做到从 Map@Subcomponent
@Subcomponent和@Component相似,他有dagger实现的抽象函数,可以添加@Modules.此外,有一个parent Component,他可以访问parent Component所有类型,但是parent 不能访问他的。类似于类的继承。
接下来创建 @Subcomponent,添加仅供登录用户使用的指令。共享CommandProcessorFactory Component中的Database实例,方便实现一个特定用户的存款和取钱。
@Subcomponent(modules = UserCommandsModule.class)
interface UserCommandsRouter {
CommandRouter router();
@Subcomponent.Factory
interface Factory {
UserCommandsRouter create(@BindsInstance Account account);
}
@Module(subcomponents = UserCommandsRouter.class)
interface InstallationModule {}
}
@Subcomponent modules指定创建实例时,Dagger可以查找依赖的类(Subcomponent独有),和@Component一样modules可以指定多个;
@Subcomponent内函数返回值,指定希望Dagger创建的类的实例;
@Subcomponent.Factory注解标注subcomponent工厂接口,其只有一个方法,并且要返回subcomponent对象。
@BindsInstance 注解参数Account,在subcomponent 中@Inject标注的构造函数、@Binds标注的方法、@Providers标注的方法都可以获取该Account。
@Module(subcomponents = UserCommandsRouter.class)定义的Module引入Component,就将subcomponent和Component建立了关系。
@Singleton
@Component(
modules = {
...
UserCommandsRouter.InstallationModule.class,
})
interface CommandProcessorFactory {
CommandProcessor commandProcessor();
}
final class LoginCommand extends SingleArgCommand {
private final Database database;
private final Outputter outputter;
private final UserCommandsRouter.Factory userCommandsRouterFactory;
@Inject
LoginCommand(Database database, Outputter outputter,UserCommandsRouter.Factory userCommandsRouterFactory) {
this.database = database;
this.outputter = outputter;
this.userCommandsRouterFactory = userCommandsRouterFactory;
}
@Override
public Result handleArg(String username) {
Database.Account account = database.getAccount(username);
outputter.output(
username + " is logged in with balance: " + account.balance());
return Result.enterNestedCommandSet(
userCommandsRouterFactory.create(account).router());
}
}
现在只有登录之后才能进行存款,我们可以从指令中移除username,因为@BindsInstance已经提供账号。
/**
* Abstract {@link Command} that expects a single argument that can be converted to {@link
* BigDecimal}.
*/
abstract class BigDecimalCommand extends SingleArgCommand {
private final Outputter outputter;
protected BigDecimalCommand(Outputter outputter) {
this.outputter = outputter;
}
@Override
protected final Result handleArg(String arg) {
BigDecimal amount = tryParse(arg);
if (amount == null) {
outputter.output(arg + " is not a valid number");
} else if (amount.signum() <= 0) {
outputter.output("amount must be positive");
} else {
handleAmount(amount);
}
return Result.handled();
}
private static BigDecimal tryParse(String arg) {
try {
return new BigDecimal(arg);
} catch (NumberFormatException e) {
return null;
}
}
/** Handles the given (positive) {@code amount} of money. */
protected abstract void handleAmount(BigDecimal amount);
}
final class DepositCommand extends BigDecimalCommand {
private final Database.Account account;
private final Outputter outputter;
@Inject
DepositCommand(Database.Account account, Outputter outputter) {
super(outputter);
this.account = account;
this.outputter = outputter;
}
@Override
protected void handleAmount(BigDecimal amount) {
account.deposit(amount);
outputter.output(account.username() + " now has: " + account.balance());
}
}
@Subcomponent修饰的类和@Component修饰的类一样,有一个工厂方法返回对象
subcomponent可以使用modules提供依赖
submomponent总是有一个 parent component(或者parent subcomponent)
subcomponent可以借助parent component资源获取依赖,但是parent 不能借助于subcomponent资源
Subcomponent.Factory在parent component中调用,创建subcomponent实例
类似的,@Component.Factory适用于@Component修饰的类,@BindsInstance同样实现运行时参数注入。
对项目进行编译 Build->Rebuild Project Run
deposit 1000
couldn't understand "deposit 1000". please try again.
login guchuanhang
guchuanhang is logged in with balance: 0
deposit 100
guchuanhang now has: 100
feature/subcomponent
final class WithdrawCommand extends BigDecimalCommand {
private final Database.Account account;
private final Outputter outputter;
@Inject
WithdrawCommand(Database.Account account, Outputter outputter) {
super(outputter);
this.account = account;
this.outputter = outputter;
}
@Override
protected void handleAmount(BigDecimal amount) {
account.withdraw(amount);
outputter.output(account.username() + " now has: " + account.balance());
}
}
@Singleton
class Database {
...
static final class Account {
...
void withdraw(BigDecimal amount) {
balance = balance.subtract(amount);
}
}
}
这个是登录之后才能执行的指令,将其添加到UserCommandsModule
@Module
abstract class UserCommandsModule {
...
@Binds
@IntoMap
@StringKey("withdraw")
abstract Command withdrawCommand(WithdrawCommand command);
}
现在我们做一些配置:
有很多方式可以实现,这里我们通过WithdrawCommand构造函数注入的方式进行实现。
@Module
interface AmountsModule {
@Provides
static BigDecimal minimumBalance() {
return BigDecimal.ZERO;
}
@Provides
static BigDecimal maximumWithdrawal() {
return new BigDecimal(1000);
}
}
@Subcomponent(modules = {UserCommandsModule.class, AmountsModule.class})
interface UserCommandsRouter {
...
final class WithdrawCommand extends BigDecimalCommand {
private final Database.Account account;
private final Outputter outputter;
private final BigDecimal minimumBalance;
private final BigDecimal maximumWithdrawal;
@Inject
WithdrawCommand(Database.Account account, Outputter outputter, BigDecimal minimumBalance,
BigDecimal maximumWithdrawal) {
super(outputter);
this.account = account;
this.outputter = outputter;
this.minimumBalance = minimumBalance;
this.maximumWithdrawal = maximumWithdrawal;
}
@Override
protected void handleAmount(BigDecimal amount) {
if (amount.compareTo(maximumWithdrawal) > 0) {
outputter.output("超过单次最大提取额度");
return;
}
BigDecimal newBalance = account.balance().subtract(amount);
if (newBalance.compareTo(minimumBalance) < 0) {
outputter.output("超过最低余额限制");
return;
}
account.withdraw(amount);
outputter.output(account.username() + " new balance is: " + account.balance());
}
}
对项目进行编译 Build->Rebuild Project
E:\IdeaProjects\DeaggerDemo\Dagger2ATMApp\src\main\java\dagger\example\atm\CommandProcessorFactory.java:10
java: [Dagger/DuplicateBindings] java.math.BigDecimal is bound multiple times:
@Provides java.math.BigDecimal dagger.example.atm.AmountsModule.maximumWithdrawal()
@Provides java.math.BigDecimal dagger.example.atm.AmountsModule.minimumBalance()
java.math.BigDecimal is injected at
dagger.example.atm.WithdrawCommand(…, maximumWithdrawal)
dagger.example.atm.WithdrawCommand is injected at
dagger.example.atm.UserCommandsModule.withdrawCommand(command)
java.util.Map is injected at
dagger.example.atm.CommandRouter(map)
dagger.example.atm.CommandRouter is requested at
dagger.example.atm.UserCommandsRouter.router() [dagger.example.atm.CommandProcessorFactory → dagger.example.atm.UserCommandsRouter]
It is also requested at:
dagger.example.atm.WithdrawCommand(…, minimumBalance, …)
提示BigDecimal绑定多次, Dagger蒙圈了,不知道用哪一个。
为了处理这种有多个相同类型返回值,使用修饰符。修饰符是被@Qualifier注释的注解。
@Qualifier
@Retention(RUNTIME)
@interface MinimumBalance {}
@Qualifier
@Retention(RUNTIME)
@interface MaximumWithdrawal {}
@Module
interface AmountsModule {
@Provides
@MinimumBalance
static BigDecimal minimumBalance() {
return BigDecimal.ZERO;
}
@Provides
@MaximumWithdrawal
static BigDecimal maximumWithdrawal() {
return new BigDecimal(1000);
}
}
毕竟要区分出来使用到的是Module里面的哪一个函数提供的返回值嘛
final class WithdrawCommand extends BigDecimalCommand {
@Inject
WithdrawCommand(Database.Account account, Outputter outputter,
@MinimumBalance BigDecimal minimumBalance,
@MaximumWithdrawal BigDecimal maximumWithdrawal) {
...
对项目进行编译 Build->Rebuild Project现在就可以了。 稍微进行测试一下
login gch
gch is logged in with balance: 0
withdraw 100
超过最低余额限制
deposit 10000
gch now has: 10000
withdraw 2000
超过单次最大提取额度
withdraw 1000
gch new balance is: 9000
概念
@Qualifier 用于区分无关但类型相同的实例
@Qualifier 常用于区分很多地方都会用到的基本类型int、String等
feature/withdraw
现在ATM只能登录不能退出,现在让我们添加一下退出功能。
final class LogoutCommand implements Command {
private final Outputter outputter;
@Inject
LogoutCommand(Outputter outputter) {
this.outputter = outputter;
}
@Override
public Result handleInput(List input) {
if (input.isEmpty()) {
outputter.output("退出登录成功");
return Result.inputCompleted();
} else {
return Result.invalid();
}
}
}
interface Command {
final class Result {
public static Result inputCompleted() {
return new Result(Status.INPUT_COMPLETED,Optional.empty());
}
@Module
abstract class UserCommandsModule {
...
@Binds
@IntoMap
@StringKey("logout")
abstract Command logout(LogoutCommand command);
对项目进行编译 Build->Rebuild Project Run 测试效果.
login gch
gch is logged in with balance: 0
deposit 10
gch now has: 10
logout
退出登录成功
deposit 10
couldn't understand "deposit 10". please try again.
feature/logout
上面我们已经配置单次取款最大额度。但是,通过多次取款完全可以超过取款最大限额。如果要设置最大限额,我们要怎么做呢?并且存款后增加取款限额呢?(在同一个session)
final class WithdrawalLimiter {
private BigDecimal remainingWithdrawalLimit;
@Inject
WithdrawalLimiter(@MaximumWithdrawal BigDecimal maximumWithdrawal) {
this.remainingWithdrawalLimit = maximumWithdrawal;
}
void recordDeposit(BigDecimal amount) {
remainingWithdrawalLimit = this.remainingWithdrawalLimit.add(amount);
}
void recordWithdrawal(BigDecimal amount) {
remainingWithdrawalLimit = this.remainingWithdrawalLimit.subtract(amount);
}
BigDecimal getRemainingWithdrawalLimit() {
return remainingWithdrawalLimit;
}
}
final class DepositCommand extends BigDecimalCommand {
private final Database.Account account;
private final Outputter outputter;
private final WithdrawalLimiter withdrawalLimiter;
@Inject
DepositCommand(Database.Account account, Outputter outputter, WithdrawalLimiter withdrawalLimiter) {
super(outputter);
this.account = account;
this.outputter = outputter;
this.withdrawalLimiter = withdrawalLimiter;
}
@Override
protected void handleAmount(BigDecimal amount) {
account.deposit(amount);
withdrawalLimiter.recordDeposit(amount);
outputter.output(account.username() + " now has: " + account.balance());
}
}
final class WithdrawCommand extends BigDecimalCommand {
private final Database.Account account;
private final Outputter outputter;
private final BigDecimal minimumBalance;
private final WithdrawalLimiter withdrawalLimiter;
@Inject
WithdrawCommand(Database.Account account, Outputter outputter,
@MinimumBalance BigDecimal minimumBalance,
WithdrawalLimiter withdrawalLimiter) {
super(outputter);
this.account = account;
this.outputter = outputter;
this.minimumBalance = minimumBalance;
this.withdrawalLimiter = withdrawalLimiter;
}
@Override
protected void handleAmount(BigDecimal amount) {
if (amount.compareTo(withdrawalLimiter.getRemainingWithdrawalLimit()) > 0) {
outputter.output("超过Session最大提取额度");
return;
}
BigDecimal newBalance = account.balance().subtract(amount);
if (newBalance.compareTo(minimumBalance) < 0) {
outputter.output("超过最低余额限制");
return;
}
account.withdraw(amount);
withdrawalLimiter.recordWithdrawal(amount);
outputter.output(account.username() + " new balance is: " + account.balance());
}
}
对项目进行编译 Build->Rebuild Project Run 测试程序
login gch
gch is logged in with balance: 0
deposit 10000
gch now has: 10000
withdraw 2000
超过Session最大提取额度
出错了,存入10000,我最大可以取出1000+10000才对,怎么2000就超过最大额度了?
相信你也有感觉,会不会是WithdrawalLimiter创建了多个实例的问题? 英雄所见略同,我也这么认为呢! 然后再WithdrawCommand和DepositCommand构造函数都打印一下WithdrawalLimiter对象地址
对项目进行编译 Build->Rebuild Project Run 进行测试。
login gch
gch is logged in with balance: 0
DepositCommand.withdrawalLimiter:org.WithdrawalLimiter@5056dfcb
WithdrawCommand.withdrawalLimiter:org.WithdrawalLimiter@2344fc66
还真不是同一个对象。
我们之前通过@Singleton注解实现了Database的全局单例,抛开使用Singleton是否有意义不说,如果给WithdrawLimit添加@Singleton,也需要给UserCommandsRouter添加@Singleton。
对项目进行编译 Build->Rebuild Project
E:\IdeaProjects\DeaggerDemo\Dagger2ATMApp\src\main\java\dagger\example\atm\CommandProcessorFactory.java:9
java: [dagger.example.atm.UserCommandsRouter] dagger.example.atm.UserCommandsRouter has conflicting scopes:
dagger.example.atm.CommandProcessorFactory also has @Singleton
@Singleton的源码如下:
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Singleton {
}
也就是作为subcomponent其scope不能与component相同。起码换个名字嘛。
@Scope
@Documented
@Retention(RUNTIME)
@interface PerSession {}
@PerSession
final class WithdrawalLimiter {
...
@PerSession
@Subcomponent(modules = {UserCommandsModule.class, AmountsModule.class})
interface UserCommandsRouter {
...
对项目进行编译 Build->Rebuild Project Run 进行测试。
login gch
gch is logged in with balance: 0
deposit 10000
gch now has: 10000
withdraw 2000
gch new balance is: 8000
withdraw 2000
gch new balance is: 6000
logout
退出登录成功
login gch
gch is logged in with balance: 6000
withdraw 600
gch new balance is: 5400
withdraw 500
超过Session最大提取额度
概念
@Scope注解声明对于同一个component(或subcomponent)实例提供一个共享的对象(也就是该范围内单例)
@Singleton 只是框架提供的@Scope 注解
@Scope 限定范围的对象的声明周期是和component对象进行绑定的
注解的名字没有任何意义(@Singleton和@PerSession源码 除了名字完全一样)
在一个JVM中多@Component创建时,@Scope修饰对象也会创建多个。
feature/session
对项目进行编译 Build->Rebuild Project Run进行测试.
login gch
gch is logged in with balance: 0
login gch
gch is logged in with balance: 0
问题出在哪里呢? @Subcomponent UserCommandsRouter集成了@Component CommandProcessorFactory [HelloWorldCommand, LoginCommand],支持的指令集合为:[HelloWorldCommand, LoginCommand, DepositCommand, WithdrawCommand]。
虽然不能从Map中删除LoginCommand,但是有另外一个方法。
在LoginCommand添加Optional
,指示当前是否为登录状态。
final class LoginCommand extends SingleArgCommand {
private final Database database;
private final Outputter outputter;
private final UserCommandsRouter.Factory userCommandsRouterFactory;
private final Optional account;
@Inject
LoginCommand(Database database, Outputter outputter, UserCommandsRouter.Factory userCommandsRouterFactory,
Optional account
) {
this.database = database;
this.outputter = outputter;
this.userCommandsRouterFactory = userCommandsRouterFactory;
this.account = account;
}
@Override
public Result handleArg(String username) {
if (this.account.isPresent()) {
outputter.output("当前已处于登录状态,退出登录前不能进行登录");
return Result.invalid();
}
Database.Account account = database.getAccount(username);
outputter.output(
username + " is logged in with balance: " + account.balance());
return Result.enterNestedCommandSet(
userCommandsRouterFactory.create(account).router());
}
}
Optional
@Module
abstract class LoginCommandModule {
@BindsOptionalOf
abstract Database.Account optionalAccount();
@BindsOptionalOf告诉Dagger,当发现Account后使用Optional.of()去创建他,如果他一直不出现Optional.empty()(使用Guava版本Optional是Optional.absent())就会返回true。
回顾一下会发现CommandProcessorFactory没有Account,UserCommandsRouter有。每一次创建UserCommandsRouter都会调用LoginCommand,每一次调用optional account不同(有登录的和无登录的),我们可以据此进行区分是否为登录状态。
概念
@IntoMap、@IntoSet等多绑定指令出现在subcomponent时,除包含自身出现的集合外,同时包含parent Component中出现的内容。
@BindsOptionalOf Optional告诉Dagger当ReturnType出现时,构建Optional对象。
找到仅仅在subcomponent中出现的内容,进行subcomponent和component的区分。
对项目进行编译 Build->Rebuild Project Run测试程序.
login gch
gch is logged in with balance: 0
login abc
当前已处于登录状态,退出登录前不能进行登录
couldn't understand "login abc". please try again.
logout
退出登录成功
login abc
abc is logged in with balance: 0
feature/repeat_login
官方版本