Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

Micronautの@Introspected

DevNexus 2019で、Micronautのセッションを聴いてきたので、メモがてら断片的に記します。

Micronautの特徴の中に、こういったものがあります。

  • Minimal use of reflection
  • Minimal use of proxies

リフレクションや動的プロキシの利用を最低限にして、起動速度の向上やフットプリントの削減を図っています。Micornautでは、リフレクションを使わずにBeanのIntrospectionができます。単純なBookクラスでの、Introspectionを例にします。

import io.micronaut.core.annotation.Introspected;

@Introspected
public class Book {
    private String isbn;
    private String name;

    public Book(String isbn, String name) {
        this.isbn = isbn;
        this.name = name;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

クラスに、io.micronaut.core.annotation.Introspectedアノテーションをつけました。@Introspectedがあるクラスは、io.micronaut.core.beans.BeanIntrospectionから扱えます。例として、newを使わずにインスタンスを作ります。

        final BeanIntrospection<Book> introspection = BeanIntrospection.getIntrospection(Book.class);
        Book b = introspection.instantiate("3", "ccc");

このコードは、リフレクションを使っていないのです。@Introspectedを外すと、実行時例外となります。

// @Introspected
public class Book {
Unexpected error occurred: No bean introspection available for type [class example.micronaut.Book]. Ensure the class is annotated with io.micronaut.core.annotation.Introspected
io.micronaut.core.beans.exceptions.IntrospectionException: No bean introspection available for type [class example.micronaut.Book]. Ensure the class is annotated with io.micronaut.core.annotation.Introspected

Ensure the class is annotated with io.micronaut.core.annotation.Introspectedとあります。

Micornautは、Ahead of Time (AOT) コンパイル/事前コンパイルとして、アノテーションプロセッサで@Introspectedがあるクラスから、BeanIntrospectionで必要となるクラスやコードを自動生成しているのです。

$ tree target/classes/example/micronaut/
target/classes/example/micronaut/
├── $Book$Introspection$$0.class
├── $Book$Introspection$$1.class
├── $Book$Introspection.class
├── $Book$IntrospectionRef$$AnnotationMetadata.class
├── $Book$IntrospectionRef.class
...

$Introspectionとついたクラスが生成されています。$$0、$$1はBookクラスのフィールドです。たとえば、Bookクラスにpriceフィールドを足してみます。

@Introspected
public class Book {
    private String isbn;
    private String name;
    private int price;
// getter and setter

すると、出力が増えます。

$ tree target/classes/example/micronaut/
target/classes/example/micronaut/
├── $Book$Introspection$$0.class
├── $Book$Introspection$$1.class
├── $Book$Introspection$$2.class
├── $Book$Introspection.class
├── $Book$IntrospectionRef$$AnnotationMetadata.class
├── $Book$IntrospectionRef.class
...

これらのクラスは、デコンパイラでは読めません(IntelliJ、JDなど)。

public final class $Book$Introspection extends io.micronaut.core.beans.AbstractBeanIntrospection {
    public $Book$Introspection() { /* compiled code */ }

    public java.lang.Object instantiate() { /* compiled code */ }

    public io.micronaut.core.type.Argument[] getConstructorArguments() { /* compiled code */ }

    public java.lang.Object instantiateInternal(java.lang.Object[] objects) { /* compiled code */ }
}

javapもエラーになります。

$ javap example.micronaut.$Book$Introspection
エラー: クラスが見つかりません: example.micronaut.
$ javap example.micronaut.$Book$Introspection$$0
エラー: クラスが見つかりません: example.micronaut.727860

コードを生成している、アノテーションプロセッサを見てみます。Mavenプロジェクトにしましたので、pom.xmlです。

            <annotationProcessorPaths>
                  <path>
                    <groupId>io.micronaut</groupId>
                    <artifactId>micronaut-inject-java</artifactId>
                    <version>${micronaut.version}</version>
                  </path>
                  <path>
                    <groupId>io.micronaut</groupId>
                    <artifactId>micronaut-validation</artifactId>
                    <version>${micronaut.version}</version>
                  </path>
            </annotationProcessorPaths>

2つありますが、Introspectionはmicronaut-inject-javaの方です。

io.micronaut.annotation.processing.TypeElementVisitorProcessorが、アノテーションプロセッサ本体です。

/**
 * <p>The annotation processed used to execute type element visitors.</p>
 *
 * @author James Kleeh
 * @author graemerocher
 * @since 1.0
 */
@SupportedAnnotationTypes("*")
public class TypeElementVisitorProcessor extends AbstractInjectAnnotationProcessor {
...
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
...
        Collection<TypeElementVisitor> typeElementVisitors = findTypeElementVisitors();
...
        for (LoadedVisitor loadedVisitor : loadedVisitors) {
            try {
                loadedVisitor.getVisitor().start(javaVisitorContext);
            } catch (Throwable e) {
                error("Error initializing type visitor [%s]: %s", loadedVisitor.getVisitor(), e.getMessage());
            }
        }
...
    }

    protected @Nonnull Collection<TypeElementVisitor> findTypeElementVisitors() {
        Map<String, TypeElementVisitor> typeElementVisitors = new HashMap<>(10);
        SoftServiceLoader<TypeElementVisitor> serviceLoader = SoftServiceLoader.load(TypeElementVisitor.class, getClass().getClassLoader());
...

io.micronaut.inject.visitor.TypeElementVisitorを実装するクラスを探して、ループでvisitしてる感じです。

TypeElementVisitorの実装クラスの1つに、io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitorがあり、このクラスが@Introspectedを処理しています。

/**
 * A {@link TypeElementVisitor} that visits classes annotated with {@link Introspected} and produces
 * {@link io.micronaut.core.beans.BeanIntrospectionReference} instances at compilation time.
 *
 * @author graemerocher
 * @since 1.1
 */
@Internal
public class IntrospectedTypeElementVisitor implements TypeElementVisitor<Introspected, Object> {

    @Override
    public void visitClass(ClassElement element, VisitorContext context) {
        final AnnotationValue<Introspected> introspected = element.getAnnotation(Introspected.class);
...

最終的には、io.micronaut.inject.beans.visitor.BeanIntrospectionWriterで、ASMを使ってクラスを出力しています。

こういった処理をコンパイル時に実行するため、Micronautでは起動が早くなる代わりに、ビルド時間が長くなっています。