ローリングコンバットピッチなう!

AIとか仮想化とかペーパークラフトとか

tomcat10用のServletをコマンドライン環境でコンパイル〜起動

動機:ヘッドレスサーバー上でJakarta Servletをビルドしてテストしたい

昨年からmicrok8sをVM上のUbuntuサーバーにインストールして、勉強がてら動作確認をしています。
rc30-popo.hatenablog.com

microk8s上で動かすpodには、podの中から周りのIPアドレス等がどう見えるか確認するための簡易テストアプリをpython + flaskで作り動かしていましたが、Javaベースでも動作確認したくなってきました。
JavaベースでなるべくシンプルにWebアプリを作る場合、Jakarta Servlettomcatで動かすのがまずは最小構成かな?と思い、まずはその辺に良く転がっているServlet版のHello Worldをビルドしたいと思ったのですが、tomcat+Servletの入門ドキュメントの類はたいていEclipse環境を前提とした説明になっています。

ちゃんとしたWebアプリを本格的に開発したいわけではなく、kubernetes環境のpod・コンテナ内で動作するJavaプログラムが、クラスタ環境内外と(特にネットワーク的に)どのように繋がっているのか?またJavaプログラムから見て各種の環境条件(環境変数等)はどの様になっているか?をテキストベースでHTTPレスポンスやログに吐き出して確認したいだけであり、テスト環境兼テストアプリ開発環境がヘッドレスのUbuntuサーバーであるため、コマンドラインインターフェイスのみでServletをビルドする方法を調査しました。

ビルドに最低限必要なもの

本エントリー末尾に参考URLとした記載したサイトを含めて、色々な情報ソースをあたった結果、最小構成のServletのビルドにはJDKに加えてtomcatに付属するservlet-api.jarがあれば良いと理解しました。(jspを使う場合はjsp-api.jarも必要)

ビルド環境及びtomcat実行環境の選定

JDKのインストールとtomcatのインストールを手動で一からやるのは面倒くさいので、docker hubを確認したところtomcatの公式docker imageとして、Eclipse temurinベースのJDKJREtomcatを加えたイメージがあることが判りました。
これをdocker hubから引っ張っていました。

$ docker pull tomcat:jdk11-temurin
$ docker pull tomcat:jre11-temurin

(ちなみに上記ではJDK11版を引張っていますが、JDK8版や17版もあります。使いたいServletのスペック等に合わせて選定してください)
docker imageを入手したら、JDK版を起動し素性を確認してみます。

$ docker run -it tomcat:jdk11-temurin /bin/bash
root@d2c71f35ad59:/usr/local/tomcat# 
root@d2c71f35ad59:/usr/local/tomcat# javac --version
javac 11.0.14.1
root@d2c71f35ad59:/usr/local/tomcat# bin/version.sh
Using CATALINA_BASE:   /usr/local/tomcat
Using CATALINA_HOME:   /usr/local/tomcat
Using CATALINA_TMPDIR: /usr/local/tomcat/temp
Using JRE_HOME:        /opt/java/openjdk
Using CLASSPATH:       /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar
Using CATALINA_OPTS:   
NOTE: Picked up JDK_JAVA_OPTIONS:  --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
Server version: Apache Tomcat/10.0.18
Server built:   Mar 9 2022 14:30:15 UTC
Server number:  10.0.18.0
OS Name:        Linux
OS Version:     4.15.0-176-generic
Architecture:   amd64
JVM Version:    11.0.14.1+1
JVM Vendor:     Eclipse Adoptium

こんな感じでtomcatのバージョンやjavac、JVMのバージョンが判ります。

テストアプリソース

JDKtomcatが入ったdocker imageが入手出来たのでテストアプリのソースを作成します。
htmlでresponseを出すのが面倒なのでtext/plainにしています。

クライアントのIPとクライアントのRequest headerを全部出力させています。

本ソースのもうひとつのポイントはtomcat10以降ではservlet関連のパッケージ名がjavax.servlet.*からjakarta.servlet.*に変更されている点です。
(正確にはtomcat10からjakarta EE 9対応となり、jakarta EE 9にてパッケージ名が変更)
またweb.xmlを使わずにソースコード上のアノテーション(@WebServlet)でHTTPアクセス時のパス名とServletの紐付けを行っています。

// tomcat servlet sample
// 

import java.io.*;
import jakarta.servlet.*; // tomcat10以降、javax.servletではなくjakarta.servletを使用する
import jakarta.servlet.http.*; // 同上
import java.util.Enumeration;
import jakarta.servlet.annotation.WebServlet; // web.xmlを使わずannotationを使用する場合に指定

