はじめに
前回 は、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
に変更する。
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のマイグレーション
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
が変わっているらしいので、バージョンアップしたコードをデプロイすると、既にログイン中のユーザーはセッション情報をデシリアライズできなくて詰む。
なので、デシリアライズするときの対応を入れる。
少し記事が古く、依存ライブラリが変更になっているので、以下の通り変更している。
- 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のメジャーバージョンをいくつか飛ばしてしまうと、とてもつらいのでバージョンアップはこまめにやろう・・・!!