ということで、宿題となっていた件を実際にやってみました。
これはWildFlyの話なので、個人ブログの方で書いてます。
前回の記事はこちら:
Jakarta MVCとは!
まずは、【会場開催】Jakarta EE&MicroProfileセミナー「待望のJakarta EE 12最新動向!データアクセス新仕様&ついに標準化されるJakarta MVCを学ぶ」 6/22(月) 開催 - 日本 Jakarta EE & MicroProfile ユーザーグループ | Doorkeeper の多田さんの資料を読みましょう。
PetClinicを移植してみた!
前提条件が整ったところで、実際に移植してみました。
PetClinicは以前、JSFへ移行してみたことがありました。その際はJSFの都合上、URLのパスが変わってしまいましたが、 今回はほぼ変更せずに動作させることができました。 そういった点で、Spring BootとJakarta MVCのそれぞれで「どういう差が出るのか」を確認する、良いサンプルになったのではないかと思います。
Jakarta MVCの実装であるKrazoではThymeleafとのインテグレーションもサポートされており、「もしや、Viewは変更なしでもいけるのでは?」と淡い期待も抱いていました。しかし、多田さんの事前情報の通り、ちょっとした差でうまくいかないことも多く大変でした。結果的に、JSFへの移行に比べて2倍くらいの時間がかかっています。とは言っても1日かかっていないので、「思ったよりは簡単にできたな」という印象です。 今後Jakarta MVCに対する情報が増えてくるともっと簡単になるはず。そのためにも情報を出していかないと。
技術に対する直接的な感想としては、Servletベースではなく、背後にJAX-RSが介在している点が、直感的な理解の妨げになりました。JAX-RSの経験不足を痛感……
多田さんの資料でもいくつかハマりポイントが挙げられていたので、本記事ではそれ以外の部分にフォーカスして書いていきます。
御託はいいから移行前後のソースを出せ!
URLごとに同じ動作をするようにはできているので、細かい差についてはソースコードの差分を確認するのが手っ取り早いと思います。
移行前
移行後
また、今回はAIに自由気ままに移行させたうえで、どうしてもうまくいかないところだけ細かく指示をする、という形式で進めました。現時点では情報が少ないJakarta Data(Jakarta EE版のRepository仕様)についてはAIが選択しなかったため、自前でRepositoryを実装しています。実務でリプレイスするなら、Jakarta Dataを使用するのが良いでしょう。
国際化がそのままできない!
Spring Bootは、独自の国際化の仕組み(いわゆる messages.properties というメッセージリソース)を持っています。これはThymeleafデフォルトの仕組みではありません。Thymeleafのデフォルトは、ファイルと同名のプロパティファイルを読み込む仕様です。*1
Krazoでは(というか、おそらくJakarta MVCの仕様では)、この国際化の仕組みがJakarta EE側と統合されておらず、ファイルごとにメッセージリソースを配置する必要が出てきます。
Krazoの実装を調べた限り、ここのカスタマイズは行えないようでした。報告は上げたので、今後の改善が期待されます。
今回は上記の事象があったため、国際化まわりについては移行を断念しました。
@ApplicationPathを使ってはいけない!
@ApplicationPathを使うことで、アノテーションベースで手軽にJakarta MVCを作り始めることができます。 しかし、これが大きな罠でした。
この形式で登録した場合、画像ファイルなどの静的リソースを透過的に扱うことができません。ベースであるJAX-RSが本来REST API(JSON/XML)を返すための仕様であり、静的リソースのハンドリングがあまり想定されていないからです。
そのため、解決策としてServletFilter形式でweb.xmlに登録します。
<!-- JAX-RS/Jakarta MVC Filter --> <filter> <filter-name>JakartaMvcFilter</filter-name> <filter-class>org.jboss.resteasy.plugins.server.servlet.FilterDispatcher</filter-class> <init-param> <param-name>jakarta.ws.rs.Application</param-name> <param-value>org.springframework.samples.petclinic.PetClinicApplication</param-value> </init-param> </filter> <filter-mapping> <filter-name>JakartaMvcFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
こうすることで、画像ファイルも透過的に扱えるようになります。このままだと、間違いなくJakarta MVCを使う上での「一番のFAQ」になってしまうでしょう……
こちらも報告はしたので、改善してくれると嬉しいですね。
JAX-RSのロード順の実装は必須
現実のアプリケーションではURLが複雑になり、複数のControllerでPATH(URL)が競合することがあります*2。 本来なら、URLに応じて最適なPATHを自動的に選んでくれればいい*3のですが、Jakarta MVCではそこの解決がうまくいかないらしく、明示的に解決順序を登録する必要がありました。
public class PetClinicApplication extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> classes = new HashSet<>(); //LinkedHashSetのほうが良い気がする・・・? // MVC Controllers classes.add(WelcomeController.class); classes.add(OwnerController.class); classes.add(PetController.class); classes.add(VisitController.class); classes.add(VetController.class); classes.add(CrashController.class); // REST Resources classes.add(VetResource.class); // Converters classes.add(LocalDateParamConverter.class); return classes; } }
ここらへんのURLの解決の仕方はうまくなってほしいところですねぇ。
ResponseとしてXMLを返す場合にはJAXBが必要
Jakarta MVCの問題ではなく、JAX-RSの問題ではあるのですが......
Spring Bootの場合は、特別なアノテーションを付けなくてもJacksonがいい感じに処理してくれましたが、JAX-RSでは次のようにJAXBアノテーションを明示する必要があります。
@XmlRootElement public class Vets { private List<Vet> vets; @XmlElement public List<Vet> getVetList() { return vets; } }
アノテーションを付け忘れた際のエラーが 「XML MessageBodyWriter not found」 と表示されるだけで非常に分かりにくいため、初見では確実にハマると思います*4。 実務でいちいちこれを対応するのは現実的ではないので、MessageBodyWriter あたりを自前でカスタマイズして、Jacksonで一括でレスポンスを返すようにしちゃったほうが良さそうですね。
ちなみにAIは、この原因をすぐに見つけ出してくれました。「こんなの知ってて当然でしょ?」くらいのトーンで出してきたので、おそらく定番のFAQなのだと思います。
その他
今回もAIに細かいノウハウについてまとめてもらいました。いくつか怪しい部分もありますが、現状のAIの限界を示すという意味も含めて、そのまま公開しています。
webjarsの解決をServletで実装する方法など、初めて知りました。AIの力は本当にすごいですね。
まとめ
早くJakarta MVCがもっと気軽に使えるようになってほしいものです。勉強会でも話題に上がっていましたが、各Jakarta EEサーバーベンダーは「フィーチャーリクエスト」という形でお客様の要望を受け付け、要望が多いものから実装・サポートを開始する仕組みを持っていたりします。
Jakarta EE 11でもOptional(任意)な仕様としてJakarta MVCは含まれているので、「早く使いたい!」という方は、ぜひ積極的にフィードバックやリクエストをあげていきましょう!
*1:15.2 Message Resolvers を参照してください
*2:foo と foo/bar の両方があった場合など
*3:最長一致したもの
*4:ちなみにJSONで返したい場合は、何もつけなくても大丈夫です
