My Note Pad

エンジニアリングや日々の雑感を書いていきます

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

これは2018年07月01日にQiitaに投稿した記事を移行したものです

はじめに

こちらでKotlin対応を行ったプロジェクトに対して、SpringBoot 2.0.2にアップデートを行いました。 ここではそのときに発生した作業などについて書いていきます。

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

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

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

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

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

変更点を抜粋

    <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レコードのみが影響を受けるのであれば、savedeleteを使う方がいいので、マイグレーションに割ける時間と相談で。

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

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

修正内容はpom.xmlspring-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

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

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複数データソース分

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

 @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);
    }
 }

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

 @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.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で返すなと・・・

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

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

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

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