購読中です 読者をやめる 読者になる 読者になる

Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3

Javaアノテーションプロセッサを使って下記と同等の機能を実現する試みの第三弾です。

  • Haskell の do 記法
  • Scala の for 内包表記
  • F# のコンピュテーション式

前回 のものを改良し、ようやく下記のような構文を実現しました。

Optional<String> res = opt$do -> {
    let a = Optional.of("a");
    let b = Optional.of("b");
    let c = opt$do -> {
        let c1 = Optional.of("c1");
        let c2 = Optional.of("c2");
        return c1 + "-" + c2;
    };
    return a + b + "/" + c;
};

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

はじめに

環境

下記のような環境を使ってビルド・実行しています。

  • JavaSE Development Kit 8u45 (1.8.0_45)
  • Gradle 2.4

構文

前回 からの変更点は以下の通りです。

  • 対象のラムダ式JCLambda)を完全に置換し、Supplier を不要にした
  • let で入れ子に対応

対象のラムダ式を別の式 (JCMethodInvocation) で完全に置換し、Supplier を無くした事でまともな構文になったと思います。

変換前 (アノテーションプロセッサ処理前)
Optional<String> res = opt$do -> {
    let a = Optional.of("a");
    let b = Optional.of("b");
    let c = opt$do -> {
        let c1 = Optional.of("c1");
        let c2 = Optional.of("c2");
        return c1 + "-" + c2;
    };
    return a + b + "/" + c;
};
変換後 (アノテーションプロセッサ処理後)
Optional<String> res = opt.bind(
    Optional.of("a"), 
    (a) -> opt.bind(
        Optional.of("b"), 
        (b) -> opt.bind(
            opt.bind(
                Optional.of("c1"), 
                (c1) -> opt.bind(
                    Optional.of("c2"), 
                    (c2) -> opt.unit(c1 + "-" + c2)
                )
            ), 
            (c) -> opt2.unit(a + b + "/" + c)
        )
    )
);

また、変数への代入だけではなく、メソッドの引数にも上記構文を使えるようにしました。

メソッド引数としての使用例
System.out.println(opt$do -> {
    let a = Optional.of("a");
    let b = Optional.of("b");
    return "***" + b + a;
});

アノテーションプロセッサの実装

Processor の実装

前回 とほぼ同じですが、DoExprVisitor の extends 元を com.sun.tools.javac.tree.TreeScanner へ変えたので、accept メソッドの呼び出し箇所が多少変わっています。

なお、JCTree へキャストしていますが、JCCompilationUnit へキャストしても問題ありません。

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

import java.util.Set;
import javax.annotation.processing.*;

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;

import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.Context;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
public class DoExprProcessor extends AbstractProcessor {
    private Trees trees;
    private Context context;

    @Override
    public void init(ProcessingEnvironment procEnv) {
        trees = Trees.instance(procEnv);
        context = ((JavacProcessingEnvironment)procEnv).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) {
        if (cu instanceof JCTree) {
            ((JCTree)cu).accept(new DoExprVisitor(context));
            // 変換内容を出力
            System.out.println(cu);
        }
    }
}

TreeVisitor の実装

前回 からの変更点は以下の通りです。

  • (a) コード生成部分を別クラス化
  • (b) 対象のラムダ式JCLambda) を全置換
  • (c) メソッド引数への対応
  • (d) extends 元を com.sun.tools.javac.tree.TreeScanner へ変更 (前回までは com.sun.source.util.TreeScanner

(b) を実現するため changeNode へ置換処理 (JCLambdaJCMethodInvocation へ差し替える事になる) を設定するようにしました。

主な処理内容は次のようになっています。

  • (1) 変数定義(JCVariableDecl)やメソッド実行(JCMethodInvocation)の箇所で該当部分を差し替えるための処理を changeNode へ設定
  • (2) ラムダの内容からソースコードを生成 (対象外なら何もしない)
  • (3) ソースコードJCExpression へパースして (実体は JCMethodInvocationpos の値を調整
  • (4) changeNode を実行しラムダ箇所を差し替え
src/main/java/sample/DoExprVisitor.java
package sample;

import com.sun.tools.javac.parser.ParserFactory;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.tree.TreeScanner;
import com.sun.tools.javac.util.Context;

import java.util.function.BiConsumer;
import java.util.stream.Stream;

public class DoExprVisitor extends TreeScanner {
    private ParserFactory parserFactory;
    private BiConsumer<JCLambda, JCExpression> changeNode = (lm, ne) -> {};
    private DoExprBuilder builder = new DoExprBuilder();

    public DoExprVisitor(Context context) {
        parserFactory = ParserFactory.instance(context);
    }

    @Override
    public void visitVarDef(JCVariableDecl node) {
        if (node.init != null) {
            // (b) (1)
            changeNode = (lm, ne) -> {
                // 変数への代入式を置換
                if (node.init == lm) {
                    node.init = ne;
                }
            };
        }
        super.visitVarDef(node);
    }

    // (c)
    @Override
    public void visitApply(JCMethodInvocation node) {
        if (node.args != null && node.args.size() > 0) {
            // (b) (1)
            changeNode = (lm, ne) -> {
                // メソッドの引数部分を置換
                if (node.args.contains(lm)) {
                    Stream<JCExpression> newArgs = node.args.stream().map(a -> (a == lm)? ne: a);
                    node.args = com.sun.tools.javac.util.List.from(newArgs::iterator);
                }
            };
        }
        super.visitApply(node);
    }

    @Override
    public void visitLambda(JCLambda node) {
        // (a) (2)
        builder.build(node).ifPresent(expr -> {
            // (3)
            JCExpression ne = parseExpression(expr);
            fixPos(ne, node.pos);

            // (b) (4) ラムダ部分を差し替え
            changeNode.accept(node, ne);
        });

        super.visitLambda(node);
    }

    // pos 値の修正
    private void fixPos(JCExpression ne, final int basePos) {
        ne.accept(new TreeScanner() {
            @Override
            public void scan(JCTree tree) {
                if(tree != null) {
                    tree.pos += basePos;
                    super.scan(tree);
                }
            }
        });
    }

    // 生成したソースコードをパース
    private JCExpression parseExpression(String doExpr) {
        return parserFactory.newParser(doExpr, false, false, false).parseExpression();
    }
}

コード生成処理の実装

該当のラムダ式を変換したソースコードを生成する処理です。

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

import com.sun.tools.javac.tree.JCTree.*;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class DoExprBuilder {
    private static final String DO_TYPE = "$do";
    private static final String VAR_PREFIX = "#{";
    private static final String VAR_SUFFIX = "}";
    // let 用のコードテンプレート
    private static final String LET_CODE = "#{var}.bind(#{rExpr}, #{lExpr} -> #{body})";
    // return 用のコードテンプレート
    private static final String RETURN_CODE = "#{var}.unit( #{expr} )";

    private Map<Class<? extends JCStatement>, CodeGenerator<JCStatement>> builderMap = new HashMap<>();

    public DoExprBuilder() {
        // let 用のコード生成
        builderMap.put(JCVariableDecl.class, (n, v, b) -> generateCodeForLet(cast(n), v, b));
        // return 用のコード生成
        builderMap.put(JCReturn.class, (n, v, b) -> generateCodeForReturn(cast(n), v, b));
    }

    public Optional<String> build(JCLambda node) {
        return getDoVar(node).map(var -> createExpression((JCBlock)node.body, var));
    }

    private String createExpression(JCBlock block, String var) {
        String res = "";

        for (JCStatement st : block.stats.reverse()) {
            res = builderMap.getOrDefault(st.getClass(), this::generateNoneCode).generate(st, var, res);
        }
        return res;
    }

    private String generateNoneCode(JCStatement node, String var, String body) {
        return body;
    }

    // let 用のソースコード生成
    private String generateCodeForLet(JCVariableDecl node, String var, String body) {
        String res = body;

        if ("let".equals(node.vartype.toString())) {
            Map<String, String> params = createParams(var);
            params.put("body", res);
            params.put("lExpr", node.name.toString());
            params.put("rExpr", node.init.toString());

            // 入れ子への対応
            if (node.init instanceof JCLambda) {
                JCLambda lm = cast(node.init);

                getDoVar(lm).ifPresent(childVar ->
                        params.put("rExpr", createExpression((JCBlock) lm.body, childVar)));
            }
            res = buildTemplate(LET_CODE, params);
        }

        return res;
    }

    // return 用のソースコード生成
    private String generateCodeForReturn(JCReturn node, String var, String body) {
        Map<String, String> params = createParams(var);
        params.put("expr", node.expr.toString());

        return buildTemplate(RETURN_CODE, params);
    }

    // 処理変数名の抽出
    private Optional<String> getDoVar(JCLambda node) {
        if (node.params.size() == 1) {
            String name = node.params.get(0).name.toString();

            if (name.endsWith(DO_TYPE)) {
                return Optional.of(name.replace(DO_TYPE, ""));
            }
        }
        return Optional.empty();
    }

    private Map<String, String> createParams(String var) {
        Map<String, String> params = new HashMap<>();

        params.put("var", var);

        return params;
    }

    // テンプレート処理
    private String buildTemplate(String template, Map<String, String> params) {
        String res = template;

        for(Map.Entry<String, String> param : params.entrySet()) {
            res = res.replace(VAR_PREFIX + param.getKey() + VAR_SUFFIX, param.getValue());
        }
        return res;
    }

    @SuppressWarnings("unchecked")
    private <S, T> T cast(S obj) {
        return (T)obj;
    }

    private interface CodeGenerator<T> {
        String generate(T node, String var, String body);
    }
}

Service Provider 設定ファイルやビルド定義は 前回 と同じものです。

Service Provider 設定ファイル

src/main/resources/META-INF/services/javax.annotation.processing.Processor
sample.DoExprProcessor
build.gradle
apply plugin: 'java'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

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

ビルド

ビルド実行
> 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/java_do_expr.jar3 が生成されます。

動作確認

下記のサンプルコードを使ってアノテーションプロセッサの動作確認を行います。

example/DoExprSample.java
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.Optional;

public class DoExprSample {
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);

        Opt<Integer> opt = new Opt<>();

        Optional<Integer> res = opt$do -> {
            let a = o1;
            let b = o2;
            let c = Optional.of(4);
            return a + b + c * 2;
        };
        // Optional[13]
        System.out.println(res);

        Opt<String> opt2 = new Opt<>();

        Optional<String> res2 = opt2$do -> {
            let a = Optional.of("a");
            let b = Optional.of("b");
            let c = opt2$do -> {
                let c1 = Optional.of("c1");
                let c2 = Optional.of("c2");
                return c1 + "-" + c2;
            };
            return a + b + "/" + c;
        };
        // Optional[ab/c1-c2]
        System.out.println(res2);

        // Optional[***ba]
        System.out.println(opt2$do -> {
            let a = Optional.of("a");
            let b = Optional.of("b");
            return "***" + b + a;
        });
    }

    static class Opt<T> {
        public Optional<T> bind(Optional<T> x, Function<T, Optional<T>> f) {
            return x.flatMap(f);
        }

        public Optional<T> unit(T v) {
            return Optional.ofNullable(v);
        }
    }
}

