アノテーションプロセッサで AST 変換 - Lombok を参考にして変数の型をコンパイル時に変更

Java のボイラープレートを補完してくれる Lombok の処理内容が興味深かったので、これを真似た簡単なサンプルプログラムを作ってみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150117/

はじめに

Lombok はアノテーションプロセッサを使って AST (抽象構文木) の変換を実施しています。

Lombok の使い方

まずは Lombok を使って下記のような Java ソースのコンパイルを試してみます。 val@Value が Lombok の機能を使用している箇所です。

Sample.java
import lombok.val;
import lombok.Value;

public class Sample {
    public static void main(String... args) {
        // lombok.val の使用
        val d = new Data("sample1", 10);
        System.out.println(d);
    }

    // lombok.Value の使用
    @Value
    private static class Data {
        private String name;
        private int value;
    }
}

Service Provider 機能(META-INF/services)を使用するため、javac 時に classpath へ lombok.jar を指定するだけで適用されます。

javac によるコンパイル (Lombok 使用)
> javac -cp lombok.jar Sample.java

CFR を使って Sample.class の内容を確認してみると、lombok.vallombok.Value が消え、代わりに型やメソッドを補完している事が分かります。

Sample.class の内容確認 (CFR 利用)
> java -jar cfr_0_94.jar Sample.class

/*
 * Decompiled with CFR 0_94.
 */
import java.beans.ConstructorProperties;
import java.io.PrintStream;

public class Sample {
    public static /* varargs */ void main(String ... arrstring) {
        Data data = new Data("sample1", 10);
        System.out.println(data);
    }

    private static final class Data {
        private final String name;
        private final int value;

        @ConstructorProperties(value={"name", "value"})
        public Data(String string, int n) {
            this.name = string;
            this.value = n;
        }

        public String getName() {
            return this.name;
        }

        public int getValue() {
            return this.value;
        }

        public boolean equals(Object object) {
            if (object == this) {
                return true;
            }
            if (!(object instanceof Data)) {
                return false;
            }
            Data data = (Data)object;
            String string = this.getName();
            String string2 = data.getName();
            if (string == null ? string2 != null : !string.equals(string2)) {
                return false;
            }
            if (this.getValue() != data.getValue()) {
                return false;
            }
            return true;
        }

        public int hashCode() {
            int n = 1;
            String string = this.getName();
            n = n * 59 + (string == null ? 0 : string.hashCode());
            n = n * 59 + this.getValue();
            return n;
        }

        public String toString() {
            return "Sample.Data(name=" + this.getName() + ", value=" + this.getValue() + ")";
        }
    }

}

Lombok の仕組み

次に、Lombok の仕組みを簡単に説明します。

Lombok はアノテーションプロセッサ内 (lombok.javac.apt.Processor) にて RoundEnvironment を元に AST を取得し変換します。

javac 実行時の処理を大雑把に書くと下記のようになっています。

  • (1) lombok.core.AnnotationProcessor を処理
  • (2) lombok.javac.apt.Processor を処理
  • (3) lombok.javac.JavacTransformer を処理
  • (4) AnnotationVisitor を処理
  • (5) 各種 AST 変換用のハンドラ (lombok.javac.handlers パッケージ内のクラス) を処理

lombok.jar の META-INF/services/javax.annotation.processing.Processor に (1) のクラス名が記載されているため、アノテーションプロセッサの仕組みによって (1) が実行されます。

(5) の各種ハンドラは lombok.javac.HandlerLibrary が以下のファイルから取得し管理します。

  • META-INF/services/lombok.javac.JavacASTVisitor (visitorHandlers)
  • META-INF/services/lombok.javac.JavacAnnotationHandler (annotationHandlers)

JavacASTVisitor インターフェース実装クラスの lombok.javac.handlers.HandleVal (現時点では唯一の JavacASTVisitor 実装ハンドラ) は、lombok.val を型として使っている変数を適切な型に変更するという処理を行います。

HandleVal の処理内容が興味深かったので、今回はこれを真似た簡易的な処理を作ります。

アノテーションプロセッサで AST 変換

ここからは、Lombok における val の処理を真似たアノテーションプロセッサを自作していきます。

(1) AST の取得

まずは AST を取得して出力するだけのアノテーションプロセッサを作ります。

Compiler Tree API を使用するので、Gradle でビルドする場合は JDK の tools.jar を dependencies へ設定しておきます。

build.gradle
apply plugin: 'java'

dependencies {
    compile files("${System.properties['java.home']}/../lib/tools.jar")
}

次に、Service Provider 設定ファイルを用意しておきます。

このファイルを用意しておけば、JAR ファイルを classpath へ指定するだけで sample.SampleProcessor1 が実行されます。

src/main/resources/META-INF/services/javax.annotation.processing.Processor
sample.SampleProcessor1

AbstractProcessor を extends してアノテーションプロセッサを作成します。

