CLOVER🍀

That was when it all began.

Jakarta ServletのServletContainerInitializerを試す

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

ServletContainerInitializerを使ったことがないなと思ったので、1度自分で試しておこうかなということで。

ServletContainerInitializer

ServletContainerInitializerは、Jakarta Servletが備えるプラグインのような仕組みです。

Jakarta Servlet Specification / Annotations and Pluggability / Shared Libraries / Runtimes Pluggability

ServletContainerInitializerインターフェースは実装クラスを作成し、META-INF/services配下にjakarta.servlet.ServletContainerInitializerファイルに
クラス名を書いておくことでService Loaderの仕組みで実装クラスがインスタンス化され、onStartupメソッドが呼び出されます。

メソッドのシグニチャはこちら。

void onStartup<200b>(Set<Class<?>> c, ServletContext ctx)

ServletContainerInitializer (Jakarta Servlet API documentation)

ServletContextが渡され、ここでなんらかの処理を実装できるわけですがServletContainerInitializerの実装クラスに
@HandlesTypesアノテーションを付与しておくことでデプロイ対象のアプリケーションに指定されたアノテーション
付与された(クラス、メソッド、フィールド)クラスや特定のクラスのサブクラスなどを受け取ることができます。

HandlesTypes (Jakarta Servlet API documentation)

@HandlesTypesアノテーションの指定がなかったり、指定したクラスに一致するClassがない場合はSetにはnullが渡されるようです。

If an implementation of ServletContainerInitializer does not have the @HandlesTypes annotation, or if there are no matches to any of the HandlesType specified, then it will get invoked once for every application with null as the value of the Set.

では、実際に試してみましょう。WildFlyで確認することにします。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.4 2024-07-16
OpenJDK Runtime Environment (build 21.0.4+7-Ubuntu-1ubuntu222.04)
OpenJDK 64-Bit Server VM (build 21.0.4+7-Ubuntu-1ubuntu222.04, mixed mode, sharing)


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

WildFlyは34.0.0.Finalを使います。

サンプルプログラムを作成する

確認用のサンプルプログラムを作成します。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>servlet-container-initializer-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <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>

    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>10.0.0</version>
            <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>33.0.2.Final</version>
                    </discover-provisioning-info>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Jakarta RESTful Web Services(JAX-RS)の有効化。

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

package org.littlewings.servlet;

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

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

JAX-RSリソースクラスとレスポンス用のRecord。

src/main/java/org/littlewings/servlet/EchoResource.java

package org.littlewings.servlet;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

@Path("/echo")
@RequestScoped
public class EchoResource {
    @Inject
    private MessageService messageService;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public EchoResponse message(@QueryParam("word") String word) {
        return new EchoResponse(messageService.decorate(word));
    }

    public record EchoResponse(String message) {
    }
}

CDI管理Beanも作成しましたが、サンプルの都合上インターフェース → 抽象クラス → 実装クラスの3段で用意しています。

src/main/java/org/littlewings/servlet/MessageService.java

package org.littlewings.servlet;

public interface MessageService {
    String decorate(String word);
}

src/main/java/org/littlewings/servlet/AbstractMessageService.java

package org.littlewings.servlet;

public abstract class AbstractMessageService implements MessageService {
}

src/main/java/org/littlewings/servlet/MessageServiceImpl.java

package org.littlewings.servlet;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class MessageServiceImpl extends AbstractMessageService {
    @Override
    public String decorate(String word) {
        return "★" + word + "★";
    }
}

最後に、ServletContainerInitializerインターフェースの実装クラスを作成。今回は受け取ったClassSetの内容を標準出力に書き出すのみと
しました。

src/main/java/org/littlewings/servlet/MyServletContainerInitializer.java

package org.littlewings.servlet;

import java.util.Set;

import jakarta.servlet.ServletContainerInitializer;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.HandlesTypes;

public class MyServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> classes, ServletContext ctx) throws ServletException {
        if (classes != null) {
            if (classes.isEmpty()) {
                System.out.println("classes is empty");
            } else {
                classes.forEach(c -> System.out.printf("received: %s%n", c));
            }
        } else {
            System.out.println("classes is null");
        }
    }
}

この時点では@HandlesTypesアノテーションは付与していません。

META-INF/services/jakarta.servlet.ServletContainerInitializerファイルに、作成した実装クラス名を記載。

src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer

org.littlewings.servlet.MyServletContainerInitializer

WildFlyを起動。

$ mvn wildfly:run

動作確認。

$ curl localhost:8080/echo?word=Hello
{"message":"★Hello★"}

この時、作成したServletContainerInitializerの実装クラスの出力内容はこちらです。@HandlesTypesアノテーションを付与していないので、
Set<Class<?>>にはnullが渡されたようです。

16:27:00,955 INFO  [stdout] (ServerService Thread Pool -- 32) classes is null

ここからいくつかバリエーションを見ていってみましょう。

ServletContainerInitializerが受け取るクラスを確認してみる

ここからは、@HandlesTypesアノテーションClassを指定して挙動を見ていってみましょう。

@Pathアノテーションを指定。

@HandlesTypes({Path.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {

@Pathアノテーションをクラスに付与したクラスが出力されました。

16:29:58,974 INFO  [stdout] (ServerService Thread Pool -- 12) received: class org.littlewings.servlet.EchoResource

@GETアノテーションを指定。

@HandlesTypes({GET.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {

@GETアノテーションをメソッドに付与したクラスが出力されました。

16:31:38,607 INFO  [stdout] (ServerService Thread Pool -- 34) received: class org.littlewings.servlet.EchoResource

@Injectアノテーションを指定。

@HandlesTypes({Inject.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {

@Injectアノテーションをフィールドに付与したクラスが出力されました。

16:32:45,833 INFO  [stdout] (ServerService Thread Pool -- 14) received: class org.littlewings.servlet.EchoResource

まあ、全部同じなのですが…。

JAX-RSApplicationクラスを指定。

@HandlesTypes({Application.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {

Applicationのサブクラスが出力されました。

16:34:27,503 INFO  [stdout] (ServerService Thread Pool -- 32) received: class org.littlewings.servlet.RestApplication

今回用意したインターフェースを指定。

@HandlesTypes({MessageService.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {

抽象クラスと実装クラスが出力されました。

16:35:48,323 INFO  [stdout] (ServerService Thread Pool -- 6) received: class org.littlewings.servlet.AbstractMessageService
16:35:48,323 INFO  [stdout] (ServerService Thread Pool -- 6) received: class org.littlewings.servlet.MessageServiceImpl

複数クラスの指定も可能です。

@HandlesTypes({Path.class, Application.class, MessageService.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {

結果。

16:37:12,508 INFO  [stdout] (ServerService Thread Pool -- 4) received: class org.littlewings.servlet.EchoResource
16:37:12,508 INFO  [stdout] (ServerService Thread Pool -- 4) received: class org.littlewings.servlet.AbstractMessageService
16:37:12,508 INFO  [stdout] (ServerService Thread Pool -- 4) received: class org.littlewings.servlet.MessageServiceImpl
16:37:12,509 INFO  [stdout] (ServerService Thread Pool -- 4) received: class org.littlewings.servlet.RestApplication

使用していないアノテーションを指定した場合。

@HandlesTypes({POST.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {

この場合、空のSetが渡されたようです。仕様的にはnullになるような気がしますが…。

16:39:12,046 INFO  [stdout] (ServerService Thread Pool -- 32) classes is empty

おわりに

ServletContainerInitializerを試してみました。

見る機会はそこそこあったものの、実際に自分で使ったことがなかったのでどういうものか把握できましたね。