タグ「HK2」が付けられているもの

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(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.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(Greeter.class).build());
    config.bind(BuilderHelper.link(HelloImpl.class).to(Hello.class).build());
  
  config.bind(BuilderHelper.link(HelloJaImpl.class).to(Hello.class).named("Japanese").build());
  
  config.bind(BuilderHelper.link(HelloDeImpl.class).to(Hello.class).named("German").build());
  
  config.bind(BuilderHelper.link(HelloFrImpl.class).to(Hello.class).named("France").build());
  
  config.bind(BuilderHelper.link(HelloItImpl.class).to(Hello.class).named("Italian").build());
  
  
config.bind(BuilderHelper.link(HelloEsImpl.class).to(Hello.class).named("Spanish").build());
  
  config.bind(BuilderHelper.link(HelloBrPtImpl.class).to(Hello.class).named("Portuguese").build());
  
  config.bind(BuilderHelper.link(HelloZhChImpl.class).to(Hello.class).named("Chinese").build());
  
  config.bind(BuilderHelper.link(HelloZhTwImpl.class).to(Hello.class).named("TraditionalChinese").build());
  
  config.bind(BuilderHelper.link(HelloKoImpl.class).to(Hello.class).named("Korean").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}

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

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

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

[jp.coppermine.samples.hk2.named.HelloItImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=Italian

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

[jp.coppermine.samples.hk2.named.HelloBrPtImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=Portuguese

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

[jp.coppermine.samples.hk2.named.HelloZhTwImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
name=TraditionalChinese

[jp.coppermine.samples.hk2.named.HelloKoImpl]S
contract={jp.coppermine.samples.hk2.named.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.English}
name=Korean

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> hello greetings;
  
  @Named("Japanese")
  @Inject
  private Hello hello;
  
  public List<string> getAll() {
    List<string> list = new ArrayList<>();
    greetings.forEach(e -> list.add(e.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 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;

import jp.coppermine.samples.hk2.qualifier.qualifiers.Chinese;
import jp.coppermine.samples.hk2.qualifier.qualifiers.France;
import jp.coppermine.samples.hk2.qualifier.qualifiers.German;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Italian;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Japanese;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Korean;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Portuguese;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Qualifiers;
import jp.coppermine.samples.hk2.qualifier.qualifiers.Spanish;
import jp.coppermine.samples.hk2.qualifier.qualifiers.TraditionalChinese;

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).build());
    config.bind(BuilderHelper.link(HelloJaImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Japanese.class)).build());
    config.bind(BuilderHelper.link(HelloDeImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(German.class)).build());
    config.bind(BuilderHelper.link(HelloFrImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(France.class)).build());
    config.bind(BuilderHelper.link(HelloItImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Italian.class)).build());
    config.bind(BuilderHelper.link(HelloEsImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Spanish.class)).build());
    config.bind(BuilderHelper.link(HelloBrPtImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Portuguese.class)).build());
    config.bind(BuilderHelper.link(HelloZhChImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Chinese.class)).build());
    config.bind(BuilderHelper.link(HelloZhTwImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(TraditionalChinese.class)).build());
    config.bind(BuilderHelper.link(HelloKoImpl.class).to(Hello.class).qualifiedBy(Qualifiers.getInstance(Korean.class)).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.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}

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

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

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

[jp.coppermine.samples.hk2.qualifier.HelloItImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Italian}

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

[jp.coppermine.samples.hk2.qualifier.HelloPtImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Portuguese}

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

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

[jp.coppermine.samples.hk2.qualifier.HelloKoImpl]S
contract={jp.coppermine.samples.hk2.qualifier.Hello}
qualifier={jp.coppermine.samples.hk2.qualifier.qualifiers.Korean}

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

その他のクラスについては、@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/khasunuma/hk2-sample にあります。

明日は @kikutaro_ さんです。

HK2 の使い方(前編)

