tokuhirom's blog

[java] モダンな APT の開発手順まとめ

APT とは Annotation Processing Tool のことで、Java でコードの自動生成を行う際に利用される。 APT を利用すると、Java クラスやリソースの自動生成が可能となる。

インターネットに情報は結構あるのだが、昔のものが多くて、Eclipse に JAR を追加して云々とかそういう感じのものが多くて辛いので調べたことをまとめておく。

アノテーションを作る

適当なアノテーションを作る。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target({
        ElementType.TYPE
})
public @interface Hello {
}

アノテーションプロセッサを実装する

AbstractProcessor を implement する。@SupportedAnnotationTypes で、処理対象のアノテーションを指定。@SupportedSourceVersion で、ソースのバージョンを指定。

ソースの生成には、テキスト処理用の template engine を利用してもよいが、いろいろやるなら javapoet などの Java コード生成用 DSL を利用したほうが、インデントや空行なども綺麗に揃うし、良い。

process() の引数にある RoundEnvironment を扱うための便利メソッドが google auto-common に入っているから、参考にするとよい。

package me.geso.sample.hello;

import java.io.IOException;
import java.io.Writer;
import java.util.Set;

import javax.annotation.Generated;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject;

import com.google.auto.common.MoreElements;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("me.geso.sample.hello.*")
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
        if (annotations.isEmpty()) {
            System.out.println("no annotations");
            return true;
        }
        log("HUAAAAAAAAAAAAAAAAAAAAA");

        final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Hello.class);
        log(elements.toString());
        for (final Element element : elements) {
            final PackageElement aPackage = MoreElements.getPackage(element);
            final TypeSpec blah = TypeSpec.classBuilder("Blah")
                .addAnnotation(AnnotationSpec.builder(Generated.class)
                    .addMember("value", "{$S}", getClass().getCanonicalName())
                    .build())
                .addModifiers(Modifier.PUBLIC)
                .addMethod(MethodSpec.methodBuilder("hello")
                    .addModifiers(Modifier.PUBLIC)
                    .addCode(
                        CodeBlock.builder()
                            .add("return \"hello\";\n")
                            .build()
                    )
                    .returns(TypeName.get(String.class))
                    .build()
                ).build();
            JavaFile javaFile = JavaFile.builder(aPackage.getQualifiedName().toString(), blah)
                .build();

            try {
                JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(aPackage.toString() + ".Blah");
                Writer writer = sourceFile.openWriter();
                javaFile.writeTo(writer);
                writer.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        return true;
    }

    private void log(String msg) {
        if (processingEnv.getOptions().containsKey("debug")) {
            processingEnv.getMessager().printMessage(Kind.NOTE, msg);
        }
    }

}

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

src/main/resources/META-INF/services/javax.annotation.processing.Processor というファイルに、アノテーションプロセッサのクラス名を記述する。

me.geso.sample.hello.MyProcessor

maven pom.xml の設定

Annotation Processor 自体のコンパイル時に APT 有効にしていると、Annotation Processor 自体のコンパイルに Annotation Processor 使おうとしてわけわかめになるということがあるので、無効にしたほうがいいかもしれない(IntelliJ だけかも)。

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <!-- disable annotation processing -->
                    <compilerArgument>-proc:none</compilerArgument>
                </configuration>
            </plugin>
        </plugins>
    </build>

テストコード書く

検索すると AptinaUnit 使えって書いてあるものが多いのだが、今どきは google compile-testing を使うとよさそう。 APT に限らず、コードコンパイルして云々するもの全般で使える。 開発もアクティブだし。

https://github.com/google/compile-testing

書き方は、javadoc か test code か google auto のコードを参考にする。

コンパイラオプションの指定をしたいところだが、現行バージョンでは指定できない。 実装はされててリリース待なので今後に期待。 https://github.com/google/compile-testing/pull/64

package me.geso.sample.hello;

import static com.google.common.truth.Truth.assert_;
import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource;

import org.junit.Test;

import com.google.common.io.Resources;
import com.google.testing.compile.JavaFileObjects;

public class ProcessorTest {

