KEIS BLOGは株式会社ケイズ・ソフトウェアが運営しています。

KEIS BLOG

JavaPoetで簡単コード生成!


shimoda01

こんにちは。以前、ドヤ顔するために iPhone6 を買ったのですが、最近フリーズが多くて OS 再インストールしたら、ブログ記事のために撮り溜めた写真が全て消えて白目になった下田です。バックアップって大事ですね!

全然関係ないですが、この記事トップの写真は、川を挟んで東京側から撮った武蔵小杉です。これからもっと発展していきそうな感じがしてワクワクしますね!

ところで、今回はいつもの Antlr ではなく、Code generation のエリアについて少し話をしたいと思います。私の知る限り、 Code generation には、2種類あります。一つは、本当にソースコードを自動生成して、実行時にそのソースファイルを資産の一部として取り込むパターン。もう一つのパターンは、実行中にメモリ上にあたかもソースが合ったかのように生成して処理してしまうパターン。後者のほうがクールですが、大抵は問題が起きたときのデバッグ時に、デバック機能の恩恵を得られない(ソースコードのマッピングが不在のため)ため、問題を特定するのが難しくなる傾向にあると思います。十分に枯れたコードであればそこが問題になることはないかもしれませんが、新しいシステムを開発している場合などは悪夢となります。一方で、前者の場合はとてもシンプルで、スタックトレースも出ますし型システムがある言語やコンパイル言語であれば、その恩恵を受けることができ、さらには、自動生成とは知らない開発者でもバグに簡単に気づくことが出来ます。私個人も気に入っているのは前者です。そこで、今回は、前者のツールを紹介したいと思います。その名も、 JavaPoet です。このツールは、インターフェースが優れていて直感的でシンプル、非常に分かりやすいツールです。始めて30分でマスターできると思いますので、是非、挑戦してみてください。仕事でも役に立つと思います。

まずは、Gradle でライブラリを取ってきます。そして、適当なクラスを用意して、定番の Hello world クラスを自動生成するように書きます。

dependencies {
    compile 'com.squareup:javapoet:1.3.0'
}
import javax.lang.model.element.Modifier;

import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

public class Main {
     public static void main(String[] args) {
          JavaFile src = JavaFile.builder(
                    "a", TypeSpec.classBuilder("Hello")
                              .addModifiers(Modifier.PUBLIC)
                              .addMethod(MethodSpec.methodBuilder("main")
                                                  .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                                                  .returns(void.class)
                                                  .addParameter(String[].class, "args")
                                                  .addStatement("$T.out.println($S)", System.class, "Hello world")
                                                  .build())
                         .build())
               .build();
          System.out.println(src);
     }
}

これを実行すると以下のようになるのですが、このインターフェースは直感的で分かりやすいと思いませんか。 Builder パターンを利用しているので、冗長な Java でも流れるようにコードを書くことが出来ます。また、実処理部分は、 String.format のような $ の部分が後に来る引数で置換される形になります。 T は型として、 S は単純な文字列として扱うなど、基本的にはこの単純なルールでコードを生成していきます。

package a;

import java.lang.String;
import java.lang.System;

public class Hello {
  public static void main(String[] args) {
    System.out.println("Hello world");
  }
}

では、更にこのツールを応用してみます。例えば以下の様な JSON があったとして、それに対応した POJO を作ってみます。

{
  "firstName": "String",
  "lastName": "String",
  "isAlive": "boolean",
  "age": "int",
  "address": "Map",
  "phoneNumbers": "Set",
  "children": "Set",
  "spouse": "String"
}
import java.io.File;
import java.util.AbstractMap;
import java.util.AbstractMap.SimpleEntry;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.lang.model.element.Modifier;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeSpec.Builder;

public class Main2 {
    public static void main(String[] args) {
        try {
            Map<String, String> value = (Map<String, String>) new ObjectMapper().readValue(new File("pojo.json"), Map.class);
            Map<String, Class<?>> mapping = new HashMap<>();
            mapping.put("String", String.class);
            mapping.put("boolean", boolean.class);
            mapping.put("int", int.class);
            mapping.put("Map", Map.class);
            mapping.put("Set", Set.class);

            List<SimpleEntry<String, ?>> members = value.entrySet()
                    .stream()
                    .map(e -> {
                        return new AbstractMap.SimpleEntry<>(e.getKey()
                                , mapping.entrySet()
                                        .stream()
                                        .filter(k -> k.getKey().equals(e.getValue()))
                                        .findFirst().get().getValue());
                    })
                    .collect(Collectors.toList());
            Builder typeBuilder = TypeSpec.classBuilder("MyPojo").addModifiers(Modifier.PUBLIC);
            addMember(typeBuilder, members);
            addGetterAndSetter(typeBuilder, members);

            JavaFile src = JavaFile.builder("a", typeBuilder.build()).build();
            System.out.println(src);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void addMember(Builder typeBuilder, List<SimpleEntry<String, ?>> members) {
        members.stream().forEach(e -> {
            typeBuilder.addField((Class) e.getValue(), e.getKey(), Modifier.PRIVATE);
        });
    }

    private static void addGetterAndSetter(Builder typeBuilder, List<SimpleEntry<String, ?>> members) {
        members.stream().forEach(e -> {
            typeBuilder.addMethod(MethodSpec.methodBuilder("get" + Character.toUpperCase(e.getKey().charAt(0)) + e.getKey().substring(1))
                    .addModifiers(Modifier.PUBLIC)
                    .returns((Class) e.getValue())
                    .addStatement("return this.$L", e.getKey())
                    .build());

            typeBuilder.addMethod(MethodSpec.methodBuilder("set" + Character.toUpperCase(e.getKey().charAt(0)) + e.getKey().substring(1))
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addParameter((Class) e.getValue(), e.getKey())
                    .addStatement("this.$L = $L", e.getKey(), e.getKey())
                    .build());
        });
    }
}

いろいろとおかしいところはありますが、そこは無視して JavaPoet の部分だけに注目してください。実質、数行で簡潔に POJO 生成のコードが記述できることに感動を覚えます。 static import などの特殊なユースケースを除けば、無名クラスなど、 java7 で出来ることは大抵記述できるようになっています。ちなみに、 JavaPoet で組み立てた JavaFile は、 java.annotation.processing.Filer に対して書き込みできるので、javax.annotation.processing.Processor を実装して Annotation ベースでコードの自動生成なんてのも可能です。

是非このツールを使ってみてください。

【関連記事】
Splunkに株価を取り込んでみた ft. Fujikawa
Introduction to Antlr!
Introduction to Antlr! Pt.2