今年は競馬ネタばっかりだったのでたまには技術的なネタでも書いてみることにする。
ちょうどお仕事でSpringBoot2.2の移行検証をやったので忘備録的に残しておく。
はじめに
お仕事でSpringBoot1.5系を使っていたがSpringBoot1.5は2019年8月にEOLを迎えた。
そのためバージョンアップをそろそろ検討しなければならない。
現在のSpringBootの最新バージョンは2.1.9だ。
ではこれに変えればいいのかというと一つ問題がある。
SpringBootが依存しているSpring Frameworkは5.1系なのだが2019年12月にはEOLとなってしまう。
EOL対応をしてもまたすぐEOLを迎えてしまうのは微妙なのでまだPRE版だがSpringBoot2.2を試すことにした。
※現在はすでに正式版のSpringBoot2.2が出ています。
前提
お仕事プロジェクトのざっくりした構成は以下のようになる。
- webアプリケーションサーバ:Servlet+JSP+Javascript tomcat起動
- apiサーバ(DB):REST+swaggerでドキュメント作成 データベースアクセスあり tomcat起動
- apiサーバ(その他):REST+swaggerでドキュメント作成 jetty起動
- 基本的にLinuxサーバ上で稼働。apiサーバの一部のみWindows上で稼働。
またプロジェクトで利用している主なミドルウェア・ライブラリは次の通り
- Java v1.8(Oracle JDK)
- MySQL 5.7.x(5.7.27)
- tomcat v8.5.x(8.5.40)
- jetty v9.4.5
- SpringBoot v1.5.12
- Thymeleaf v2.1.6(SpringBoot依存)
- MyBatis v3.4.4(mybatis-spring-boot-starter v1.3.0依存)
- Swagger v1.5.10(springfox-swagger2 v2.6.1依存)
- Lombok v1.16.16(SpringBoot依存)
- maven v3.5.2
- SpringToolSuite(STS) 3.8.x(3.8.4)
上記構成でSpringBoot2.2.0.RC1を試してみた。ちなみにJavaはVMをOpenJDKに変えて検証した。
検証まとめ
はじめに検証結果を先に書いておく。検証手順および理由は後で書く。
- ミドルウェアの対応が必須
- jettyを9.4.5から9.4.9以上に上げる必要がある
- tomcatの起動オプションに-Dspring.jndi.ignore=trueを追加する必要がある
- 一部クラスのパッケージ名・利用メソッドの変更が必須 → コンパイルエラーがたくさん
- プロパティファイルの変更も必須
検証手順
事前情報
SpringBoot1.5系からSpringBoot2系へのマイグレーション情報があるためまずはそれを読む。
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide
pom修正
pom.xmlの依存関係の修正をおこなう。
現行プロジェクトでは親プロジェクトの下にそれぞれモジュール毎の子プロジェクトが存在する。
STSでSpringBootプロジェクトを作成するとデフォルトの親プロジェクトはSpringBootとなるがこの依存は外している。
代わりに親プロジェクトに依存関係としてplatform-bomを追加して利用していた。
だがすでにこれはEOLを迎えているため代わりの定義が必要になる。
<dependencymanagement>
<dependencies>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-dependencies</artifactid>
<version>2.2.0.RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencymanagement>
またSpringBoot2.2はまだmavenのセントラルレポジトリには登録されていないのでレポジトリ指定も必要。
<repositories>
<repository>
<id>spring-snapshots</id>
<url>https://repo.spring.io/snapshot
<snapshots><enabled>true</enabled></snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<url>https://repo.spring.io/milestone
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<url>https://repo.spring.io/snapshot
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<url>https://repo.spring.io/milestone
</pluginRepository>
</pluginRepositories>
STS上で親プロジェクト右クリック>Maven>update projectを実施する。ちなみに事前にBuild Automatically(Buildメニュー)は一時的に無効にしておいたほうがいい。一気にビルドが走るため非常に時間がかかる(というかSTSが死んだ)。一つ一つ子プロジェクト毎にビルドすること。
このときコンパイルエラーが山程出るので修正することにする。
ソース修正
まずはコンパイルエラーレベルの修正をする。
- パッケージの変更
SpringBootServletInitializer,ErrorController,ErrorAttribute,LocalServerPortsなどが変わっている。このあたりはSTSでCtrl+Shit+Oで一発。らくちん。
- 引数の変更
RestTemplateBuilderのsetConnectTimeoutやsetReadTimeout。引数がintからDurationに変わっている。時刻関連の扱いは基本的にDurationになったっぽい。
.setConnectTimeout(profile.getConnectTimeout_msecs()) → .setConnectTimeout(Duration.ofMillis(profile.getConnectTimeout_msecs()))
ErrorAttributes.getError。引数がServletRequestAttributesからWebRequestに変わっている。
- コンパイルの警告修正
@NotEmptyと@NotBlankが非推奨になった。正確にはorg.hibernate.validator.constraintsパッケージのものが非推奨となった。
標準のjavax.validation.constraintsに変更。これに伴いmessage.propertiesにエラーメッセージ(日本語)を追加。
javax.validation.constraints.NotBlank.message=必須項目です。
org.springframework.http.MediaType.APPLICATION_JSON_UTF8が非推奨となった。これによりapplication/json;charset=UTF-8というContent-Typeは非推奨となった。APPLICATION_JSONに置き換えた。単体試験で大量に使っていたためSTSで一括置換。便利。
- 設定ファイルの警告修正
設定ファイルの設定名が変わっているため変更。ちなみにプロパティファイルの警告はSpring Yaml Properties Editorで開かないと警告はでない。
spring.messages.cache-seconds → spring.messages.cache-duration
endpoints.enabled → management.endpoints.enabled-by-default
endpoints.health.enabled → management.endpoint.health.enabled
server.context-path → server.servlet.context-path ※jar起動しているアプリケーションのみ
security.filter-order → spring.security.filter.order
endpoints.health.path → management.endpoints.web.path-mapping.health
上記のものはエディタ上でエラー表示と共に何に変えればいいか出力されるのでそれに従えばよい。
security.basic.enabled
security.enable-csrf
management.security.enabled
これらはWebSecurityConfigurerで定義するようになった。プロパティによる有効・無効の切り替えは不可能になった。環境毎に変えたければ独自のプロパティを作成してWebSecurityConfigurerで設定する感じになる。
お仕事のプロジェクトではデフォルト設定(BASIC認証は無効でCSRFチェックは有効)で問題ないためプロパティを削除するのみ。
management.endpoints.web.base-pathの設定を追加。healthチェックなどはデフォルトだと/actuator/healthなどとtopが変わる。既存踏襲とするために設定を追加した。
単体試験による確認
事前準備としてpowermockのバージョンを2.0.2に変更した。SpringBoot2系ではMockito1.xからMockito2.0に変わったため。以下は単体試験実施時に発生したエラーをもとに修正した内容になる。
- 振る舞いが変わっている
org.springframework.http.HttpStatus.toString()の応答結果がStatusコード(数値文字列)からコード+名称に変わった。 例えば404は404 Not Foundと返る。
- Thymeleafのjson変換方法が変わっている
お仕事プロジェクトではWebサーバにおいてJSへデータを渡すためにThymeleafにてjavaオブジェクトをjsonに変換して書き出しているがその書き出し内容が変わっていた。
・enumの出力が{$type:string, $name:string} からstringへ変わった。
例) Hoge.ONE → {'$type':'Hoge', '$name':'ONE'} → "ONE"
・stringの括りが'から"へ変更。
- 応答結果の検証(assert)が文字化けのためエラーとなる
MediaTypeがapplication/jsonとなったことに伴い、応答結果の検証で日本語部分が文字化けてエラーとなってしまう。
具体的にはMockMvcを使った以下の検証が文字化ける。
mvc.perform(post("/hoge")
~略~
.with(csrf()))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"message\":\"エラーなんだけど?\"")))
今まではContext-Typeがapplication/json;charset=UTF-8となっていたため問題なかったがcharsetがなくなったことにより単体試験実施端末のデフォルト文字コードでエンコードされるようになり不一致となった(Windows上で実施)。
これはjsonとして明示的に比較すればきちんとUTF-8でエンコードされるようになる。
.andExpect(jsonPath("$.message").value("エラーなんだけど?"))
また、比較項目が多数あるため直接jsonとして比較していたものも同様のエラーが出ていた。
.andExpect(content().json(JsonUtil.convert(expectedList))) → JsonUtilでリストをjson化
この場合は、byteとして直接変換すればよい。
byte expectedContent = JsonUtil.convert(expectedList).getBytes(StandardCharsets.UTF_8);
~略~
.andExpect(content().bytes(expectedContent))
- エラーの応答結果が文字化ける
応答結果がエラーの場合(HttpStatusが400系か500系)の応答結果をjsonからJavaオブジェクトに変換すると日本語が文字化けている。これはお仕事プロジェクト固有の問題。
プロジェクトではエラー応答の場合、HttpStatusCodeExceptionからresponseBodyを受け取り(jsonの想定)それをjavaオブジェクトに変換している。
このresponseBodyはgetResponseBodyAsStringで受け取っているがこの処理はbyteをDEFAULT_CHARSETでString化している。このDEFAULT_CHARSETは実行している環境で変わる。実行している環境はWindowsであるためUTF-8ではエンコードされず文字化けた。
対応としてはgetResponseBodyAsByteArrayで応答結果をbyte[]で受け取りそれをUTF_8でエンコードするように処理を変更した。
- ErrorControllerのdispatchが変更
error(HttpServletRequest req, HttpServletResponse res, WebRequest webRequest)のreqのMethodはエラーが発生した元のmethodだったが、変更後はreqのMethodはDispatch後のメソッド(GET)になった。
この影響もプロジェクト固有の問題。プロジェクトではHttpRequestMethodNotSupportedExceptionが発生した時のエラー処理においてエラーが発生したHTTP Methodを取得するためにreqから取得していたがこれがNGとなった(DELETE非サポートの確認でログ上にはDELETEと出力されるはずがGETとログ出力されている)。HttpRequestMethodNotSupportedExceptionからMethodを受け取るように処理を変更した。
起動による確認
- apiサーバ(DB)が起動しなかった
tomcat上(STSではPivotal tc Server上)で起動しなかった。tomcatv9.0でも駄目。SpringBootアプリケーションとして起動するとなぜか起動した。
起動しない場合は以下のエラーが出る。エラー箇所は常にParameterMappingではなくResultMapになるときもあった。
Description:
Failed to bind properties under 'mybatis.configuration.mapped-statements[0].parameter-map.parameter-mappings[0]' to org.apache.ibatis.mapping.ParameterMapping:
Reason: Failed to extract parameter names for org.apache.ibatis.mapping.ParameterMapping(org.apache.ibatis.mapping.ParameterMapping$1)
Action:
Update your application's configuration
tomcatのVMオプションに-Dspring.jndi.ignore=trueを付けることで起動するようになった。ちなみにspring.propertiesに左記プロパティを追加してもいけるらしい。application.ymlでは駄目なので注意すること。
上記プロパティを指定しないと何故かmyBatisのプロパティ読み込みが失敗してしまう。SpringBootアプリケーションならば問題なく起動するのだが。
以下は参考にしたURL。
→ これを見るとfixしたらしいのだが……? デグレった?
- apiサーバ(その他)が起動しない
jetty上で起動しなかった。STS上(Pivotal tc Server)では起動した。
jettyの場合は以下のエラーが出る。
WebAppContext:main: Failed startup of context o.e.j.w.WebAppContext..略
java.lang.RuntimeException: Error scanning entry module-info.class from jar ..略
java.lang.RuntimeException: Error scanning entry META-INF/versions/9/org/apache/logging/log4j/util/ProcessIdUtil.class from jar ..略
どうも依存ライブラリ中にjava9対応のものがあると9.4.9より前のjettyだとエラーとなってしまうらしい。SpringBootというよりはjettyの問題であるようだ。
jettyを9.4.9にすることで起動した。
ちなみにjettyのコンソールログを出力するにはにはstart.iniに--module=console-captureを追記するとよい。
参考にしたURLは以下。
java - Error scanning entry "module-info.class" when starting Jetty server - Stack Overflow
もう一つの解決方法としては依存しているライブラリのバージョン(classmateとlog4j-api)をさげてビルドすることだ(classmateは1.4,log4j-apiは2.8.2)。
だが一応起動はするが素直にjettyのバージョンを上げたほうがよさげではある。
追加検証
SpringBoot2.2.1が正式にリリースとなったので改めて検証した。
そしてtomcat起動不能の件は解消していることを確認した(VMオプションの指定がなくても大丈夫)。
jettyの件はjettyの問題であるためやはり駄目だった。