HK2 の使い方(後編)

この記事は GlassFish Advent Calendar 2014 の 21 日目です。昨日は「HK2 の使い方(前編)」です。本日は HK2 で利用可能な様々な DI について見てゆきます。

1. @Inject によるインジェクション

@Inject によるインジェクション(JSR 330)が可能です。

package jp.coppermine.samples.hk2.inject;

import javax.inject.Inject;

import org.jvnet.hk2.annotations.Service;

@Service
public class Greeter {
  
  @Inject
  private Hello hello;
  
  public String greet() {
    return hello.say();
  }
}

この DI を実現するには、ServiceLocator に対して以下のようなバインドを追加するか、

// Obtain the DynamicConfiguration object for binding a service to a contract.
DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
DynamicConfiguration config = dcs.createDynamicConfiguration();

// binding a service to a contract.
config.bind(BuilderHelper.link(Greeter.class).build());
config.bind(BuilderHelper.link(AnotherGreeter.class).build());
config.bind(BuilderHelper.link(HelloImpl.class).to(Hello.class).build());
config.commit();

あるいは META-INF/hk2-locator/default に以下を含めます。

[jp.coppermine.samples.hk2.inject.Greeter]S

[jp.coppermine.samples.hk2.inject.AnotherGreeter]S

[jp.coppermine.samples.hk2.inject.HelloImpl]S
contract={jp.coppermine.samples.hk2.inject.Hello}

同じ要領で Provider インジェクション(下記)も可能です。

package jp.coppermine.samples.hk2.provider;

import javax.inject.Inject;
import javax.inject.Provider;

import org.jvnet.hk2.annotations.Service;

@Service
public class Greeter {
  
  @Inject
  private Provider<hello> greeting;
  
  public String greet() {
    return greeting.get().say();
 }
}

さらに、コンストラクタ・インジェクションやメソッド・インジェクション(Setter インジェクション)もサポートされています。上記 Greeter クラスをコンストラクタ・インジェクションを用いて書き換えた例を以下に示します。

package jp.coppermine.samples.hk2.inject;

import javax.inject.Inject;

import org.jvnet.hk2.annotations.Service;

@Service
public class Greeter {

  private Hello hello;
  
  @Inject
  public Greeter(Hello hello) {
    this.hello = hello;
  }
  
  public String greet() {
    return hello.say();
  }
}

2. @Named を用いた Service のインジェクション

Contract に 対して複数の Service がある場合、@Named で修飾して(JSR 330)インジェクション対象の Service を選択することができます。また、IterableProvider を用いて対象となるすべての Service を取得することも可能です。

まず、Contract と Service の関連づけをすべて手動で行う場合のコードを示します。@Named を解決するため BuilderHelper#named(String) メソッドを用いていることに注目してください。

package jp.coppermine.samples.hk2.named;

import org.glassfish.hk2.api.DynamicConfiguration;
import org.glassfish.hk2.api.DynamicConfigurationService;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.api.ServiceLocatorFactory;
import org.glassfish.hk2.utilities.BuilderHelper;

public class ManualAppMain {

  public static void main(String[] args) {
    // Obtain a ServiceLocator object
    ServiceLocatorFactory factory = ServiceLocatorFactory.getInstance();
    ServiceLocator locator = factory.create("default");
    
    // Obtain the DynamicConfiguration object for binding a service to a contract.
    DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
    DynamicConfiguration config = dcs.createDynamicConfiguration();
    
    // binding a service to a contract.
    config.bind(BuilderHelper.link(Greeter.class).build());
    config.bind(BuilderHelper.link(HelloImpl.class).to(Hello.class).named("English").build());
    config.bind(BuilderHelper.link(JaHelloImpl.class).to(Hello.class).named("Japanese").build());
    config.bind(BuilderHelper.link(DeHelloImpl.class).to(Hello.class).named("German").build());
    config.bind(BuilderHelper.link(FrHelloImpl.class).to(Hello.class).named("French").build());
    config.bind(BuilderHelper.link(EsHelloImpl.class).to(Hello.class).named("Spanish").build());
    config.bind(BuilderHelper.link(UkHelloImpl.class).to(Hello.class).named("Ukraine").build());
    config.bind(BuilderHelper.link(ZhHelloImpl.class).to(Hello.class).named("Chinese").build());
    config.commit();
    
    // Obtain the value from a Message object and output it
    Greeter greeter = locator.getService(Greeter.class);
    
    System.out.println("All greetings:");
    greeter.getAll().forEach(System.out::println);
    
    System.out.println();
    
    System.out.println("Say: " + greeter.greet());
  }

}

