みなさんこんにちは、電通総研コーポレート本部システム推進部の佐藤太一です。
この記事では、Mavenを使ってJava EE向けに作られた既存のアプリケーションのコードに一切手を加えることなくJakara EE対応のアプリケーションに変換する方法を説明します。
はじめに
2017年にJava EEがJakarta EEになってから7年が経つわけですが、皆さんの手元にあるシステムはJakarta EEに対応できていますか?
Java EEを選択するようなアプリケーションの保守運用では、名前空間だけの変更とは言ってもリスクを取りたくないと考えるようなシステムは多いでしょう。もしかしたら、Java EEどころかJ2EEのまま動かしているかもしれませんね。
ここで紹介する手法は、maven-shade-pluginのrelocationを使ってjarファイルの中に格納されたclassが参照するパッケージ名を置換するというやり方です。
この記事のサンプルコードを動かすにあたって、Java 21とMaven 3.5以上をあらかじめインストールしておいてください。
レガシーアプリケーションの実装
まずは、javax.servlet
を使って簡単なアプリケーションを実装してみましょう。
作業用のディレクトリにlegacy
という名前で新しいディレクトリを作成します。その後、以下の内容で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>com.example</groupId> <artifactId>legacy</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.3</version> <type>jar</type> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.mvnsearch</groupId> <artifactId>toolchains-maven-plugin</artifactId> <version>4.5.0</version> <executions> <execution> <goals> <goal>toolchain</goal> </goals> </execution> </executions> <configuration> <toolchains> <jdk> <version>1.8</version> </jdk> </toolchains> </configuration> </plugin> </plugins> </build> </project>
使っているServletが2.3と大分古いですが、これくらい古くてもきちんと動くJavaは本当に素晴らしいですね。
org.mvnsearch:toolchains-maven-plugin
は、指定したJDKを自動的にダウンロードして、コンパイルや実行時に使えるようにしてくれるプラグインです。ここでは、Java 1.8を指定しています。
正直言って、事前にJavaを二種類入れておいてくださいと言われたら、僕なら記事を読むのを諦めますね。
Mavenでプロジェクトを定義したので、さらに/legacy/src/main/java/com/example/legacy/
とディレクトリを作成します。作成したディレクトリの中にMyServlet.java
というファイル名で以下の内容を保存します。
package com.example.legacy; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.*; public class MyServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().append("Hello World!").flush(); } }
15年ぶりくらいにHttpServletを継承するクラスを書いたように思います。
importしている名前空間がjavax.servlet
であることに注目してください。
ファイルを保存したら以下のコマンドを実行して、ローカルリポジトリにこのアプリケーションをインストールしておきます。
mvn install
モダンアプリケーションの実装
次は、最新のアプリケーションを実装します。
作業用のディレクトリに、modern
という名前で新しいディレクトリを作成します。その後、以下の内容で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>com.example</groupId> <artifactId>modern</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.1</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>21</java.version> <start-class>com.example.modern.Main</start-class> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>legacy</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
最新のSpring Boot Webを使ったアプリケーションのプロジェクト定義です。
先ほどローカルにインストールした com.example:legacy:1.0-SNAPSHOT
を依存ライブラリに追加してあります。
Mavenでプロジェクトを定義したので、さらに/modern/src/main/java/com/example/modern/
とディレクトリを作成します。作成したディレクトリの中にMain.java
というファイル名で以下の内容を保存します。
package com.example.modern; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.context.annotation.Bean; import com.example.legacy.MyServlet; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; @SpringBootApplication public class Main { @Bean public ServletContextInitializer myServlets() { return new ServletContextInitializer() { @Override public void onStartup(ServletContext servletContext) throws ServletException { servletContext.addServlet("myServlet", MyServlet.class).addMapping("/"); } }; } public static void main(String[] args) { SpringApplication.run(Main.class, args); } }
Spring BootのServletContextInitializerを使ってServletをデプロイしています。
ここで参照している名前空間はjakarta.servlet
ですね。
つまり、以下のコマンドを使ってアプリケーションをコンパイルしようとするとエラーになります。
mvn package
出力されるエラーは、名前空間がずれているため以下のようなものになるでしょう。
[ERROR] /C:/work/java/legacy-replacement/modern/src/main/java/com/example/modern/Main.java:[22,47] javax.servlet.http.HttpServletにアクセスできません javax.servlet.http.HttpServletのクラス・ファイルが見つかりません
今回例示しているレガシーアプリケーションは、ファイル1つだけなのでファイルを直接変更してしまっても大した手間ではありません。しかし、2024年現在も生き残っているレガシーアプリケーションは、名前空間の一括置換ですらはばかられるものが多いでしょう。
無用なリスクは取りたくないと考える人がいても非難しがたいものです。
マイグレーションプロジェクトの実装
ここからは、ビルド済みのレガシーアプリケーションを最新の環境に対応できるようにするマイグレーションプロジェクトを定義します。
作業用のディレクトリに、legacy-uber
という名前で新しいディレクトリを作成します。その後、以下の内容で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>com.example</groupId> <artifactId>legacy-uber</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.example</groupId> <!-- 1. --> <artifactId>legacy</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <!-- 2. --> <version>3.5.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <useBaseVersion>true</useBaseVersion> <createDependencyReducedPom>false</createDependencyReducedPom> <artifactSet> <excludes> <!-- 3. --> <exclude>junit:junit</exclude> <exclude>commons-logging:commons-logging</exclude> </excludes> </artifactSet> <transformers> <!-- 4. --> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" /> </transformers> <relocations> <!-- 5. --> <relocation> <pattern>javax.servlet</pattern> <shadedPattern>jakarta.servlet</shadedPattern> </relocation> <relocation> <pattern>javax.transaction</pattern> <shadedPattern>jakarta.transaction</shadedPattern> </relocation> </relocations> <filters> <!-- 6. --> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.MF</exclude> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> <exclude>META-INF/*.txt</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
このプロジェクトについて説明していきましょう。pom.xmlの中にコメントした数字に沿って説明します。
- 依存ライブラリとして、
com.example:legacy:1.0-SNAPSHOT
を指定しています。今回はありませんが、ここから推移的に依存するライブラリも多数あることでしょう。例えば、commons-fileupload などは色んな場所で使われていますが、Jakarta EEに対応したバージョンはリリースされていません。 maven-shade-plugin
を使ったパッケージング実施しています。これを使うと依存ライブラリも含めて全て単一のjarファイルにまとめるUberJarを作成できます。com.example:legacy:1.0-SNAPSHOT
が依存するライブラリは基本的に全てUbeJarの中に取込むのですが、取込んでしまう必要のないものや、取込んでしまうと不都合のあるものを除外しています。特に、 commons-loggingは単一のクラスパス上に複数バージョン存在していると正しく動作しない可能性が高いので除外しています。- 近年のJavaでは
META-INF/services
以下に所定のルールでファイルを配置する事で拡張できるフレームワークが非常に多いので、それらのファイルが誤って失われてしまわないように、ServicesResourceTransformerを指定しています。他にもトランスフォーマーは色々定義されているので一度見てみると良いでしょう。Resource Transformersを参照してください。 - このrelocationsタグで定義されている内容が今回の主題です。一つ目のrelocationタグでは
javax.servlet
をjakarta.servlet
に置換しています。二つ目のタグでは、javax.transaction
をjakarta.transaction
に置換しています。これは単なる文字列置換ではないのでサブパッケージについては、それぞれ指定しましょう。 - これは、おまけです。META-INFディレクトリの中によく格納されているものを取り除いています。MANIFEST.MFについては、取り除くと問題のある場合がある一方で、取り除かずに内容をマージしないといけない場合もありますので注意してください。
ファイルを保存したら以下のコマンドを実行して、ローカルリポジトリにこのUberJarをインストールしておきます。
mvn install
UberJarの中身を確認する
UberJarの中身を念のため確認しておきましょう。
ビルドが正常に終了しているなら、/legacy-uber/target
ディレクトリの中に二つのjarファイルが格納されているはずです。
プレフィックスとして、original-
が付いているのはshadingする前のjarファイルです。ファイルサイズが少しだけ小さいですね。
shadingされている方のjarファイルを展開して中身を確認しましょう。ここでは、legacy-uber-1.0-SNAPSHOT.jar
です。jarファイルはZIP圧縮されているファイルなので中からMainServlet.classを取り出します。
バイナリエディタで開くとjavax.servlet
がjakarta.servlet
へ置換されていることが分かりますね。
マイグレーションプロジェクトの参照
レガシーアプリケーションをマイグレーションしたUberJarをローカルにインストールしたので、最新のアプリケーションからは、そのライブラリを参照するように依存性を変更します。
その結果、以下のようになります。
<?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>com.example</groupId> <artifactId>modern</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.1</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>21</java.version> <start-class>com.example.modern.Main</start-class> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>legacy-uber</artifactId> <version>1.0-SNAPSHOT</version> <exclusions> <exclusion> <groupId>*</groupId> <artifactId>*</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
変更点は。com.example:legacy:1.0-SNAPSHOT
に対する依存性をcom.example:legacy-uber:1.0-SNAPSHOT
に対する依存性に変更している部分です。
legacy-uberは、依存ライブラリも含めて単一のjarファイルの中に格納されていますので、推移的な依存性は必要ありません。よって、exclusionsタグを使って推移的な依存性を全て無視するようにしてあります。
では、以下のコマンドを使って再度アプリケーションをビルドしてみましょう。
mvn clean package
今度はエラー出力なくビルドが完了します。
動作を確認したければ、以下のコマンドを実行してTomcatが起動したら、ブラウザで http://localhost:8080
にアクセスします。
mvn spring-boot:run
ServletでレンダリングしたHello World!
が表示されますね。
まとめ
この記事では、古いアプリケーションのソースコードやビルド済みバイナリには手を加えることなく、新しい環境で既存資産を流用する方法を説明しました。
長期間メンテナンスされているアプリケーションは価値ある資産です。とはいえ、古い事そのものが原因で新しい取り組みを行えないなら、それは技術的な解決があるという事を示しました。
この記事を読んだ皆さんが、希望を持ってレガシーアプリケーションの現代化に取り組めることを祈って記事の結びとします。
執筆:@sato.taichi、レビュー:@yamashita.tsuyoshi
(Shodoで執筆されました)