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

# はじめに

[こちら](https://qiita.com/Yuki10/items/3dd563b76dceebbe154b)でKotlin対応を行ったプロジェクトに対して、SpringBoot 2.0.2にアップデートを行いました。
ここではそのときに発生した作業などについて書いていきます。

- マイグレーション前
 - 1.5.8.RELEASE

- マイグレーション後
 - 2.0.2.RELEASE

※Spring Boot2.0からはJava8以降が必須になっています。

また、このプロジェクトはREST APIのサーバーとして動作しているため、Thymeleafのマイグレーションなどは回避することができました。
別途、Spring MVCのプロジェクトをマイグレーションしているので、そのあたりは別で書こうと思います。

### 以下のサイトや記事を主に参考にさせていただきました。

- [Spring-Boot-2.0-Migration-Guide](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide)
- [Spring Boot 1.5.10 → Spring Boot 2.0.0 にしたときの覚書](https://qiita.com/yukina-ge/items/1ca029ed69494bfd36d6#1-gradlew%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%A2%E3%83%83%E3%83%97)

# まずはSpring Bootのバージョンアップ

`pom.xml`を修正してSpring Bootのバージョンを上げていきます。

変更点を抜粋

```pom.xml
    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
 -    <version>1.5.8.RELEASE</version>
 +    <version>2.0.2.RELEASE</version>
      <relativePath />
    </parent>

・・・

      <dependency>
        <!-- https://docs.spring.io/spring-session/docs/current/reference/html5/guides/java-redis.html -->
        <groupId>org.springframework.session</groupId>
 -      <artifactId>spring-session</artifactId>
 +      <artifactId>spring-session-data-redis</artifactId>
      </dependency>

・・・

      <!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#jackson--json-support -->
 +    <dependency>
 +      <groupId>org.springframework.boot</groupId>
 +      <artifactId>spring-boot-starter-json</artifactId>
 +    </dependency>

・・・
      <!- 元々HikariPCを使用していたので依存関係を削除 -->
      <!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#configuring-a-datasource -->
 -    <dependency>
 -      <groupId>com.zaxxer</groupId>
 -      <artifactId>HikariCP</artifactId>
 -    </dependency>

・・・

      <!-- ここはテストが終わったら削除 ->
      <!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#before-you-start -->
 +    <dependency>
 +      <groupId>org.springframework.boot</groupId>
 +      <artifactId>spring-boot-properties-migrator</artifactId>
 +      <scope>runtime</scope>
 +    </dependency>
```

## ApplicationクラスのWebMvcConfigurerAdapterを使わないようにする

```
- public class XxxApplication extends WebMvcConfigurerAdapter {
+ public class XxxApplication implements WebMvcConfigurer{
```

# ビルド->修正->再ビルド

`pom.xml`の設定が終わったら、ビルドして修正を繰り返していきます。

## JPA RepositoryのfindOneでコンパイルエラー
まずは以下のようなエラーが発生しました。

```
Error:(41, 41) Kotlin: Type inference failed: fun <S : #{テーブル名}!> findOne(p0: Example<S!>!): Optional<S!>!
cannot be applied to
(Long)
Error:(41, 49) Kotlin: Type mismatch: inferred type is Long but Example<(???..???)>! was expected
```

これはSpring DataのCRUDメソッドが変更になっていることに起因します。
https://spring.io/blog/2017/06/20/a-preview-on-spring-data-kay#improved-naming-for-crud-repository-methods

1行目のエラーは、`findOne`から`findById`に変更する。
2行目のエラーは、`findById`の戻り値が`JavaのOptional`になっているので、`Optional`から`get`する。

同様に、`save`, `delete`の引数も`List`を渡すものから`Entity`を渡すものに変わっているため、代わりに `saveAll`, `deleteAll` メソッドを呼ぶようにする。
もちろん、1レコードのみが影響を受けるのであれば、`save`、`delete`を使う方がいいので、マイグレーションに割ける時間と相談で。

## ConfigureRedisActionを解決できない

```
Error:(13, 53) java: パッケージorg.springframework.session.data.redis.configは存在しません
```

順番が前後していますが、ElastiCacheを利用するために設定している以下の定義で、`ConfigureRedisAction`が解決できなくなっていました。
これは`spring-session-data-redis`というパッケージに切り出されたためです。

https://docs.spring.io/spring-session/docs/current/reference/html5/guides/java-redis.html

```java
	@Bean
	public static ConfigureRedisAction configureRedisAction() {
		return ConfigureRedisAction.NO_OP;
	}
```

修正内容は`pom.xml`の`spring-session`の部分を参照。
また、`lettuce`の依存関係を追加していないのは、元々`spring-boot-starter-data-redis`が依存関係に含まれているためです。

## org.hibernate.validator.constraints.NotBlankなどの非推奨化

これに合わせて、`javax.validation.constraints.NotEmpty`などを使うように置き換えています。


## jdbcのautoconfigureでエラー

```
Error:(7, 51) java: シンボルを見つけられません
  シンボル:   クラス DataSourceBuilder
  場所: パッケージ org.springframework.boot.autoconfigure.jdbc
```

これは複数のデータソースを扱っているために発生しているようでした。


```application.properties
hoge.datasource.driver-class-name        = com.mysql.jdbc.Driver
hoge.datasource.url                    = jdbc:mysql://127.0.0.1:33306/hoge
hoge.datasource.username                = hoge
hoge.datasource.password                = fuga
# その他は省略
# 上記x複数データソース分
```

元々は以下のように設定されていました

```HogeDatabaseConfig.java
 @Configuration
 @EnableTransactionManagement
 @EnableJpaRepositories(entityManagerFactoryRef = "hogeEntityManagerFactory", transactionManagerRef = "hogeTransactionManager", basePackages = {
 		"jp.xxx.data.hoge.repository" })
 public class HogeDatabaseConfig extends AbstractDatabaseConfig {
 
 	@Bean(name = "hogeDataSource")
	@ConfigurationProperties(prefix = "hoge.datasource")
 	public DataSource hogeDataSource() {
		return DataSourceBuilder.create().build();
 	}
 
 	@Bean(name = "hogeEntityManagerFactory")
	public LocalContainerEntityManagerFactoryBean hogeEntityManagerFactory(EntityManagerFactoryBuilder builder,
		return builder.dataSource(hogeDataSource)
		        .packages("jp.xxx.data.hoge.entity").persistenceUnit("hoge")
				.properties(buildProperties()).build();
 	}
 
 	@Bean(name = "hogeTransactionManager")
 	public PlatformTransactionManager hogeTransactionManager(
 			@Qualifier("hogeEntityManagerFactory") EntityManagerFactory hogeEntityManagerFactory) {
 		return new JpaTransactionManager(hogeEntityManagerFactory);
 	}
 }
```

が、以下のように修正しました。
この部分についてはもう少しうまいやり方があるんじゃないかなーとは思っています。


```HogeDatabaseConfig.java
 @Configuration
 @ConfigurationProperties(prefix = "hoge.datasource")
 @EnableTransactionManagement
 @EnableJpaRepositories(entityManagerFactoryRef = "hogeEntityManagerFactory", transactionManagerRef = "hogeTransactionManager", basePackages = {
 		"jp.xxx.data.hoge.repository" })
 public class HogeDatabaseConfig extends AbstractDatabaseConfig {
 
	@Autowired
	private Environment env;

 	@Bean(name = "hogeDataSource")
 	public DataSource hogeDataSource() {
		final DriverManagerDataSource dataSource = new DriverManagerDataSource();
		dataSource.setDriverClassName(env.getProperty("hoge.datasource.driver-class-name"));
		dataSource.setUrl(env.getProperty("hoge.datasource.url"));
		dataSource.setUsername(env.getProperty("hoge.datasource.username"));
		dataSource.setPassword(env.getProperty("hoge.datasource.password"));
		return dataSource;
 	}
 
 	@Bean(name = "hogeEntityManagerFactory")
	public EntityManagerFactory hogeEntityManagerFactory(
 			@Qualifier("hogeDataSource") DataSource hogeDataSource) {
		final LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
		factoryBean.setDataSource(hogeDataSource);
		factoryBean.setPersistenceUnitName("hoge");
		factoryBean.setPackagesToScan("jp.xxx.data.hoge.entity");
		factoryBean.setJpaPropertyMap(buildProperties());
		factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
		factoryBean.afterPropertiesSet();
		return factoryBean.getNativeEntityManagerFactory();
 	}
 
 	@Bean(name = "hogeTransactionManager")
 	public PlatformTransactionManager hogeTransactionManager(
 			@Qualifier("hogeEntityManagerFactory") EntityManagerFactory hogeEntityManagerFactory) {
 		return new JpaTransactionManager(hogeEntityManagerFactory);
 	}
 }
```

## ここでTomcatは起動

Tomcatは起動したものの、以下のエラーログが流れていました。

```
java.lang.ClassCastException: jp.xxx.service.HogeService$Fuga cannot be cast to org.springframework.core.io.support.ResourceRegion
	at org.springframework.http.converter.ResourceRegionHttpMessageConverter.writeResourceRegionCollection(ResourceRegionHttpMessageConverter.java:182)
	at org.springframework.http.converter.ResourceRegionHttpMessageConverter.writeInternal(ResourceRegionHttpMessageConverter.java:139)
	at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:102)
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:272)
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:180)
	at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:119)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
```

データソースにはPostgreSQLもあるので発生しているようです。

これは`hibernate.properties`を作成して配置しておくことで回避しました。

```hibernate.properties
hibernate.jdbc.lob.non_contextual_creation = true
```

ここまでで起動までにエラーは発生しなくなりました。

## 突然のClassCastException

```
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method jp.xxx.controller.HogeController.createFuga, parameter form
	at jp.xxx.controller.HogeController.createCampaign(HogeController.kt)
	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)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:209)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
```

`form`の引数の型を`HogeForm`から`HogeForm?`のnull許容型に変換して対応しました。

## 日付型の変換エラー

```
Failed to convert value of type 'java.lang.String[]' to required type 'java.util.Date'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2018-06-11T00:00:00'; nested exception is java.lang.IllegalArgumentException
```

ここは少しハマって原因がよくわからない部分ではあったのですが、データを受け取っていたオブジェクトがKotlinのDataクラスだったものを、普通のKotlinクラスにすることで動作しました。

## PUTだとformの値が渡らない

原因を調べる時間がなかったのでPUTしている部分をPOSTに書き換えました・・・

## REST Controllerの戻り値の型が`List<Object>`だとJSONに変換できずにエラーになる。

エラーログ取り忘れ。

元々はこんな実装
そもそもList<Object>で返すなと・・・

```java
    @GetMapping("/hoge")
    public List<Object> hoge() {
        return hogeService.hoges();
    }
```

戻りの型が結構複雑だったので、Controller側でJSONにして返すようにしました。


```java
    @GetMapping("/hoge")
    public String hoge() throws JsonProcessingException {
        final List<Object> hoges = hogeService.hoges();
        final ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(hoges);
    }
```

以上で、マイグレーションが完了し、移行させることができました。