アノテーションプロセッサで 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.val
や lombok.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 内の var
を java.lang.Object
へ変更するように実装します。
CompilationUnitTree
(AST) は Visitor パターンで処理できるようになっています。
今回は var を使っている変数定義の部分を処理させたいだけなので、TreeScanner
の visitVariable
メソッドをオーバーライドした内部クラス (VarVisitor
) を用意しました。
JCVariableDecl
の vartype
フィールドに変数の型が設定されており、これを変更すれば変数の型が変わります。
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) のように ActorSystem
と FlowMaterializer
オブジェクトを作成し、処理完了時に 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.loglevel
を off
にしてログ出力を無効化する事もできますが、そうするとエラーログすら出力されなくなるのでご注意下さい。
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.java を Flow
を使って書き換えると下記のようになります。
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 に該当する処理が見当たらなかったので、filter
と AtomicInteger
を使って自前で実装してみました。
なお、下記 (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 でビルドしましたので、ファイルは以下のような構成になっています。
ビルド定義
アプリケーションの実行やアーカイブ化を簡単に行うため、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
) は PathTokens
の get()
メソッドで取得します。
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 / iBatis の API を使って 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行目だけを出力する結果となります。