電通総研 テックブログ

電通総研が運営する技術ブログ

コードを変更せずにJava EEからJakarta EEへマイグレーションする

みなさんこんにちは、電通総研コーポレート本部システム推進部の佐藤太一です。

この記事では、Mavenを使ってJava EE向けに作られた既存のアプリケーションのコードに一切手を加えることなくJakara EE対応のアプリケーションに変換する方法を説明します。

はじめに

2017年にJava EEJakarta 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の中にコメントした数字に沿って説明します。

  1. 依存ライブラリとして、 com.example:legacy:1.0-SNAPSHOT を指定しています。今回はありませんが、ここから推移的に依存するライブラリも多数あることでしょう。例えば、commons-fileupload などは色んな場所で使われていますが、Jakarta EEに対応したバージョンはリリースされていません。
  2. maven-shade-plugin を使ったパッケージング実施しています。これを使うと依存ライブラリも含めて全て単一のjarファイルにまとめるUberJarを作成できます。
  3. com.example:legacy:1.0-SNAPSHOTが依存するライブラリは基本的に全てUbeJarの中に取込むのですが、取込んでしまう必要のないものや、取込んでしまうと不都合のあるものを除外しています。特に、 commons-loggingは単一のクラスパス上に複数バージョン存在していると正しく動作しない可能性が高いので除外しています。
  4. 近年のJavaではMETA-INF/services以下に所定のルールでファイルを配置する事で拡張できるフレームワークが非常に多いので、それらのファイルが誤って失われてしまわないように、ServicesResourceTransformerを指定しています。他にもトランスフォーマーは色々定義されているので一度見てみると良いでしょう。Resource Transformersを参照してください。
  5. このrelocationsタグで定義されている内容が今回の主題です。一つ目のrelocationタグではjavax.servletjakarta.servletに置換しています。二つ目のタグでは、javax.transactionjakarta.transaction に置換しています。これは単なる文字列置換ではないのでサブパッケージについては、それぞれ指定しましょう。
  6. これは、おまけです。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.servletjakarta.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で執筆されました