dagger2 入门指南ATM

文章目录

  • 演示dagger2使用
    • 环境搭建
    • 前奏
      • 创建指令接口
      • 指令路由器
      • 创建Main
    • 开始集成Dagger2注解
      • 自动生成CommandRouter实例
      • 使用自动生成的CommandRouterFactory获取CommandRouter对象
    • 添加第一个指令
      • 实现HelloWorld 指令
      • 将HelloWorldCommand添加到CommandRouter
    • 依赖接口而不依赖实现
      • 修改CommandRouter依赖接口不依赖具体的类
      • 提示Dagger获取Command接口
      • CommandRouterFactory添加module,获取Command的提示
    • 提取输出
      • 创建输出接口
      • 修改HelloWorldCommand使用Outputter
      • 提供Outputter
    • 添加登录指令
      • 创建单参数抽象类SingleArgCommand
      • 创建登录指令类LoginCommand
      • 创建LoginCommandModule绑定LoginCommand
      • 在Component使用LoginCommandModule
    • 一个参数两个指令
      • 修改LoginCommandModule
      • 修改HelloWorldModule
      • CommandRouter直接注入多Command
    • 特定类型的操作
      • 创建Database
      • 登录用户打印余额
    • 保持状态
      • 创建存款指令DepositCommand
      • 创建DepositCommand对应Module
      • 添加Component
    • 先登录后存款
      • 引入指令处理器
    • 调整Command
      • 修改Component
      • 修改CommandLineAtm
    • 创建内嵌指令
      • 创建Subcomponent
      • 在Component添加subcomponent
      • 在LoginCommand添加UserCommandsRouter
      • 添加BigDecimalCommand,简化存款指令
      • 修改DepositCommand
    • 添加取款指令
      • 取款指令工具类
      • Database.Account 添加取款方法
      • 在UserCommandsModule添加取款指令
      • 定义账户额度限制Provider
      • 添加到UserCommandsRouter
      • 修改取款指令,注入额度限制
      • 定义区分数额的修饰符
      • 给AmoutModule 提供金额的函数添加注解
      • 提取指令构造函数参数添加注解
    • 退出登录
      • 退出指令
      • Database.Result 添加退出功能
      • 添加UserCommandsModule,注册到登录后的指令中
    • 设置取款最大限额
      • WithdrawalLimiter跟踪最大限额和余额变化
      • 将WithdrawalLimiter添加到存款指令和取款指令
      • 定义SessionScope PerSession注解
      • 给WithdrawLimit添加注解
      • 给component添加注解
    • 避免重复登录
      • LoginCommand添加是否已登录判断
      • LoginCommandModule声明如何创建`Optional`

参考地址: 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

开始集成Dagger2注解

自动生成CommandRouter实例

创建接口添加@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();
    }
...

使用自动生成的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

添加第一个指令

让我们创建第一个指令,改变例子不能处理任何指令的的状况。

实现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

依赖接口而不依赖实现

修改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

提取输出

现在,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);
    }
}

  1. 创建使用接口说明的@Module、@Binds函数
@Module
public abstract class OutputterModule {

    @Binds
    abstract Outputter getOutputer(SystemOutOutputter outputter);
}

  1. 将@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

添加登录指令

创建单参数抽象类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

一个参数两个指令

目前为止 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

特定类型的操作

既然用户可以进行登录了,让我们添加一些只有登录用户才能进行的操作。

创建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

保持状态

代码里面有点小问题,你发现了吗? 为了方便找到他,我们添加一些代码。

创建存款指令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

先登录后存款

为了实现先登录后存款的宏大目标,让我们先对代码进行重构。

引入指令处理器

引入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

创建内嵌指令

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

添加取款指令

取款指令工具类

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

退出登录

现在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

设置取款最大限额

上面我们已经配置单次取款最大额度。但是,通过多次取款完全可以超过取款最大限额。如果要设置最大限额,我们要怎么做呢?并且存款后增加取款限额呢?(在同一个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

避免重复登录

对项目进行编译 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
官方版本

你可能感兴趣的:(java,dagger2)