java_do_expr3.jar を使って上記ソースファイルをコンパイルします。

出力内容(変換後のソースコード)を見る限りは変換できているようです。

コンパイル
> javac -cp ../build/libs/java_do_expr3.jar DoExprSample.java

・・・
public class DoExprSample {
    ・・・
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);
        Opt<Integer> opt = new Opt<>();
        Optional<Integer> res = opt.bind(o1, (a)->opt.bind(o2, (b)->opt.bind(Optional.of(4), (c)->opt.unit(a + b + c * 2))));
        System.out.println(res);
        Opt<String> opt2 = new Opt<>();
        Optional<String> res2 = opt2.bind(Optional.of("a"), (a)->opt2.bind(Optional.of("b"), (b)->opt2.bind(opt2.bind(Optional.of("c1"), (c1)->opt2.bind(Optional.of("c2"), (c2)->opt2.unit(c1 + "-" + c2))), (c)->opt2.unit(a + b + "/" + c))));
        System.out.println(res2);
        System.out.println(opt2.bind(Optional.of("a"), (a)->opt2.bind(Optional.of("b"), (b)->opt2.unit("***" + b + a))));
    }
    ・・・
}

DoExprSample を実行すると正常に動作しました。

実行結果
> java DoExprSample

Optional[13]
Optional[ab/c1-c2]
Optional[***ba]

Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現2

前回 に引き続き、今回も Javaアノテーションプロセッサを使って下記と同等機能を実現します。

  • Haskell の do 記法
  • Scala の for 内包表記
  • F# のコンピュテーション式

今回は、F# のコンピュテーション式を模した下記のような構文 (前回断念したもの) を使用します。

Supplier<Optional<Integer>> res = opt$do -> {
    let a = o1;
    let b = o2;
    return a + b;
};

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

改良版は 「Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3」 を参照

はじめに

基本的な変換方法は 前回 と同じですが、かなりシンプルになっていると思います。

変数名$do の $do は変換対象としてマーキングするために付けています。

変換前 (アノテーションプロセッサ処理前)
Supplier<Optional<Integer>> res = opt$do -> {
    let a = o1;
    let b = o2;
    return a + b;
};
変換後 (アノテーションプロセッサ処理後)
Supplier<Optional<Integer>> res = () -> opt.bind(o1, a -> opt.bind(o2, b -> opt.unit(a + b)));

アノテーションプロセッサの実装

Processor の実装

アノテーションプロセッサの本体は 前回 と同じです。

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

import java.util.Set;
import javax.annotation.processing.*;

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;

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

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
public class DoExprProcessor extends AbstractProcessor {
    private Trees trees;
    private Context context;

    @Override
    public void init(ProcessingEnvironment procEnv) {
        trees = Trees.instance(procEnv);
        context = ((JavacProcessingEnvironment)procEnv).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) {
        // AST 変換
        cu.accept(new DoExprVisitor(context), null);
        // 変換後のソースを出力
        System.out.println(cu);
    }
}

TreeVisitor の実装

基本的な変換内容は 前回 と同じですが、下記の点が異なります。

  • (1) 対象処理を変換したソースコードを作って JCExpression へパース
  • (2) 生成した JCExpression 内の全 pos の値を修正
  • (3) JCLambda の body を差し替え

(2) が重要で、posソースコード内の位置) の値を調整しておかないと変換後の AST をコンパイルする段階でエラーになります。 (前回失敗した理由)

新しく生成した JCExpression木構造をたどって全要素の pos を変更するために com.sun.tools.javac.tree.TreeScannerscan メソッドをオーバーライドして使っています。

また、今回の構文ではラムダの paramKindIMPLICIT となりますので(前回はラムダ引数の型を指定していたので EXPLICIT だった)、ラムダの引数を消去した際に paramKindEXPLICIT へ変更しています。

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

import com.sun.source.tree.*;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.parser.ParserFactory;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.util.Context;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

public class DoExprVisitor extends TreeScanner<Void, Void> {
    private static final String DO_TYPE = "$do";

    private ParserFactory parserFactory;
    private Map<String, TemplateBuilder> builderMap = new HashMap<>();

