これは、なにをしたくて書いたもの?
ServletContainerInitializerを使ったことがないなと思ったので、1度自分で試しておこうかなということで。
ServletContainerInitializer
ServletContainerInitializer
は、Jakarta Servletが備えるプラグインのような仕組みです。
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
インターフェースの実装クラスを作成。今回は受け取ったClass
のSet
の内容を標準出力に書き出すのみと
しました。
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-RSのApplication
クラスを指定。
@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
を試してみました。
見る機会はそこそこあったものの、実際に自分で使ったことがなかったのでどういうものか把握できましたね。