@SupportedAnnotationTypes("*") アノテーションを付与する事で、アノテーションの使用有無に関わらずコンパイル対象の全ソースを RoundEnvironment から取得できるようになります。

Lombok のソースでは JCCompilationUnit へキャストして使っていますが、CompilationUnitTree が AST に該当します。

CompilationUnitTree を取得するため Trees を使っています。

src/main/sample/SampleProcessor1.java
package sample;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedSourceVersion;
import javax.annotation.processing.SupportedAnnotationTypes;

import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import com.sun.source.tree.CompilationUnitTree;

import com.sun.source.util.Trees;
import com.sun.source.util.TreePath;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*") //コンパイル対象の全ソースを対象とする
public class SampleProcessor1 extends AbstractProcessor {
    private Trees trees;

    @Override
    public void init(ProcessingEnvironment procEnv) {
        trees = Trees.instance(procEnv);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // コンパイル対象の全ソースを処理
        roundEnv.getRootElements().stream().map(this::toUnit).forEach(u -> {
            System.out.println("----- CompilationUnitTree -----");
            // AST の内容を出力
            System.out.println(u);
        });

        return false;
    }

    // AST の取得
    private CompilationUnitTree toUnit(Element el) {
        TreePath path = trees.getPath(el);
        return path.getCompilationUnit();
    }
}

今回は Java 8 の API を使ったので @SupportedSourceVersion(SourceVersion.RELEASE_8) を付けています。

ビルド

> gradle build

:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

動作確認 (アノテーションプロセッサの適用)

ビルド結果の build/libs/anp-sample1.jar ファイルを以下の Java ソース (A1.java と A2.java) のコンパイルに使ってみます。

A1.java
public class A1 {
    public void sample() {
        int i = 10;
    }
}

下記の var は後で使うので今のところは気にしないで下さい。

A2.java
public class A2 {
    // 下記の var はインターフェースやクラスで定義しても同じ
    @interface var {}

    public var a = 10;
    public String b = "bbb";

    public void sample() {
        var msg = "test data";
        System.out.println(msg);
    }
}

Lombok と同様に javac 時に classpath へ anp-sample1.jar を指定するだけです。

実行結果 (javac 実行)
> javac -cp anp-sample1.jar *.java

----- CompilationUnitTree -----

public class A1 {

    public A1() {
        super();
    }

    public void sample() {
        int i = 10;
    }
}
----- CompilationUnitTree -----

public class A2 {

    public A2() {
        super();
    }

    @interface var {
    }
    public var a = 10;
    public String b = "bbb";

    public void sample() {
        var msg = "test data";
        System.out.println(msg);
    }
}
A2.java:5: エラー: 不適合な型: intをvarに変換できません:
        public var a = 10;
                       ^
A2.java:9: エラー: 不適合な型: Stringをvarに変換できません:
                var msg = "test data";
                          ^
エラー2個

CompilationUnitTree オブジェクトを println するとソースが出力されました。

ここで重要なのは、var を使った箇所のエラーが SampleProcessor1 処理の後に出力されている点です。 (var は int や String では無いので当然のエラーです)

つまり、型チェックはアノテーションプロセッサの後に実施される事が分かります。

であれば、アノテーションプロセッサ内で適切な型に変えてしまえばコンパイルを通す事が可能という事になり、Lombok の val は実際にこの仕組みを利用しています。

(2) AST 内の var 型を Object へ変更

次に、(1) でエラーが発生した A2.java 内の varjava.lang.Object へ変更するように実装します。

CompilationUnitTree (AST) は Visitor パターンで処理できるようになっています。

今回は var を使っている変数定義の部分を処理させたいだけなので、TreeScannervisitVariable メソッドをオーバーライドした内部クラス (VarVisitor) を用意しました。

JCVariableDeclvartype フィールドに変数の型が設定されており、これを変更すれば変数の型が変わります。

vartype が var という名前の型 (所属パッケージに関係なく) であれば java.lang.Object へ変更するようにしてみました。

src/main/sample/SampleProcessor2.java
package sample;
・・・
import com.sun.source.tree.VariableTree;
・・・
import com.sun.tools.javac.model.JavacElements;

import com.sun.tools.javac.processing.JavacProcessingEnvironment;

import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.JCTree.JCExpression;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
public class SampleProcessor2 extends AbstractProcessor {
    private Trees trees;
    private TreeMaker maker;
    private JavacElements elements;

    @Override
    public void init(ProcessingEnvironment procEnv) {
        trees = Trees.instance(procEnv);

        JavacProcessingEnvironment env = (JavacProcessingEnvironment)procEnv;

        maker = TreeMaker.instance(env.getContext());
        elements = JavacElements.instance(env.getContext());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // コンパイル対象の全ソースを処理
        roundEnv.getRootElements().stream().map(this::toUnit).forEach(this::processUnit);

        return false;
    }

    private CompilationUnitTree toUnit(Element el) {
        TreePath path = trees.getPath(el);
        return path.getCompilationUnit();
    }