    public DoExprVisitor(Context context) {
        parserFactory = ParserFactory.instance(context);
        // let 用の変換内容
        builderMap.put("let",
                new TemplateBuilder("${var}.bind(${rExpr}, ${lExpr} -> ${body})", this::createBindParams));
        // return 用の変換内容
        builderMap.put("return",
                new TemplateBuilder("${var}.unit( ${expr} )", this::createBasicParams));
    }

    @Override
    public Void visitLambdaExpression(LambdaExpressionTree node, Void p) {
        if (node instanceof JCLambda) {
            JCLambda lm = (JCLambda)node;

            if (lm.params.size() == 1) {
                getDoVar(lm.params.get(0)).ifPresent(var -> {
                    // ラムダの引数を消去
                    lm.params = com.sun.tools.javac.util.List.nil();
                    lm.paramKind = JCLambda.ParameterKind.EXPLICIT;

                    // (1) 対象処理を変換したソースコードを作って JCExpression へパース
                    JCExpression ne = parseExpression(createExpression((JCBlock)lm.body, var));
                    // (2) 生成した JCExpression 内の全 pos の値を修正
                    fixPos(ne, lm.pos);
                    // (3) JCLambda の body を差し替え
                    lm.body = ne;
                });
            }
        }
        return super.visitLambdaExpression(node, p);
    }

    // pos の値を修正する
    private void fixPos(JCExpression ne, int basePos) {
        ne.accept(new com.sun.tools.javac.tree.TreeScanner() {
            @Override
            public void scan(JCTree tree) {
                if(tree != null) {
                    tree.pos += basePos;
                    super.scan(tree);
                }
            }
        });
    }

    // 対象処理を変換したソースコード (Expression) を生成
    private String createExpression(JCBlock block, String var) {
        Stream<String> revExpr = block.stats.reverse().stream().map(s -> s.toString().replaceAll(";", ""));

        return revExpr.reduce("", (acc, v) -> {
            int spacePos = v.indexOf(" ");
            String action = v.substring(0, spacePos);

            if (builderMap.containsKey(action)) {
                acc = builderMap.get(action).build(var, acc, v.substring(spacePos + 1));
            }

            return acc;
        });
    }

    // 生成したソースコード (Expression) を JavacParser で JCExpression へ変換
    private JCExpression parseExpression(String doExpr) {
        return parserFactory.newParser(doExpr, false, false, false).parseExpression();
    }

    private Optional<String> getDoVar(JCVariableDecl param) {
        String name = param.name.toString();

        return name.endsWith(DO_TYPE)? Optional.of(name.replace(DO_TYPE, "")): Optional.empty();
    }

    private Map<String, String> createBindParams(String var, String body, String expr) {
        Map<String, String> params = createBasicParams(var, body, expr);

        String[] vexp = expr.split("=");
        params.put("lExpr", vexp[0]);
        params.put("rExpr", vexp[1]);

        return params;
    }

    private Map<String, String> createBasicParams(String var, String body, String expr) {
        Map<String, String> params = new HashMap<>();

        params.put("var", var);
        params.put("body", body);
        params.put("expr", expr);

        return params;
    }

    private interface ParamCreator {
        Map<String, String> create(String var, String body, String expr);
    }

    private class TemplateBuilder {
        private static final String VAR_PREFIX = "\\$\\{";
        private static final String VAR_SUFFIX = "\\}";

        private String template;
        private ParamCreator paramCreator;

        TemplateBuilder(String template, ParamCreator paramCreator) {
            this.template = template;
            this.paramCreator = paramCreator;
        }

        public String build(String var, String body, String expr) {
            return buildTemplate(template, paramCreator.create(var, body, expr));
        }

        private String buildTemplate(String template, Map<String, String> params) {
            return params.entrySet().stream().reduce(template,
                    (acc, v) -> acc.replaceAll(VAR_PREFIX + v.getKey() + VAR_SUFFIX, v.getValue()),
                    (a, b) -> a);
        }
    }
}

Service Provider 設定ファイルやビルド定義も 前回 と同じものです。

Service Provider 設定ファイル

src/main/resources/META-INF/services/javax.annotation.processing.Processor
sample.DoExprProcessor
build.gradle
apply plugin: 'java'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

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

ビルド

ビルド実行
> 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/java_do_expr.jar2 が生成されました。

動作確認

下記のサンプルコードを使ってアノテーションプロセッサの動作確認を行います。

example/DoExprSample.java
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.Optional;

public class DoExprSample {
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);

        Opt<Integer> opt = new Opt<>();
        // アノテーションプロセッサで変換する処理1
        Supplier<Optional<Integer>> res = opt$do -> {
            let a = o1;
            let b = o2;
            let c = Optional.of(4);
            return a + b + c * 2;
        };

        // Optional[13]
        System.out.println(res.get());

        Opt<String> opt2 = new Opt<>();
        // アノテーションプロセッサで変換する処理2
        Supplier<Optional<String>> res2 = opt2$do -> {
            let a = Optional.of("a");
            let b = Optional.of("b");
            return a + b;
        };

        // Optional["ab"]
        System.out.println(res2.get());
    }
    // Optional 用の bind・unit メソッド実装クラス
    static class Opt<T> {
        public Optional<T> bind(Optional<T> x, Function<T, Optional<T>> f) {
            return x.flatMap(f);
        }

        public Optional<T> unit(T v) {
            return Optional.ofNullable(v);
        }
    }
}

java_do_expr2.jar を使って上記ソースファイルをコンパイルします。

出力内容(変換後のソースコード)を見る限り正常に変換できているようです。

コンパイル
> javac -cp ../build/libs/java_do_expr2.jar DoExprSample.java

・・・
public class DoExprSample {
    ・・・
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);
        Opt<Integer> opt = new Opt<>();
        Supplier<Optional<Integer>> res = ()->opt.bind(o1, (a)->opt.bind(o2, (b)->opt.bind(Optional.of(4), (c)->opt.unit(a + b + c * 2))));
        System.out.println(res.get());
        Opt<String> opt2 = new Opt<>();
        Supplier<Optional<String>> res2 = ()->opt2.bind(Optional.of("a"), (a)->opt2.bind(Optional.of("b"), (b)->opt2.unit(a + b)));
        System.out.println(res2.get());
    }
    ・・・
}

DoExprSample を実行すると正常に動作しました。

実行結果
> java DoExprSample

Optional[13]
Optional[ab]

Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現

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

前回は変数の型を var から java.lang.Object へ変更しただけでしたが、今回は下記と同等な機能の簡易版をアノテーションプロセッサで実現してみます。

  • Haskell の do 記法
  • Scala の for 内包表記
  • F# のコンピュテーション式

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

改良版は 「Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3」 を参照

はじめに

前回 と同様にアノテーションプロセッサにおける下記の特徴を利用し、コンパイル前に AST (抽象構文木) を書き換えます。

変換例

今回は、以下のような変換をアノテーションプロセッサ内で実施します。 Haskell の do 記法というよりは F# のコンピュテーション式に近くなっています。

変換前 (アノテーションプロセッサ処理前)
Supplier<Optional<Integer>> res = ($do<Optional, Integer> opt) -> {
    let a = o1;
    let b = o2;
    return a + b;
};
変換後 (アノテーションプロセッサ処理後)
Supplier<Optional<Integer>> res = ()->{
    return opt.bind(o1, new java.util.function.Function<Integer, Optional<Integer>>(){
        @Override
        public Optional<Integer> apply(Integer a) {
            return opt.bind(o2, new java.util.function.Function<Integer, Optional<Integer>>(){
                @Override
                public Optional<Integer> apply(Integer b) {
                    return opt.unit(a + b);
                }
            });
        }
    });
};

変換内容

変換前の構文と変換方法を簡単に説明します。

ラムダの引数部分は変換に必要な情報を渡すために使用し、$do という未定義のクラスは変換対象かどうかをマーキングする目的で使います。

