1. 概要

CDI(Contexts and Dependency Injection)は、JavaEE6以降に含まれている標準の依存性注入フレームワークです。

これにより、ドメイン固有のライフサイクルコンテキストを介してステートフルコンポーネントのライフサイクルを管理し、タイプセーフな方法でコンポーネント(サービス)をクライアントオブジェクトに挿入できます。

このチュートリアルでは、CDIの最も関連性の高い機能を詳しく調べ、クライアントクラスに依存性を注入するためのさまざまなアプローチを実装します。

2. DYDI(日曜大工の依存性注入)

一言で言えば、フレームワークにまったく頼らずにDIを実装することは可能です。

このアプローチは、一般にDYDI(Do-it-Yourself Dependency Injection)として知られています。

DYDIを使用すると、必要な依存関係を単純な古いファクトリ/ビルダーを介してクライアントクラスに渡すことにより、アプリケーションコードをオブジェクトの作成から分離します。

基本的なDYDI実装は次のようになります。

public interface TextService {
    String doSomethingWithText(String text);
    String doSomethingElseWithText(String text);    
}
public class SpecializedTextService implements TextService { ... }
public class TextClass {
    private TextService textService;
    
    // constructor
}
public class TextClassFactory {
      
    public TextClass getTextClass() {
        return new TextClass(new SpecializedTextService(); 
    }    
}

もちろん、DYDIはいくつかの比較的単純なユースケースに適しています。

サンプルアプリケーションのサイズと複雑さが増し、相互接続されたオブジェクトのより大きなネットワークを実装すると、大量のオブジェクトグラフファクトリで汚染されることになります。

これには、オブジェクトグラフを作成するためだけに多くの定型コードが必要になります。 これは完全にスケーラブルなソリューションではありません。

DIをもっとうまくやることはできますか? もちろん、我々はできます。 これがまさにCDIが登場する場所です。

3. 簡単な例

CDIは、DIを簡単なプロセスに変えます。要約すると、サービスクラスをいくつかの簡単な注釈で装飾し、クライアントクラスで対応するインジェクションポイントを定義するだけです。

CDIが最も基本的なレベルでDIを実装する方法を紹介するために、単純な画像ファイル編集アプリケーションを開発するとします。 画像ファイルを開いたり、編集したり、書き込んだり、保存したりすることができます。

3.1. 「beans.xml」ファイル

まず、「beans.xml」ファイルを「src / main / resources /META-INF/」フォルダーに配置する必要があります。 このファイルに特定のDIディレクティブがまったく含まれていない場合でも、CDIを起動して実行するために必要です

<beans xmlns="http://java.sun.com/xml/ns/javaee" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
  http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

3.2. サービスクラス

次に、GIF、JPG、およびPNGファイルに対して上記のファイル操作を実行するサービスクラスを作成しましょう。

public interface ImageFileEditor {
    String openFile(String fileName);
    String editFile(String fileName);
    String writeFile(String fileName);
    String saveFile(String fileName);
}
public class GifFileEditor implements ImageFileEditor {
    
    @Override
    public String openFile(String fileName) {
        return "Opening GIF file " + fileName;
    }
    
    @Override
    public String editFile(String fileName) {
      return "Editing GIF file " + fileName;
    }
    
    @Override
    public String writeFile(String fileName) {
        return "Writing GIF file " + fileName;
    }

    @Override
    public String saveFile(String fileName) {
        return "Saving GIF file " + fileName;
    }
}
public class JpgFileEditor implements ImageFileEditor {
    // JPG-specific implementations for openFile() / editFile() / writeFile() / saveFile()
    ...
}
public class PngFileEditor implements ImageFileEditor {
    // PNG-specific implementations for openFile() / editFile() / writeFile() / saveFile()
    ...
}

3.3. クライアントクラス

最後に、コンストラクターで ImageFileEditor 実装をとるクライアントクラスを実装し、@Injectアノテーションを使用してインジェクションポイントを定義しましょう。

public class ImageFileProcessor {
    
    private ImageFileEditor imageFileEditor;
    
