5
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Spring Boot 1.4系から2.0系へのマイグレーションでやったこと

Posted at

はじめに

前回 は、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のバージョンを指定します。

pom.xml
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version>
    <relativePath />
  </parent>

HikariPCの依存関係を削除

元々HikariPCを使用していたので、依存関係から削除します。

pom.xml
    <dependency>
	<groupId>com.zaxxer</groupId>
	<artifactId>HikariCP</artifactId>
    </dependency>

コンパイルしてエラーを潰していく

SpringBootServletInitializerが見つからない

1.png

SpringBootServletInitializer のパッケージが変わっているので、再importする

org.springframework.boot.context.embedded.が見つからない

2.png

パッケージが org.springframework.boot.web.servlet. に変わっているので再importする

DataSourceBuilderが見つからない

3.png

パッケージが変わっているので再importする

WebMvcConfigurerAdapterの非推奨化

extends WebMvcConfigurerAdapterimplements WebMvcConfigurer に変更する

@EnableWebMvcSecurityの非推奨化

@EnableWebSecurity に変更する。

org.apache.velocity.appが見つからない

4.png

velocity から mustache に移行する。

pom.xml
-    <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が見つからない

5.png

以下の依存関係を追加する

pom.xml
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-json</artifactId>
	</dependency>

以下のパッケージで importしなおす
org.springframework.boot.configurationprocessor.json.JSONObject

SpringApplication.runでエラー

6.png

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のメソッド変更まわりの対応

7.png

黙々と直していく

AutoConfigureTestDatabaseが見つからない

8.png

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;

以下のようなコンフィグファイルを作成しておいておく

WebRequestContextListener.java
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に以下の設定を追加することで対処

application.properties
spring.jpa.hibernate.use-new-id-generator-mappings=false

STGなどへのデプロイ後

プロファイルの読み込みエラー

logback-spring.xml<springProfile> でプロファイルが正しく読み込めずにエラー
executableJarの場合、-Dspring-boot.run.profiles=環境名 のように定義すればいい。

今回のプロジェクトではtomcatにwarをデプロイするので、
application.propertiesspring-boot.run.profiles=環境名 みたいに定義してやる。

実際のところは、pom.xmlでwarファイル生成時にプロファイルを分けているので、以下のように定義しています。

application.properties
spring-boot.run.profiles=${spring.profiles.active}
pom.xml
<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を作成する

HttpSessionConfig.java
@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 が変わっているらしいので、バージョンアップしたコードをデプロイすると、既にログイン中のユーザーはセッション情報をデシリアライズできなくて詰む。

なので、デシリアライズするときの対応を入れる。

少し記事が古く、依存ライブラリが変更になっているので、以下の通り変更している。

HttpSessionConfig.java
- 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));
+	}
SafeDeserializationRepository.java
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のメジャーバージョンをいくつか飛ばしてしまうと、とてもつらいのでバージョンアップはこまめにやろう・・・!!

参考ページ

5
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?