処理変数(変換例の opt)には bind・unit メソッドを持つオブジェクトインスタンスの変数名を指定します。

また、型推論は難しそうだったので、コンテナ型 (モナドの型) と要素の型を $do クラスの型引数で指定するようにしています。

変換前
・・・ = ($do<コンテナ型, 要素型> 処理変数) -> {
    let 変数1 = 式1;
    ・・・
    return 結果式;
};

変換は以下のように実施します。

ラムダの引数部分 ($do の箇所) を全て消去します。 (変換に必要な情報を渡しているだけなので)

(a) ($do<コンテナ型, 要素型> 処理変数) -> {} の変換内容
・・・ = () -> {
    ・・・
}

let の部分は bind メソッドを使った処理へ変換します。

(b) let 変数1 = 式1; の変換内容
return 処理変数.bind(式1, new java.util.function.Function<要素型, コンテナ型<要素型>>(){
    @Override
    public コンテナ型<要素型> apply(要素型 変数1) {
        ・・・
    }
});

return の部分は unit メソッドを使った処理へ変換します。

(c) return 結果式; の変換内容
return 処理変数.unit( 結果式 );

備考

本当は以下のようなシンプルな仕様で実現したかったのですが、変換後のコンパイル時にエラーが発生してしまい、うまく解決できなかったので今回は断念しました。

変換前 (失敗版)
Supplier<Optional<Integer>> res = opt$do -> {
    let a = o1;
    let b = o2;
    return a + b;
};
変換後 (失敗版)
Supplier<Optional<Integer>> res = () -> opt.bind(o1, a -> opt.bind(o2, b -> opt.unit(a + b)));
変換後のコンパイルエラー例 (Java 1.8.0_45)
java.lang.AssertionError: Value of x -1
        at com.sun.tools.javac.util.Assert.error(Assert.java:133)
        at com.sun.tools.javac.util.Assert.check(Assert.java:94)
        at com.sun.tools.javac.util.Bits.incl(Bits.java:200)
        at com.sun.tools.javac.comp.Flow$AbstractAssignAnalyzer.visitLambda(Flow.java:2254)
        at com.sun.tools.javac.tree.JCTree$JCLambda.accept(JCTree.java:1624)

ただし、変換後のソースをファイルへ出力し javac で普通にコンパイルすれば問題なく成功しますので、実現不可能では無いと思います。

と書きましたが、生成した箇所の pos の値を全て調整し直せば、 上記の構文で実現できることが判明しました。 (Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現2 参照)

アノテーションプロセッサの実装

それでは本題に入ります。

Processor の実装

まずは、アノテーションプロセッサの本体を実装します。

こちらは 前回SampleProcessor2.java とほぼ同じ内容です。

変換後のソースを確認するため accept 後の CompilationUnitTreeprintln するようにしています。

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

import java.util.Set;
import javax.annotation.processing.*;

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;

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

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
public class DoExprProcessor extends AbstractProcessor {
    private Trees trees;
    private Context context;

    @Override
    public void init(ProcessingEnvironment procEnv) {
        trees = Trees.instance(procEnv);
        context = ((JavacProcessingEnvironment)procEnv).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) {
        // AST 変換
        cu.accept(new DoExprVisitor(context), null);
        // 変換後のソースを出力
        System.out.println(cu);
    }
}

TreeVisitor の実装

次に、変換処理を実装します。

ラムダを変更するため TreeScanner を extends し visitLambdaExpression メソッドをオーバーライドします。

LambdaExpressionTree だとラムダの内容を変更できないので JCLambda へキャストし、以下のような処理を実施します。

  • (1) 変換対象かどうかをチェック (引数の型に $do を使っているかどうか等)
  • (2) ラムダの引数を消去
  • (3) ラムダの処理内容(let や return)を元に変換後のソースを生成
  • (4) (3) を JavacParser を使って JCStatement へ変換
  • (5) ラムダの処理内容を (4) の結果で差し替え (JCLambdabody.stats の値を変更)

通常は、JCStatement を直接構築して差し替えると思うのですが、JCStatement を自前で構築するのは大変そうだったので、部分的なソースコードを作り (3) 、それを JavacParser にパースさせる事で JCStatement を得ています (4)。

また、JCTree 内で使用されている List クラスは java.util.List ではなく com.sun.tools.javac.util.List なので注意。

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

import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.parser.ParserFactory;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.util.Context;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.stream.Stream;

public class DoExprVisitor extends TreeScanner<Void, Void> {
    private static final String DO_TYPE = "$do";
    // let 変換用のソースコード
    private static final String BIND_CODE = " return ${var}.bind(${rExpr}, new java.util.function.Function<${vType}, ${mType}<${vType}>>(){" +
            "  @Override public ${mType}<${vType}> apply(${vType} ${lExpr}){ ${body} }" +
            " });";
    // return 変換用のソースコード
    private static final String UNIT_CODE = "  return ${var}.unit( ${expr} );";

    private ParserFactory parserFactory;
    private Map<String, TemplateBuilder> builderMap = new HashMap<>();

    public DoExprVisitor(Context context) {
        parserFactory = ParserFactory.instance(context);
        // (b) let 用の変換内容
        builderMap.put("let", new TemplateBuilder(BIND_CODE, this::createBindParams));
        // (c) return 用の変換内容
        builderMap.put("return", new TemplateBuilder(UNIT_CODE, this::createUnitParams));
    }

    @Override
    public Void visitLambdaExpression(LambdaExpressionTree node, Void p) {
        if (node instanceof JCLambda) {
            JCLambda lm = (JCLambda)node;

            // (1) 変換対象かどうかをチェック
            if (isSingleTypeApplyParam(lm)) {
                JCVariableDecl param = lm.params.get(0);

                // (1) 変換対象かどうかをチェック
                if (isDoType(param)) {
                    // (2) ラムダの引数を消去
                    lm.params = com.sun.tools.javac.util.List.nil();

                    JCBlock block = (JCBlock)lm.body;
                    // 変換後の処理内容(Statement)を作成 (3) (4)
                    JCStatement newStats = parseStatement(createStatement(block, createBaseParams(param)));
                    // (5) ラムダの処理内容を差し替え
                    block.stats = com.sun.tools.javac.util.List.of(newStats);
                }
            }
        }
        return super.visitLambdaExpression(node, p);
    }
    // (3) ラムダの処理内容を変換したソースコードを生成
    private String createStatement(JCBlock block, Map<String, String> params) {
        // ラムダの内容 (JCStatement のリスト) を逆順化して個別に文字列化
        Stream<String> revStats = block.stats.reverse().stream().map(s -> s.toString().replaceAll(";", ""));

        // 逆順化したリストを順次変換
        return revStats.reduce("", (acc, v) -> {
            int spacePos = v.indexOf(" ");
            String action = v.substring(0, spacePos);

            if (builderMap.containsKey(action)) {
                acc = builderMap.get(action).build(params, acc, v.substring(spacePos + 1));
            }

            return acc;
        });
    }
    // (4) 生成したソースコードを JavacParser で JCStatement へ変換
    private JCStatement parseStatement(String doStat) {
        return parserFactory.newParser(doStat, false, false, false).parseStatement();
    }
    // (1)
    private boolean isDoType(JCVariableDecl param) {
        String type = ((JCTypeApply)param.vartype).clazz.toString();
        return DO_TYPE.equals(type);
    }
    // (1)
    private boolean isSingleTypeApplyParam(JCLambda lm) {
        return lm.params.size() == 1
                && lm.params.get(0).vartype instanceof JCTypeApply;
    }

