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

# はじめに


[前回](https://qiita.com/Yuki10/items/b4394bd6278b3ae8eb2b) は、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](https://qiita-image-store.s3.amazonaws.com/0/24467/f2c6d88e-790b-903c-80cd-87cb09621336.png)

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

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

![2.png](https://qiita-image-store.s3.amazonaws.com/0/24467/fbc5ae40-45a9-b11e-2235-4c5238232ac8.png)

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

### DataSourceBuilderが見つからない

![3.png](https://qiita-image-store.s3.amazonaws.com/0/24467/ee553272-db1c-b6ef-58c0-eec986c29dde.png)

パッケージが変わっているので再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が見つからない

![4.png](https://qiita-image-store.s3.amazonaws.com/0/24467/6cdead28-088b-9ab2-87de-24fbbc1dab72.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](https://qiita-image-store.s3.amazonaws.com/0/24467/cb9670f3-7918-486d-2a5c-dc548068d667.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](https://qiita-image-store.s3.amazonaws.com/0/24467/c0e2ca79-f9eb-4139-582b-d39c7872c51a.png)

SpringApplication.runの引数が変更になっている

以下のように修正

```java
        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](https://qiita-image-store.s3.amazonaws.com/0/24467/46f13385-b050-2f14-84dc-88083016f29b.png)

黙々と直していく

https://spring.io/blog/2017/06/20/a-preview-on-spring-data-kay#improved-naming-for-crud-repository-methods

### AutoConfigureTestDatabaseが見つからない

![8.png](https://qiita-image-store.s3.amazonaws.com/0/24467/8c5cd549-d282-90cf-964a-ac4bde88c5bd.png)


`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に置き換える

こんな感じで機械的に

```bash
find src/main/resources -type f -name "*.html" -print | xargs sed -i  -e 's/th:substituteby/th:replace/g'
```

### linkタグのcss読み込みのtype="text/css"を削除

こんな感じで機械的に

```bash
find src/main/resources -type f -name "*.html" -print | xargs sed -i  -e 's@type=\"text/css\"@@g'
```

### inline="text" / inline="inline"を削除

一応中身を見ながら、削除していく。

### Scriptタグ内のthymeleafが展開されない

こういうやつ

```js
<script>(window.dataLayer || (window.dataLayer = [])).push(<span th:remove="tag" th:utext="${hoge}"/>)</script>
```

こんな風に修正する

```js
<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.properties` に `spring-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>
```

ビルド時にプロファイル

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

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

https://sdqali.in/blog/2016/11/02/handling-deserialization-errors-in-spring-redis-sessions/

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


```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のメジャーバージョンをいくつか飛ばしてしまうと、とてもつらいのでバージョンアップはこまめにやろう・・・！！

# 参考ページ

- [Spring Boot 2.0 Migration Guide](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide)
- [Spring Boot Security 2.0](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Security-2.0)
- [Common application properties](https://github.com/spring-projects/spring-boot/blob/56a7aa552f04b9b9276255c278346ce5b8686567/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc)
- [Spring Session](https://docs.spring.io/spring-session/docs/current/reference/html5/)
- [Spring Security 4.2 主な変更点](https://qiita.com/kazuki43zoo/items/ef6cc6d05d68a8cb7a0e)
- [Spring Boot 1.4 Release Notes](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.4-Release-Notes#generator-mappings)