    private void processUnit(CompilationUnitTree cu) {
        // Visitor パターンで AST を処理
        cu.accept(new VarVisitor(), null);
    }

    private class VarVisitor extends TreeScanner<Void, Void> {
        // 変数定義の処理
        @Override
        public Void visitVariable(VariableTree node, Void p) {
            System.out.println("visitVariable: " + node);

            if (node instanceof JCVariableDecl) {
                JCVariableDecl vd = (JCVariableDecl)node;

                if ("var".equals(vd.vartype.toString())) {
                    JCExpression ex = maker.Ident(elements.getName("java"));
                    ex = maker.Select(ex, elements.getName("lang"));
                    ex = maker.Select(ex, elements.getName("Object"));
                    // 型を java.lang.Object へ変更
                    vd.vartype = ex;
                }
            }
            return null;
        }
    }
}

java.lang.Object の JCExpression を構築している箇所はもっと上手い方法がありそうですが、とりあえず Lombok の処理を真似ました。

src/main/resources/META-INF/services/javax.annotation.processing.Processor
sample.SampleProcessor2

ビルド

> gradle build

・・・

動作確認 (アノテーションプロセッサの適用)

(1) の時と同様に A1.java と A2.javaコンパイルに使ってみます。

実行結果 (javac 実行)
> javac -cp anp-sample2.jar *.java

visitVariable: int i = 10
visitVariable: public var a = 10
visitVariable: public String b = "bbb"
visitVariable: var msg = "test data"

今回はコンパイルエラーが発生せず、A1.java と A2.java 内の変数定義を visitVariable で処理しています。

CFR で A2.class の内容を確認してみると、var を使っていた変数の型が、一応 java.lang.Object に変わっています。 ("test data" の直接の型は String となっていますが)

A2.class の内容確認 (CFR 利用)
> java -jar cfr_0_94.jar A2.class

/*
 * Decompiled with CFR 0_94.
 */
import java.io.PrintStream;

public class A2 {
    public Object a = 10;
    public String b = "bbb";

    public void sample() {
        String string = "test data";
        System.out.println((Object)string);
    }

    static @interface var {
    }

}

最後に、この仕組みを流用すれば Java で型に別名を付ける機能 (ソース内だけの型エイリアス・型シノニムのようなもの) を実現できるかもしれません。

Akka Streams で skip・take 処理

前回の 「Reactor で skip・take 処理」 と同様の処理を Akka Streams を使用し Java 8 で実装してみました。

  • Akka Streams 1.0 M2

ソースは http://github.com/fits/try_samples/tree/master/blog/20150112/

はじめに

Gradle を使ってビルド・実行するため、下記のような build.gradle を用意しました。

build.gradle
apply plugin: 'application'

repositories {
    jcenter()
}

dependencies {
    compile 'com.typesafe.akka:akka-stream-experimental_2.11:1.0-M2'
}

mainClassName = 'sample.Sample1'
//mainClassName = 'sample.Sample2'
//mainClassName = 'sample.PublisherSample1'
//mainClassName = 'sample.PublisherSample2'

Iterable を使った処理1

Iterable を使った固定的なデータストリームに対して skip・take 処理を実装してみます。

まず、下記 (2) のように ActorSystemFlowMaterializer オブジェクトを作成し、処理完了時に ActorSystem を shutdown する処理 (3) を用意しておきます。

今回は sample1 ~ sample6 のデータを Java 8 の Stream を使って作りましたが、Stream は Iterable インターフェースを持っておらず、Akka Streams には今のところ Source.from(Stream) のようなメソッドも用意されていないので、List へ変換するなどして Iterable 化する必要があります。 (4)

(5) では、(4) を使って Source を生成し、skip・take 処理を実施しています。 なお、Akka Streams では skip 処理を drop メソッドとして用意しています。

最後に、(1) は Akka の不要なログ出力 (info レベル) を抑制するための措置です。 (設定ファイルで設定する方法もあります)

akka.logleveloff にしてログ出力を無効化する事もできますが、そうするとエラーログすら出力されなくなるのでご注意下さい。

Sample1.java
package sample;

import akka.actor.ActorSystem;
import akka.dispatch.OnComplete;
import akka.stream.FlowMaterializer;
import akka.stream.javadsl.Source;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;

import scala.runtime.BoxedUnit;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Sample1 {
    public static void main(String... args) {
        // (1) Akka の不要なログ出力を抑制するための設定
        final Config config = ConfigFactory.load()
            .withValue("akka.loglevel", ConfigValueFactory.fromAnyRef("error"));

        // (2) ActorSystem・FlowMaterializer の作成
        final ActorSystem system = ActorSystem.create("sample", config);
        final FlowMaterializer materializer = FlowMaterializer.create(system);

        // (3) 終了時の処理
        final OnComplete<BoxedUnit> complete = new OnComplete<BoxedUnit>() {
            @Override
            public void onComplete(Throwable failure, BoxedUnit success) {
                system.shutdown();
            }
        };

        // (4) sample1 ~ sample6
        List<String> data = IntStream.range(1, 7).mapToObj(i -> "sample" + i).collect(Collectors.toList());
        // 以下でも可
        //Iterable<String> data = () -> IntStream.range(1, 7).mapToObj(i -> "sample" + i).iterator();

        // (5) 処理
        Source.from(data)
            .drop(3)
            .take(2)
            .map(s -> "#" + s)
            .foreach(System.out::println, materializer)
            .onComplete(complete, system.dispatcher());
    }
}

