MicronautのBean定義のところらへん

今日は娘達とマリオをしたりしながらスキマ時間で、ぼーっとMicronautのドキュメントの3.1-3.5までを読んで手を動かしてみた。

https://docs.micronaut.io/latest/guide/index.html#ioc

ソースはここ。

https://github.com/bufferings/hello-mn/tree/20190301-ioc/src/main/java/hello/mn/ioc

Javaは11を使ってて、Micronautは1.0.4を使ってる。

❯ sdk current java

Using java version 11.0.1-open

❯ sdk current micronaut

Using micronaut version 1.0.4

## 通常のDI

こういう感じでBeanを定義しておいて:

public interface Snack {
  String name();
}

@Singleton
public class Chocolate implements Snack {
  @Override
  public String name() {
    return "Chocolate";
  }
}

こんな感じでDIして使うことができる。他のインジェクション方法も使えるみたいだけど、コンストラクターインジェクションが好きなので、それ以外は僕は使わないと思う:

@Controller("/hello/ioc")
public class HelloIocController {

  private final Snack snack;

  public HelloIocController(Snack snack) {
    this.snack = snack;
  }

  @Get(produces = MediaType.TEXT_PLAIN)
  public String index() {
    return snack.name();
  }
}

実行するとこうなる:

❯ curl localhost:8080/hello/ioc
Chocolate

## Qualifying By Name

Bean定義が複数ある場合は、インジェクトするBeanを名前で指定できる。Engineを実装したV6EngineV8Engineがこんな風に定義されている場合:

public interface Engine {

  int getCylinders();

  String start();
}

@Singleton
public class V6Engine implements Engine {
  int cylinders = 6;

  @Override
  public int getCylinders() {
    return cylinders;
  }

  @Override
  public String start() {
    return "Starting V6";
  }
}

@Singleton
public class V8Engine implements Engine {
  int cylinders = 8;

  @Override
  public int getCylinders() {
    return cylinders;
  }

  @Override
  public String start() {
    return "Starting V8";
  }
}

@Namedアノテーションを使って、こんな風にインジェクトできる:

@Controller("/hello/nq")
public class HelloNamedQualifierController {

  private final Engine v6Engine;

  private final Engine v8Engine;

  public HelloNamedQualifierController(
      @Named("v6") Engine v6Engine,
      @Named("v8") Engine v8Engine) {
    this.v6Engine = v6Engine;
    this.v8Engine = v8Engine;
  }

  @Get(produces = MediaType.TEXT_PLAIN)
  public String index() {
    return v6Engine.start() + ", " + v8Engine.start();
  }
}

実行するとこうなる:

❯ curl localhost:8080/hello/nq
Starting V6, Starting V8

この辺で「名前のルール」が気になって昼間にメモを残しておいた:

bufferings.hatenablog.com

## Qualifying By Annotation

@Named使う代わりに、自分でアノテーション作ることもできるよ、って書いてあって試した。

@Qualifier
@Retention(RUNTIME)
public @interface V6 {
}

@Qualifier
@Retention(RUNTIME)
public @interface V8 {
}

こんな感じで作ると、"V6"とか"V8"のBeanが取得できるみたい。試しにもう一個作ってみた。

@Qualifier
@Retention(RUNTIME)
public @interface V8Engine {
}

"V8Engine"にしたけどこれも動いた。あんまり使うつもりがないから「アノテーション名で判別してるんだろうなー」って雰囲気だけ理解しておいた。こんな風に使える:

@Controller("/hello/aq")
public class HelloAnnotationQualifierController {

  private final Engine v6Engine;

  private final Engine v8Engine;

  private final Engine v8Engine2;

  public HelloAnnotationQualifierController(
      @V6 Engine v6Engine,
      @V8 Engine v8Engine,
      @V8Engine Engine v8Engine2) {
    this.v6Engine = v6Engine;
    this.v8Engine = v8Engine;
    this.v8Engine2 = v8Engine2;
  }

  @Get(produces = MediaType.TEXT_PLAIN)
  public String index() {
    return v6Engine.start() + ", "
        + v8Engine.start() + ", "
        + v8Engine2.start();
  }
}

実行するとこうなる:

❯ curl localhost:8080/hello/aq
Starting V6, Starting V8, Starting V8