続いて、META-INF/hk2-locator/default を用いる例を示します。

package jp.coppermine.samples.hk2.named;

import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.utilities.ServiceLocatorUtilities;

public class AutomaticAppMain {

  public static void main(String[] args) {
    // Obtain a ServiceLocator object
    ServiceLocator locator = ServiceLocatorUtilities.createAndPopulateServiceLocator();
    
    // Obtain the value from a Message object and output it
    Greeter greeter = locator.getService(Greeter.class);
    
    System.out.println("All greetings:");
    greeter.getAll().forEach(System.out::println);
    
    System.out.println();
    
    System.out.println("Say: " + greeter.greet());
  }

}
[jp.coppermine.samples.hk2.named.Greeter]S

[jp.coppermine.samples.hk2.named.HelloImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=English

[jp.coppermine.samples.hk2.named.JaHelloImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=Japanese

[jp.coppermine.samples.hk2.named.DeHelloImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=German

[jp.coppermine.samples.hk2.named.FrHelloImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=French

[jp.coppermine.samples.hk2.named.EsHelloImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=Spanish

[jp.coppermine.samples.hk2.named.UkHelloImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=Ukraine

[jp.coppermine.samples.hk2.named.ZhHelloImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=Chinese

META-INF/hk2-locator/default ファイル側には、Contract を指定する contract 属性に加え、@Named を特定する named 属性があることに注目してください。

最後に、Greeter のコードを見てみます。

package jp.coppermine.samples.hk2.named;

import java.util.ArrayList;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Named;

import org.glassfish.hk2.api.IterableProvider;
import org.jvnet.hk2.annotations.Service;

@Service
public class Greeter {
  
  @Inject
  private IterableProvider<Hello> greetings;
  
  @Named("Japanese")
  @Inject
  private Hello hello;
  
  public List getAll() {
    List list = new ArrayList<>();
    for(Hello greeting : greetings) {
      list.add(greeting.say());
    }
    return list;
  }
  
  public String greet() {
    return hello.say();
  }
}

IterableProvider<Hello> とラップした形式でインジェクションを行うことで、対象となるすべての Service をイテレータで取得できるようになります。

3. Qualifier を用いた Service のインジェクション

@Named アノテーションの代わりに Qualifier を付加する(JSR 330)ことでも同じようなことが可能です。

まず、Contract と Service の関連づけをすべて手動で行う場合のコードを示します。Qualifier を解決するため BuilderHelper#qualifiedBy(Annotation) メソッドを用いていることに注目してください。また今回は、Annotation のインスタンスを取得するために Qualifiers インタフェースの static メソッドを作成しました。アノテーションのインスタンスは警告を無視してアノテーションの実装クラスを作成して取得する方法もありますが、今回のように既に付加されているアノテーションを Class#getAnnotations() メソッドで取得して絞り込む方法も使えます。

package jp.coppermine.samples.hk2.qualifier;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Stream;

import javax.inject.Qualifier;

import jp.coppermine.samples.hk2.qualifier.qualifiers.Chinese;
import jp.coppermine.samples.hk2.qualifier.qualifiers.English;
import jp.coppermine.samples.hk2.qualifier.qualifiers.French;
import jp.coppermine.samples.hk2.qualifier.qualifiers.German;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Japanese;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Spanish;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Ukraine;

import org.glassfish.hk2.api.DynamicConfiguration;
import org.glassfish.hk2.api.DynamicConfigurationService;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.api.ServiceLocatorFactory;
import org.glassfish.hk2.utilities.BuilderHelper;

public class ManualAppMain {

  public static void main(String[] args) {
    // Obtain a ServiceLocator object
    ServiceLocatorFactory factory = ServiceLocatorFactory.getInstance();
    ServiceLocator locator = factory.create("default");
    
    // Obtain the DynamicConfiguration object for binding a service to a contract.
    DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
    DynamicConfiguration config = dcs.createDynamicConfiguration();
    
    // binding a service to a contract.
    config.bind(BuilderHelper.link(Greeter.class).build());
    config.bind(BuilderHelper.link(HelloImpl.class).to(Hello.class).qualifiedBy(Qualifiers.find(HelloImpl.class, English.class).get()).build());
    config.bind(BuilderHelper.link(JaHelloImpl.class).to(Hello.class).qualifiedBy(Qualifiers.find(JaHelloImpl.class, Japanese.class).get()).build());
    config.bind(BuilderHelper.link(DeHelloImpl.class).to(Hello.class).qualifiedBy(Qualifiers.find(DeHelloImpl.class, German.class).get()).build());
    config.bind(BuilderHelper.link(FrHelloImpl.class).to(Hello.class).qualifiedBy(Qualifiers.find(FrHelloImpl.class, French.class).get()).build());
    config.bind(BuilderHelper.link(EsHelloImpl.class).to(Hello.class).qualifiedBy(Qualifiers.find(EsHelloImpl.class, Spanish.class).get()).build());
    config.bind(BuilderHelper.link(UkHelloImpl.class).to(Hello.class).qualifiedBy(Qualifiers.find(UkHelloImpl.class, Ukraine.class).get()).build());
    config.bind(BuilderHelper.link(ZhHelloImpl.class).to(Hello.class).qualifiedBy(Qualifiers.find(ZhHelloImpl.class, Chinese.class).get()).build());
    config.commit();
    
    // Obtain the value from a Message object and output it
    Greeter greeter = locator.getService(Greeter.class);
    
    System.out.println("All greetings:");
    greeter.getAll().forEach(System.out::println);
    
    System.out.println();
    
    System.out.println("Say: " + greeter.greet());
  }
  
  static interface Qualifiers {
    public static Stream find(Class<?> clazz) {
      return Arrays.asList(clazz.getAnnotations()).stream().filter(e -> e.annotationType().isAnnotationPresent(Qualifier.class));
    }
    public static Optional find(Class<?> clazz, Class<? extends Annotation> annotationClass) {
      return find(clazz).filter(e -> e.annotationType() == annotationClass).findFirst();
    }
  }
}

続いて、META-INF/hk2-locator/default を用いる例を示します。

package jp.coppermine.samples.hk2.qualifier;

import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.utilities.ServiceLocatorUtilities;

public class AutomaticAppMain {

  public static void main(String[] args) {
    // Obtain a ServiceLocator object
    ServiceLocator locator = ServiceLocatorUtilities.createAndPopulateServiceLocator();
    
    // Obtain the value from a Message object and output it
    Greeter greeter = locator.getService(Greeter.class);
    
    System.out.println("All greetings:");
    greeter.getAll().forEach(System.out::println);
    
    System.out.println();
    
    System.out.println("Say: " + greeter.greet());
  }

}
[jp.coppermine.samples.hk2.qualifier.Greeter]S

[jp.coppermine.samples.hk2.qualifier.HelloImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.English}

[jp.coppermine.samples.hk2.qualifier.JaHelloImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Japanese}

[jp.coppermine.samples.hk2.qualifier.DeHelloImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.German}

[jp.coppermine.samples.hk2.qualifier.FrHelloImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.French}

[jp.coppermine.samples.hk2.qualifier.EsHelloImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Spanish}

[jp.coppermine.samples.hk2.qualifier.UkHelloImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Ukraine}

[jp.coppermine.samples.hk2.qualifier.ZhHelloImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Chinese}

@Named を解決したときには named 属性を追加しましたが、Qualifier を解決する場合には qualifier 属性を追加し、Qualifier の完全クラス名を Contract と同様の形式で記述します。

Greeter クラスについては、@Named の場合とほぼ同じ(@Named を Qualifier に置き換えただけ)ですので省略します。

4. まとめ

HK2 は GlassFish の基盤であると同時に、JSR 330 ベースの DI も提供しています。事前に Inhabitants Generator を用いてクラスの依存関係をファイル(META-INF/hk2-locator/service-locator-name)に出力する必要はありますが、その分だけ動的にクラスを操作する量が少なく、他のライブラリとのコンフリクトを起こしにくいとも言えます。Java EE においては、DI は CDI (Weld)の一機能として利用することが大半だと思われますが、純粋な DI (JSR 330)の選択肢として、GlassFish や Jersey には HK2 があることを覚えておくと、いつか役に立つ日が来るかもしれません。

前回と今回のサンプルは https://github.com/btnrouge/hk2-sample にあります。

明日は @kikutaro_ さんです。