また、(4) (5) の箇所は下記のように実装する事もできます。

Stream<String> stream = IntStream.range(1, 7).mapToObj(i -> "sample" + i);

Source.from((Iterable<String>)stream::iterator)
    .drop(3)
    ・・・
実行結果
> gradle -q run

#sample4
#sample5

Iterable を使った処理2

Sample1.javaFlow を使って書き換えると下記のようになります。
Flow によってストリームの加工処理部分を分離できます。

Sample2.java
・・・
import akka.stream.javadsl.Flow;
import akka.stream.javadsl.Sink;
・・・

public class Sample2 {
    public static void main(String... args) {
        ・・・

        Flow<String, String> flow = Flow.<String>create()
            .drop(3)
            .take(2)
            .map(s -> "#" + s);

        List<String> data = IntStream.range(1, 7).mapToObj(i -> "sample" + i).collect(Collectors.toList());

        flow.runWith(
            Source.from(data), 
            Sink.foreach(System.out::println), 
            materializer
        ).onComplete(complete, system.dispatcher());
    }
}
実行結果
> gradle -q run

#sample4
#sample5
build.gradle
・・・
//mainClassName = 'sample.Sample1'
mainClassName = 'sample.Sample2'

Publisher を使った処理1

これまで固定的なデータを使って処理を行いましたが、前回Broadcaster のような処理 (ストリームへ任意の要素を送信) を Akka Streams で実現するには org.reactivestreams.Publisher を利用できます。

RxJava 等と組み合わせるのが簡単だと思いますが、今回は Publisher の実装クラスを自作してみました。(SamplePublisher クラス)

まずは sample1 ~ sample6 をそのまま出力する処理を実装してみましたが、2点ほど注意点がありました。

  • (1) akka.stream.materializer.initial-input-buffer-size のデフォルト値である 4 を超えたデータ数がバッファに格納されるとエラー (Input buffer overrun) が発生する
  • (2) Subscriber に対して onSubscribe しないと onNext が機能しない

(1) を回避するために buffer メソッドを使って十分なバッファ数を確保するように変更しました。

OverflowStrategy に関しては用途に応じて設定する事になると思いますが、dropXXX や error を使用すると、バッファが溢れた際にデータが欠ける事になるので注意が必要です。

ちなみに、(1) のデフォルト設定は akka-stream-experimental_2.11-1.0-M2.jar 内の reference.conf ファイルで設定されています。

また、(2) の対策のため、Subscriber (実体は ActorProcessor) へ何も処理を行わない Subscription を onSubscribe しています。

PublisherSample1.java
・・・
import akka.stream.OverflowStrategy;

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
・・・

public class PublisherSample1 {
    public static void main(String... args) {
        ・・・

        try (SamplePublisher<String> publisher = new SamplePublisher<>()) {

            Source.from(publisher)
                // (1) Input buffer overrun の防止策
                .buffer(10, OverflowStrategy.backpressure())
                .map(s -> "#" + s)
                .foreach(System.out::println, materializer)
                .onComplete(complete, system.dispatcher());

            IntStream.range(1, 7)
                .mapToObj(i -> "sample" + i)
                .forEach(d -> publisher.send(d));
        }
    }

    private static class SamplePublisher<T> implements Publisher<T>, AutoCloseable {
        private List<Subscriber<? super T>> list = new ArrayList<>();

        @Override
        public void subscribe(Subscriber<? super T> s) {
            list.add(s);
            // (2) 下記を実施しないと onNext が機能しない
            s.onSubscribe(new Subscription() {
                @Override public void request(long n) {}
                @Override public void cancel() {}
            });
        }

        @Override
        public void close() {
            list.forEach(Subscriber::onComplete);
        }

        public void send(T msg) {
            // Subscriber へデータ送信
            list.forEach(s -> s.onNext(msg));
        }
    }
}

SamplePublisher クラス内で Subscriber を List にて管理していますが、上記処理では subscribe は 1度しか実行されないので List で管理する必要は特にありません。

実行結果
> gradle -q run

#sample1
#sample2
#sample3
#sample4
#sample5
#sample6
build.gradle
・・・
//mainClassName = 'sample.Sample2'
mainClassName = 'sample.PublisherSample1'

Publisher を使った処理2

最後に skip(drop)・take の処理を追加してみます。

PublisherSample2.java
・・・