    @Test
    public void testProcess() throws Exception {
        // Compiler option coming soon.
        // https://github.com/google/compile-testing/pull/64

        assert_().about(javaSource())
            .that(JavaFileObjects.forResource(Resources.getResource("HelloWorld.java")))
            .processedWith(new MyProcessor())
            .compilesWithoutError()
            .and()
            .generatesSources(JavaFileObjects.forSourceString("foo.bar.baz.Blah", "package foo.bar.baz;\n"
                    + "\n"
                    + "import java.lang.String;\n"
                    + "import javax.annotation.Generated;\n"
                    + "\n"
                    + "@Generated({\"me.geso.sample.hello.MyProcessor\"})\n"
                    + "public class Blah {\n"
                    + "  public String hello() {\n"
                    + "    return \"hello\";\n"
                    + "  }\n"
                    + "}"));
    }
}

参考になるコード

google auto

https://github.com/google/auto/

google auto は APT 関連のサンプルとしてよい。

auto commmon は、APT 用のユーティリティクラスとして便利。

APT の実際の使い道

コンパイル時に計算するとか、ボイラープレートコードの生成とかには役立つが、実際、既存のコードを変更できるわけではないので、できることは意外と少ない。

例えば、Dagger2 のようなものにはよい。DI 対象がコンパイル時に確定するので、実行時の速度を抑えることができる。Android 等の始動速度が必要なことが有用なテクニックとなる。

生成されたコードは target/generated-sources/ を見れば確認できるので、実際の動作が想像しやすい。デバッグもしやすくなる。JVM bytecode の知識がなくても修正可能だし、デバッグ等もやりやすい。

lombok のような AST を処理するものよりはよほどやりやすい。

参考資料

http://www.slideshare.net/vvakame/apt-7568357

Created: 2015-03-26 06:59:22
Updated: 2015-03-26 06:59:22

[perl] Error in tempfile() using template /tmp/XXXXXXXXXX: Have exceeded the maximum number of attempts (1000) to open temp file/dir at hoge.pl line 14. のようなエラーが発生した場合の対処方法

File::Temp を使っていると、表題のようなエラーが発生することがある。 これは一件すると、ファイルを開く処理が失敗した用に見えて、ファイル開き過ぎとかかなあと思うんだけど、実際はそういうことではないようだ。

File::Temp の実装はこのへんになっていて、要するに EEXIST が発生した場合に 1000 回ファイル名を変えてリトライして、その結果としてダメだったというときにいい感じにあがる例外である。 https://github.com/Perl-Toolchain-Gang/File-Temp/blob/master/lib/File/Temp.pm#L602

ここを見れば分かる通り、EEXIST の時だけしか次のループにいかないので、OS 的な問題ではない。 https://github.com/Perl-Toolchain-Gang/File-Temp/blob/master/lib/File/Temp.pm#L529

ファイル名のテンプレートは /tmp/XXXXXXXXXX となっており、デフォルトでは X が 10 文字である。10 文字に対して、X は [0-9a-zA-Z_] のうち一文字が使用されるので、63パターンもある。

my @CHARS = (qw/ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
                a b c d e f g h i j k l m n o p q r s t u v w x y z
                0 1 2 3 4 5 6 7 8 9 _
            /);

ということは、984930291881790849 通りのパターンがあるので、通常ならばコンフリクトすることはない。

しかし、このファイル名の生成は Perl5 の rand() 関数を用いている。Perl5 の rand() 関数は fork() した場合に seed の再生成が行われることはないので、 conflict する可能性が考えられる。

以下のような再現コードにより、まさにその現象が起きていることが確認できる。

use strict;
use 5.018000;

use File::Temp qw/tempfile/;

rand(); # set seed

my $pid = fork() // die $!;
if ($pid) {
    # parent
    sleep 1;
    eval {
        tempfile();
    };
    say $@;
    kill $pid;
    waitpid($pid, -1);
} else {
    my @tempfiles;
    for (1..2000) {
        push @tempfiles, tempfile();
    }
    warn "created";
    sleep 2;
    exit 1;
}

以上まで調査した結果、この事象はマニュアルに書いてあることが判明した。

If you are forking many processes in parallel that are all creating
temporary files, you may need to reset the random number seed using
srand(EXPR) in each child else all the children will attempt to walk
through the same set of random file names and may well cause themselves to
give up if they exceed the number of retry attempts.

以上です。よろしくお願いします。

Created: 2015-03-23 11:15:01
Updated: 2015-03-23 11:15:01

がんばれ父ちゃん

韓国では2番目に有名な日本酒らしい。 http://www.excite.co.jp/News/bit/E1283751448805.html

日本では見たこと無いが、新潟で作られているらしい。

