はじめに
Javaでバックエンドを開発することは20年以上前から定番だとは思います。
現在では、node.jsやPython、Go等に取って代わられるようになっているとはいえ、
大規模システムだと、まだまだJavaは現役。
Javaは、COBOLと同じくこの先も無くならない存在なんて話も聞こえています。
(LLM使って自動コンバートする時代が来るまでであれば・・・ですが)
筆者はSE歴20年のため、最初に出会ったフレームワークはStrutsでしたが、2024年時点でのJavaのバックエンドのフレームワークの二大巨頭はSpringとJakartaEE(旧JavaEE)です。
現状、日本において圧倒的に多いのはSpringの書籍(それもSpringBootが大半)ですが、とはいえ、JakartaEE(やその中のMicroProfile)も十分に使えるものになってきてます。
ということで、本記事では、SpringとJakartaEEを比較して、SpringからJakartaEEに移行できるか?について検証してみました。
1. 前提
前提として今回は、SPAアプリケーションを前提としたフロントエンド・バックエンドをRESTful APIで接続する構成として、検証はcurlにより実施しました。
(0) 共通
- JDKバージョン: 21.0.3 (OpenJDK 64-Bit Server VM Microsoft-9388422)
- HW: MacBook Pro (プロセッサ:Intel Core i5 2.3GHz DualCore)
- OS: macOS Ventura 13.6.6
(1) SpringBoot構成
- バージョン: 3.3.1
- 主なライブラリ:
- Spring Web
(2) JakartaEE(MicroProfile)構成
- 実装ライブラリ: Quarkus
- バージョン: 3.11.3
- 主なライブラリ:
- quarkus-rest
- quarkus-rest-jaxb
2.環境構築
今回のアプリですが、単純な管理ツール(戦国武将管理ツール)にしました。本記事では、
- Controller層でリクエストを受付
- Service層でのデータを作って返す
という流れで、DBからのデータ取得等は、一旦対象外としています。
3.Springを利用した 場合
Springのプログラムは比較的簡単に作成できるかと思います。
(1) プレゼンテーション層(Controller層)
package com.example.commander.ctrl;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.commander.srv.Commander;
import com.example.commander.srv.CommanderSearchService;
@RestController
@RequestMapping("/commander")
public class CommanderController {
private final CommanderSearchService search;
@Autowired
public CommanderController(CommanderSearchService searchService) {
this.search = searchService;
}
@GetMapping(value = "/searchAll")
public List<Commander> searchAll() {
return this.search.getAll();
}
@GetMapping(value = "/{id}")
public Commander get(@PathVariable String id) {
Optional<Commander> commander = this.search.getCommander(id);
if(commander.isPresent()) {
return commander.get();
} else {
return null;
}
}
@GetMapping("/")
public String index() {
return "Server is Running ";
}
}
(2) ビジネスロジック層(Service層)
package com.example.commander.srv;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
@Service
public class CommanderSearchService {
private List<Commander> createData() {
List<Commander> commanders = new ArrayList<>();
commanders.add(new Commander("T002", "本多 忠勝", 50, 90));
commanders.add(new Commander("T003", "鳥居 元忠", 40, 80));
return commanders;
}
public List<Commander> getAll() {
return this.createData();
}
public Optional<Commander> getCommander(String id) {
List<Commander> commanders = this.createData();
return commanders.stream()
.filter(commander -> id.equals(commander.id()))
.findFirst();
}
}
package com.example.commander.srv;
import jakarta.validation.constraints.*;
public record Commander(
/** ID */ @Pattern(regex="^[A-Z][0-9]{3}$") String id,
/** 武将名 */ @Size(max=10) String name,
/** 知力 */ @Min(0) @Max(100) int intelligence,
/** 武力 */ @Min(0) @Max(100) int millitary
) {}
(3) 実行
curlコマンドなどで実行すると、結果が返ってきます。
$ curl http://localhost:8080/commander/searchAll
[{"id":"T002","name":"本多 忠勝","intelligence":50,"millitary":90},{"id":"T003","name":"鳥居 元忠","intelligence":40,"millitary":80}]
ここまではよくあるSpringの話ですね。
4. MicroProfileを利用した場合
続いて、MicroProfileの場合です。
こちらは何かしらの実装でやることになりますが、今回はQuarkusを使用しようと思います。(但し、Quarkus独自のライブラリ群は使用しない)
(0) 環境構築
MicroProfileはSpringBoot(Spring Initializr)程有名ではないと思うので、まずセットアップからです。
https://start.microprofile.io/ に行くと、SpringBootにおけるSpring Initializrのようなプロジェクト作成画面(MicroProfile Starter)が出てきます。
こちらで、MicroProfileのv3.2を選ぶと、Quarkusを利用できます。Helidonはv3.3まで、OpenLivertyはv5.0まで対応してそうでした。
MicroProfileのStarterだと、バージョン3.2までしか選択できず、Javaのバージョンも8か11しか選べません。今回はJava21を使用したいのでQuakursのStarterを使用しました。
(1) プレゼンテーション層(Controller層)
MicroProfile(Quarkus)を利用した場合のプログラムソースです。
Springとの違いですが、ズバリ、アノテーションです。
ロジックには一切、手を入れていません。
package com.example.commander.ctrl;
import java.util.List;
import java.util.Optional;
import com.example.commander.srv.Commander;
import com.example.commander.srv.CommanderSearchService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@ApplicationScoped
@Path("/commander")
public class CommanderController {
private final CommanderSearchService search;
@Inject
public CommanderController(CommanderSearchService searchService) {
this.search = searchService;
}
@GET
@Path("/searchAll")
@Produces(MediaType.APPLICATION_JSON)
public List<Commander> searchAll() {
return this.search.getAll();
}
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Commander get(@PathParam("id") String id) {
Optional<Commander> commander = this.search.getCommander(id);
if(commander.isPresent()) {
return commander.get();
} else {
return null;
}
}
@GET
@Path("/")
@Produces(MediaType.TEXT_PLAIN)
public String index() {
return "Server is Running ";
}
}
(2) ビジネスロジック層(Service層)
続いてビジネスロジックです。
Springの場合も、ビジネスロジックにはなるべく外部ライブラリの依存をさせないのが通例で、DIコンテナに対して存在を認識させるだけで良いです。
ということで、Springで言うところの@Component
を、@ApplicationScoped
や@Singleton
に変更するだけです。
package com.example.commander.srv;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CommanderSearchService {
// 以下略
// Springの場合から変更なし
この状態でcurlを打つと、Springと全く同じようにJSONデータが取得できます。
5. SpringとMicroProfileの比較
4章でご説明した通り、SpringとMicroProfileのRESTアクセスにおける違いはアノテーションのみになります。
Spring | MicroProfile | 内容 |
---|---|---|
@Autowired |
@Inject |
DIコンテナで登録されているクラスのインスタンスをメンバ変数にセット |
@Component |
@ApplicationScoped |
DIコンテナでシングルトンパターンによりプロセスで1つだけ対象オブジェクトを生成・管理。いずれも@Singleton というアノテーションも準備。Springのデフォルトは@Singleton
|
@Service @RestController @Repository
|
@ApplicationScoped |
各レイヤーにおける役割も明示したアノテーション。MicroProfileでは定義されていない |
@GetMapping @PostMapping @PutMapping @DeleteMapping
|
@GET @POST @PUT @DELETE
|
HTTP各メソッドの発行。Springの場合はvalue 属性でパス名をセットできるが、MicroProfileの場合は@Path アノテーションが別途必要 |
@RequestMapping のproduces 属性 |
@Produces |
Controllerが返すデータ種別(Content-Type)を指定(例:MediaType.APPLICATION_XML_VALUE )。ちなみに、SpringにおいてJSONで返す場合、@RestController を使っていれば特段気にする必要はないが、@Controller を使っている場合、返り値は「次に遷移する画面ビュー」を指定することがデフォルトとなっており、データを返したい場合は@ResponseBody (属性なし)をメソッドに対して付与する必要有 |
@PathVariable |
@PathParam |
Pathに"{}"で指定した可変値を取得するアノテーション。Springの場合は、引数名と同じ名称のものを取得も、MicroProfileでは明示的な定義が必要 |
@RequestParam |
@QueryParam |
GETリクエストパラメータを取得 |
@Parameter |
@FormParam |
formタグ内のデータ渡し |
@CookieValue |
@CookieParam |
クッキーからパラメータを取得 |
一部、サンプルに出てこないアノテーションも紹介してしまいましたが、基本的なREST受付機能において、SpringとMicroProfileはアノテーションが違うだけ、ということが改めてご理解いただけると幸甚です。尚、SpringでもJakarta Validationがサポートされているので、短項目チェックのタグについては、Spring、MicroProfile共に同一のクラスを使用できます。
6. 移行方法
では、本記事のテーマであるSpringからのMicroProfileへの移行をやってみようかと思います。
考えられる手順は以下の通りです。
(1) Springのアノテーションをラッピングした自作アノテーションを作る
(2) 自作アノテーションインターフェースと業務アプリのみで構成される業務アプリjarを作成
(3) 業務アプリjarをMicroProfile環境に持ち込み実行
上記について検証し、ある程度フィージビリティが見えました。
ただ、まだ解決できていない問題が1件残ってしまっており、そちらについて解説します。
ラッピング自作アノテーションの作成
アノテーションは内部的にはインターフェースで、java.lang.annotation
を継承したインタフェースです。ですが、アノテーションを継承元とした「アノテーション」を作成することはできません。
○ public @interface ChildAnnotation
○ public interface ChildAnnotation extends java.lang.annotation.Annotation
× public @interface ChildAnnotation extends ParentAnnotation
× public interface ChildAnnotation extends ParentAnnotation
ではどのように継承すればよいか、についてご説明致します。
①ANNOTATION_TYPEへの付与ができるアノテーションの場合
ラッピング自作アノテーションのつくり方ですが、アノテーションの中で、@Target
属性でElementType.ANNOTATION_TYPE
が指定されているアノテーションの場合、自作アノテーションに該当アノテーションを付与することで実現できます。同じアノテーション名にする場合は、コピー元のアノテーションをフルパスで設定すると実現できます。
package com.example.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@org.springframework.stereotype.Autowired
public @interface CAutowired {
}
例えば、@RestController
や@RestMapping
は属性を持っていますが、属性は以下のように設定します。
package com.example.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@org.springframework.web.bind.annotation.RestController()
public @interface CRestController {
String value() default "";
}
属性名無しの項目の場合、value()
メソッドを用意するとセットできます。
内部的な動作について
@RestController
ではなく、@CRestController
の属性としてvalue()
を定義しているのに、@RestController
の属性として読み込まれるか?ですが、大丈夫です。アノテーションの使い方を理解すると分かっていただけるかと思います。
②ANNOTATION_TYPEへの付与ができないアノテーションの場合
①のやり方は@Target
にANNOTATION_TYPE
が付与されている場合のみで、@PathVariable
等の各メソッドの引数に付与されているアノテーションの場合、@Target
にPARAMETER
しか付いていないため、①のやり方ではコンパイルエラーとなりオーバーライドできません。
この場合、以下の対応方法が考えられます。
【案A】 カスタマイズAnnotationを使用しているプログラムソースを文字列として抽出し、利用したいフレームワークのアノテーションに置換して、ソースを自動生成する
【案B】 カスタマイズAnnotationを使用しているプログラムソースの中身をリフレクションを使用して解析し、利用したいフレームワークのアノテーションに置き換える
【案B】にするといわゆるアプリケーションフレームワークの動きに近く、実行時に、環境に合わせて吸収してくれて有難いのですが、何か障害が発生するときには実行時例外になってしまいます。
また、全然別のアプローチとして、GET/POST等のパラメータ類の受け渡しについては、OpenAPI Specification(OASファイル)からOpenAPI Generatorを使用して自動生成するという手段も考えられます。
今回の記事では検証しきれなかったため、こちらは宿題とさせていただき、
【案A】【案B】共に今後、実装してみたいと思います。
(2) 自作アノテーションによるラッピング
ご参考まで、自作したアノテーションを使用方法ですが、当たり前ですが、Spring FrameworkやMicroProfileで使用している自作したアノテーションに置き換えるだけです。
以下がService層のクラスになります。
パッケージのインポートの箇所をご覧いただければ分かるかと思いますが、Java SEのAPIと、今回作った自作アノテーションだけに依存しており、SpringやMicroProfileに依存させずにプログラムを作成できるため、このプログラムを他環境に持っていってそのまま使えるところまでは確認できました。
package com.example.commander.srv;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import com.example.annotation.CService;
@CService
public class CommanderSearchService {
private List<Commander> createData() {
List<Commander> commanders = new ArrayList<>();
commanders.add(new Commander("T002", "本多 忠勝", 50, 90));
commanders.add(new Commander("T003", "鳥居 元忠", 40, 80));
return commanders;
}
public List<Commander> getAll() {
return this.createData();
}
public Optional<Commander> getCommander(String id) {
List<Commander> commanders = this.createData();
return commanders.stream()
.filter(commander -> id.equals(commander.id()))
.findFirst();
}
}
ただし、Controller層のAPIの受けとなる引数につけるAnnotationについては、残念ながら置き換えができません。(1)で記載した通り、更なる研究が必要なので、ここではSpring Frameworkのアノテーションである@PathVariable
や@RequestMethod
をそのまま使っています。
package com.example.commander.ctrl;
import java.util.List;
import java.util.Optional;
// 置き換え可能なアノテーションはコメントアウト
// import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
// import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
// import org.springframework.web.bind.annotation.RestController;
import com.example.annotation.CController;
import com.example.annotation.CInject;
import com.example.annotation.CRequestMapping;
import com.example.commander.srv.Commander;
import com.example.commander.srv.CommanderSearchService;
@CController
@CRequestMapping("/commander")
public class CommanderController {
private final CommanderSearchService search;
@CInject
public CommanderController(CommanderSearchService searchService) {
this.search = searchService;
}
@CRequestMapping(value = "/searchAll", method = RequestMethod.GET)
public List<Commander> searchAll() {
return this.search.getAll();
}
@CRequestMapping(value = "/{id}", method = RequestMethod.GET)
public Commander get(@PathVariable String id) {
Optional<Commander> commander = this.search.getCommander(id);
if(commander.isPresent()) {
return commander.get();
} else {
return null;
}
}
@CRequestMapping(value = "/", method = RequestMethod.GET)
public String index() {
return "Server is Running ";
}
}
まとめ
Spring FrameworkとMicroProfile間のフレームワーク移行が可能か?について検証しました。
両者が大変似ている構成を取っているため、かなりの部分まで簡単に移行できそうなところは進められましたが完全には実現できず、残念ながら宿題が残っている状態となってしまいました。
(期待されていた方がいらっしゃったら大変申し訳ありません)
引数へのアノテーションについては、機械的な置き換えや、LLMを使用した自動書き換えが必要です。
途中にも書きました通り、リフレクションを使用した読み込み時の自動書き換えができると良いので、良いアイディアが浮かび、実現できましたら、この続きの記事として紹介させていただけたら幸甚です。
参考ページ
- 【Spring Boot】 アノテーションまとめ
-
Javaフレームワーク開発入門(木村 聡 著)
2010年発行の本ですが、Javaのリフレクションに触れている数少ない本の一つです。