public class PublisherSample2 {
    public static void main(String... args) {
        ・・・

        try (SamplePublisher<String> publisher = new SamplePublisher<>()) {

            Source.from(publisher)
                // Input buffer overrun の防止策
                .buffer(10, OverflowStrategy.backpressure())
                .drop(3)
                .take(2)
                .map(s -> "#" + s)
                .foreach(System.out::println, materializer)
                .onComplete(complete, system.dispatcher());

            IntStream.range(1, 7)
                .mapToObj(i -> "sample" + i)
                .forEach(d -> publisher.send(d));
        }
    }

    private static class SamplePublisher<T> implements Publisher<T>, AutoCloseable {
        ・・・
    }
}
実行結果
> gradle -q run

#sample4
#sample5
build.gradle
・・・
//mainClassName = 'sample.PublisherSample1'
mainClassName = 'sample.PublisherSample2'

Reactor で skip・take 処理

Bacon.js で skip・take 処理」と同様の処理を Reactor を使用し Java 8 で実装してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150104-2/

はじめに

今回は Gradle を使ってビルド・実行するため、下記のような build.gradle を用意しました。

build.gradle
apply plugin: 'application'

repositories {
    maven { url 'http://repo.spring.io/libs-snapshot' }
    jcenter()
}

dependencies {
    compile 'io.projectreactor:reactor-core:2.0.0.M2'
    runtime 'org.slf4j:slf4j-nop:1.7.9'
}

mainClassName = 'sample.Sample1'
//mainClassName = 'sample.Sample2'

単純な処理

ストリームへ送信された sample1 ~ sample6 をそのまま出力する処理を実装しました。 Broadcaster を使えばストリームへ要素を送信する事ができます。

Sample1.java
package sample;

import reactor.Environment;
import reactor.rx.Promise;
import reactor.rx.Streams;
import reactor.rx.stream.Broadcaster;

public class Sample1 {
    public static void main(String... args) throws Exception {
        Environment env = new Environment();

        Broadcaster<String> stream = Streams.broadcast(env);

        Promise<String> promise = stream.observe(System.out::println).next();

        Streams.range(1, 6).consume( i -> stream.onNext("sample" + i) );

        promise.await();

        env.shutdown();
    }
}

実行結果は下記の通りです。

実行結果
> gradle -q run

sample1
sample2
sample3
sample4
sample5
sample6

skip・take 処理

次に、skip と take 処理ですが、Reactor には skip に該当する処理が見当たらなかったので、filterAtomicInteger を使って自前で実装してみました。

なお、下記 (1) の箇所のように next メソッドを使って Promise を取得した場合は take した最初の要素しか出力されない点に注意が必要です。

take した全ての要素を出力させるには、下記 (2) の箇所のように最後の要素を取得する tap メソッド (戻り値は TapAndControls<O>) を使ったり、全要素をリスト化して取得する toList メソッド (戻り値は Promise<java.util.List<O>>) 等を使ったりする必要がありました。

Sample2.java
package sample;

import reactor.Environment;
import reactor.rx.Promise;
import reactor.rx.Stream;
import reactor.rx.Streams;
import reactor.rx.stream.Broadcaster;
import reactor.rx.action.support.TapAndControls;

import java.util.concurrent.atomic.AtomicInteger;

public class Sample2 {
    public static void main(String... args) throws Exception {
        Environment env = new Environment();

        skipAndTakeSampleA(env);

        System.out.println("-----");

        skipAndTakeSampleB(env);

        env.shutdown();
    }

    private static void skipAndTakeSampleA(Environment env) throws Exception {
        Broadcaster<String> stream = Streams.broadcast(env);

        // (1) next を使うと take した最初の要素のみ出力
        Promise<String> promise = skip(stream, 2)
            .take(3)
            .map(s -> "#" + s)
            .observe(System.out::println)
            .next();

        Streams.range(1, 6).consume( i -> stream.onNext("sampleA-" + i) );

        promise.await();
    }

    private static void skipAndTakeSampleB(Environment env) throws Exception {
        Broadcaster<String> stream = Streams.broadcast(env);

        // (2) take した全要素を出力するには tap や toList 等を使う必要あり
        TapAndControls<String> tap = skip(stream, 2)
            .take(3)
            .map(s -> "#" + s)
            .observe(System.out::println)
            .tap();

        Streams.range(1, 6).consume( i -> stream.onNext("sampleB-" + i) );

        tap.get();
    }

    // skip 処理
    private static <T> Stream<T> skip(Stream<T> st, int num) {
        AtomicInteger counter = new AtomicInteger();

        return st.filter(s -> counter.incrementAndGet() > num);
    }
}

実行結果は下記の通りです。 (build.gradle の mainClassName を変更して実行)

実行結果
> gradle -q run

#sampleA-3
-----
#sampleB-3
#sampleB-4
#sampleB-5
build.gradle
・・・
//mainClassName = 'sample.Sample1'
mainClassName = 'sample.Sample2'