    @Inject
    public ImageFileProcessor(ImageFileEditor imageFileEditor) {
        this.imageFileEditor = imageFileEditor;
    }
}

簡単に言えば、 @Injectアノテーションは、CDIの実際の主力製品です。 これにより、クライアントクラスでインジェクションポイントを定義できます。

この場合、 @Inject は、コンストラクターにImageFileEditor実装を挿入するようにCDIに指示します。

さらに、フィールド(フィールドインジェクション)とセッター(セッターインジェクション)で@Injectアノテーションを使用してサービスをインジェクトすることもできます。 これらのオプションについては後で説明します。

3.4. 溶接を使用したImageFileProcessorオブジェクトグラフの構築

もちろん、CDIが正しいImageFileEditor実装をImageFileProcessorクラスコンストラクターに挿入することを確認する必要があります。

そのためには、まず、クラスのインスタンスを取得する必要があります。

CDIの使用はJavaEEアプリケーションサーバーに依存しないため、JavaSEのCDIリファレンス実装であるWeldを使用してこれを行います。

public static void main(String[] args) {
    Weld weld = new Weld();
    WeldContainer container = weld.initialize();
    ImageFileProcessor imageFileProcessor = container.select(ImageFileProcessor.class).get();
 
    System.out.println(imageFileProcessor.openFile("file1.png"));
 
    container.shutdown();
}

ここでは、 WeldContainer オブジェクトを作成し、次に ImageFileProcessor オブジェクトを取得し、最後にその openFile()メソッドを呼び出しています。

予想どおり、アプリケーションを実行すると、CDIは DeploymentException:をスローして大声で文句を言います。

Unsatisfied dependencies for type ImageFileEditor with qualifiers @Default at injection point...

CDIは、 ImageFileProcessorコンストラクターに挿入するImageFileEditor実装を認識していないため、この例外が発生します。

CDIの用語では、これはあいまいなインジェクション例外として知られています。

3.5. @Defaultおよび@Alternativeアノテーション

このあいまいさを解決するのは簡単です。 CDIは、デフォルトで、インターフェースのすべての実装に@Defaultアノテーションを付けます。

したがって、どの実装をクライアントクラスに注入する必要があるかを明示的に指示する必要があります。

@Alternative
public class GifFileEditor implements ImageFileEditor { ... }
@Alternative
public class JpgFileEditor implements ImageFileEditor { ... }
public class PngFileEditor implements ImageFileEditor { ... }

この場合、GifFileEditorJpgFileEditor@Alternativeアノテーションを付けたので、CDIは PngFileEditor @Default アノテーションが付いたデフォルト)は、注入する実装です。

アプリケーションを再実行すると、今回は期待どおりに実行されます。

Opening PNG file file1.png

さらに、PngFileEditor@Defaultアノテーションを付け、他の実装を代替として保持すると、上記と同じ結果が得られます。

これは、一言で言えば、サービスクラスの@Alternativeアノテーションを切り替えるだけで、実装の実行時インジェクションを非常に簡単に交換できることを示しています。

4. フィールドインジェクション

CDIは、フィールドインジェクションとセッターインジェクションの両方をすぐにサポートします。

フィールドインジェクションを実行する方法は次のとおりです( @Defaultアノテーションと@Alternativeアノテーションを使用してサービスを修飾するためのルールは同じままです):

@Inject
private final ImageFileEditor imageFileEditor;

5. セッターインジェクション

同様に、セッターインジェクションを行う方法は次のとおりです。

@Inject 
public void setImageFileEditor(ImageFileEditor imageFileEditor) { ... }

6. @Namedアノテーション

これまで、クライアントクラスでインジェクションポイントを定義し、 @Inject @Default 、および@Alternativeアノテーションを使用してサービスをインジェクトする方法を学習しました。ほとんどのユースケースをカバーしています。

それでも、CDIを使用すると、@Namedアノテーションを使用してサービスインジェクションを実行することもできます。

このメソッドは、意味のある名前を実装にバインドすることにより、サービスを注入するためのより意味のある方法を提供します。

@Named("GifFileEditor")
public class GifFileEditor implements ImageFileEditor { ... }

@Named("JpgFileEditor")
public class JpgFileEditor implements ImageFileEditor { ... }

@Named("PngFileEditor")
public class PngFileEditor implements ImageFileEditor { ... }

ここで、 ImageFileProcessor クラスのインジェクションポイントをリファクタリングして、名前付きの実装と一致させる必要があります。

@Inject 
public ImageFileProcessor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