    private Map<String, String> createBaseParams(JCVariableDecl param) {
        Map<String, String> params = new HashMap<>();

        params.put("var", param.name.toString());

        JCTypeApply paramType = (JCTypeApply)param.vartype;
        params.put("mType", paramType.arguments.get(0).toString());
        params.put("vType", paramType.arguments.get(1).toString());

        return params;
    }

    private Map<String, String> createBindParams(String body, String expr) {
        Map<String, String> params = createUnitParams(body, expr);

        String[] divexp = expr.split("=");
        params.put("lExpr", divexp[0]);
        params.put("rExpr", divexp[1]);

        return params;
    }

    private Map<String, String> createUnitParams(String body, String expr) {
        Map<String, String> params = new HashMap<>();

        params.put("body", body);
        params.put("expr", expr);

        return params;
    }
    // テンプレート処理を実施するためのクラス
    private class TemplateBuilder {
        private static final String VAR_PREFIX = "\\$\\{";
        private static final String VAR_SUFFIX = "\\}";

        private String template;
        private BiFunction<String, String, Map<String, String>> paramCreator;

        TemplateBuilder(String template, BiFunction<String, String, Map<String, String>> paramCreator) {
            this.template = template;
            this.paramCreator = paramCreator;
        }

        public String build(Map<String, String> params, String body, String expr) {
            return buildTemplate(
                    buildTemplate(template, params),
                    paramCreator.apply(body, expr));
        }

        private String buildTemplate(String template, Map<String, String> params) {
            return params.entrySet().stream().reduce(template,
                    (acc, v) -> acc.replaceAll(VAR_PREFIX + v.getKey() + VAR_SUFFIX, v.getValue()),
                    (a, b) -> a);
        }
    }
}

JCLambda の変換イメージ例

JCLambda の大まかな変換イメージを書くと以下のようになります。

ソースコード
($do<Optional, String> opt2) -> {
    let a = Optional.of("a");
    let b = Optional.of("b");
    return a + b;
}

上記のソースを JCLambda 化すると以下のようになります。 実際はもっと複雑ですが、適当に簡略化しています。 (init や expr の内容も実際は JCMethodInvocation 等が入れ子になっています)

変換前 JCLambda の内容
JCLambda(
    params = [
        JCVariableDecl(
            name = opt2,
            vartype = $do<Optional, String>
        )
    ],
    paramKind = EXPLICIT,
    body = JCBlock(
        stats = [
            JCVariableDecl(
                name = a,
                vartype = let,
                init = Optional.of("a")
            ),
            JCVariableDecl(
                name = b,
                vartype = let,
                init = Optional.of("a")
            ),
            JCReturn(
                expr = a + b
            )
        ]
    )
)

DoExprVisitor では上記を以下のように変更します。

変換後 JCLambda の内容
JCLambda(
    params = [],
    paramKind = EXPLICIT,
    body = JCBlock(
        stats = [
            JCReturn(
                expr = opt2.bind(Optional.of("a"), ・・・)
            )
        ]
    )
)

今回は body に設定されている JCBlock をそのまま使いましたが、body の値を直接変更する方法も考えられます。

Service Provider 設定ファイル

アノテーションプロセッサで sample.DoExprProcessor を使用するように Service Provider の設定ファイルを用意します。

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

ビルド

ビルド定義ファイルは以下の通り。

build.gradle
apply plugin: 'java'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

dependencies {
    compile files("${System.properties['java.home']}/../lib/tools.jar")
}
ビルド実行
> 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/java_do_expr.jar が生成されました。

動作確認

最後に、下記のサンプルコードを使ってアノテーションプロセッサの動作確認を行います。

example/DoExprSample.java
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.Optional;

public class DoExprSample {
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);

        Opt<Integer> opt = new Opt<>();
        // アノテーションプロセッサで変換する処理1
        Supplier<Optional<Integer>> res = ($do<Optional, Integer> opt) -> {
            let a = o1;
            let b = o2;
            let c = Optional.of(4);
            return a + b + c * 2;
        };

        // Optional[13]
        System.out.println(res.get());

        Opt<String> opt2 = new Opt<>();
        // アノテーションプロセッサで変換する処理2
        Supplier<Optional<String>> res2 = ($do<Optional, String> opt2) -> {
            let a = Optional.of("a");
            let b = Optional.of("b");
            return a + b;
        };

        // Optional["ab"]
        System.out.println(res2.get());
    }
    // Optional 用の bind・unit メソッド実装クラス
    static class Opt<T> {
        public Optional<T> bind(Optional<T> x, Function<T, Optional<T>> f) {
            return x.flatMap(f);
        }

        public Optional<T> unit(T v) {
            return Optional.ofNullable(v);
        }
    }
}

java_do_expr.jar を使って上記ソースファイルをコンパイルします。

出力内容(変換後のソースコード)を見る限り正常に変換できているようです。

コンパイル
> javac -cp ../build/libs/java_do_expr.jar DoExprSample.java

・・・
public class DoExprSample {
    ・・・
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);
        Opt<Integer> opt = new Opt<>();
        Supplier<Optional<Integer>> res = ()->{
            return opt.bind(o1, new java.util.function.Function<Integer, Optional<Integer>>(){

                @Override()
                public Optional<Integer> apply(Integer a) {
                    return opt.bind(o2, new java.util.function.Function<Integer, Optional<Integer>>(){

                        @Override()
                        public Optional<Integer> apply(Integer b) {
                            return opt.bind(Optional.of(4), new java.util.function.Function<Integer, Optional<Integer>>(){

                                @Override()
                                public Optional<Integer> apply(Integer c) {
                                    return opt.unit(a + b + c * 2);
                                }
                            });
                        }
                    });
                }
            });
        };
        System.out.println(res.get());
        Opt<String> opt2 = new Opt<>();
        Supplier<Optional<String>> res2 = ()->{
            return opt2.bind(Optional.of("a"), new java.util.function.Function<String, Optional<String>>(){

                @Override()
                public Optional<String> apply(String a) {
                    return opt2.bind(Optional.of("b"), new java.util.function.Function<String, Optional<String>>(){

                        @Override()
                        public Optional<String> apply(String b) {
                            return opt2.unit(a + b);
                        }
                    });
                }
            });
        };
        System.out.println(res2.get());
    }
    ・・・
}

DoExprSample を実行すると正常に動作しました。

実行結果
> java DoExprSample

Optional[13]
Optional[ab]

Spring を使った Web アプリケーションへ Ehcache を適用し JMX でモニタリング

Spring を使った Web アプリケーションへ Ehcache を適用し、JMX でキャッシュ状況を取得できるようにしてみました。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20150508/

Spring へ Ehcache を適用

Spring には Cache Abstraction 機能が用意されており、Cache 用のアノテーションメソッドへ付ければメソッドの戻り値をキャッシュできます。 (デフォルトでは、メソッドの引数がキャッシュキーとなります)

実際のキャッシュ処理には以下のようなライブラリや API が利用でき、今回は Ehcache を使用します。

  • ConcurrentMap
  • Ehcache
  • Guava
  • GemFire
  • JSR-107 (JCache)

なお、キャッシュ用アノテーションは以下が利用でき、今回は JSR-107 のアノテーションを使用します。

Service クラス

まずはキャッシュを適用するメソッドを実装します。

今回は JSR-107 の @CacheResult アノテーションを使いました。

src/main/java/sample/service/SampleService.java
package sample.service;

import org.springframework.stereotype.Service;
import javax.cache.annotation.CacheResult;

@Service
public class SampleService {
    @CacheResult(cacheName = "sample")
    public String sample(String id) {
        return "sample: " + id + ", " + System.currentTimeMillis();
    }
}

Controller クラス

次に、キャッシュを適用したメソッドを呼び出す処理を実装します。

