CLOVER🍀

That was when it all began.

Jakarta Contexts and Dependency Injection(CDI)のコンテナ起動・終了時になにか処理をする

これは、なにをしたくて書いたもの?

Jakarta Contexts and Dependency Injection(CDI)のコンテナ起動・終了時になにか処理をしたいと思った場合に、どうしたらいいのか
よく知らなかったので見ておくことにしました。

ちなみにJakarta Servletの場合はこちらですね。

Jakarta ServletのServletContainerInitializerを試す - CLOVER🍀

@Initialized/@BeforeDestroyed/@DestroyedかStartup/Shutdownか

調べてみると、やり方としては2つありそうです。

ひとつは@Initialized@BeforeDestroyed@Destroyedアノテーションを使って、あるスコープのコンテキストの初期化/破棄イベントの
通知を受け取ること。

Jakarta Contexts and Dependency Injection / CDI Lite / Scopes and contexts / Contextual instances and contextual references / Context management for built-in scopes

それぞれコンテキストの初期化後、破棄前、破棄後、ですね。

Initialized (Jakarta Context Dependency Injection API)

BeforeDestroyed (Jakarta Context Dependency Injection API)

Destroyed (Jakarta Context Dependency Injection API)

ここでスコープをApplicationScopedにすると、実質的にアプリケーションの起動・終了時に紐付けることができます。

このやり方はQuarkusのガイドにも比較のために載っていますね。

Application Initialization And Termination / Listening for startup and shutdown events / What is the difference from @Initialized(ApplicationScoped.class) and @Destroyed(ApplicationScoped.class)

もうひとつはStartupおよびShutdownイベントを受け取る方法です。

Jakarta Contexts and Dependency Injection / CDI Lite / Events / Observable container lifecycle events

Startupは、@Initialized(ApplicationScope.class)の後に発生します。

This event is fired after the event with qualifier @Initialized(ApplicationScope.class) but before processing requests.

Startup (Jakarta Context Dependency Injection API)

Shutdown@BeforeDestroyed(ApplicationScoped.class)の発生よりも遅くはない、とされています。

This event is fired during CDI container shutdown but not later than the event with qualifier @BeforeDestroyed(ApplicationScoped.class).

Shutdown (Jakarta Context Dependency Injection API)

ちなみにどちらのイベントも@Observesアノテーションで通知を受け取るのですが、複数のリスナーがある場合は@Priorityアノテーション
優先順位付けできることが書かれています。

Observer methods for this event are encouraged to specify @Priority to determine ordering with lower priority numbers being recommended for platform/framework/library integration and higher numbers for user applications.

Jakarta Contexts and Dependency Injection / CDI Lite / Events / Observable container lifecycle events

Priority (Jakarta Annotations API 2.1.1 API)

とりあえず、簡単に試してみることにします。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu124.04)
OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu124.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.5, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-49-generic", arch: "amd64", family: "unix"

準備

Maven依存関係などは、以下のように定義。

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>jakarta.platform</groupId>
                <artifactId>jakarta.jakartaee-bom</artifactId>
                <version>10.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>jakarta.ws.rs</groupId>
            <artifactId>jakarta.ws.rs-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.enterprise</groupId>
            <artifactId>jakarta.enterprise.cdi-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.inject</groupId>
            <artifactId>jakarta.inject-api</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>ROOT</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-maven-plugin</artifactId>
                <version>5.0.1.Final</version>
                <executions>
                    <execution>
                        <id>package</id>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <overwrite-provisioned-server>true</overwrite-provisioned-server>
                    <discover-provisioning-info>
                        <version>34.0.1.Final</version>
                    </discover-provisioning-info>
                </configuration>
            </plugin>
        </plugins>
    </build>

実行はWildFly 34.0.1.Finalをプロビジョニングして行います。

サンプルコード

@Initialized@BeforeDestroyed@Destroyedアノテーションを使って、@ApplicationScopedコンテキストのイベントを受け取る
リスナー。

