dagger2 入门指南ATM

文章目录

参考地址: https://dagger.dev/tutorial/01-setup
代码仓库地址:https://gitee.com/guchuanhang/dagger-tutorial

演示dagger2使用

完成指令行ATM应用,演示dagger2使用。可以跟踪账户余额,在控制台接收指令。

> deposit 20
> withdraw 10

环境搭建

这里使用的 Intellij IDEA社区版,使用Maven进行构建。Maven内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dagger.example.atm</groupId>
    <artifactId>Dagger2ATMApp</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>8</java.version>
    </properties>
    <!-- 配置国内源-->
    <repositories>
        <repository>
            <id>alimaven</id>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>alimaven</id>
            <url>https://maven.aliyun.com/repository/public</url>
        </pluginRepository>
    </pluginRepositories>

    <!-- 配置dagger2依赖 -->
    <dependencies>
        <dependency>
            <groupId>com.google.dagger</groupId>
            <artifactId>dagger</artifactId>
            <version>2.41</version>
        </dependency>

        <dependency>
            <groupId>com.google.dagger</groupId>
            <artifactId>dagger-compiler</artifactId>
            <version>2.41</version>
            <optional>true</optional>
        </dependency>
    </dependencies>
    <!-- 配置支持dagger2 生成代码的框架-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <phase>none</phase>
                    </execution>
                    <execution>
                        <id>default-testCompile</id>
                        <phase>none</phase>
                    </execution>
                    <execution>
                        <id>java-compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>java-test-compile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

在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<String> 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<String, Command> commands = new HashMap<>();

  Result route(String input) {
    List<String> 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<String> 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<String> 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<String> input) {
        if (!input.isEmpty()) {
            return Result.invalid();
        }
        System.out.println("world!");
        return Result.handled();
    }
}

将HelloWorldCommand添加到CommandRouter

final class CommandRouter {
  private final Map<String, Command> 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<String> 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<String> 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<String,Command>.

注意: 现在@StringKey指定了指令的key,Command接口key()可以删掉了。

CommandRouter直接注入多Command

final class CommandRouter {
    private  Map<String, Command> commands ;

    @Inject
    public CommandRouter(Map<String,Command> 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<String, Account> 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<String> 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<CommandRouter> 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<String> 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<CommandRouter> nestedCommandRouter;

        private Result(Status status, Optional<CommandRouter> 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<CommandRouter> 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<String, Command>创建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<java.lang.String,dagger.example.atm.Command> 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<String> 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<Account>,指示当前是否为登录状态。

LoginCommand添加是否已登录判断

final class LoginCommand extends SingleArgCommand {
    private final Database database;
    private final Outputter outputter;
    private final UserCommandsRouter.Factory userCommandsRouterFactory;
    private final Optional<Database.Account> account;

    @Inject
    LoginCommand(Database database, Outputter outputter, UserCommandsRouter.Factory userCommandsRouterFactory,
                 Optional<Database.Account> 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<Account>

@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<ReturnType>告诉Dagger当ReturnType出现时,构建Optional<ReturnType>对象。
找到仅仅在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
官方版本

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值