名前付き実装を使用してフィールドおよびセッターインジェクションを実行することもできます。これは、@Defaultおよび@Alternativeアノテーションを使用する場合と非常によく似ています。

@Inject 
private final @Named("PngFileEditor") ImageFileEditor imageFileEditor;

@Inject 
public void setImageFileEditor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

7. @Producesアノテーション

サービスは、追加の依存関係を処理するために注入される前に、いくつかの構成を完全に初期化する必要がある場合があります。

CDIは、 @Produces アノテーションを通じて、これらの状況をサポートします。

@Produces を使用すると、完全に初期化されたサービスの作成を担当するファクトリクラスを実装できます。

@Produces アノテーションがどのように機能するかを理解するために、 ImageFileProcessor クラスをリファクタリングして、コンストラクターで追加のTimeLoggerサービスを利用できるようにします。

このサービスは、特定の画像ファイル操作が実行された時刻をログに記録するために使用されます。

@Inject
public ImageFileProcessor(ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... } 
    
public String openFile(String fileName) {
    return imageFileEditor.openFile(fileName) + " at: " + timeLogger.getTime();
}
    
// additional image file methods

この場合、 TimeLogger クラスは、SimpleDateFormatCalendarの2つの追加サービスを利用します。

public class TimeLogger {
    
    private SimpleDateFormat dateFormat;
    private Calendar calendar;
    
    // constructors
    
    public String getTime() {
        return dateFormat.format(calendar.getTime());
    }
}

完全に初期化されたTimeLoggerオブジェクトを取得するために、CDIにどこを見ればよいかをどのように伝えますか?

TimeLogger ファクトリクラスを作成し、そのファクトリメソッドに@Producesアノテーションを付けるだけです。

public class TimeLoggerFactory {
    
    @Produces
    public TimeLogger getTimeLogger() {
        return new TimeLogger(new SimpleDateFormat("HH:mm"), Calendar.getInstance());
    }
}

ImageFileProcessor インスタンスを取得するたびに、CDIは TimeLoggerFactory クラスをスキャンし、 getTimeLogger()メソッドを呼び出します( @Producesで注釈が付けられているため) アノテーション)、最後に TimeLoggerサービスを挿入します。

Weld を使用してリファクタリングされたサンプルアプリケーションを実行すると、次のように出力されます。

Opening PNG file file1.png at: 17:46

8. カスタム修飾子

CDIは、依存関係を限定し、あいまいなインジェクションポイントを解決するためのカスタム修飾子の使用をサポートしています。

カスタム修飾子は非常に強力な機能です。 セマンティック名をサービスにバインドするだけでなく、インジェクションメタデータもバインドします。 RetentionPolicy やリーガルアノテーションターゲット( ElementType )などのメタデータ。

アプリケーションでカスタム修飾子を使用する方法を見てみましょう。

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface GifFileEditorQualifier {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface JpgFileEditorQualifier {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface PngFileEditorQualifier {}

次に、カスタム修飾子をImageFileEditor実装にバインドしましょう。

@GifFileEditorQualifier
public class GifFileEditor implements ImageFileEditor { ... }
@JpgFileEditorQualifier
public class JpgFileEditor implements ImageFileEditor { ... }
@PngFileEditorQualifier
public class PngFileEditor implements ImageFileEditor { ... }

最後に、 ImageFileProcessor クラスのインジェクションポイントをリファクタリングしましょう。

@Inject
public ImageFileProcessor(@PngFileEditorQualifier ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... }

アプリケーションをもう一度実行すると、上記と同じ出力が生成されます。

カスタム修飾子は、名前とアノテーションメタデータを実装にバインドするためのきちんとしたセマンティックアプローチを提供します。

さらに、カスタム修飾子を使用すると、より制限的なタイプセーフなインジェクションポイントを定義できます(@Defaultおよび@Alternativeアノテーションの機能よりも優れています)

サブタイプのみがタイプ階層で修飾されている場合、CDIはサブタイプのみを挿入し、基本タイプは挿入しません。

9. 結論

間違いなく、 CDIは依存性注入を簡単にします。追加のアノテーションのコストは、組織化された依存性注入を実現するための労力はほとんどありません。

DYDIがまだCDIよりも優れている場合があります。 単純なオブジェクトグラフのみを含む非常に単純なアプリケーションを開発する場合のように。

いつものように、この記事に示されているすべてのコードサンプルは、GitHubから入手できます。