まぁ、複数の実装が共通のプレフィックスを持ってる場合とかに便利なのかな?バージョン番号とかでV1V2の2つの実装が沢山ある場合に、このアノテーションを作っておけば便利そうではある。あー、でも、Microserviceって文脈なら、もうそれは別のサービスに分けてルーティングをバージョン分けした方が良さそうか。ま、こういう機能があるんだなってくらいでいっか。

## Primaryアノテーション

複数の実装がある場合に、Primaryアノテーションを使うとそのBeanが優先される。逆の意味のSecondaryもあるみたい。

public interface ColorPicker {
  String color();
}

@Primary
@Singleton
public class Green implements ColorPicker {

  @Override
  public String color() {
    return "green";
  }
}

@Singleton
public class Blue implements ColorPicker {

  @Override
  public String color() {
    return "blue";
  }
}

普通に使うと、PrimaryのGreenがDIされる。

@Controller("/hello/pt")
public class HelloPrimaryTypeController {

  private final ColorPicker colorPicker;

  public HelloPrimaryTypeController(ColorPicker colorPicker) {
    this.colorPicker = colorPicker;
  }

  @Get(produces = MediaType.TEXT_PLAIN)
  public String index() {
    return colorPicker.color();
  }
}

実行するとこうなる:

❯ curl localhost:8080/hello/pt
green

んー。テストのときに上書きしたくてやるぐらい?しか用途を思いつかん。あー、でもあれか、Blueの方は名前を指定すれば利用できるから、通常はGreenを使うけどたまにBlueを使いたいときとかに便利なのか。ふむふむ。

## Container Type

複数のBeanが定義されているときに、IterableとかArrayとかが使えるみたい。あと、Optionalを使うとBeanがない場合にはemptyになるみたい。他にもいくつか対応してるみたい。まぁ、これもあんまり使わんと思うけど一応試しておいた。

@Controller("/hello/ct")
public class HelloContainerTypeController {

  private final List<Engine> engines;

  private final List<ColorPicker> colorPickers;

  public HelloContainerTypeController(
      List<Engine> engines,
      List<ColorPicker> colorPickers) {
    this.engines = engines;
    this.colorPickers = colorPickers;
  }

  @Get(produces = MediaType.TEXT_PLAIN)
  public String index() {
    return engines.stream().map(Engine::start).collect(Collectors.joining(", ")) + ", "
        + colorPickers.stream().map(ColorPicker::color).collect(Collectors.joining(", "));
  }
}

実行するとこうなる:

❯ curl localhost:8080/hello/ct
Starting V8, Starting V6, blue, green

## BeanContext

IoCのコンテナはBeanContextなのかな。実際はそれを継承したApplicationContextがあって、それを実装したDefaultApplicationContextが使われてる。

こんな風にコンストラクターでInjectしてみたら使えた。

@Controller("/hello/bc")
public class HelloBeanContextController {

  private final BeanContext beanContext;

  private final ApplicationContext applicationContext;

  public HelloBeanContextController(
      BeanContext beanContext,
      ApplicationContext applicationContext) {
    this.beanContext = beanContext;
    this.applicationContext = applicationContext;
  }

  @Get(produces = MediaType.TEXT_PLAIN)
  public String index() {
    return beanContext.getBean(Engine.class, Qualifiers.byName("v6")).start() + ", "
        + beanContext.getBean(Engine.class, Qualifiers.byName("v8")).start() + ", "
        + beanContext.getBean(ColorPicker.class).color() + ", "
        + applicationContext.getBean(Engine.class, Qualifiers.byName("v6")).start() + ", "
        + applicationContext.getBean(Engine.class, Qualifiers.byName("v8")).start() + ", "
        + applicationContext.getBean(ColorPicker.class).color();
  }
}

実行するとこうなる:

❯ curl localhost:8080/hello/bc
Starting V6, Starting V8, green, Starting V6, Starting V8, green

どうなんかなー。使うなら、ApplicationContextをDIしといたらいいんかな。

## ということで

3.5までを読みながら触ってみたお話でした。

普段は特に複数Beanを定義することもないだろうから、シンプルにBeanを定義してそれをDIして使う、ってぐらいだと思ってる。しばらく時間が取れなさそうだから、また気が向いたときに続き読もうと思う。