Bacon.js で skip・take 処理

リアクティブプログラミング用ライブラリの Bacon.js を Node.js 上で使用し、「RxJS で行単位のファイル処理」 で試したような skip・take 処理のサンプルを実装してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150104/

はじめに

npm でインストールするために下記のような package.json を用意します。 今回は CoffeeScript で実装するので coffee-script も追加しています。

package.json
{
  ・・・
  "dependencies": {
    "baconjs": "*",
    "coffee-script": "*"
  }
}

npm install でインストールします。

インストール
> npm install

Bus の利用

まずは Bus を使って、ストリームへ push した内容をそのままログ出力する処理を実装してみます。

Bus は後から要素を push できるストリームですが、Bacon.js には他にも色々とストリームを作る方法が用意されています。 (下記は一部)

  • 配列からストリームを作る Bacon.fromArray
  • コールバック関数を使ってストリームを作る Bacon.fromCallback, Bacon.fromNodeCallback (Node.js 用のコールバック関数へ適用)
  • 任意のストリームを作る Bacon.fromBinder
sample1.coffee
Bacon = require('baconjs').Bacon

bus = new Bacon.Bus()

bus.log()

[1..6].forEach (i) -> bus.push "sample#{i}"

bus.end()

実行結果は下記の通りです。 Bus へ push した sample1 ~ sample6 を順次出力しています。

実行結果
> coffee sample1.coffee

sample1
sample2
sample3
sample4
sample5
sample6
<end>

skip・take 処理

次に、下記のような加工処理を追加してみました。

  • (1) 2つの要素を無視 (skip)
  • (2) 3つの要素だけを取得 (take)
  • (3) 先頭に # を付ける (map)
sample2.coffee
Bacon = require('baconjs').Bacon

bus = new Bacon.Bus()

bus.skip(2).take(3).map((s) -> "##{s}").log()

[1..6].forEach (i) -> bus.push "sample#{i}"

bus.end()

実行結果は下記の通りです。

実行結果
> coffee sample2.coffee

#sample3
#sample4
#sample5
<end>

Ratpack で Java Web アプリケーション作成

Ratpack は以前 「Ratpack + JHaml + Morphia で MongoDB を使った Web アプリ開発」 で試しましたが、3年以上経っているので改めて試してみました。

今回は単純な Java Web アプリケーションを Ratpack で作成する事にします。

ソースは http://github.com/fits/try_samples/tree/master/blog/20141229/

Web アプリケーションの作成

今回の作成手順は以下のようになります。

  • (1) HandlerFactory インターフェースの実装クラスを作成
  • (2) ratpack.properties で handlerFactory を設定

ファイル構成

今回は Gradle でビルドしましたので、ファイルは以下のような構成になっています。

  • build.gradle
  • src/main/java/AppHandlerFactory.java
  • src/ratpack/ratpack.properties

ビルド定義

アプリケーションの実行やアーカイブ化を簡単に行うため、Gradle 用の ratpack-java プラグインを使います。

ratpack-java を使う場合、ratpack の依存設定 (dependencies) は要らないようですが、slf4j だけは実行に要るようです。

build.gradle
buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'io.ratpack:ratpack-gradle:0.9.11'
    }
}

apply plugin: 'io.ratpack.ratpack-java'

repositories {
    jcenter()
}

dependencies {
    runtime 'org.slf4j:slf4j-simple:1.7.5'
}

(1) HandlerFactory 実装クラスの作成

とりあえず /sample/xxx へアクセスすると単に "sample - xxx" を出力するだけの処理を実装しました。 (xxx は任意の文字列)

出力には Context オブジェクトの render() メソッドを使用し、URL のパラメータ部分 (下記の :id) は PathTokensget() メソッドで取得します。

src/main/java/AppHandlerFactory.java
import static ratpack.handling.Handlers.*;

import ratpack.handling.Context;
import ratpack.handling.Handler;
import ratpack.launch.HandlerFactory;
import ratpack.launch.LaunchConfig;

public class AppHandlerFactory implements HandlerFactory {
    @Override
    public Handler create(LaunchConfig config) throws Exception {
        return chain(
            path("sample/:id", ctx -> 
                ctx.render("sample - " + ctx.getPathTokens().get("id")))
        );
    }
}

(2) handlerFactory の設定

作成した AppHandlerFactory を handlerFactory へ設定します。

こうする事で、実行時に handlerFactory として AppHandlerFactory が使用されます。

src/ratpack/ratpack.properties
handlerFactory=AppHandlerFactory

テスト実行

gradle run でテスト実行できます。

> gradle run

:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:configureRun
:prepareBaseDir UP-TO-DATE
:run
[main] INFO ratpack.server.internal.NettyRatpackServer - Ratpack started for http://localhost:5050
・・・

http://localhost:5050/sample/abc へアクセスすると、sample - abc が表示されます。

成果物の生成

