PPAPで学ぶ、Daggerによる非同期処理

前回は、DaggerのDI機能を紹介しました。
PPAPで学ぶDaggerによるDI - きしだのはてな
DaggerはDIコンテナとしては知られていますが、非同期処理フレームワークとして使えることはあまり知られていないと思います。機能はあったのに、ドキュメントがなかったし、ドキュメントも非同期処理に使えることがわかりにくいし。
https://google.github.io/dagger/producers.html


サーバーサイドではSpringFrameworkやCDIなどのDIコンテナがすでに使われているのと、DaggerのDIでは機能不足であるため、Daggerが候補になることはあまりありません。
でも、非同期処理フレームワークとしてであれば、SpringFrameworkやCDIを使っている状況でも有用です。

ExecutorModuleの定義

非同期処理を行うために、スレッド管理のためのExecutorを定義するモジュールが必要になります。ここには@Productionが必要になります。

@Module
public class ExecutorModule {
    static ExecutorService service;
    
    @Provides
    @Production
    static Executor executor() {
        return service == null 
            ? service = Executors.newCachedThreadPool() 
            : service;
    }
}

呼び出しグラフの定義

呼び出しの依存関係をグラフとして定義します。DIの依存関係を、そのまま呼び出しの依存関係にする感じですが、アノテーションは次のように別のものを使います。

DI Async
@Module @ProducerModule
@Component @ProductionComponent
@Provides @Produces


@ProductionComponentのmodulesに、Executor管理のモジュールを追加しておく必要があります。ListenableFutureを返す処理であっても、利用側はそのままオブジェクトを受け取れるのがいいところ。

@ProducerModule
public class AsyncPiko {

    @ProductionComponent(modules = {AsyncPiko.class, ExecutorModule.class})
    interface Taro {
        ListenableFuture<ApplePen> applePen();
    }
 
    @Produces
    public ListenableFuture<Pen> iHaveAPen() {
        return task("pen", Pen::new, 100);
    }
    
    @Produces
    public ListenableFuture<Apple> iHaveAnApple() {
        return task("apple", Apple::new, 200);
    }
    
    @Produces
    public ListenableFuture<ApplePen> applePen(Pen pen, Apple apple) {
        return task("applepen", () -> new ApplePen(apple, pen), 100);
    }
    
    private static <T> ListenableFuture<T> task(String name, Supplier<T> supplier, long millis) {
        ListenableFutureTask<T> task = ListenableFutureTask.create(() -> {
            System.out.println("start " + name);
            try {
                Thread.sleep(millis);
            } catch (InterruptedException ignored) {
            }
            T result = supplier.get();
            System.out.println("finish " + name);

            return result;
        });
        task.run();
        return task;
    }
}

並列処理感を出したりListenableFutureの処理をするために、ウェイトを入れてログを出すtaskメソッドを定義しています。

実行

実行はDIのときと同じ感じになります。ExecutorModuleにはstaticメソッドの@Providesしかないため、Daggerのbuilderにインスタンスを渡す必要はありません。

public class AsyncPPAP {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        AsyncPiko.Taro pikotaro = DaggerAsyncPiko_Taro.builder()
                .asyncPiko(new AsyncPiko())
                .build();
        ListenableFuture<ApplePen> applePen = pikotaro.applePen();
        System.out.println("start ppap");
        System.out.println(applePen.get());
        System.out.println("finish ppap");
        
        ExecutorModule.service.shutdown();
    }
}


実行するとこんな感じになります。

penとappleが並列に処理されて、両方が終わったときにapplePenの処理が始まっていることがわかります。
このような並列的な依存関係の処理を、Daggerが管理してくれます。

同じ型のオブジェクトがかぶった場合

非同期処理のためにDaggerを使うと、同じ型の値を扱うことが多くなるので、型による依存性解決だけでは足りなくなります。
次のように、penPineappleやpenPineappleApplePenは文字列を返す非同期処理であることにしてみます。

    @Produces
    public ListenableFuture<String> penPineapple(Pen pen) {
        return task("penPineapple", () -> pen + "パイナッポー", 200);
    }
    
    @Produces
    public ListenableFuture<String> penPineappleApplePen(ApplePen applePen, String penPineapple) {
        return task("penPineappleApplePen", () -> penPineapple + applePen, 50);
    }


そして、@ProductionComponentにStringを返す処理を追加します。

    @ProductionComponent(modules = {AsyncPiko.class, ExecutorModule.class})
    interface Taro {
        ListenableFuture<ApplePen> applePen();
        ListenableFuture<String> ppap();
    }    


そうすると、なんかエラーが出ます。

このように、依存情報の不備をコンパイル時に見つけてくれるのが、Daggerのいいところですね。


そういう場合には、@Qualifierなアノテーションを定義します。

    @Qualifier
    @Retention(RetentionPolicy.RUNTIME)
    @interface PenPineappleApplePen{}


このアノテーションを、利用側と提供側につけます。

    @Produces
    @PenPineappleApplePen
    public ListenableFuture<String> penPineappleApplePen(ApplePen applePen, String penPineapple) {
        return task("penPineappleApplePen", () -> penPineapple + applePen, 50);
    }
    @ProductionComponent(modules = {AsyncPiko.class, ExecutorModule.class})
    interface Taro {
        @PenPineappleApplePen
        ListenableFuture<String> ppap();
    }    


そうすると、無事コンパイルが通って実行することができます。


ソースはgithubで。
https://github.com/kishida/dagger_sample/tree/async