依存性注入のポテンシャルを引き出せ

# はじめに
依存性注入(DI)は三層レイヤードアーキテクチャではなく、ポートとアダプタであり、コンストラクタと違い、"先に呼び出して準備しておける"という事は、字面だけではどこの教科書にも書かれていますが、イマイチピンと来ない人も多いでしょう。
@Qualifierアノテーションの使い方を覚えれば、 **コントローラーでアノテーションを付与するだけでサービスクラスを跨いで、サービスクラスで呼び出すモデルを分ける** なんてことも可能になります。用途別に、呼び出すモデルを変えることで、ポリモーフィックなJavaの持ち味を引き出し、長期間の開発においても、既存のコードにデグレードを起こす事なく柔軟な新規開発が可能となります。

# Spring BootでコントローラでQualifierを指定する
Copyright (C) 2025 [RyuHazako]
SpringBootで@Qualifierアノテーションを導入する時に、同じClass型で複数のBeanを定義する時複数Bean定義方法を紹介します。
同じサービスクラス、複数のQualifierエンティティを作った時に、コントローラーからサービスを呼ぶ時に@Qualifierアノテーションをつけるだけでサービスクラスで呼び出すエンティティ型を識別できるので、開発途中にモデルの型が増えても、これまで実装してきた機能のデグレードが発生しにくい堅牢な開発が可能です。

サービス自体はインターフェースで定義し、コントローラで@Qualifierを指定して、そのインターフェースのどの実装を使用するかを選択します。

0. インターフェースの定義
まず、エンティティが実装するインターフェースを定義します。
```java:qualifier.java
public interface BaseEntity {
    Long getId();
    String getTitle();
}
```

1. エンティティの定義

```java:qualifier.java
import org.springframework.stereotype.Component;

// 共通のインターフェースを実装
@Component
@Qualifier("book1")
class Book1 implements BaseEntity {
    private Long id;
    private String title;

    public Book1() {}

    public Book1(Long id, String title) {
        this.id = id;
        this.title = title;
    }

    @Override
    public Long getId() { return id; }
    @Override
    public String getTitle() { return title; }

    @Override
    public String toString() {
        return "Book1{" +
                "id=" + id +
                ", title='" + title + '\'' +
                "}";
    }
}

@Component
@Qualifier("book2")
class Book2 implements BaseEntity {
    private Long id;
    private String title;
    private String author;

    public Book2() {}

    public Book2(Long id, String title, String author) {
        this.id = id;
        this.title = title;
        this.author = author;
    }

    @Override
    public Long getId() { return id; }
    @Override
    public String getTitle() { return title; }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    @Override
    public String toString() {
        return "Book2{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", author='" + author + '\'' +
                "}";
    }
}
```

2. サービスインターフェースと実装の定義

```java:qualifier.java
import org.springframework.stereotype.Service;

// サービスのインターフェース
interface EntityService {
    String getEntityDetails();
}

// サービスの実装
@Service
class EntityServiceImpl implements EntityService {
    private final BaseEntity entity;

    public EntityServiceImpl(BaseEntity entity) {
        this.entity = entity;
    }

    @Override
    public String getEntityDetails() {
        return "ID: " + entity.getId() + ", Title: " + entity.getTitle();
    }
}
```

ここでは、サービスの実装クラス(EntityServiceImpl)を定義し、コンストラクタでBaseEntityを受け取ります。これにより、コントローラで指定されたBaseEntityの実装が注入されます。
3. コントローラの定義

```java:qualifier.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

@RestController
public class MyController {

    private final EntityService entityService1;
    private final EntityService entityService2;

    @Autowired
    public MyController(
        @Qualifier("book1") EntityService entityService1,
        @Qualifier("book2") EntityService entityService2) {
        this.entityService1 = entityService1;
        this.entityService2 = entityService2;
    }

    @GetMapping("/entity1")
    public String getEntity1() {
        return entityService1.getEntityDetails();
    }

    @GetMapping("/entity2")
    public String getEntity2() {
        return entityService2.getEntityDetails();
    }
}
```

この例では、コントローラで@Qualifierアノテーションを使用して、どのBaseEntityをEntityServiceImplに注入するかを指定しています。具体的には、@Qualifier("book1")を指定したentityService1にはBook1が、@Qualifier("book2")を指定したentityService2にはBook2が注入されます。
このように、コントローラで@Qualifierを指定することで、サービス実装内で使用するエンティティをコントローラ側で選択できます。

# Qualifierアノテーション自体を社内に導入したい時用のテストコード
実際にプロジェクトに導入する際は説得が必要な場面もある事でしょう。
@Qualifierアノテーションのテストを行うためのコード例をいくつか紹介します。
ここでは、JUnitとSpring Frameworkのテスト機能を利用した例を示します。

**テストケース1：基本的なQualifierの動作確認**

このテストケースでは、異なるQualifierを持つBeanが正しく注入されることを確認します。