この記事は GlassFish Advent Calendar 2014 の 20 日目です。昨日は「GlassFish 4.1 のロギング」です。

9 日目の記事「GlassFish 4.1でJAX-RS=CDI連携の単体テストを行う方法」にて、HK2 による DI を併用した Jersey Test Framework の使い方についてご紹介しました。今回は本日と明日の 2 回に分けて、HK2 そのものについて見てゆきたいと思います。

1. HK2 の歴史

HK2 はもともと、JSR 277: Java Module System をベースとした簡易なモジュールシステムとして開発がスタートしました。JSR 277 は端的に言うと、JAR の代わりに Java Application Module(JAM)という新しいモジュールと JAM を支援する仕組みを導入しようという試みでした。JAM には JAR が持っていなかったバージョンや依存関係のメタ情報等を保持することになっていました。また、これとは別に JSR 294: Improved Modularity Support in the Java Programming Language というモジュールの動的管理を行う仕組みも提案され、JSR 277 と JSR 294 の組み合わせも想定されていましたが、JSR 291: Dynamic Component Support for Java SE という、当時台頭し始めていた OSGi を Java SE 上で扱えるようにする別規格の提案によって、JSR 277 および JSR 294 は「保留」となりました。もっとも、これらを保留に追い込んだ JSR 291 自身も本当に JSR として規格化すべきだったのか疑問視する声があり(実際に JSR 提出から承認まで終始一貫して当時の Sun に反対され続けた異例の JSR でした)、さらにはリファレンス実装が一般公開されていないなどの問題がありました。

こうして Java 標準のモジュール管理システムの整備が遅れている間に、Groovy を DSL として依存関係を解決する Gradle が人気を集めるようになり、Equinox や Apache Felix など OSGi ランタイム(これらは JSR 291 の実装でもあります)による動的モジュール管理が普及しました。そういった背景の中で、Java SE 7 の仕様として「Project Jigsaw」が提案されました。Project Jigsaw は提案当時ですら未決事項が多く、Java SE 7 と Java SE 8 の 2 度に渡り先送りされ、Java SE 9 への導入を目標として JSR 376: Java Platform Module System という新しい JSR で再提案されました。JSR 376 は過去に保留となっていた JSR 277 および JSR 294 の仕様も取り込んだ形となる見込みで、OpenJDK 9 では従来の rt.jar に相当する部分が既に JSR 376 ベースのモジュールに変更されています。

HK2 は Java のモジュール管理システムに関する仕様が二転三転する中で、GlassFish の OSGi ランタイム上で動作するライブラリとしての位置を確立しました。GlassFish のモジュールは最低限 OSGi バンドルでなければなりませんが、主要モジュールはさらに HK2 を共通基盤として構築されています。

2. HK2 とは?

前章では HK2 の歴史について振り返ってみましたが、ここでは現在の HK2 の仕様に照らし合わせて、HK2 がどのような役割を示すものかをまとめてみます。

2.1. モジュール管理システムとしての役割

HK2 が当初から持っていた役割です。モジュールのロード、依存性管理、バージョン管理などを引き受けます。GlassFish v3 以降においては、モジュールのロードに OSGi を使うことができます(Java SE API の ServiceLoader を利用したプラグインのロードに対応します)。

HK2 草創期によく使われた簡単なサンプルがあります。GlassFish v3 を以下のたった 1 コマンドで実行できる、というものです。

C:\Users\hasunuma>java -jar C:\glassfish4\glassfish\modules\glassfish.jar

このコマンドには、クラスパスの指定がありません。glassfish.jar は OSGi bundle としてビルドされ(glassfish.jar のソースコードを見るとはっきりします)、.jar の中で依存関係を解決しているため、クラスパスを明示する必要がないのです。glassfish.jar の依存モジュールもまた OSGi bundle であればそちらでも依存関係を解決しているため、カスケード式に依存関係が解決され、最終的にクラスパスの指定なしで GlassFish を起動することができるのです。

