これは、なにをしたくて書いたもの?
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を試してみました。
見る機会はそこそこあったものの、実際に自分で使ったことがなかったのでどういうものか把握できましたね。