@WebServlet("/")
public class hello extends HttpServlet {
        protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException {
                response.setContentType("text/plain");
                PrintWriter resout = response.getWriter();
                resout.println("Servlet test program.");
                String clientIpAddr = request.getRemoteAddr();
                resout.println("  Client IP = " + clientIpAddr);

                Enumeration headernames = request.getHeaderNames();
                int headercount = 0;
                while(headernames.hasMoreElements()){
                        String headername = (String)headernames.nextElement();
                resout.println("Servlet test program.");
                String clientIpAddr = request.getRemoteAddr();
                resout.println("  Client IP = " + clientIpAddr);

                Enumeration headernames = request.getHeaderNames();
                int headercount = 0;
                while(headernames.hasMoreElements()){
                        String headername = (String)headernames.nextElement();
                        Enumeration headervals = request.getHeaders(headername);
                        while(headervals.hasMoreElements()){
                                String headerval = (String)headervals.nextElement();
                                resout.println("  Header("+headercount+")="+headername + ":" + headerval);
                                headercount++;
                        }

                }

        }

}

テストアプリのコンパイルとWebアプリのパッケージ作成

コンパイル

ソースコードを格納したディレクトリに移動します。
ソースコードのファイル名をhello.javaとすると、下記の様にdocker runコマンドで tomcat:jdk11-temurinをコンテナ起動、javacにclasspathを下記の様に指定してソースをコンパイルします。

下記のdocker runコマンドではコンテナの/homeにホストのカレントディレクトリをマウントし、そこをワークディレクトリとしてコンパイル作業をさせています。

$ docker run -v $PWD:/home -w /home -it tomcat:jdk11-temurin javac -classpath /usr/local/tomcat/lib/servlet-api.jar hello.java

コンパイルに成功するとカレントディレクトリ下にhello.classが作成されます。

パッケージ作成

ホスト上にアプリケーションを配置するためのディレクトリを作成します。仮にそのディレクトリ名をhelloとするとhelloをトップに下記の階層でディレクトリを作成します。

hello/WEB-INF/classes

作成したclasses配下にhello.javaコンパイルしたclassファイル、hello.classをコピーします。

tomcat環境にテストアプリを組み込んで起動

コンテナイメージ tomcat:jre11-temurinに作成したアプリ配置ディレクトリを/usr/local/tomcat/webappsの下にマウント、更にコンテナのポート8080(tomcatがコンテナ内のポート8080で起動します)にホスト側の任意のポートをアサインしてコンテナを起動します。
先ほどのhelloディレクトリにcd後、docker runコマンドを下記の様に指定します。

$ docker run -v $PWD:/usr/local/tomcat/webapps/hello -it -p 8888:8080 tomcat:jre11-temurin
Using CATALINA_BASE:   /usr/local/tomcat
Using CATALINA_HOME:   /usr/local/tomcat
Using CATALINA_TMPDIR: /usr/local/tomcat/temp
Using JRE_HOME:        /opt/java/openjdk
Using CLASSPATH:       /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar
Using CATALINA_OPTS:   
NOTE: Picked up JDK_JAVA_OPTIONS:  --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
04-May-2022 15:00:50.372 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name:   Apache Tomcat/10.0.18
04-May-2022 15:00:50.428 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built:          Mar 9 2022 14:30:15 UTC
04-May-2022 15:00:50.429 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.0.18.0
04-May-2022 15:00:50.430 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name:               Linux
04-May-2022 15:00:50.432 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version:            4.15.0-176-generic
04-May-2022 15:00:50.433 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture:          amd64
04-May-2022 15:00:50.434 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home:             /opt/java/openjdk
04-May-2022 15:00:50.435 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version:           11.0.14.1+1
04-May-2022 15:00:50.435 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor:            Eclipse Adoptium
04-May-2022 15:00:50.437 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE:         /usr/local/tomcat
04-May-2022 15:00:50.438 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME:         /usr/local/tomcat
04-May-2022 15:00:50.613 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED
04-May-2022 15:00:50.615 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED
04-May-2022 15:00:50.619 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED
04-May-2022 15:00:50.624 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
04-May-2022 15:00:50.626 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
04-May-2022 15:00:50.627 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties
04-May-2022 15:00:50.629 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
04-May-2022 15:00:50.630 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=2048
04-May-2022 15:00:50.632 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources
04-May-2022 15:00:50.635 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=0027
04-May-2022 15:00:50.637 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dignore.endorsed.dirs=
04-May-2022 15:00:50.638 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat
04-May-2022 15:00:50.640 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat
04-May-2022 15:00:50.641 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp
04-May-2022 15:00:50.831 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [1.2.31] using APR version [1.6.5].
04-May-2022 15:00:50.835 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent APR capabilities: IPv6 [true], sendfile [true], accept filters [false], random [true], UDS [true].
04-May-2022 15:00:50.943 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 1.1.1f  31 Mar 2020]
04-May-2022 15:00:52.241 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
04-May-2022 15:00:52.336 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [2930] milliseconds
04-May-2022 15:00:52.562 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
04-May-2022 15:00:52.564 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/10.0.18]
04-May-2022 15:00:52.596 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/hello]
04-May-2022 15:00:54.368 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/hello] has finished in [1,772] ms
04-May-2022 15:00:54.388 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
04-May-2022 15:00:54.470 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [2132] milliseconds

下記のメッセージでtomcatがwebappディレクトリ下に配置したhelloアプリを認識したことが判ります。

04-May-2022 15:00:52.596 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/hello]
04-May-2022 15:00:54.368 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/hello] has finished in [1,772] ms

同じホスト上からcurlコマンドでtomcatにアクセスした結果です。

$ curl http://localhost:8888/hello/
Servlet test program.
  Client IP = 172.17.0.1
  Header(0)=host:localhost:8888
  Header(1)=user-agent:curl/7.61.0
  Header(2)=accept:*/*

他のマシンからdockerを起動しているVMIPアドレス(以下の例では192.168.11.252)を指定してアクセスした結果。

$ curl http://192.168.11.252:8888/hello/
Servlet test program.
  Client IP = 192.168.11.254
  Header(0)=host:192.168.11.252:8888
  Header(1)=user-agent:curl/7.65.3
  Header(2)=accept:*/*

アプリをwarファイルに纏めた上でtomcat上で起動する

「パッケージ作成」のところで作成したhello/ディレクトリ配下で下記のコマンドを実行します。

$ jar cvf ../hello.war .

これでhello/の一つ上のディレクトリにhello.warが作成されるので、 tomcat:jre11-temurinであればこのファイルを/usr/local/tomcat/webapps/直下に置く形でコンテナ起動すればhelloアプリがtomcatに認識されます。