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レコードのみが影響を受けるのであれば、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
@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
これは複数のデータソースを扱っているために発生しているようでした。
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); }
以上で、マイグレーションが完了し、移行させることができました。