gradle distZip を実行すると、build/distributions へ zip ファイルが生成されます。

> gradle distZip

:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar
:startScripts
:distZip

BUILD SUCCESSFUL

zip ファイルを解凍して、bin ディレクトリの起動スクリプト (今回のサンプルでは ratpack_sample) を実行すれば Web アプリケーションを単体起動できます。

MyBatis / iBatis の動的 SQL を API で作成

MyBatis / iBatisAPI を使って DB へ接続せずに Mapper XML の動的 SQL を作成する方法です。

ソースは http://github.com/fits/try_samples/tree/master/blog/20141221/

MyBatis の場合

動的 SQL の結果を取得する手順は下記のようになります。

  • (1) Configuration をインスタンス
  • (2) (1) と Mapper XML で XMLMapperBuilder をインスタンス
  • (3) Mapper XML をパース
  • (4) (1) から指定の SQL に対応した MappedStatement を取得
  • (5) (4) で取得した MappedStatement へパラメータを渡して動的 SQL を構築、結果の SQL を取得

今回は Groovy で実装してみました。

動的 SQL のパラメータはコマンドライン引数で JSON 文字列として指定するようにしています。

mybatis_sql_gen.groovy
@Grab('org.mybatis:mybatis:3.2.8')
import org.apache.ibatis.session.*
import org.apache.ibatis.builder.xml.*

import groovy.json.JsonSlurper

if (args.length < 3) {
    println '<mybatis mapper xml> <sql id> <json params>'
    return
}
// (1)
def config = new Configuration()
// (2)
def parser = new XMLMapperBuilder(new File(args[0]).newInputStream(), config, "", config.sqlFragments)
// (3)
parser.parse()
// (4)
def st = config.getMappedStatement(args[1])
// パラメータの作成(JSON 文字列から)
def params = new JsonSlurper().parseText args[2]
// (5)
def sql = st.getBoundSql(params).sql

println sql

実行

下記 Mapper XML を使って実行してみます。

mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="sample">
  <select id="findData">
    SELECT * FROM data
    WHERE
      title like #{title}
    <if test="author != null and author.name != null">
      AND author_name like #{author.name}
    </if>
    <if test="types">
      AND type in 
      <foreach item="type" collection="types" open="(" separator="," close=")">
        #{type}
      </foreach>
    </if>
  </select>
</mapper>
実行例1

まずはパラメータ無しの場合。{}

> groovy mybatis_sql_gen.groovy mapper.xml findData "{}"

SELECT * FROM data
    WHERE
      title like ?
実行例2

次に、author.name と types パラメータを指定した場合。{"author":{"name": 1}, "types": [1, 2, 3]}

なお、今回の動的 SQL ではパラメータの有無しか見ていませんので、値には適当な数値 (1[1, 2, 3]) を使っています。

> groovy mybatis_sql_gen.groovy gen.groovy mapper.xml findData "{\"author\":{\"name\": 1}, \"types\": [1, 2, 3]}"

SELECT * FROM data
    WHERE
      title like ?

      AND author_name like ?


      AND type in
       (
        ?
       ,
        ?
       ,
        ?
       )

iBatis の場合

iBatis の場合も、使用する API は異なりますが同じような手順で処理できます。

ibatis_sql_gen.groovy
@Grab('org.apache.ibatis:ibatis-sqlmap:2.3.4.726')
import com.ibatis.sqlmap.engine.builder.xml.*
import com.ibatis.sqlmap.engine.scope.*

import groovy.json.JsonSlurper

if (args.length < 3) {
    println '<ibatis mapper xml> <sql id> <json params>'
    return
}

def state = new XmlParserState()

def parser = new SqlMapParser(state)
parser.parse(new File(args[0]).newInputStream())

// SqlMapExecutorDelegate を取得
def dlg = state.config.delegate

def st = dlg.getMappedStatement(args[1])
def sql = st.sql

def scope = new StatementScope(new SessionScope())
scope.statement = st

// パラメータの作成(JSON 文字列から)
def params = new JsonSlurper().parseText args[2]

println sql.getSql(scope, params)

実行

下記 Mapper XML を使って同様に実行してみます。

mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlMap namespace="sample">
  <select id="findData">
    SELECT * FROM data
    WHERE
      title like #title#
    <isNotNull property="author">
      <isNotNull property="author.name">
        AND author_name like #author.name#
      </isNotNull>
    </isNotNull>
    <isNotNull property="types">
      AND type in
      <iterate property="types" open="(" conjunction="," close=")">
        #types[]#
      </iterate>
    </isNotNull>
  </select>
</sqlMap>
実行例1
> groovy ibatis_sql_gen.groovy mapper.xml findData "{}"

     SELECT * FROM data     WHERE       title like ?
実行例2
> groovy ibatis_sql_gen.groovy mapper.xml findData "{\"author\":{\"name\": 1}, \"types\": [1, 2, 3]}"

     SELECT * FROM data     WHERE       title like ?                     AND author_name like ?                        AND type in       (         ?       ,     ?       ,         ?       )

