この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:4
ページ更新者:guest
更新日時:2026-06-11 07:10:02

タイトル: CDI
SEOタイトル: CDI完全ガイド(Inject/Produces/Qualifier/Scope/Spring DIとの違い)

この記事の要点
  • CDI (Contexts and Dependency Injection) = Jakarta EE の標準 DI 仕様(JSR 299 / 365)
  • 中核アノテーション: @Inject / @Produces / @Qualifier / @Named
  • スコープ: @ApplicationScoped / @SessionScoped / @RequestScoped / @Dependent / @ConversationScoped
  • Bean 発見: CDI 1.x は beans.xml 必須、CDI 2.0+ は暗黙的発見がデフォルト
  • Producer メソッドで外部依存を Bean 化、Interceptor / Decorator で AOP
  • イベントバス: Event<T> + @Observes で疎結合の通知機構
  • Jakarta CDI 4.x で名前空間が jakarta.enterprise.context へ移行(旧 javax.enterprise.context

CDI とは

CDI (Contexts and Dependency Injection for Java) は Jakarta EE 標準の DI(依存性注入)仕様です。Spring の @Autowired に相当するものを、Jakarta EE / MicroProfile の世界では CDI が担います。実装は Weld(WildFly/GlassFish)、OpenWebBeans(TomEE)等。

基本: @Inject

import jakarta.inject.Inject;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class UserService {

    @Inject
    private UserRepository repo;     // フィールド注入

    @Inject
    private EmailSender sender;

    public void register(String email) {
        User u = new User(email);
        repo.save(u);
        sender.send(email, "welcome");
    }
}

// コンストラクタ注入(推奨)
@ApplicationScoped
public class UserService {
    private final UserRepository repo;
    private final EmailSender sender;

    @Inject
    public UserService(UserRepository repo, EmailSender sender) {
        this.repo = repo;
        this.sender = sender;
    }
}

スコープ

スコープ有効範囲用途
@ApplicationScopedアプリ全体で 1 インスタンスサービス、キャッシュ
@SessionScopedHTTP セッションログインユーザ情報
@RequestScopedHTTP リクエストリクエストコンテキスト
@ConversationScoped会話 (JSF 等)ウィザード
@Dependent (既定)注入先と寿命同じシンプルユーティリティ
@SessionScoped + Serializable必須
@SessionScoped
public class UserContext implements Serializable {
    private User user;
    public User getUser() { return user; }
    public void setUser(User u) { this.user = u; }
}

@RequestScoped
public class RequestId {
    private final String id = UUID.randomUUID().toString();
    public String get() { return id; }
}

@Qualifier で型を区別

同じインターフェースに複数実装があるとき、Qualifier で選択します。

import jakarta.inject.Qualifier;
import java.lang.annotation.*;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER})
public @interface Stripe {}

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER})
public @interface PayPal {}

// 実装
@Stripe
@ApplicationScoped
public class StripeProcessor implements PaymentProcessor { ... }

@PayPal
@ApplicationScoped
public class PayPalProcessor implements PaymentProcessor { ... }

// 注入時に指定
@Inject @Stripe
private PaymentProcessor processor;

@Produces で外部依存を Bean 化

自分が書いていないクラス(外部 JAR、設定値)を Bean として登録できます。

import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class AppConfig {

    @Produces
    public DataSource dataSource() {
        HikariConfig cfg = new HikariConfig();
        cfg.setJdbcUrl("jdbc:postgresql://localhost/app");
        return new HikariDataSource(cfg);
    }

    @Produces @ApplicationScoped
    public ObjectMapper jacksonMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }

    @Produces
    @ConfigValue("app.api.url")
    public String apiUrl(InjectionPoint ip) {
        return System.getenv("API_URL");
    }
}

// 利用側
@Inject
private DataSource ds;

@Inject
private ObjectMapper json;

@Named とビューレイヤ連携

JSF や式言語 (EL) からアクセスできる名前を付けます。

import jakarta.inject.Named;
import jakarta.enterprise.context.SessionScoped;

@Named("userCtx")       // EL: #{userCtx.user}
@SessionScoped
public class UserContext implements Serializable {
    private User user;
    public User getUser() { return user; }
    public void setUser(User u) { this.user = u; }
}

Interceptor(AOP)

@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Logged {}

@Interceptor
@Logged
@Priority(Interceptor.Priority.APPLICATION)
public class LoggingInterceptor {
    @AroundInvoke
    public Object intercept(InvocationContext ctx) throws Exception {
        long start = System.nanoTime();
        try {
            return ctx.proceed();
        } finally {
            long ms = (System.nanoTime() - start) / 1_000_000;
            System.out.printf("[%s] %d ms%n", ctx.getMethod().getName(), ms);
        }
    }
}

// 利用
@Logged
public void register(String email) { ... }

Event Bus(疎結合通知)

// イベント定義
public class UserRegistered {
    private final long userId;
    public UserRegistered(long userId) { this.userId = userId; }
    public long getUserId() { return userId; }
}

// 発火側
@ApplicationScoped
public class UserService {
    @Inject Event<UserRegistered> event;

    public void register(String email) {
        User u = repo.save(new User(email));
        event.fire(new UserRegistered(u.getId()));
        // 4.0+ は event.fireAsync(...) で非同期
    }
}

// 観察側(複数 OK、疎結合)
@ApplicationScoped
public class WelcomeEmailListener {
    public void onUserRegistered(@Observes UserRegistered e) {
        emailSender.send(e.getUserId(), "welcome");
    }
}

@ApplicationScoped
public class AnalyticsListener {
    public void onUserRegistered(@Observes UserRegistered e) {
        analytics.track("signup", e.getUserId());
    }
}

beans.xml と Bean 発見モード

モード挙動用途
allすべての class を Bean 候補古い CDI 1.0 互換
annotated明示的アノテーションのみ Bean 化(既定)★ 推奨
noneCDI 無効化パフォーマンス重視
<!-- META-INF/beans.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       version="4.0"
       bean-discovery-mode="annotated">
</beans>

Spring DI との比較

機能CDISpring
注入@Inject@Autowired
Bean 定義@ApplicationScoped@Component / @Service
Producer / Factory@Produces@Bean
条件付きExtension SPI@ConditionalOnXxx
スコープ標準 5 種類標準 5 種類 + カスタム
イベントEvent<T> / @ObservesApplicationEvent / @EventListener
インターセプタ@InterceptorBindingAspectJ / Pointcut
標準Jakarta 仕様、複数実装Spring エコ実質単独

Jakarta CDI 4.x 主要変更点

  • 名前空間: javax.enterprise.*jakarta.enterprise.*
  • 非同期 Observer (@ObservesAsync) 強化
  • Build-Time Compatible Extensions(Quarkus 向け軽量 SPI)
  • SE モード(CDI.current() でアプリ外から起動可能)
  • Bean 発見 annotated がデフォルト

FAQ

Q: @Inject でなく @Autowired が使える?
A: Spring の中では使える。CDI / Jakarta EE の中では @Inject (JSR-330) が標準です。

Q: 循環依存はどうなる?
A: コンストラクタ注入では起動時にエラー。Provider<T> や Instance で遅延注入して回避できます。

Q: テストでモック注入したい
A: Arquillian(実コンテナテスト)、Weld SE 単体テスト、または Mockito + コンストラクタ注入。