今回のようなケースでは、CGLIB によるプロキシを使ってキャッシュ処理を差し込んでおり、内部的なメソッド呼び出しにはキャッシュを適用しないようなのでご注意ください。

src/main/java/sample/controller/SampleController.java
package sample.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.inject.Inject;
import sample.service.SampleService;

@Controller
public class SampleController {
    @Inject
    private SampleService sampleService;

    @RequestMapping("/sample/{id}")
    @ResponseBody
    public String sample(@PathVariable("id") String id) {
        // キャッシュを適用したメソッドの実行
        return sampleService.sample(id);
    }
}

設定クラス

設定クラスでは、下記を実施することで Ehcache を適用できます。

  • @EnableCaching でキャッシュを有効化
  • EhCacheCacheManager を Bean 定義

Ehcache の設定ファイル ehcache.xml をクラスパスから取得するように ClassPathResource を使っています。

今回は JMXモニタリングできるように Ehcache の ManagementService も Bean 定義しています。

src/main/java/sample/config/WebConfig.java
package sample.config;

import net.sf.ehcache.management.ManagementService;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jmx.support.MBeanServerFactoryBean;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
@EnableCaching //キャッシュ機能の有効化
public class WebConfig {
    // CacheManager の定義
    @Bean
    public CacheManager cacheManager() {
        EhCacheCacheManager manager = new EhCacheCacheManager();
        manager.setCacheManager(ehcache().getObject());

        return manager;
    }

    @Bean
    public EhCacheManagerFactoryBean ehcache() {
        EhCacheManagerFactoryBean ehcache = new EhCacheManagerFactoryBean();
        ehcache.setConfigLocation(new ClassPathResource("ehcache.xml"));

        return ehcache;
    }

    // JMX 設定
    @Bean
    public MBeanServerFactoryBean mbeanServer() {
        MBeanServerFactoryBean factory = new MBeanServerFactoryBean();
        factory.setLocateExistingServerIfPossible(true);

        return factory;
    }
    // Ehcache 用の JMX 設定
    @Bean
    public ManagementService managementService() {
        ManagementService service = new ManagementService(ehcache().getObject(), mbeanServer().getObject(), true, true, true, true);
        service.init();

        return service;
    }
}

実行クラス

Spring Boot で実行するための実行クラスです。

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

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class Application {
    public static void main(String... args) {
        SpringApplication.run(Application.class, args);
    }
}

ビルド定義

Spring の Cache Abstraction 機能は 2つのモジュールに分かれており、Ehcache を使用するには spring-context-support が必要です。

モジュール 備考
spring-context Cache の基本機能
spring-context-support 各種キャッシュライブラリ用の実装

今回使用した spring-boot-starter-webspring-context-support を依存関係に含んでいなかったため、依存定義をする必要がありました。

build.gradle
apply plugin: 'spring-boot'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE'
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web:1.2.3.RELEASE'
    // 以下は org.springframework.cache.ehcache のために必要
    compile 'org.springframework:spring-context-support:4.1.6.RELEASE'

    // @Inject アノテーションのために必要
    compile 'javax:javaee-api:7.0'
    // @CacheResult アノテーションのために必要
    compile 'javax.cache:cache-api:1.0.0-PFD'

    compile 'net.sf.ehcache:ehcache:2.10.0'
}

キャッシュ設定

Ehcache の設定は以下の通り。 キャッシュの有効期間を 10 秒としています。

src/main/resources/ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
  <cache name="sample"
         maxElementsInMemory="100"
         eternal="false"
         timeToLiveSeconds="10" />
</ehcache>

実行

bootRun タスクで Web アプリケーションを起動します。

起動
> gradle bootRun

・・・
:bootRun

・・・
2015-05-04 18:37:07.807  INFO 6704 --- [           main] sample.Application                       : Started Application in 5.674 seconds (JVM running for 6.24)
2015-05-04 18:37:13.436  INFO 6704 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
・・・

何度か同じ URL へアクセスしてみると、10秒間のキャッシュが効いている事を確認できました。

動作確認
$ curl http://localhost:8080/sample/aaa
sample: aaa, 1430732146256

$ curl http://localhost:8080/sample/aaa
sample: aaa, 1430732146256

$ curl http://localhost:8080/sample/aaa
sample: aaa, 1430732146256

$ curl http://localhost:8080/sample/aaa
sample: aaa, 1430732158070

JMX でキャッシュ状況を取得

JMX を使って Ehcache のキャッシュヒット状況などを確認してみます。

Attach API による JMX エージェント (MBean サーバー) の適用

Java の Attach API を使えば、ローカルで実行中の Java プロセスへアタッチして JMX エージェント (MBean サーバー) を動的に適用できます。

そのため、アプリケーションの起動時 (gradle bootRun) に JMX 用の実行時オプションを指定しておく必要はありません。

Attach API を使って JMX エージェントを適用する手順は以下の通りです。

  • (1) Java VM の ID (プロセスID) を指定して VM へアタッチ
  • (2) (1) を使って JMX 用のサービス URL を取得
  • (3) (2) が null の場合に JMX エージェントを適用し (2) を再実施

(2) の戻り値が null の場合、JMX エージェントが未適用という事ですので、management-agent.jarloadAgentJMX エージェントを適用します。

一度 JMX エージェントを適用しておけば、それ以降はサービス URL を取得できるので、その URL を使って MBean サーバーへ接続します。

Ehcache の JMX

Ehcache の JMX オブジェクト名は以下のようになります。

  • net.sf.ehcache:type=CacheStatistics,CacheManager=__DEFAULT__,name=<キャッシュ名>

net.sf.ehcache:type=CacheStatistics,* をオブジェクト名に指定して queryNames すれば Ehcache に関する全オブジェクト名を取得できます。

そうして取得したオブジェクト名を使って CacheHits 等を getAttribute すればキャッシュのヒット数などを取得できます。

Groovy で実装

今回は Ehcache の JMX からキャッシュヒット数などを取得する処理を Groovy で実装してみました。

ehcache_jmx1.groovy
import javax.management.ObjectName
import javax.management.remote.JMXConnectorFactory
import javax.management.remote.JMXServiceURL

import com.sun.tools.attach.VirtualMachine

// JMX のサービス URL 取得処理
def getServiceUrl = {
    it.agentProperties.getProperty('com.sun.management.jmxremote.localConnectorAddress')
}

def pid = args[0]
// (1) Java VM へアタッチ
def vm = VirtualMachine.attach(pid)
// (2) JMX のサービス URL 取得
def url = getServiceUrl(vm)

if (url == null) {
    def javaHome = vm.systemProperties.getProperty('java.home')
    // (3) JMX エージェントを適用
    vm.loadAgent("${javaHome}/lib/management-agent.jar")
    // JMX のサービス URL を再取得
    url = getServiceUrl(vm)
}

vm.detach()

// MBean サーバーへの接続
def con = JMXConnectorFactory.connect(new JMXServiceURL(url))
def server = con.getMBeanServerConnection()

def cacheName = new ObjectName('net.sf.ehcache:type=CacheStatistics,*')
// EhCache に関する JMX オブジェクト名の取得
server.queryNames(cacheName, null).each { name ->
    println "# ${name}"

    // ヒット回数などの属性値を取得
    def res = [
        'CacheHits',   // キャッシュのヒット回数
        'CacheMisses', // キャッシュのミス回数
        'InMemoryHits',
        'InMemoryMisses'
    ].collectEntries { attr ->
        [attr, server.getAttribute(name, attr)]
    }

    println res
}

con.close()
実行例
> groovy ehcache_jmx1.groovy 6704

# net.sf.ehcache:type=CacheStatistics,CacheManager=__DEFAULT__,name=sample
[CacheHits:7, CacheMisses:3, InMemoryHits:8, InMemoryMisses:2]