Sodium で関数型リアクティブプログラミング2 - skip・take 処理

前回に続き、Sodium を試してみます。

今回は 「RxJava で行単位のファイル処理 - Groovy, Java Lambda」 で実装したものと同等の処理を Sodium を使って実装してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20141209/

skip・take 処理

Sodium には、RxJava の際に使った skip (指定した処理数だけ無視する) や take (指定した処理数だけ取り出す) のようなメソッドが用意されていないようなので、Event と Behavior を組み合わせて実装してみる事にします。

Event クラスには gate メソッドがあり、このメソッドを使えば Behavior オブジェクトの値が true の場合だけイベントを発生するような Event オブジェクトを作成できます。

つまり、指定した数のイベントを受け取れば true と false が反転する Behavior オブジェクトを用意して gate メソッドへ与えてやれば skip や take の処理を実現した Event オブジェクトを作成できそうです。

なお、skip と take の違いは、false から true への変化か true から false への変化かの違いしかありませんので、下記のサンプルでは batch メソッドとして共通化し、判定処理を引数 Lambda1<Integer, Boolean> cond として渡すようにしました。

batch メソッドは以下のような処理内容となっています。

  • (1) イベントの発生数をカウントアップする Behavior を用意 (実際は BehaviorSink を使用)
  • (2) イベント発生時に (1) をカウントアップ
  • (3) (1) のカウント値が条件に合致した場合のみイベントを発生させる Event を作成
ReadLineFile.java
import sodium.*;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.function.*;

class ReadLineFile {
    public static void main(String... args) throws Exception {
        // skip 処理
        Function<Integer, Function<Event<String>, Event<String>>> skip = n -> {
            return ev -> batch( v -> v >= n, n, ev);
        };
        // take 処理
        Function<Integer, Function<Event<String>, Event<String>>> take = n -> {
            return ev -> batch( v -> v < n, n, ev);
        };

        // 1行スキップして 3行取得する Event を作成する処理を合成 (b) (c)
        Function<Event<String>, Event<String>> skipAndTake3 = skip.apply(1).andThen( take.apply(3) );

        // (a)
        EventSink<String> es = new EventSink<>();
        // (d)
        Listener esl = skipAndTake3.apply(es).map( v -> "# " + v ).listen( System.out::println );
        // ファイルを行単位で処理
        readFileLines(args[0], es);

        esl.unlisten();
    }

    private static void readFileLines(String fileName, EventSink<String> es) throws IOException {
        // ファイルを行単位で EventSink へ send
        try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
            br.lines().forEach( es::send );
        }
    }
    // skip・take の共通処理
    private static Event<String> batch(Lambda1<Integer, Boolean> cond, int n, Event<String> ev) {
        // (1) イベント発生数をカウントする Behavior を用意
        BehaviorSink<Integer> counter = new BehaviorSink<>(0);
        // (2) イベント発生時に counter をカウントアップ
        ev.listen( v -> counter.send(counter.sample() + 1) );
        // (3) counter の値が条件に合致した場合のみイベント発生する Event を作成
        return ev.gate(counter.map(cond));
    }
}

上記は、以下の 4つの Event が (a) -> (b) -> (c) -> (d) の順でイベントを伝播するような構成となっており、(d) に到達した場合のみ System.out::println を実行するようになっています。

  • (a) EventSink
  • (b) skip で作成した Event
  • (c) take で作成した Event
  • (d) 先頭に "# " を付与する Event

また、batch メソッドの実装内容に関しては counter の無駄なカウントアップを防止するため、下記のように unlisten するようにした方が望ましいかもしれません。

unlisten する処理を追加した batch メソッドの例
    private static Event<String> batch(Lambda1<Integer, Boolean> cond, int n, Event<String> ev) {
        BehaviorSink<Integer> counter = new BehaviorSink<>(0);
        final ArrayList<Listener> list = new ArrayList<>(1);

        list.add(ev.listen( v -> {
            int newValue = counter.sample() + 1;
            counter.send(newValue);

            if (newValue >= n) {
                list.stream().forEach( li -> li.unlisten() );
            }
        }));

        return ev.gate(counter.map(cond));
    }

実行

それでは、下記ファイルを使って実行してみます。

test1.txt
1a
2b
3c
4d
5e

実行結果は以下の通りです。 1行スキップした後、3行を先頭に "# " を付与して出力しています。

実行結果
> java -cp .;sodium.jar ReadLineFile test1.txt

# 2b
# 3c
# 4d

test1.txt を処理した際の (a) ~ (d) の Event に対するイベント伝播状況をまとめると以下のようになっていると考えられます。(○ は伝播する、× は伝播しない)

対象行 (a) (b) (c) (d)
1行目 1a × ×
2行目 2b
3行目 3c
4行目 4d
5行目 5e ×

(d) まで到達した 2 ~ 4行目だけを出力する結果となります。