Java
Micronaut

Micronautのコンパイル時依存解決の仕組みを逆コンパイルで見る


はじめに

先日、Micronaut 1.0がリリースされました。

Micronautは、起動が高速なフルスタックフレームワークと言われています。

SpringのようなリフレクションベースのIoCコンテナではなく、コンパイル時に依存関係を解決することで、高速に起動できるとのこと。

実際にどれだけ高速になるかは別の記事を参考にするとして、本記事では、コンパイル時にどのように依存解決しているのか、逆コンパイルをして確かめてみようと思います。


準備

ドキュメントに従ってプロジェクトを作成します。

ここではMavenプロジェクトを作成します。

mn create-app --build maven hello-world

maven-compiler-pluginのconfigurationに、micronautのプラグインがアノテーションプロセッサとして登録されていました。


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>

今回はこんな構成でサンプルコードを作りました。

resourcesとかtestとかは省いています。

hello-world

│ pom.xml

└─src
└─main
└─java
└─hello
└─world
Application.java
SampleController.java
SampleService.java

それぞれのファイルの中身はこんな感じです。

/hello にアクセスされたら、 "Hello" を返すだけの単純なプログラムです。


Application.java

package hello.world;

import io.micronaut.runtime.Micronaut;

public class Application {

public static void main(String... args) {
Micronaut.run(Application.class);
}

}



SampleController.java

package hello.world;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;

@Controller("/hello")
public class SampleController {

private final SampleService sampleService;

public SampleController(SampleService sampleService) {
this.sampleService = sampleService;
}

@Get(produces = MediaType.TEXT_PLAIN)
public String hello() {
return sampleService.getHello();
}

}



SampleService.java

package hello.world;

import javax.inject.Singleton;

@Singleton
public class SampleService {

public String getHello() {
return "Hello";
}

}



コンパイルと解析

mvn clean package でビルドします。

すると、 target/classes に次のクラスファイルが生成されます。

$SampleControllerDefinition$$exec1$$AnnotationMetadata.class

$SampleControllerDefinition$$exec1.class
$SampleControllerDefinition.class
$SampleControllerDefinitionClass$$AnnotationMetadata.class
$SampleControllerDefinitionClass.class
$SampleServiceDefinition.class
$SampleServiceDefinitionClass$$AnnotationMetadata.class
$SampleServiceDefinitionClass.class
Application.class
SampleController.class
SampleService.class

何やらたくさんのクラスファイルが生成されています。

下3つの Application.classSampleController.classSampleService.class は自作クラスなので良し。

他のクラスについて、ピックアップで記載します。


依存関係を解決する処理

「~DefinitionClass」で行っています。

SampleControllerのDefinitionClassを見てみましょう。


\$SampleControllerDefinitionClass

package hello.world;

import io.micronaut.context.AbstractBeanDefinition;
import io.micronaut.context.BeanContext;
import io.micronaut.context.BeanResolutionContext;
import io.micronaut.context.DefaultBeanContext;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationUtil;
import io.micronaut.core.type.Argument;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.BeanFactory;
import io.micronaut.inject.annotation.DefaultAnnotationMetadata;
import java.util.Collections;
import java.util.Map;

public class $SampleControllerDefinition extends AbstractBeanDefinition<SampleController> implements BeanFactory<SampleController> {
protected $SampleControllerDefinition(Class<SampleController> type, AnnotationMetadata constructorAnnotationMetadata, boolean requiresReflection, Argument[] arguments) {
super(type, constructorAnnotationMetadata, requiresReflection, arguments);
super.addExecutableMethod(new $SampleControllerDefinition$$exec1());
}

public $SampleControllerDefinition() {
this(SampleController.class,
new DefaultAnnotationMetadata(/* 長いので省略 */),
false,
new Argument[] {
Argument.of(SampleService.class,
"sampleService",
(AnnotationMetadata)null,
(Argument[])null)
});
}

@Override
public SampleController build(BeanResolutionContext resolutionContext, BeanContext context, BeanDefinition<SampleController> definition) {
SampleController instance = new SampleController((SampleService)super.getBeanForConstructorArgument(resolutionContext, context, 0));
instance = (SampleController)this.injectBean(resolutionContext, context, instance);
return instance;
}

@Override
protected Object injectBean(BeanResolutionContext resolutionContext, BeanContext context, Object bean) {
SampleController instance = (SampleController)bean;
return super.injectBean(resolutionContext, (DefaultBeanContext)context, bean);
}

@Override
protected AnnotationMetadata resolveAnnotationMetadata() {
return $SampleControllerDefinitionClass.$ANNOTATION_METADATA;
}
}


SampleController のコンストラクタの引数に渡すオブジェクトの情報は、$SampleControllerDefinition() デフォルトコンストラクタの中で Argument として、Micronautに渡しています。

build() メソッドで、コンストラクタインジェクションが行われています。

当然ながら、リフレクションではなく new SampleController() していますね。

今回のコードでは存在しませんが、 injectBean() メソッドの中で、他のインジェクションを行います。

また、もう1つの $SampleControllerDefinition() コンストラクタの中で、APIの受け口である hello() を表す $SampleControllerDefinition$$exec1 をMicronautに渡しています。

このクラスについては次の節で記載します。


実行ポイントの登録

SampleControllerhello() メソッドに対する情報(アノテーションのパラメータ等)が、 $SampleControllerDefinition$$exec1$$AnnotationMetadata として生成されます。

今回はAPIは1つですが、2つ以上のAPIが存在する場合は、 exec2exec3 ・・・とAPIの数だけクラスが生成されます。

API実行の処理は、 $SampleControllerDefinition$$exec1 にあります。


\$SampleControllerDefinition\$\$exec1

package hello.world;

import io.micronaut.context.AbstractExecutableMethod;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.type.Argument;

public class $SampleControllerDefinition$$exec1 extends AbstractExecutableMethod {
public static final AnnotationMetadata $ANNOTATION_METADATA = new $SampleControllerDefinition$$exec1$$AnnotationMetadata();

public $SampleControllerDefinition$$exec1() {
super(SampleController.class, "hello", Argument.of(String.class, "hello"));
}

@Override
public Object invokeInternal(Object instance, Object[] arguments) {
return ((SampleController)instance).hello();
}

protected AnnotationMetadata resolveAnnotationMetadata() {
return $ANNOTATION_METADATA;
}
}


Micronautが invokeInternal() メソッドを呼ぶことで、APIが実行されます。

ここまで、確かにリフレクションを使わないコードとなっていました。


注意点

今回のサンプルにはありませんが、フィールドへのインジェクションを行う際は、リフレクションが使われます。

生成されるコードではありませんが、「~DefinitionClass」の injectBean() メソッドの中でMicronautの AbstractBeanDefinition#injectBeanField() メソッドが呼ばれるようになり、このメソッドの中でリフレクションを使ってインジェクションしています。

当然と言えば当然ですが、せっかくのMicronautのメリットを最大限に活かしきれないため、フィールドインジェクションではなくコンストラクタインジェクションを使うのが良いでしょう。


まとめ

コードを読み解いて確認しましたが、確かにリフレクションを使わないコードとなっていました。

まだまだ出たばかりでこれからのフレームワークですが、起動が高速になるメリットはクラウドアプリケーション、FaaSで大きなアドバンテージになるでしょう。