また、JMX のサービス URL は ConnectorAddressLink.importFrom(<プロセスID>) で取得する事も可能です。

ehcache_jmx2.groovy
・・・
import com.sun.tools.attach.VirtualMachine
import sun.management.ConnectorAddressLink

// JMX のサービス URL 取得処理 (ConnectorAddressLink 利用版)
def getServiceUrl = {
    ConnectorAddressLink.importFrom(it as int)
}

def pid = args[0]
def url = getServiceUrl(pid)

if (url == null) {
    def vm = VirtualMachine.attach(pid)

    def javaHome = vm.systemProperties.getProperty('java.home')
    vm.loadAgent("${javaHome}/lib/management-agent.jar")

    vm.detach()

    url = getServiceUrl(pid)
}
・・・

Spring MVC で Controller を動的に切り替える - RequestMappingHandlerMapping のサブクラス利用

Spring MVC では、基本的に @RequestMapping アノテーションで指定した URL パターンに合致する Controller のメソッドを実行し、org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping クラスがその処理を担っています。

そこで、試しに RequestMappingHandlerMapping のサブクラスを使って、実行対象の Controller (@RequestMapping のパス違い) を動的に切り替えるようにしてみました。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20150507/

はじめに

今回は、以下のように Query string (URL) の debug パラメータ有無によって、実行する Controller を切り替える処理を RequestMappingHandlerMapping のサブクラスで実現します。

URL 実行するメソッド
/sample/xxx SampleController.sample
/sample/xxx?debug DebugSampleController.sample

ただし、Controller を切り替えなくても他にやり様はいくらでもありますので、本件の実用性は低いと思います。

実装

Controller クラス

まずは Controller を 2つ用意します。

src/main/java/sample/controller/SampleController.java
package sample.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class SampleController {
    @RequestMapping("/sample/{id}")
    @ResponseBody
    public String sample(@PathVariable("id") String id) {
        return "sample: " + id + ", " + System.currentTimeMillis();
    }
}

2つ目には @RequestMapping パスの先頭に /debug を付けました。

src/main/java/sample/controller/DebugSampleController.java
package sample.controller;
・・・
@Controller
public class DebugSampleController {
    @RequestMapping("/debug/sample/{id}")
    @ResponseBody
    public String sample(@PathVariable("id") String id) {
        return "debug-sample: " + id + ", " + new Date();
    }
}

RequestMappingHandlerMapping サブクラス

次に、本題の RequestMappingHandlerMapping サブクラスを実装します。

とりあえず、下記のように実装すれば別の Controller を呼び出せます。

  • (1) lookupHandlerMethod メソッドをオーラーライドし lookupPath を変更
  • (2) HttpServletRequest.getServletPath() の値を (1) に合わせて変更

(1) を実施しただけでは内部的なパスのチェック処理に引っかかるので (2) も合わせて実施する必要があります。

下記では、Query string へ debug が付いていた場合に Controller を選出するパス (lookupPath) の先頭に /debug を追加するように実装し、getServletPath() の戻り値を変更するために HttpServletRequestWrapper のサブクラスを使っています。

src/main/java/sample/mapping/SampleRequestMappingHandlerMapping.java
package sample.mapping;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class SampleRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
    // (1)
    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        return super.lookupHandlerMethod(
            changePath(lookupPath, request), 
            new SampleHttpServletRequest(request)
        );
    }

    // Controller を選出するパスを書き換える
    private String changePath(String path, HttpServletRequest request) {
        if (request.getParameter("debug") != null) {
            return "/debug" + path;
        }
        return path;
    }

    class SampleHttpServletRequest extends HttpServletRequestWrapper {
        public SampleHttpServletRequest(HttpServletRequest req) {
            super(req);
        }
        // (2) 
        @Override
        public String getServletPath() {
            return changePath(super.getServletPath(), this);
        }
    }
}

設定クラス

SampleRequestMappingHandlerMapping を適用するように Bean 定義を行います。

src/main/java/sample/config/WebConfig.java
package sample.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import sample.mapping.SampleRequestMappingHandlerMapping;

@Configuration
@EnableWebMvc
public class WebConfig {
    @Bean
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new SampleRequestMappingHandlerMapping();
    }
}

実行クラス

今回は Spring Boot を使って実行しますので、そのための実行クラスも用意します。

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

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class Application {
    public static void main(String... args) {
        SpringApplication.run(Application.class, args);
    }
}

ビルド定義

Gradle のビルド定義ファイルは以下の通りです。 (Spring Boot 利用)

build.gradle
apply plugin: 'spring-boot'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE'
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web:1.2.3.RELEASE'
}

実行

bootRun タスクを実行し、Tomcat 上で Web アプリケーションを起動しておきます。

起動
> gradle bootRun

・・・
:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.3.RELEASE)
・・・
2015-05-03 16:43:04.306  INFO 4860 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-05-03 16:43:04.308  INFO 4860 --- [           main] sample.Application                       : Started Application in 5.001 seconds (JVM running for 6.5)

/sample/xxx/sample/xxx?debug へアクセスすると、?debug の有無で実行結果 (実行対象 Controller) が変化する事を確認できました。

動作確認
$ curl http://localhost:8080/sample/abc
sample: abc, 1430639107957

$ curl http://localhost:8080/sample/abc?debug
debug-sample: abc, Sun May 03 16:45:12 JST 2015

Apache Spark でロジスティック回帰

以前 ※ に R や Julia で試したロジスティック回帰を Apache Spark の MLlib (Machine Learning Library) を使って実施してみました。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20150427/

※「 R でロジスティック回帰 - glm, MCMCpack 」、「 Julia でロジスティック回帰-glm

はじめに

R の時と同じデータを使いますが、ヘッダー行を削除しています。(「R でロジスティック回帰 - glm, MCMCpack」 参照)

データ data4a.csv
8,1,9.76,C
8,6,10.48,C
8,5,10.83,C
・・・

データ内容は以下の通り。個体 i それぞれにおいて 「 { N_i } 個の観察種子のうち生きていて発芽能力があるものは { y_i } 個」 となっています。

項目 内容
N 観察種子数
y 生存種子数
x 植物の体サイズ
f 施肥処理 (C: 肥料なし, T: 肥料あり)

体サイズ x と肥料による施肥処理 f が種子の生存する確率(ある個体 i から得られた種子が生存している確率)にどのように影響しているかをロジスティック回帰で解析します。

MLlib によるロジスティック回帰

今回は org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS を使用します。

LogisticRegressionWithLBFGS について

LogisticRegressionWithLBFGS で以前と同様のロジスティック回帰を実施するには以下が必要です。

  • setIntercept で true を設定

この値が false (デフォルト値) の場合、結果の intercept 値が 0 になります。

なお、今回のように二項分布を使う場合は numClasses の値を変更する必要はありませんが (デフォルト値が 2 のため)、応答変数が 3状態以上の多項分布を使う場合は setNumClasses で状態数に応じた値を設定します。

LabeledPoint について

LogisticRegressionWithLBFGS へ与えるデータは LabeledPoint で用意します。

R や Julia では 応答変数 ~ 説明変数1 + 説明変数2 + ・・・ のように応答変数と説明変数を指定しましたが、LabeledPoint では下記のようにメンバー変数で表現します。

メンバー変数 応答変数・説明変数
label 応答変数
features 説明変数

値は Double とする必要がありますので、f 項目のような文字列値は数値化します。

更に、二項分布を使う場合 (numClasses = 2) は応答変数の値が 0 か 1 でなければなりません。

LabeledPoint への変換例

例えば、以下のようなデータを応答変数 y 項目、説明変数 x と f 項目の LabeledPoint へ変換する場合

変換前のデータ (N = 8, y = 6)
8,6,10.48,C