```java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

// サービスインターフェース
interface GreetingService {
    String greet();
}

// 特定のQualifierを持つ実装
@Component
@Qualifier("english")
class EnglishGreetingService implements GreetingService {
    @Override
    public String greet() {
        return "Hello";
    }
}

// 別のQualifierを持つ実装
@Component
@Qualifier("japanese")
class JapaneseGreetingService implements GreetingService {
    @Override
    public String greet() {
        return "こんにちは";
    }
}

// Qualifierを使って注入するコンポーネント
@Component
class GreetingController {
    @Autowired
    @Qualifier("english")
    private GreetingService englishGreeter;

    @Autowired
    @Qualifier("japanese")
    private GreetingService japaneseGreeter;

    public String getEnglishGreeting() {
        return englishGreeter.greet();
    }

    public String getJapaneseGreeting() {
        return japaneseGreeter.greet();
    }
}

// テスト用の設定クラス
@Configuration
class TestConfig {
    @Bean
    public EnglishGreetingService englishGreetingService() {
        return new EnglishGreetingService();
    }

    @Bean
    public JapaneseGreetingService japaneseGreetingService() {
        return new JapaneseGreetingService();
    }

    @Bean
    public GreetingController greetingController() {
        return new GreetingController();
    }
}

public class QualifierAnnotationTest {

    @Test
    void testSpecificBeanInjection() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
        GreetingController controller = context.getBean(GreetingController.class);

        assertNotNull(controller.getEnglishGreeting());
        assertEquals("Hello", controller.getEnglishGreeting());
        assertNotNull(controller.getJapaneseGreeting());
        assertEquals("こんにちは", controller.getJapaneseGreeting());

        context.close();
    }
}
```

**テストケース2：Qualifierがない場合の挙動確認（NoUniqueBeanDefinitionException）**

このテストケースでは、同じ型のBeanが複数存在し、Qualifierを指定しない場合に `NoUniqueBeanDefinitionException` が発生することを確認します。

```java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import static org.junit.jupiter.api.Assertions.assertThrows;

interface ReportService {
    void generateReport();
}

@Component
class PdfReportService implements ReportService {
    @Override
    public void generateReport() {
        // PDFレポート生成処理
    }
}

@Component
class ExcelReportService implements ReportService {
    @Override
    public void generateReport() {
        // Excelレポート生成処理
    }
}

@Component
class ReportGenerator {
    @Autowired
    private ReportService reportService; // Qualifierがないため例外が発生するはず
}

@Configuration
class NoQualifierTestConfig {
    @Bean
    public PdfReportService pdfReportService() {
        return new PdfReportService();
    }

    @Bean
    public ExcelReportService excelReportService() {
        return new ExcelReportService();
    }

    @Bean
    public ReportGenerator reportGenerator() {
        return new ReportGenerator();
    }
}

public class NoQualifierTest {

    @Test
    void testNoQualifierWithMultipleBeans() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(NoQualifierTestConfig.class);
        assertThrows(NoUniqueBeanDefinitionException.class, () -> context.getBean(ReportGenerator.class));
        context.close();
    }
}
```

**テストケース3：@Primaryアノテーションとの組み合わせ**

@Primaryアノテーションは、Qualifierが指定されていない場合にデフォルトで使用されるBeanを指定します。このテストケースでは、@Primaryと@Qualifierがどのように連携するかを確認します。

```java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

interface PaymentService {
    String processPayment();
}

@Component
@Primary
class CreditCardPaymentService implements PaymentService {
    @Override
    public String processPayment() {
        return "Processing payment with Credit Card";
    }
}

@Component
@Qualifier("paypal")
class PaypalPaymentService implements PaymentService {
    @Override
    public String processPayment() {
        return "Processing payment with Paypal";
    }
}

@Component
class PaymentProcessor {
    @Autowired
    private PaymentService defaultPaymentService; // @Primaryが適用される

    @Autowired
    @Qualifier("paypal")
    private PaymentService paypalService;

    public String getDefaultPaymentResult() {
        return defaultPaymentService.processPayment();
    }

    public String getPaypalPaymentResult() {
        return paypalService.processPayment();
    }
}

@Configuration
class PrimaryQualifierTestConfig {
    @Bean
    public CreditCardPaymentService creditCardPaymentService() {
        return new CreditCardPaymentService();
    }

    @Bean
    public PaypalPaymentService paypalPaymentService() {
        return new PaypalPaymentService();
    }

    @Bean
    public PaymentProcessor paymentProcessor() {
        return new PaymentProcessor();
    }
}

public class PrimaryQualifierTest {

    @Test
    void testPrimaryAndQualifierInteraction() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PrimaryQualifierTestConfig.class);
        PaymentProcessor processor = context.getBean(PaymentProcessor.class);

        assertNotNull(processor.getDefaultPaymentResult());
        assertEquals("Processing payment with Credit Card", processor.getDefaultPaymentResult());
        assertNotNull(processor.getPaypalPaymentResult());
        assertEquals("Processing payment with Paypal", processor.getPaypalPaymentResult());

        context.close();
    }
}
```

これらのテストケースは、@Qualifierアノテーションの基本的な機能、同じ型のBeanが複数存在する場合の挙動、そして@Primaryアノテーションとの連携を確認するのに役立ちます。必要に応じて、より複雑なシナリオやカスタムのQualifierアノテーションを作成してテストすることも可能です。

これらのコードを実行するには、JUnit 5とSpring Frameworkのテスト関連の依存関係をプロジェクトに追加する必要があります。MavenやGradleなどのビルドツールを使用している場合は、それぞれの設定ファイルに依存関係を追加してください。

**Mavenの `pom.xml` の例：**

```xml
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.x.x</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.x.x</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.x.x</version>
    <scope>test</scope>
</dependency>
```

`(バージョン番号)` の部分は、実際に使用するSpring FrameworkとJUnitのバージョンに置き換えてください。

これらのテストコードが、@Qualifierアノテーションの理解に役立つことを願っています。