src/main/java/org/littlewings/cdi/ApplicationScopedLifecycleListener.java

package org.littlewings.cdi;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.BeforeDestroyed;
import jakarta.enterprise.context.Destroyed;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;

@ApplicationScoped
public class ApplicationScopedLifecycleListener {
    void onInitialized(@Observes @Initialized(ApplicationScoped.class) Object initialized) {
        System.out.printf("[%s] initialized, event = %s%n", getClass().getSimpleName(), initialized);
    }

    void onBeforeDestroyed(@Observes @BeforeDestroyed(ApplicationScoped.class) Object beforeDestroyed) {
        System.out.printf("[%s] before destroyed, event = %s%n", getClass().getSimpleName(), beforeDestroyed);
    }

    void onDestroyed(@Observes @Destroyed(ApplicationScoped.class) Object destroyed) {
        System.out.printf("[%s] destroyed, event = %s%n", getClass().getSimpleName(), destroyed);
    }
}

ひとまず受け取った内容を標準出力に書き出しているだけです。

ちなみに、これ自体もCDI管理Beanです。

もうひとつはStartupおよびShutdownイベントを受け取るリスナー。

src/main/java/org/littlewings/cdi/ApplicationLifecycleEventListener.java

package org.littlewings.cdi;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.Shutdown;
import jakarta.enterprise.event.Startup;

@ApplicationScoped
public class ApplicationLifecycleEventListener {
    void onStartup(@Observes Startup startup) {
        System.out.printf("[%s] startup, event = %s%n", getClass().getSimpleName(), startup);
    }

    void onShutdown(@Observes Shutdown shutdown) {
        System.out.printf("[%s] shutdown, event = %s%n", getClass().getSimpleName(), shutdown);
    }
}

こちらも受け取った内容を標準出力に書き出しているだけですね。

あとはなんとなくJakarta RESTful Web Services(JAX-RS)も使っておきます。

JAX-RSの有効化。

src/main/java/org/littlewings/cdi/RestApplication.java

package org.littlewings.cdi;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/")
public class RestApplication extends Application {
}

JAX-RSリソースクラス。

src/main/java/org/littlewings/cdi/HelloResource.java

package org.littlewings.cdi;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/hello")
public class HelloResource {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String message() {
        return "Hello World!!";
    }
}

確認してみる

確認してみましょう。

パッケージング。

$ mvn package

プロビジョニングしたWildFlyを起動。

$ target/server/bin/standalone.sh

起動時にはこんなログが出力されます。

17:31:47,569 INFO  [stdout] (ServerService Thread Pool -- 33) [ApplicationScopedLifecycleListener] initialized, event = io.undertow.servlet.spec.ServletContextImpl@1475f3fd
17:31:47,571 INFO  [stdout] (ServerService Thread Pool -- 33) [ApplicationLifecycleEventListener] startup, event = jakarta.enterprise.event.Startup@cc8284e

確かに@Initialized(ApplicationScoped.class)の方がStartupの前に来ています。

動作確認。

$ curl localhost:8080/hello
Hello World!!

WildFlyを停止します。

17:33:01,007 INFO  [stdout] (ServerService Thread Pool -- 9) [ApplicationLifecycleEventListener] shutdown, event = jakarta.enterprise.event.Shutdown@33464610
17:33:01,008 INFO  [stdout] (ServerService Thread Pool -- 9) [ApplicationScopedLifecycleListener] before destroyed, event = io.undertow.servlet.spec.ServletContextImpl@1475f3fd
17:33:01,009 INFO  [stdout] (ServerService Thread Pool -- 9) [ApplicationScopedLifecycleListener] destroyed, event = io.undertow.servlet.spec.ServletContextImpl@1475f3fd

こちらはShutdownの方が先に来ていますね。

ということで確認できましたと。

おわりに

CDIでコンテナの起動・終了時になにか処理をしたい場合について調べてみました。

このあたり、あまり使っていないのでパッと思い浮かばないんですよね。1度試しておいてよかったかなと思います。