Created: 2015-03-21 13:01:08
Updated: 2015-03-21 13:01:08

Xslate の mark_raw は template に書かない

Xslate の mark_raw は template 側では使わずに、Controller ないし Model で使って欲しい。

ある文字列が、HTML として処理すべきものかどうかは、サーバー側で判断すべき事案であって、マークアップエンジニアが判断すべき事項ではないから、というのが大きい。

「タグがうまく入力できないんですけど」というディレクターからの起票によって、それをマークアップエンジニアがなんとなく TT の | html はずして XSS 大発生! というような事態は、昭和の時代には多発していたという。

そのような惨事を我々は繰り返すべきではない。

歴史に学ばなくてはならない。

Created: 2015-03-20 18:21:15
Updated: 2015-03-20 18:21:15

ant と気合でファイルを読み込んで表示したい。

ant でファイルを読み込みたい。という思いがある。

loadFile → echo でいけます。

<?xml version="1.0" encoding="UTF-8"?>
<project name="change_your_project_name" default="cat" xmlns:artifact="antlib:org.apache.maven.artifact.ant">
    <target name="cat">
        <loadfile
                    property="git.properties"
                    srcFile="build.xml"/>
        <echo message="${git.properties}"/>
    </target>
</project>
Created: 2015-03-17 19:31:35
Updated: 2015-03-17 19:31:35

routes が /{id:[0-9]{5}}/{title:[a-zA-Z_]+} のような形式に対応していた

Java 用の simple な dispatcher ライブラリである routes の 0.5.0 が出ました。

今回のバージョンでは @moznion からのパッチにより、より柔軟なディスパッチングルールの定義が可能になります。

avans で利用する場合は以下のような形式で利用できるようになります。

@GET("/{id:[0-9]{5}}/{title:[a-zA-Z_]+}")
public void foo(@PathParam("id") long id, @PathParam("title") String title) {
    ...
}

変更点はこの辺りです。 https://github.com/tokuhirom/routes/commit/869dd5796562a09a567d7b1f57a9c6c3c73b6ac7

以上です。よろしくお願いします。

Created: 2015-03-17 15:41:51
Updated: 2015-03-17 15:41:51

avans 1.10.0 が出てます

avans の 1.10.0 が出ています。 avans-tinyvalidator が @BeanParam を validation していなかったので、するように変更しました。

Created: 2015-03-16 10:18:44
Updated: 2015-03-16 10:18:44

Google Guice で、Module の設定を上書きしたい

テストコード等で Module の設定を上書きしたいトキがあると思う。 そういう時には Modules.override() を使えばいい。

Guice.createInjector(Modules.override(new ProductionModule()).with(new TestModule()));

とかそういう感じ。

ref. http://stackoverflow.com/questions/483087/overriding-binding-in-guice

Created: 2015-03-13 10:28:19
Updated: 2015-03-13 10:28:19

google guice の依存関係を visualize したいよ、って人

https://github.com/google/guice/wiki/Grapher

以下のように実装すればよい。graphviz 用の .dot ファイルが生成される。

PrintWriter out = new PrintWriter(new File(filename), "UTF-8");

Injector injector = Guice.createInjector(new GraphvizModule());
GraphvizGrapher grapher = injector.getInstance(GraphvizGrapher.class);
grapher.setOut(out);
grapher.setRankdir("TB");
grapher.graph(demoInjector);
out.close();

以下のように、png を生成する。

dot -T png my_injector.dot > my_injector.png
Created: 2015-03-11 08:10:28
Updated: 2015-03-11 08:10:28

servlet 3.0 の multipart 関連処理を有効にする方法

Java の Servlet API では 3.0 以後、multipart 関連の処理が可能だが、デフォルトでは利用不可となっている。

これを有効にするには web.xml に以下のように記述すればよろしい。

最新の avans ではデフォルトで有効になっている。

<servlet>
    <servlet-name>main</servlet-name>
    <servlet-class>me.geso.avans.AvansServlet</servlet-class>
    <init-param>
        <param-name>package</param-name>
        <param-value>me.geso.sample.controller</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <multipart-config>
        <max-file-size>5242880</max-file-size>
        <max-request-size>27262976</max-request-size>
        <file-size-threshold>32768</file-size-threshold>
    </multipart-config>
</servlet>
Created: 2015-03-03 15:56:06
Updated: 2015-03-03 15:56:06