# Dagger Tutorial **Repository Path**: guchuanhang/dagger-tutorial ## Basic Information - **Project Name**: Dagger Tutorial - **Description**: dagger2 官方入门教程 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-08-06 - **Last Updated**: 2023-08-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: dagger2 ## README @[toc] 翻译地址: https://dagger.dev/tutorial/01-setup 代码仓库地址:https://gitee.com/guchuanhang/dagger-tutorial # 演示dagger2使用 完成指令行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 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 创建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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Fprepare/) ## 开始集成Dagger2注解 ### 自动生成CommandRouter实例 创建接口添加@Component,函数返回CommandRouter实例。 ``` @Component interface CommandRouterFactory { CommandRouter router(); } ``` 对项目进行编译 Build->Rebuild Project java: 警告: 源发行版 11 需要目标发行版 11 [“java: 警告: 源发行版 11 需要目标发行版 11”错误解决](https://blog.csdn.net/u012660464/article/details/127995658) ``` 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(); } ... ``` ### 使用自动生成的CommandRouterFactory获取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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature/init/) ## 添加第一个指令 让我们创建第一个指令,改变例子不能处理任何指令的的状况。 ### 实现HelloWorld 指令 ``` 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(); } } ``` ### 将HelloWorldCommand添加到CommandRouter ``` 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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Ffirst_command/) ## 依赖接口而不依赖实现 修改CommandRouter构造函数依赖Comand接口而不是具体的类,方便以后进行扩展.eg. DepositCommand. ### 修改CommandRouter依赖接口不依赖具体的类 ``` @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更多的提示信息。 ### 提示Dagger获取Command接口 创建Module,提示Dagger如何创建Command ``` @Module abstract class HelloWorldModule { @Binds abstract Command helloWorldCommand(HelloWorldCommand command); } ``` ### CommandRouterFactory添加module,获取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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Fdepending_interface/) ## 提取输出 现在,HelloWorldCommand直接使用System.out.println()输出。为了方便扩展,添加一层抽象Outputter,避免直接调用System.out。这样,以后更换输出函数避免修改HelloWorldCommand。 ### 创建输出接口 ``` interface Outputter { void output(String output); } ``` ### 修改HelloWorldCommand使用Outputter ``` 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(); } ``` ### 提供Outputter 通过前面我们可以知道,对于接口的注入需要进行下面三个步骤: 1. 创建被@Inject修饰构造函数的实现类 ``` public class SystemOutOutputter implements Outputter{ @Inject public SystemOutOutputter(){ } @Override public void output(String output) { System.out.println(output); } } ``` 2. 创建使用接口说明的@Module、@Binds函数 ``` @Module public abstract class OutputterModule { @Binds abstract Outputter getOutputer(SystemOutOutputter outputter); } ``` 3. 将@Module添加到@Component,实现Component依赖的查找 ``` @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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Fouputter/) ## 添加登录指令 ### 创建单参数抽象类SingleArgCommand ``` /** 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); } ``` ### 创建登录指令类LoginCommand ``` 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(); } } ``` ### 创建LoginCommandModule绑定LoginCommand ``` @Module abstract class LoginCommandModule { @Binds abstract Command loginCommand(LoginCommand command); } ``` ### 在Component使用LoginCommandModule ``` @Component(modules = {LoginCommandModule.class, SystemOutModule.class}) interface CommandRouterFactory { CommandRouter router(); } ``` 对项目进行编译 Build->Rebuild Project Run ``` login gch gch is logged in. ``` [feature/login_command](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Flogin_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注入。 ### 修改LoginCommandModule ``` @Module abstract class LoginCommandModule { @Binds @IntoMap @StringKey("login") abstract Command loginCommand(LoginCommand command); } ``` ### 修改HelloWorldModule ``` @Module abstract class HelloWorldModule { @Binds @IntoMap @StringKey("hello") abstract Command helloWorldCommand(HelloWorldCommand command); } ``` @StringKey + @IntoMap,告诉Dagger如何创建Map. `注意:` 现在@StringKey指定了指令的key,Command接口key()可以删掉了。 ### CommandRouter直接注入多Command ``` 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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Fmultibinding/) ## 特定类型的操作 既然用户可以进行登录了,让我们添加一些只有登录用户才能进行的操作。 ### 创建Database 跟踪每一个用户和他们的账户余额。 ``` 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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Flogin_print/) ## 保持状态 代码里面有点小问题,你发现了吗? 为了方便找到他,我们添加一些代码。 ### 创建存款指令DepositCommand ``` 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(); } } ``` ### 创建DepositCommand对应Module ``` @Module abstract class UserCommandsModule { @Binds @IntoMap @StringKey("deposit") abstract Command depositCommand(DepositCommand command); } ``` ### 添加Component ``` @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注解。 1. 给Database添加@Singleton注解 2. 给@Component添加@Singleton注解,声明@Singleton修饰的Database实例在其他依赖中进行共享 ``` @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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Fsingleton/) ## 先登录后存款 为了实现先登录后存款的宏大目标,让我们先对代码进行重构。 ### 引入指令处理器 引入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(); } } ``` ## 调整Command ``` 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 } } ``` ### 修改Component 删除CommandRouterFactory,创建CommandProcessorFactory,其内容如下: ``` @Singleton @Component(modules = {LoginCommandModule.class, HelloWorldModule.class, UserCommandsModule.class, OutputterModule.class}) interface CommandProcessorFactory { CommandProcessor commandProcessor(); } ``` ### 修改CommandLineAtm ``` 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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Frefactor/) ## 创建内嵌指令 Dagger 已经可以做到从 Map创建CommandRouter,但是这些指令任何用户都可用。并且每个指令都要指定用户名,必须每次都输入用户名。下面让我们添加一个用户session和指令组,只对完成登录的用户可用。但是怎么创建两个拥有不同Map的CommandRouters?(一个已登录用户使用,一个未登录用户使用)`@Subcomponent` @Subcomponent和@Component相似,他有dagger实现的抽象函数,可以添加@Modules.此外,有一个parent Component,他可以访问parent Component所有类型,但是parent 不能访问他的。类似于类的继承。 接下来创建 @Subcomponent,添加仅供登录用户使用的指令。共享CommandProcessorFactory Component中的Database实例,方便实现一个特定用户的存款和取钱。 ### 创建Subcomponent ``` @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建立了关系。 ``` ### 在Component添加subcomponent ``` @Singleton @Component( modules = { ... UserCommandsRouter.InstallationModule.class, }) interface CommandProcessorFactory { CommandProcessor commandProcessor(); } ``` ### 在LoginCommand添加UserCommandsRouter ``` 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已经提供账号。 ### 添加BigDecimalCommand,简化存款指令 ``` /** * 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); } ``` ### 修改DepositCommand ``` 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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Fsubcomponent/) ## 添加取款指令 ### 取款指令工具类 ``` 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()); } } ``` ### Database.Account 添加取款方法 ``` @Singleton class Database { ... static final class Account { ... void withdraw(BigDecimal amount) { balance = balance.subtract(amount); } } } ``` ### 在UserCommandsModule添加取款指令 这个是登录之后才能执行的指令,将其添加到UserCommandsModule ``` @Module abstract class UserCommandsModule { ... @Binds @IntoMap @StringKey("withdraw") abstract Command withdrawCommand(WithdrawCommand command); } ``` 现在我们做一些配置: 1. 限制用户取款,账户额度不能小于某个值 2. 设置单次交易可以取款最大值 有很多方式可以实现,这里我们通过WithdrawCommand构造函数注入的方式进行实现。 ### 定义账户额度限制Provider ``` @Module interface AmountsModule { @Provides static BigDecimal minimumBalance() { return BigDecimal.ZERO; } @Provides static BigDecimal maximumWithdrawal() { return new BigDecimal(1000); } } ``` ### 添加到UserCommandsRouter ``` @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 {} ``` ### 给AmoutModule 提供金额的函数添加注解 ``` @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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Fwithdraw/) ## 退出登录 现在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(); } } } ``` ### Database.Result 添加退出功能 ``` interface Command { final class Result { public static Result inputCompleted() { return new Result(Status.INPUT_COMPLETED,Optional.empty()); } ``` ### 添加UserCommandsModule,注册到登录后的指令中 ``` @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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Flogout/) ## 设置取款最大限额 上面我们已经配置单次取款最大额度。但是,通过多次取款完全可以超过取款最大限额。如果要设置最大限额,我们要怎么做呢?并且存款后增加取款限额呢?(在同一个session) ### WithdrawalLimiter跟踪最大限额和余额变化 ``` 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; } } ``` ### 将WithdrawalLimiter添加到存款指令和取款指令 ``` 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相同。起码换个名字嘛。 ### 定义SessionScope PerSession注解 ``` @Scope @Documented @Retention(RUNTIME) @interface PerSession {} ``` ### 给WithdrawLimit添加注解 ``` @PerSession final class WithdrawalLimiter { ... ``` ### 给component添加注解 ``` @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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Fmax_withdraw/) ## 避免重复登录 对项目进行编译 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``,指示当前是否为登录状态。 ### LoginCommand添加是否已登录判断 ``` 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()); } } ``` ### LoginCommandModule声明如何创建`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](https://gitee.com/guchuanhang/dagger-tutorial/tree/feature%2Frepeat_login/) [官方版本](https://gitee.com/guchuanhang/dagger-tutorial/tree/master/)