次のようになります。

変換後のデータイメージ
LabeledPoint(label: 1.0, features: Vector(10.48, 0.0))
LabeledPoint(label: 1.0, features: Vector(10.48, 0.0))
LabeledPoint(label: 1.0, features: Vector(10.48, 0.0))
LabeledPoint(label: 1.0, features: Vector(10.48, 0.0))
LabeledPoint(label: 1.0, features: Vector(10.48, 0.0))
LabeledPoint(label: 1.0, features: Vector(10.48, 0.0))
LabeledPoint(label: 0.0, features: Vector(10.48, 0.0))
LabeledPoint(label: 0.0, features: Vector(10.48, 0.0))

8個(N)の中で 6個(y)生存していたデータのため、 label (応答変数) の値が 1.0 (生存) のデータ 6個と 0.0 のデータ 2個へ変換します。

ちなみに、f 項目の値が C の場合は 0.0、T の場合は 1.0 としています。

実装

実装してみると以下のようになります。

LogisticRegression.scala
import org.apache.spark.SparkContext
import org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.linalg.Vectors

object LogisticRegression extends App {
    // f項目の値を数値へ変換
    val factor = (s: String) => s match {
        case "C" => 0
        case _ => 1
    }

    val sc = new SparkContext("local", "LogisticRegression")

    // データの準備 (100行のデータ -> 800個の LabeledPoint)
    val rdd = sc.textFile(args(0)).map(_.split(",")).flatMap { d =>
        val n = d(0).toInt
        val x = d(1).toInt
        // 説明変数の値
        val v = Vectors.dense(d(2).toDouble, factor(d(3)))

        // 応答変数が 1 のデータ x 個と 0 のデータ n - x 個を作成
        List.fill(x)( LabeledPoint(1, v) ) ++ 
            List.fill(n -x)( LabeledPoint(0, v) )
    }

    // ロジスティック回帰の実行
    val res = new LogisticRegressionWithLBFGS()
//      .setNumClasses(2) //省略可
        .setIntercept(true)
        .run(rdd)

    println(res)
}

ビルド

以下のような Gradle ビルド定義ファイルを使って実行します。

build.gradle
apply plugin: 'scala'
apply plugin: 'application'

mainClassName = 'LogisticRegression'

repositories {
    jcenter()
}

dependencies {
    compile 'org.scala-lang:scala-library:2.11.6'

    compile('org.apache.spark:spark-mllib_2.11:1.3.1') {
        // ログ出力の抑制
        exclude module: 'slf4j-log4j12'
    }

    // ログ出力の抑制
    runtime 'org.slf4j:slf4j-nop:1.7.12'
}

run {
    if (project.hasProperty('args')) {
        args project.args.split(' ')
    }
}

不要な WARN ログ出力を抑制するため以下のファイルも用意しました。

src/main/resources/log4j.properties
log4j.rootLogger=off

実行

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

実行結果
> gradle run -Pargs=data4a.csv

:clean
:compileJava UP-TO-DATE
:compileScala
:processResources
:classes
:run
(weights=[1.952347703282676,2.021401680901667], intercept=-19.535421113192506)

BUILD SUCCESSFUL

以前に実施した R の結果 (Estimate の値) とほとんど同じ値になっています。

R の glm 関数による結果
Coefficients:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept) -19.5361     1.4138  -13.82   <2e-16 ***
x             1.9524     0.1389   14.06   <2e-16 ***
fT            2.0215     0.2313    8.74   <2e-16 ***

Java 8 でグルーピング処理 - List<V> を Map<K, V> へ変換

Java 8 で List<V>Map<K, V> へ変換するようなグルーピング処理をいくつか試してみました。

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

はじめに

今回は、下記をリスト化した List<Data>id でグルーピングして Map<String, Data> へ変換します。

class Data {
    private String id;
    private String name;

    public Data(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }
    ・・・
}

Java 8 より前のバージョンでは以下のようにすると思います。

拡張 for 利用
List<Data> dataList = Arrays.asList(
    new Data("d1", "sample1"),
    new Data("d2", "sample2"),
    new Data("d3", "sample3")
);

Map<String, Data> res = new HashMap<>();
for (Data d : dataList) {
    res.put(d.getId(), d);
}

また、Java 8 で Map<String, List<Data>> へ変換するなら Collectors.groupingBy を使うだけです。

groupingBy で Map<String, List > へ変換
Map<String, List<Data>> res = dataList.stream().collect(
    Collectors.groupingBy(Data::getId)
);

(1) forEach

まずは、拡張 for の変わりに forEach メソッドを使用する方法です。

Map<String, Data> res = new HashMap<>();
dataList.forEach(d -> res.put(d.getId(), d));

(2) toMap

次は、Collectors.toMap を使用する方法です。

toMap の 2引数版

Map<String, Data> res = dataList.stream().collect(
    Collectors.toMap(Data::getId, d -> d)
);

もしくは

Map<String, Data> res = dataList.stream().collect(
    Collectors.toMap(Data::getId, UnaryOperator.identity())
);

ここで、2引数版の toMap メソッドには以下のような注意点があります。

  • 同一キーを持つオブジェクトを複数含んでいると IllegalStateException を throw する

例えば、以下は IllegalStateException となります。

IllegalStateException が発生するコード例
List<Data> dataList2 = Arrays.asList(
    new Data("d1", "sample1"),
    new Data("d2", "sample2"),
    new Data("d3", "sample3"),
    new Data("d1", "sample1-b") // d1 が重複
);

// IllegalStateException: Duplicate key Data(d1, sample1) が発生
Map<String, Data> res = dataList2.stream().collect(
    Collectors.toMap(Data::getId, d -> d)
);
IllegalStateException エラー内容
Exception in thread "main" java.lang.IllegalStateException: Duplicate key Data(d1, sample1)

IllegalStateException を発生させないようにするには、3引数版の toMap を使います。

toMap の 3引数版

第 3引数で同一キーの値が複数あった場合にどちらを選択するかを指定します。

最初の要素を採用する場合
// 結果 [ d1: Data(d1, sample1), d2: Data(d2, sample2), d3: Data(d3, sample3) ]
Map<String, Data> res = dataList2.stream().collect(
    Collectors.toMap(Data::getId, d -> d, (d1, d2) -> d1)
);
最後の要素を採用する場合
// 結果 [ d1: Data(d1, sample1-b), d2: Data(d2, sample2), d3: Data(d3, sample3) ]
Map<String, Data> res = dataList2.stream().collect(
    Collectors.toMap(Data::getId, d -> d, (d1, d2) -> d2)
);

(3) groupingBy + collectingAndThen

あまり実用的では無いと思いますが、groupingBy と collectingAndThen を組み合わせる方法も考えられます。

// 結果 [ d1: Data(d1, sample1), d2: Data(d2, sample2), d3: Data(d3, sample3) ]
Map<String, Data> res = dataList2.stream().collect(
    Collectors.groupingBy(
        Data::getId,
        Collectors.collectingAndThen(
            Collectors.toList(),
            a -> a.get(0) //最初の要素を採用
        )
    )
);

toList を minBy 等で代用する事も可能です。

Map<String, Data> res = dataList2.stream().collect(
    Collectors.groupingBy(
        Data::getId,
        Collectors.collectingAndThen(
            Collectors.minBy((a, b) -> 0),
            a -> a.get()
        )
    )
);

(4) collect の 3引数版

最後に、3引数版の collect を使う方法です。

パラレル実行で使用する第 3引数が必須となっている点が微妙だと思います。 (下記 Map::putAll の箇所を null にすると NullPointerException となります)

Map<String, Data> res = dataList.stream().collect(
    HashMap::new,
    (m, d) -> m.put(d.getId(), d),
    Map::putAll
);