AWS Lambda で Dagger 2 を使う

AWS Lambda + API Gateway で Serverless Application を構築する際に Dagger 2 を使うサンプルです。

AWS Lambda 関数を使用する際のベストプラクティス - AWS Lambda において、Java では Dagger の利用が推奨されています。

依存関係の複雑さを最小限に抑えます。フレームワークを単純化して、実行コンテキスト起動時のロードの高速化を優先します。たとえば、Spring Framework などの複雑なフレームワークよりも、Dagger や Guice などの単純な Java 依存関係インジェクション (IoC) フレームワークを使用します。

アプリケーション構成

  • ApiHandler: Lambda のエントリポイント
  • Controller: ServerlessInput / ServerlessOutput を操作する
  • Service: 実際の処理を行う

ライブラリ

  • Dagger 2.14

Dagger 系クラスの説明

  • アプリケーション全体でインスタンスを共有するための AppComponent, AppModule, (@Singletone annotation)
  • HTTPリクエスト単位でインスタンスを生成・共有するための RequestComponent ,RequestModule, @Request annotation
  • RequestComponentAppComponent の Subcomponent とし、リクエストを処理するタイミングで生成する (appComponent.newRequest())
  • RequestComponent の生成時には、そのリクエストに紐づいた情報をRequestModule を使って各オブジェクトに渡すようにする (RequestModule のコンストラクタに設定する)
    • (TODO Binding Instances が使えるかもしれない。ただし型ではなく名前ベースになる)

Dagger 関連ソースコード

di/AppComponent.java

@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
  RequestComponent newRequest(RequestModule requestModule);
}

di/AppModule.java

@Module
public class AppModule {
  @Provides
  @Singleton
  public static AuthModel provideAuthModel() {
    return new AuthModel();
  }
}

di/RequestScope.java

@Scope
@Retention(RetentionPolicy.RUNTIME)
@interface RequestScope {}

di/RequestComponent.java

@RequestScope
@Subcomponent(modules = {RequestModule.class})
public interface RequestComponent {
  SystemController newSystemController();

  UserController newUserController();
}

di/RequestModule.java

@Module
public class RequestModule {
  private final String serverlessInput;
  private final Object context;

  public RequestModule(String serverlessInput, Object context) {
    this.serverlessInput = serverlessInput;
    this.context = context;
  }

  @Provides
  public SystemService provideSystemService() {
    return new SystemService();
  }

  @Provides
  @RequestScope
  public UserInfo provideUserInfo() {
    return new UserInfo();
  }

  @Provides
  @Named("ServerlessInput")
  public String provideServerlessInput() {
    return serverlessInput;
  }

  @Provides
  @Named("Context")
  public Object provideContext() {
    return context;
  }
}

アプリケーションソースコード

build.gradle

plugins {
    id 'java-library'
    id "net.ltgt.apt" version "0.10"
    id 'eclipse'
}

repositories {
    jcenter()
}

dependencies {
    api 'org.apache.commons:commons-math3:3.6.1'

    testCompile 'org.jmockit:jmockit:1.42'
    testCompile 'org.junit.jupiter:junit-jupiter-api:5.2.0'
    implementation 'com.google.guava:guava:23.0'
    implementation 'org.apache.httpcomponents:httpclient:4.5.6'

    testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.2.0'
    testRuntime "org.junit.platform:junit-platform-launcher:1.2.0"
    compile 'com.google.dagger:dagger:2.17'
    apt 'com.google.dagger:dagger-compiler:2.17'
}

app/ApiHandler.java

public class ApiHandler {
  private final AppComponent appComponent;

  public static void main(String[] args) {
    ApiHandler handler = new ApiHandler();
    String result = handler.handleRequest("version", "context");
    System.out.println(result);
    String result2 = handler.handleRequest("version", "context");
    System.out.println(result2);
    String result3 = handler.handleRequest("user/aaa", "context");
    System.out.println(result3);
  }

  public ApiHandler() {
    System.out.println("Create handler");
    appComponent = DaggerAppComponent.create();
  }

  public String handleRequest(String input, String context) {
    System.out.println("---start ApiHandler#handleRequest()---");
    RequestModule requestModule = new RequestModule(input, context);
    RequestComponent requestComponent = appComponent.newRequest(requestModule);
    if (input.startsWith("user")) {
      UserController uc = requestComponent.newUserController();
      return uc.getUser();

app/UserController.java

public class UserController {
  private final UserService userService;
  private final UserInfo userInfo;
  private final String serverlessInput;
  private final Object context;

  @Inject
  public UserController(
      UserService userService,
      UserInfo userInfo,
      @Named("ServerlessInput") String serverlessInput,
      @Named("Context") Object context) {
    System.out.println("Create SystemController");
    this.userService = userService;
    this.userInfo = userInfo;
    this.serverlessInput = serverlessInput;
    this.context = context;
  }