Spring Boot 1.4系から2.0系へのマイグレーションでやったこと
これは2018年08月07日にQiitaに投稿した記事を移行したものです
はじめに
前回は、SpringBoot 1.5からSpring Boot 2.0へのバージョンアップを行いました。 今回は別のプロジェクトで、1.4から2.0に上げた際にハマった事などを書いていきます。
今回主に苦労したのは、
- 問題の原因が 1.4->1.5の部分なのか、1.5->2.0の部分なのかの切り分けに時間がかかった
- Thymeleafの影響が大きいのでテストが大変
- SpringSessionでのシリアライズ/デシリアライズまわり
- 本番デプロイ時の対策
あたりです。
Spring Bootのバージョンを上げる
spring-boot-starter-parentのバージョンを更新
pom.xmlでSpringBootのバージョンを指定します。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <relativePath /> </parent>
HikariPCの依存関係を削除
元々HikariPCを使用していたので、依存関係から削除します。
<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency>
コンパイルしてエラーを潰していく
SpringBootServletInitializerが見つからない
SpringBootServletInitializer
のパッケージが変わっているので、再importする
org.springframework.boot.context.embedded.が見つからない
パッケージが org.springframework.boot.web.servlet.
に変わっているので再importする
DataSourceBuilderが見つからない
パッケージが変わっているので再importする
WebMvcConfigurerAdapterの非推奨化
extends WebMvcConfigurerAdapter
を implements WebMvcConfigurer
に変更する
@EnableWebMvcSecurityの非推奨化
@EnableWebSecurity
に変更する。
https://docs.spring.io/spring-security/site/docs/current/reference/html/mvc.html
org.apache.velocity.appが見つからない
velocity
から mustache
に移行する。
- <dependency> - <groupId>org.apache.velocity</groupId> - <artifactId>velocity</artifactId> - </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-mustache</artifactId> + </dependency>
velocityのテンプレートファイル *.vm
を mustacheのテンプレートファイル *.mustache
に置換する。
※ここは application.properties
などで変更可
mustacheのテンプレートの形式に合わせて
${hoge} ↓ {{hoge}}
こんな感じで全部置き換える
shellとかで一括置換してもいいけど、IntelliJのリファクタでやると呼び出し元コードも発見して教えてくれるので数が少なければIntelliJのリファクタがいいかもしれない。
org.jsonが見つからない
以下の依存関係を追加する
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> </dependency>
以下のパッケージで importしなおす
org.springframework.boot.configurationprocessor.json.JSONObject
SpringApplication.runでエラー
SpringApplication.runの引数が変更になっている
以下のように修正
public static void main(String[] args) { - Object[] objects = { HogeApplication.class, FugaService.class }; - SpringApplication.run(objects, args); + final SpringApplication application = new SpringApplication(HogeApplication.class, FugaService.class); + application.run(args); }
ついでに以下のクラスを継承する
SpringBootServletInitializer
JPAのメソッド変更まわりの対応
黙々と直していく
AutoConfigureTestDatabaseが見つからない
import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestDatabase;
から
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
にパッケージを変更
Thymeleafのマイグレーション
https://www.thymeleaf.org/doc/articles/thymeleaf3migration.html
th:substitutebyを th:replaceに置き換える
こんな感じで機械的に
find src/main/resources -type f -name "*.html" -print | xargs sed -i -e 's/th:substituteby/th:replace/g'
linkタグのcss読み込みのtype="text/css"を削除
こんな感じで機械的に
find src/main/resources -type f -name "*.html" -print | xargs sed -i -e 's@type=\"text/css\"@@g'
inline="text" / inline="inline"を削除
一応中身を見ながら、削除していく。
Scriptタグ内のthymeleafが展開されない
こういうやつ
<script>(window.dataLayer || (window.dataLayer = [])).push(<span th:remove="tag" th:utext="${hoge}"/>)</script>
こんな風に修正する
<script type="text/javascript" th:inline="javascript">/*<![CDATA[*/ (window.dataLayer || (window.dataLayer = [])).push(/*[(${hoge})]*/) /*]]>*/</script>
その他
SpringBootでは@EnableWebMvcがついていたら削除する
SessionScope
SpringSecurityの onAuthenticationSuccess
実行時に @SessionScope
を参照できなくてエラー
Error creating bean with name 'user': Scope 'session' is not active for the current thread;
以下のようなコンフィグファイルを作成しておいておく
package jp.hoge; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.RequestContextListener; import javax.servlet.annotation.WebListener; @Configuration @WebListener public class WebRequestContextListener extends RequestContextListener { }
Hibernate SaveAndFlushメソッドでエラー
java.sql.SQLSyntaxErrorException: Table 'hoge.hibernate_sequence' doesn't exist at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:536) at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:513) at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:115) at com.mysql.cj.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:1983) at com.mysql.cj.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1826) at com.mysql.cj.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1923) at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
GenerationType
を変更する
- @GeneratedValue(strategy=GenerationType.AUTO) + @GeneratedValue(strategy=GenerationType.IDENTITY)
実行中にHibernateエラー
org.springframework.dao.InvalidDataAccessResourceUsageException: error performing isolated work; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: error performing isolated work at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:242) at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:225) at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527) at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242) at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:135) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212) at com.sun.proxy.$Proxy127.save(Unknown Source) at jp.hoge.service.FugaService.execute(FugaService.java:218) at jp.hoge.controller.FugaController.execute(FugaController.java:101) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498)
HibernateのGenerator mappingsが変更になっている。
application.propertiesに以下の設定を追加することで対処
spring.jpa.hibernate.use-new-id-generator-mappings=false
STGなどへのデプロイ後
プロファイルの読み込みエラー
logback-spring.xml
の <springProfile>
でプロファイルが正しく読み込めずにエラー
executableJarの場合、-Dspring-boot.run.profiles=環境名
のように定義すればいい。
今回のプロジェクトではtomcatにwarをデプロイするので、
application.properties
に spring-boot.run.profiles=環境名
みたいに定義してやる。
実際のところは、pom.xmlでwarファイル生成時にプロファイルを分けているので、以下のように定義しています。
spring-boot.run.profiles=${spring.profiles.active}
<profiles> <profile> <id>local</id> <properties> <spring.profiles.active>local</spring.profiles.active> </properties> </profile> <profile> <id>stg</id> <properties> <spring.profiles.active>stg</spring.profiles.active> </properties> </profile> </profiles>
ビルド時にプロファイル
mvn package -Pstg
という感じで、 application.properties
に埋め込んでいる。
SpringSecurity経由でのログイン後にSession取得後のUserオブジェクトのメンバ変数が全てnullになっている
Userのメンバ変数にアクセスしようとしてNullPointerExepotionが発生 とはいえ、Redisの中身を見てもシリアライズされているため読めない・・・
※Memberは @SessionScope
一旦、以下のような感じでJSONに変換してRedisに登録してくれるSerializerを作成する
@ConditionalOnProperty(name = "spring.session.store-type", havingValue = "redis", matchIfMissing = false) @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 20 * 24 * 60 * 60) //default=1800 -> 20days @Configuration public class HttpSessionConfig implements BeanClassLoaderAware { @Autowired private LettuceConnectionFactory lettuceConnectionFactory; private ClassLoader classLoader; @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer() { final ObjectMapper mapper = new ObjectMapper() .registerModules(SecurityJackson2Modules.getModules(classLoader)) .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); return new GenericJackson2JsonRedisSerializer(mapper); } // Elasticache用 @Bean public static ConfigureRedisAction configureRedisAction() { return ConfigureRedisAction.NO_OP; } @Override public void setBeanClassLoader(final ClassLoader classLoader) { this.classLoader = classLoader; } }
そうするとlogin完了後からControllerに遷移する際に、全フィールドnullの状態でRedisにセットされていた。
ログイン後のControllerで User
オブジェクト内の id
がnullかどうか判定し、nullであれば詰め直すように対応。
ログイン以降は問題なくセッションを扱えている。
本当はJSON形式でセッション情報を持てるようにしたかったものの、Userクラスの構造が複雑なネスト構造になっていたため、時間との兼ね合いで断念・・・ Userクラスに情報をもたせすぎ・・・・
本番デプロイ時の対応
SpringSession のバージョンアップに伴い、内部的にシリアライズ・デシリアライズで扱う SerialVersionUID
が変わっているらしいので、バージョンアップしたコードをデプロイすると、既にログイン中のユーザーはセッション情報をデシリアライズできなくて詰む。
なので、デシリアライズするときの対応を入れる。
https://sdqali.in/blog/2016/11/02/handling-deserialization-errors-in-spring-redis-sessions/
少し記事が古く、依存ライブラリが変更になっているので、以下の通り変更している。
- public class HttpSessionConfig { + public class HttpSessionConfig extends RedisHttpSessionConfiguration { + @Autowired + RedisTemplate<Object, Object> redisTemplate; + @Bean + @Override + public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) { + return super.springSessionRepositoryFilter(new SafeDeserializationRepository<>(sessionRepository, redisTemplate)); + }
public class SafeDeserializationRepository<S extends Session> implements SessionRepository<S> { private final SessionRepository<S> delegate; private final RedisTemplate<Object, Object> redisTemplate; private static final String BOUNDED_HASH_KEY_PREFIX = "spring:session:sessions:"; public SafeDeserializationRepository(SessionRepository<S> delegate, RedisTemplate<Object, Object> redisTemplate) { this.delegate = delegate; this.redisTemplate = redisTemplate; } @Override public S createSession() { return delegate.createSession(); } @Override public void save(S session) { delegate.save(session); } @Override public S findById(String id) { try { return delegate.findById(id); } catch(SerializationException e) { log.info("Deleting non-deserializable session with key {}", id); redisTemplate.delete(BOUNDED_HASH_KEY_PREFIX + id); return null; } } @Override public void deleteById(String id) { delegate.deleteById(id); } }
これで既存のユーザーは、一度ログアウトさせられ、再ログインで新しいSessionデータをRedisに保存し、以降はそちらを参照する。
まとめ
大体、上記の対応で本番環境で動作するようになりました。 (他にもある気がするけど)
また、本来であればTomcatは8.5系以上にしなければならないのですが、8.0で問題なさそうであったため、Tomcatのバージョンアップは見送りました。
SpringBootのメジャーバージョンをいくつか飛ばしてしまうと、とてもつらいのでバージョンアップはこまめにやろう・・・!!