さらに HK2 では、クラスローダーを跨いだ依存関係の解決もできます。以下をご覧ください。

runtime-network-of-class-loaders.png

これは川口耕介さんがかつて GlassFish v3 の新しいアーキテクチャを解説する際に使用した(と思われる)資料から引用したものです。

モジュール管理システムとしての HK2 については、後日改めてご紹介したいと考えています。

2.2. DI コンテナとしての役割

HK2 は JSR 330 に準拠した DI コンテナとして動作させることができます。JSR 330 の RI である Google Guice と密に連携させることも可能です。

Java SE 環境に限って言えば、HK2 と Guice ではエントリ・ポイントの記述が大きく異なります。これは Weld SE やその他 JSR 330 実装についても言えることです。ただし、一度 DI コンテナが稼働すれば、@Inject アノテーションで DI を実現できるのはどの実装でも変わりません。

DI コンテナとしての HK2 については、この後の章と次回でご紹介します。

3. HK2 による DI について

3.1. Service と Contract

HK2 における DI でまず押さえておきたいのは Service と Contract です。いずれも POJO ですが、クラスレベル・アノテーションが @Service か @Contract かの違いがあります。

Service は実際に処理を行う具象クラスです。多くの場合、Contract となるインタフェースの実装または抽象クラスのサブクラスとして実装しますが、必ずしも Contract を必要としているわけではありません。このあたりは、インタフェースなしで EJB がインジェクトできるのと同じ感覚です。Service には @Service アノテーションを付加します。

Contract はインジェクション・ポイントとなるインタフェースまたは抽象クラスです。これらの実装またはサブクラスがインジェクト可能な Service となります。

@Contract
public interface Foo {
  // ...
}

@Service
public interface FooImpl implements Foo {
  // ...
}

@Service
public class Bar {
  // ...
}

以上に簡単な例を挙げました。ここには Service である FooImpl と Bar があります。FooImpl は Contract である Foo の実装で、インジェクション・ポイント Foo または FooImpl にインジェクトされます。Bar は Contract がない Service で、Bar 自身がインジェクション・ポイントとなります。

HK2 では、Service を Contract にマッピングする作業が必要です。この方法には、コードに記述する方法と、設定ファイルで指定する方法があります。設定ファイルについては HK2 に付属するユーティリティで自動生成します。ただし、今回はクラス数が少ない上に、設定ファイルをブラックボックス化して説明したくないため、設定ファイルを手動で記述する方法をご紹介します。

3.2. 最も簡単な例

まず初めに、最も簡単な例を示します。この例は Contract の Message インタフェースと Service の MessageImpl クラスを用意し、Message ← MessageImpl の DI を実現します。このくらいの規模ではエントリ・ポイントのオーバーヘッドの比率が非常に大きくなりますが、HK2 の DI を利用するために必要な手順を学習する上で必要なことですので、頭の片隅に置いておいてください。

package jp.coppermine.samples.hk2.instance;

import org.jvnet.hk2.annotations.Contract;

@Contract
public interface Message {
  String get();
}
package jp.coppermine.samples.hk2.instance;

import org.jvnet.hk2.annotations.Service;

@Service
public class MessageImpl implements Message {

  @Override
  public String get() {
    return "Hello, world";
  }

}

まず、ServiceLocator を取得する必要があります。ServiceLocator は登録されている Service の中から適切なものをインスタンス化する役目を持ちます。ServiceLocator は ServiceLocatorFactory から取得することができます。

ServiceLocatorFactory factory = ServiceLocatorFactory.getInstance();
ServiceLocator locator = factory.create("default");

ServiceLocator には生成時に JVM で一意となるような任意の名前を付けます。ここでは "default" という名前の ServiceLocator を生成しています。

次に、ServiceLocator に対して Service を登録する必要があります。コードで Service を登録するには DynamicConfiguration を使用します。DynamicConfiguration は DynamicConfigurationService から取得します。さらに DynamicConfigurationService は ServiceLocator から取得します。すべての ServiceLocator には ServiceLocator 自身、DynamicConfigurationService、デフォルトの InjectionResolver 実装(JSR 330 リゾルバ、@Inject の解決に用いる)の 3 つの Service が登録されています。

DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
DynamicConfiguration config = dcs.createDynamicConfiguration();

こうして取得した DynamicConfiguration を用いて Service と Contract の関連づけを行っていきます。

config.bind(BuilderHelper.link(MessageImpl.class).to(Message.class).build());
config.commit();

最後の commit メソッド実行で、すべての関連づけが完了し、有効となります。

以上をまとめます。

package jp.coppermine.samples.hk2.instance;

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) throws Exception {
    // 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(MessageImpl.class).to(Message.class).build());
    config.commit();
    
    // Obtain the value from a Message object and output it
    Message message = locator.getService(Message.class);
    System.out.println(message.get());
  }

}

今回は 1 組だけの関連づけでしたが、@Inject で DI を行うとこのような Service と Contract の対応がいくつも発生し、そのすべてに対して bind メソッドで関連づけを行わなければなりません。HK2 の DI は、Service と Contract の対応付けを静的に記述する必要があり、そこが Weld 等との違いとなります。HK2 には Inhabitants Generator というツールが用意されており、クラスパスを走査して Service と Contract の対応を洗い出し、関連づけのファイルを自動生成します。このファイルは META-INF/hk2-locator/service-locator-mame に配置することで機能します(Inhabitants Generator ではそこまでを自動化することもできます)。今回は Inhabitants Generator の出力形式を知るために、META-INF/hk2-locator/default ファイルをエディタで作成してみましょう(繰り返しになりますが、このファイルは本来、自動生成するものです)。

[jp.coppermine.samples.hk2.instance.MessageImpl]S
contract={jp.coppermine.samples.hk2.instance.Message}

最初の行に Service クラスの完全名を [ ]S で囲んで記述します。行末の 'S' は忘れないようにしてください。2 行目以降にはその Service に結びつく属性が続きます。上記の例では Contract の完全名を記述しています。Contract へのインジェクションを行う場合は contract 属性は必須です。

では、Service と Contract の関連づけを META-INF/hk2-locator/default で行うコードを示します。<

package jp.coppermine.samples.hk2.instance;

import org.glassfish.hk2.api.DynamicConfigurationService;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.api.ServiceLocatorFactory;

public class PopulateAppMain {

  public static void main(String[] args) throws Exception {
    // 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);
    dcs.getPopulator().populate();
    
    // Obtain the value from a Message object and output it
    Message message = locator.getService(Message.class);
    System.out.println(message.get());
  }

}

先ほどのコードとの相違は、DynamicConfiguration の bind メソッドと commit メソッドを呼び出していたところが、DynamicConfigurationService の getPopulator メソッドと、それで取得した Populator クラスの populate メソッドの呼び出しに置き換わったことです。先ほどのコードでは Service が増えると bind メソッドの呼び出しが大量に発生しますが、こちらのコードでは bind に相当する情報がすべてファイルにありますので、コードは増加しません。

以上の処理は HK2 のエントリ・ポイントとして頻繁に発生しうるので、便利な書き方が用意されています。

package jp.coppermine.samples.hk2.instance;

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
    Message message = locator.getService(Message.class);
    System.out.println(message.get());
  }

}

この方法では、ServiceLocator の取得から Service と Contract の関連づけまですべてを 1 行で行えます。また、この方法では ServiceLocator 名を指定していませんが、その場合は "default" という名前が使用されます。前のコードの ServiceLocator 名を "default" としていたのは、筆者がこの命名規約を知っていたからです。

4. まとめ

今回は HK2 の歴史と概要、そしてプログラミングを行う上で知っておきたいエントリ・ポイントの書き方についてお話しました。明日は HK2 が提供する様々な DI についてご紹介します。

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