Help us understand the problem. What is going on with this article?

Kotlin with Spring Boot 2.0で簡単なRest APIを実装する

More than 1 year has passed since last update.

概要

以前にKotlin with Spring Boot 1.5で簡単なRest APIを実装するというタイトルで書いた記事のSpring Boot 2.0版です。
現在(2018/2)の最新バージョンであるRC2を使用しています。

  • 2018/3/1にSpring Boot 2.0.0 GAがリリースされました。(Spring Boot 2.0 goes GA)
  • 2018/5/9にSpring Boot 2.0.2がリリースされましたのでソースコードおよび記事を修正しました。(Spring Boot 2.0.2)

ソースコードはrubytomato/demo-kotlin-spring2にあります。

環境

  • Windows 10 Professional
  • Java 1.8.0_172
  • Kotlin 1.2.31
  • Spring Boot 2.0.2
    • Spring Framework 5.0.5
    • MySQL 5.7.19
    • Gradle 4.5.1
  • IntelliJ IDEA 2017.3

参考

1.5から移行する場合

Spring Boot 2.0 Migration Guideが用意されています。

アプリケーション

アプリケーションのひな型はSpring Initializrで生成しました。
生成したbuild.gradleのkotlinのバージョンには1.2.20が指定されていますが、Spring Boot
2.0.0は1.2.21までサポートしているので、この記事では1.2.21を使用しています。

build.gradle

buildscript {
    ext {
        kotlinVersion = "1.2.31"
        springBootVersion = "2.0.2.RELEASE"
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
        classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}")
    }
}

apply plugin: "kotlin"
apply plugin: "kotlin-spring"
apply plugin: "kotlin-jpa"
apply plugin: "org.springframework.boot"
apply plugin: "io.spring.dependency-management"
apply plugin: "kotlin-kapt"

group = "com.example"
version = "0.0.3-SNAPSHOT"
sourceCompatibility = 1.8
targetCompatibility = 1.8
compileKotlin {
    kotlinOptions {
        freeCompilerArgs = ["-Xjsr305=strict"]
        jvmTarget = "1.8"
    }
}
compileTestKotlin {
    kotlinOptions {
        freeCompilerArgs = ["-Xjsr305=strict"]
        jvmTarget = "1.8"
    }
}
compileJava {
    options.compilerArgs << "-Xlint:unchecked,deprecation"
}
compileTestJava {
    options.compilerArgs << "-Xlint:unchecked,deprecation"
}

repositories {
    mavenCentral()
}

dependencies {
    // for web
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("com.fasterxml.jackson.module:jackson-module-kotlin")
    // for db
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    runtime("mysql:mysql-connector-java")
    // compile("javax.xml.bind:jaxb-api:2.1")
    // kotlin
    compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    compile("org.jetbrains.kotlin:kotlin-reflect")
    // for dev
    kapt("org.springframework.boot:spring-boot-configuration-processor")
    runtime("org.springframework.boot:spring-boot-devtools")
    compileOnly("org.springframework.boot:spring-boot-configuration-processor")
    // for test
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("com.h2database:h2")
}

-Xjsr305 compiler flag

Spring Initializrで生成したプロジェクトでは、デフォルトでstrictモードになっています。
strictモードでは、Java側のオブジェクトのフィールドにNullableアノテーションが付いている場合、Kotlin側でNullableとして扱わないとコンパイルエラーになります。

例えば、次のコードはresult.body.idの箇所がコンパイルエラーになります。
(strictモードでなければコンパイルエラーにはなりませんが警告はでます。)

@Test
fun `test get`() {
    val result: ResponseEntity<Memo> = testRestTemplate.getForEntity("/memo/1", Memo::class.java)

    assertThat(result.body.id).isEqualTo(1L)
    // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Memo? :compileTestKotlin FAILED
}

これはbodyフィールドにNullableアノテーションが付いてるためです。

public class HttpEntity<T> {

    @Nullable
    private final T body;

}

コンパイルを通すには次のようにNullableとして扱います。

assertThat(result.body?.id).isEqualTo(1L)
// もしくはresult.body!!.id

依存性管理

依存性管理にプラグインio.spring.dependency-managementが必要になりました。

javax.xml.bind

コメントアウトしてあるjavax.xml.bind:jaxb-apiは、Java9のときに必要になります。(原因はhttps://github.com/spring-projects/spring-boot/issues/11205)

json

2.0からspring-boot-starter-jsonというjson用のstarterが用意されています。
spring-boot-starter-webの依存関係に含まれているので明示的に指定する必要はありませんが、kotlinでは別途com.fasterxml.jackson.module:jackson-module-kotlinが必要になります。

application.yml

spring:
  application:
    name: kotlin with spring-boot 2.0
  output:
    ansi:
      enabled: detect
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://localhost:3306/demo_db?useSSL=false
    username: demo_user
    password: demo_pass
    hikari:
      connection-test-query: select 1
      connection-timeout: 10000
      maximum-pool-size: 2
      minimum-idle: 2
  jpa:
    open-in-view: true
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        use_sql_comments: true
        generate_statistics: true
  jackson:
    serialization:
      write-dates-as-timestamps: false
      write-durations-as-timestamps: true

server:
  servlet:
    context-path: /app
  port: 9000

logging.file: demo.log
logging:
  file:
    max-size: 50MB
    max-history: 10
  level:
    root: info
    org.springframework: info
    org.hibernate: info
    org.hibernate.SQL: debug
    com.example.demo: debug

2.0からデフォルトのデータソースがHikariCPになっています。
HikariCPのプロパティを設定するにはspring.datasource.hikari.*に指定します。

また、application.properties(.yml)ファイルは名前空間の変更などが行われているので、1.5からの移行をサポートするモジュールが用意されています。
使い方は以下のモジュールを組み込むだけです。組み込むと書き換えが必要なプロパティがあればコンソールに警告を表示し、一時的に新しいプロパティへ変換して起動してくれます。

Maven

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-properties-migrator</artifactId>
</dependency>

Gradle

runtime("org.springframework.boot:spring-boot-properties-migrator")

アプリケーションの起動クラス

2.0からrunApplicationという関数が用意されています。

package com.example.demokotlin2

@SpringBootApplication
class DemoKotlin2Application

fun main(args: Array<String>) {
    runApplication<DemoKotlin2Application>(*args)
}

エンティティとリポジトリ

リポジトリはSpring Data Jpaのバージョンが上がったため若干変更点がありました。

エンティティ

1.5から変更はありませんでした。

package com.example.demokotlin2.entity

import java.io.Serializable
import java.time.LocalDateTime
import javax.persistence.*

@Entity
@Table(name = "memo")
data class Memo(
    @Column(name = "title", nullable = false)
    var title: String,
    @Column(name = "description", nullable = false)
    var description: String,
    @Column(name = "done", nullable = false)
    var done: Boolean = false,
    @Column(name = "updated", nullable = false)
    var updated: LocalDateTime = LocalDateTime.now(),
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0
) : Serializable

リポジトリ

この例ではJpaRepositoryインタフェースを実装していますが、このインタフェースで定義されているメソッドのシグネチャがいくつか変わっているので、1.5から移行する場合はコードの修正が必要になるかもしれません。

package com.example.demokotlin2.repository

import com.example.demokotlin2.entity.Memo
import org.springframework.data.jpa.repository.JpaRepository

interface MemoRepository : JpaRepository<Memo, Long>
  • シグネチャの変更点は補足欄にまとめてあります。

サービス

1.5と比べて特筆すべき変更点はありませんでした。

package com.example.demokotlin2.service

import com.example.demokotlin2.entity.Memo
import org.springframework.data.domain.Pageable

interface MemoService {
    fun findById(id: Long): Memo?
    fun findAll(page: Pageable): List<Memo>
    fun store(memo: Memo)
    fun removeById(id: Long)
}
package com.example.demokotlin2.service.impl

import com.example.demokotlin2.entity.Memo
import com.example.demokotlin2.repository.MemoRepository
import com.example.demokotlin2.service.MemoService
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class MemoServiceImpl(
    private val repository: MemoRepository) : MemoService {

    @Transactional(readOnly = true)
    override fun findById(id: Long): Memo? {
        return repository.findById(id).orElseGet(null)
    }

    @Transactional(readOnly = true)
    override fun findAll(page: Pageable): List<Memo> {
        return repository.findAll(page).content
    }

    @Transactional(timeout = 10)
    override fun store(memo: Memo) {
        repository.save(memo)
    }

    @Transactional(timeout = 10)
    override fun removeById(id: Long) {
        repository.deleteById(id)
    }

}

コントローラ

1.5と比べて特筆すべき変更点はありませんでした。

package com.example.demokotlin2.controller

import com.example.demokotlin2.entity.Memo
import com.example.demokotlin2.service.MemoService
import org.springframework.data.domain.Pageable
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping(path = ["memo"])
class MemoController(
    private val service: MemoService) {

    @GetMapping(path = ["{id}"], produces = [MediaType.APPLICATION_JSON_UTF8_VALUE])
    fun id(@PathVariable(value = "id") id: Long): ResponseEntity<Memo> {
        val memo = service.findById(id)
        return if (memo != null) {
            ResponseEntity.ok(memo)
        } else {
            ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }

    @GetMapping(path = ["list"], produces = [MediaType.APPLICATION_JSON_UTF8_VALUE])
    fun list(page: Pageable): ResponseEntity<List<Memo>> {
        return ResponseEntity.ok(service.findAll(page))
    }

    @PostMapping(produces = [MediaType.TEXT_PLAIN_VALUE], consumes = [MediaType.APPLICATION_JSON_UTF8_VALUE])
    fun store(@RequestBody memo: Memo): ResponseEntity<String> {
        service.store(memo)
        return ResponseEntity.ok("success")
    }

    @DeleteMapping(path = ["{id}"], produces = [MediaType.TEXT_PLAIN_VALUE])
    fun remove(@PathVariable(value = "id") id: Long): ResponseEntity<String> {
        service.removeById(id)
        return ResponseEntity.ok("success")
    }

}
  • ページング、ソートの設定をカスタマイズできるプロパティが追加されています。
spring.data.web.pageable.*
spring.data.web.sort.*

たとえば、1ページに表示する最大件数は次のプロパティで設定できます。

spring.data.web.pageable.default-page-size= 10

APIの動作確認

検索

> curl -v http://localhost:9000/app/memo/1
> curl -v http://localhost:9000/app/memo/list

ページング

> curl -v http://localhost:9000/app/memo/list?page=0&size=10

新規登録

> curl -v -H "Content-Type:application/json" -d @memo.json -X POST http://localhost:9000/app/memo

削除

> curl -v -X DELETE http://localhost:9000/app/memo/1

テストコード

リポジトリの単体テスト

package com.example.demokotlin2.repository

import com.example.demokotlin2.entity.Memo
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
import org.springframework.test.context.junit4.SpringRunner
import org.assertj.core.api.Assertions.*
import org.springframework.test.context.jdbc.Sql
import java.time.LocalDateTime

@RunWith(SpringRunner::class)
@DataJpaTest
class MemoRepositoryTests {

    @Autowired
    lateinit var entityManager: TestEntityManager

    @Autowired
    lateinit var sut: MemoRepository

    @Test
    @Sql(statements = ["INSERT INTO memo (id, title, description, done, updated) VALUES (1, 'memo test', 'memo description', FALSE, CURRENT_TIMESTAMP)"])
    fun findById() {
        val expected = entityManager.find(Memo::class.java, 1L)
        val actual = sut.findById(expected.id).orElseGet { null }
        assertThat(actual).isNotNull()
        assertThat(actual).isEqualTo(expected)
    }

    @Test
    fun save() {
        val updated = LocalDateTime.of(2018, 2, 19, 18, 12, 0)
        val expected = Memo(title = "test title", description = "test description", done = true, updated = updated)
        sut.saveAndFlush(expected)
        val actual = entityManager.find(Memo::class.java, expected.id)
        assertThat(actual).isEqualTo(expected)
    }

    @Test
    @Sql(statements = ["INSERT INTO memo (id, title, description, done, updated) VALUES (1, 'memo test', 'memo description', FALSE, CURRENT_TIMESTAMP)"])
    fun delete() {
        val expected = entityManager.find(Memo::class.java, 1L)
        sut.deleteById(expected.id)
        sut.flush()
        val actual = entityManager.find(Memo::class.java, expected.id)
        assertThat(actual).isNull()
    }

}

サービスの単体テスト

package com.example.demokotlin2.service

import com.example.demokotlin2.entity.Memo
import com.example.demokotlin2.repository.MemoRepository
import com.example.demokotlin2.service.impl.MemoServiceImpl
import org.junit.*
import org.mockito.*
import org.mockito.Mockito.*
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.quality.Strictness
import org.assertj.core.api.Assertions.*
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import java.util.*

class MemoServiceImplTests {

    @Rule
    @JvmField
    val rule: MockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)

    @Mock
    lateinit var repository: MemoRepository

    @InjectMocks
    lateinit var sut: MemoServiceImpl

    @Test
    fun findById() {
        val expected = Optional.ofNullable(Memo(id = 11, title = "title", description = "description", done = true))
        Mockito.`when`(repository.findById(anyLong())).thenReturn(expected)

        val actual = sut.findById(11)

        assertThat(actual).isNotNull()
        assertThat(actual).isEqualTo(expected.get())

        verify(repository, times(1)).findById(anyLong())
    }

    @Test
    fun findAllWithEmptyList() {
        val pageable: PageRequest = PageRequest.of(0, 5)
        val expected = emptyList<Memo>()
        val page = PageImpl<Memo>(expected, pageable, 0)
        Mockito.`when`(repository.findAll(eq(pageable))).thenReturn(page)

        val actual = sut.findAll(pageable)

        assertThat(actual).hasSize(0)

        verify(repository, times(1)).findAll(eq(pageable))
    }

    @Test
    fun findAll() {
        val pageable: PageRequest = PageRequest.of(0, 5)
        val expected = listOf(
            Memo(id = 1, title = "title A", description = "desc A", done = true),
            Memo(id = 2, title = "title B", description = "desc B", done = false),
            Memo(id = 3, title = "title C", description = "desc C", done = true)
        )
        val page = PageImpl<Memo>(expected, pageable, 3)
        Mockito.`when`(repository.findAll(eq(pageable))).thenReturn(page)

        val actual = sut.findAll(pageable)

        assertThat(actual).hasSize(3)
        assertThat(actual).containsSequence(expected)

        verify(repository, times(1)).findAll(eq(pageable))
    }

}

コントローラーのテスト

単体

package com.example.demokotlin2.controller

import com.example.demokotlin2.entity.Memo
import com.example.demokotlin2.service.MemoService
import com.fasterxml.jackson.databind.ObjectMapper
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mockito
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.context.junit4.SpringRunner
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import java.nio.charset.Charset

@RunWith(SpringRunner::class)
@WebMvcTest(MemoController::class)
class MemoControllerTests {

    @Autowired
    lateinit var mvc: MockMvc
    @Autowired
    lateinit var objectMapper: ObjectMapper
    @MockBean
    lateinit var memoService: MemoService

    private val contentType = MediaType(MediaType.APPLICATION_JSON.type,
            MediaType.APPLICATION_JSON.subtype, Charset.forName("utf8"))

    @Test
    fun getOne() {
        val expected = Memo(id=1L, title="test title", description="test description", done=true)
        val expectedJson = objectMapper.writeValueAsString(expected)
        Mockito.`when`(memoService.findById(anyLong())).thenReturn(expected)

        val builder = MockMvcRequestBuilders.get("/memo/{id}", 1)
                .accept(MediaType.APPLICATION_JSON_UTF8)

        val result = mvc.perform(builder)
                .andExpect(status().isOk)
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$").isNotEmpty)
                .andExpect(jsonPath("$.title").value(expected.title))
                .andExpect(jsonPath("$.description").value(expected.description))
                .andExpect(jsonPath("$.done").value(expected.done))
                .andDo(`print`())
                .andReturn()

        assertThat(result.response.contentAsString).isEqualTo(expectedJson)
    }

}

結合

package com.example.demokotlin2.controller

import com.example.demokotlin2.DemoKotlin2Application
import com.example.demokotlin2.entity.Memo
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.test.context.junit4.SpringRunner
import org.assertj.core.api.Assertions.*
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType

@RunWith(SpringRunner::class)
@SpringBootTest(classes = [DemoKotlin2Application::class],
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MemoControllerTests {

    class MemoList : MutableList<Memo> by ArrayList()

    @Autowired
    lateinit var testRestTemplate: TestRestTemplate

    @Test
    fun getOne() {
        val result = testRestTemplate.getForEntity("/memo/1", Memo::class.java)

        assertThat(result).isNotNull
        assertThat(result.statusCode).isEqualTo(HttpStatus.OK)
        assertThat(result.headers.contentType).isEqualTo(MediaType.APPLICATION_JSON_UTF8)
        assertThat(result.body?.id).isEqualTo(1L)
    }

    @Test
    fun pagination() {
        val result = testRestTemplate.getForEntity("/memo/list?page={page}&size={size}", MemoList::class.java, 0, 3)

        assertThat(result).isNotNull
        assertThat(result.statusCode).isEqualTo(HttpStatus.OK)
        assertThat(result.headers.contentType).isEqualTo(MediaType.APPLICATION_JSON_UTF8)
        assertThat(result.body).hasSize(3)
    }

}

補足

M1~M7、RC1,RC2のリリースノート

M1からRC2までのリリースノートの内容を大まかにまとめました。(2018/2/19)
ただ、すべてを確認、検証するには時間的に難しかったので、個人的に興味を引いたもの、影響の大きそうなものをピックアップしています。

Spring-Boot-2.0.0-M1-Release-Notes

★baselineの変更

  • Java8 or laterが必要です。
  • Java6,7はサポートされません。

★主要な依存関係のバージョンの変更

component Spring Boot 1.5.10 Spring Boot 2.0.0
Spring Framework 4.3.14 5.0.4 :arrow_up:
Spring Security 4.2.4 5.0.3 :arrow_up:
Spring Data Commons 1.13.0 2.0.5 :arrow_up:
Spring Data Jpa 1.11.10 2.0.5 :arrow_up:
Aspectj 1.8.13 1.8.13
javassist 3.21.0-GA 3.22.0-GA :arrow_up:
Jetty 9.4.8 9.4.8
Tomcat 8.5.27 8.5.28 :arrow_up:
Undertow 1.4.22 1.4.22
Hibernate 5.0.12 5.2.14 :arrow_up:
Jackson 2.8.10 2.9.4 :arrow_up:
Gson 2.8.2 2.8.2
JUnit 4.12 4.12
JUnit-Jupiter 5.1.0
mockito 1.10.19 2.15.0 :arrow_up:
Maven 3.2+ 3.2+
Gradle 2.9 or later 4 :arrow_up:
flyway 3.2.1 5.0.7 :arrow_up:
liquibase 3.5.3 3.5.5 :arrow_up:
kotlin 1.2.21

Relaxed binding

プロパティの緩やかなバインディング(Relaxed binding)が改善されています。
詳細はRelaxed Binding 2.0に説明があります。

プロパティの記述法

プロパティの記法には次のものがあります。

記法 example desc
camel-case person.firstName
kebab-case person.first-name .propertiesと.ymlファイルでの使用を推奨
snake-case person.first_name kebab-caseの代替
upper-case PERSON_FIRSTNAME システム環境変数を使用する場合に推奨

プロパティは(1)prefixと(2)nameに分けることができ、prefixとnameでは使える記法が異なります。

my-app.my-module.foo.first-name= ruby
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^  ^^^^
 |                    |           |
(1)                  (2)         (3)
  1. prefix : kebab-case
  2. name : kebab-case、camel-case、snake-case
  3. value : プロパティの値

2.0以降は、ConfigurationPropertiesアノテーションのprefix属性の記述方法が変わっています。

例えば、次のプロパティをバインドするために

my-app.my-module.foo.first-name= ruby
my-app.my-module.foo.last-name= tomato
my-app.my-module.foo.age= 10

prefixをkebab-case("my-app.my-module.foo")で記述することはできますが、

OK
@Component
@ConfigurationProperties(prefix = "my-app.my-module.foo")
data class FooConfig(
    var firstName: String = "",
    var lastName: String = "",
    var age: Int = 0
)

snake-case("my_app.my_module.foo")やcamel-case("myApp.myModule.foo")で記述すると実行時に例外をスローします。
1.5まではこれらの記法でも例外は起こりませんでしたが、プロパティの書き方に統一性がないと期待通りにバインドされませんでした。

NG
Caused by: org.springframework.boot.context.properties.source.InvalidConfigurationPropertyNameException: Configuration property name 'my_app.my_module.foo' is not valid

プロパティを設定する側では、次の記法はすべてバインドできます。

OK
my-app.my-module.foo.first-name= ruby
OK
my_app.my_module.foo.first_name= ruby
OK
myApp.myModule.foo.firstName= ruby

OS環境変数

OK
> SET MY_APP_MY_MODULE_FOO_FIRST_NAME= ruby

記法が混在した場合

OK
my-app.my-module.foo.first_name= ruby
my-app.my-module.foo.firstName= ruby
my-app.my-module.foo.FIRST_NAME= ruby

ConditionalOnPropertyアノテーションのprefixおよびname属性もkebab-caseで記述します。
ただし、こちらはsnake-caseやcamel-caseで記述しても例外がスローされないので注意が必要です。

OK
@ConditionalOnProperty(prefix = "my-app.my-module.foo", name = ["test-flag"])

ちなみに、プロパティが特定の値のときにバインディングしたい場合はhavingValueで条件を指定します。
この例では"my-app.my-module.foo.test-flag= true"の時にBarPropertiesのオブジェクトがDIコンテナに登録されます。

@Bean
@ConditionalOnProperty(prefix = "my-app.my-module.foo", name = ["test-flag"], havingValue = "true")
fun bar(foo: FooProperties): BarProperties {
    return BarProperties(foo.name)
}

data class BarProperties(val name: String)

またバインディングAPIは以下のように変更されました。

package class
old org.springframework.boot.bind RelaxedDataBinder
new org.springframework.boot.context.properties.bind Binder

例外

プロパティの中には、Springが利用するのではなく組み込んだライブラリへ渡すためのプロパティ定義があります。
たとえば下記のspring.jpa.properties.hibernate.*がそうですが、決められた記法で記述しないと認識されませんでした。
この例で言えばname部分(show_sql)はsnake-caseでなければ認識されなかったです。

spring.jpa.properties.hibernate.show_sql= true

Feature Remove

  • Remote CRaSH shell
  • Spring Loaded
  • Spring Boot CLIからテスト機能

組み込みコンテナのパッケージの変更

package interface
old org.springframework.boot.context.embedded EmbeddedServletContainer
new org.springframework.boot.web.server WebServer

Jetty

package class
old org.springframework.boot.context.embedded.jetty JettyEmbeddedServletContainerFactory
new org.springframework.boot.web.embedded.jetty JettyServletWebServerFactory
JettyReactiveWebServerFactory

Tomcat

package class
old org.springframework.boot.context.embedded.tomcat TomcatEmbeddedServletContainerFactory
new org.springframework.boot.web.embedded.tomcat TomcatServletWebServerFactory
TomcatReactiveWebServerFactory

undertow

package class
old org.springframework.boot.context.embedded.undertow UndertowEmbeddedServletContainerFactory
new org.springframework.boot.web.embedded.undertow UndertowServletWebServerFactory
UndertowReactiveWebServerFactory

Netty (補足)

  • Nettyは2.0から追加されたWebFluxのデフォルトのengineです。
package class
new org.springframework.boot.web.embedded.netty NettyReactiveWebServerFactory

★デフォルトのdatasourceの変更

デフォルトのdatasourceがtomcatからHikariCPに変わっています。

HikariCP

明示的に指定する場合

デフォルトなので明示的に指定する必要はありませんが、あえて記述すると次のようになります。

HikariCP
spring.datasource.type= com.zaxxer.hikari.HikariDataSource

HikariCPのプロパティ設定

spring.datasource.hikari.*
property name type default
allow-pool-suspension Boolean false
auto-commit Boolean true
catalog String driver default
connection-init-sql String none
connection-test-query String none
connection-timeout Long 30000 ms
data-source-class-name String none
data-source-j-n-d-i String
data-source-properties Map
driver-class-name String none
health-check-properties Map
health-check-registry Object none (HikariCP 2.3.2+ supports Dropwizard HealthChecks.)
idle-timeout Long 600000 ms
initialization-fail-timeout Long 1 s
isolate-internal-queries Boolean false
jdbc-url String none
leak-detection-threshold Long 0 ms
login-timeout Integer (officialに記載なし)
max-lifetime Long 1800000 ms
maximum-pool-size Integer 10
metric-registry Object none
metrics-tracker-factory Factory (officialに記載なし)
minimum-idle Integer same as maximumPoolSize
password String none
pool-name String auto-generated
read-only Boolean false
register-mbeans Boolean false
scheduled-executor service none
schema String driver default
transaction-isolation String driver default
username String none
validation-timeout Long 5000ms
Tomcat

tomcatに戻したい場合

Maven

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <exclusions>
        <exclusion>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jdbc</artifactId>
</dependency>

Gradle

compile("org.springframework.boot:spring-boot-starter-data-jpa") {
    exclude group: "com.zaxxer", module: "HikariCP"
}
compile("org.apache.tomcat:tomcat-jdbc")

デフォルトの設定を上書きします。

Tocmcat
spring.datasource.type= org.apache.tomcat.jdbc.pool.DataSource

Tomcat Connection Poolのプロパティ設定

spring.datasource.tomcat.*

Common Attributes

property name type default
defaultAutoCommit boolean JDBC driver default
defaultReadOnly boolean
defaultTransactionIsolation String JDBC driver default
defaultCatalog String
driverClassName String
username String
password String
maxActive int 100
maxIdle int same as maxActive
minIdle int same as initialSize
initialSize int 10
maxWait int 30000 ms
testOnBorrow boolean false
testOnConnect boolean false
testOnReturn boolean false
testWhileIdle boolean false
validationQuery String
validationQueryTimeout int -1
validatorClassName String null
timeBetweenEvictionRunsMillis int 5000 ms
numTestsPerEvictionRun int *Property not used
minEvictableIdleTimeMillis int 60000 ms
accessToUnderlyingConnectionAllowed boolean
removeAbandoned boolean false
removeAbandonedTimeout int 60 s
logAbandoned boolean false
connectionProperties String null
poolPreparedStatements boolean *Property not used
maxOpenPreparedStatements int *Property not used
  • Tomcat JDBC Enhanced Attributesは割愛します。

spring-boot-starter-webへの依存関係の削除

  • spring-boot-starter-mustache
  • spring-boot-starter-thymeleaf

これらのstarterからspring-boot-starter-webへの依存関係が削除されています。
具体的にはpom.xmlのdependencyから次の記述が削除されています。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

WebFluxのサポート

  • spring-boot-starter-webflux
  • spring-boot-starter-reactor-netty (default web engine)

Reactive dataのサポート

Reactive対応のData Repositoryが追加されています。

  • MongoDB (spring-boot-starter-data-mongodb-reactive)
  • Redis (spring-boot-starter-data-redis-reactive)
  • Cassandra (spring-boot-starter-data-cassandra-reactive)

Proxying strategyのデフォルトの変更

デフォルトがCGLIBになりました。設定の変更はproxy-target-classプロパティで行います。

spring.aop.proxy-target-class= true
  • true : CGLIB subclass-based
  • false : JDK interface-based

application.propertiesの名前空間の変更

Servlet-specific server properties

old new
server.context-parameters.* server.servlet.context-parameters.*
server.context-path server.servlet.context-path
server.jsp.init-parameters.* server.servlet.jsp.init-parameters.*
server.jsp.registered server.servlet.jsp.registered
server.path server.servlet.path

Multipart configuration

old new
spring.http.multipart.* spring.servlet.multipart.*

webEnvironment flagの指定方法の変更

webfluxが追加されたことにより、新しいプロパティspring.main.web-application-typeが追加されています。

Spring Boot 1.5までは、webアプリケーションとして起動させない(組み込みwebコンテナが立ち上がらない)ようにするには、下記のように設定していましたが

deprecated
spring.main.web-environment= false

2.0以降は新しいプロパティspring.main.web-application-typeにnoneを指定します。

new
spring.main.web-application-type= none

このプロパティに指定できる値は次の3種類です。

  • none
    • The application should not run as a web application and should not start an embedded web server
  • servlet
    • The application should run as a servlet-based web application and should start an embedded servlet web server.
  • reactive
    • The application should run as a reactive web application and should start an embedded reactive web server.

Mustache templateのデフォルト値変更

Mustacheテンプレートエンジンのテンプレートファイルのデフォルト拡張子が.htmlから.mustacheに変わっています。

spring.mustache.suffix= .mustache

★Gradle Pluginの変更

こちらのブログ(Spring Boot's new Gradle plugin)にポストされたとおり、Gradle Pluginの機能が変更されています。

実行可能なjar/warの生成

実行可能jar又はwarの生成は、従来bootRepackageタスクで行っていましたが、bootJar又はbootWarタスクに置き換えられました。

dependency-management-plugin

依存関係の管理を行うdependency-management-pluginを明示的に組み込む必要があります。

apply plugin: 'io.spring.dependency-management'

Spring-Boot-2.0.0-M2-Release-Notes

spring-boot-starter-quartzの追加

Quartz Job Schedulerというジョブスケジューラがspring-boot-starterとしてサポートされました。

Spring Data Web configuration

ページングやソートの設定がapplication.propertiesで出来るようになっています。

example
# SpringDataWebProperties
spring.data.web.pageable.default-page-size= 20
spring.data.web.pageable.max-page-size= 2000
# Whether to expose and assume 1-based page number indexes.
spring.data.web.pageable.one-indexed-parameters= false
spring.data.web.pageable.page-parameter= page
# General prefix to be prepended to the page number and page size parameters.
spring.data.web.pageable.prefix= ""
# Delimiter to be used between the qualifier and the actual page number and size properties.
spring.data.web.pageable.qualifier-delimiter= _
spring.data.web.pageable.size-parameter= size
spring.data.web.sort.sort-parameter= sort
  • prefix
    • 各パラメータのprefixを指定します。
    • 例) prefix=p_ → パラメータ名はp_pageやp_sizeとなります。
  • one-indexed-parameters
    • false、ページ番号の始まりを0とします。
      • page=0は1ページ目、page=1は2ページ目を指します。
    • true、ページ番号の始まりを1とします。
      • page=0とpage=1は同じ1ページ目、page=2は2ページ目を指します。
  • qualifier-delimiter
    • qualifierとパラメータの区切り文字を指定します。
    • 1つのURLに2つ(以上)のページ情報を含めたい場合に利用します。

このプロパティはClass SpringDataWebPropertiesに反映されます。

サンプルコード

import org.springframework.data.domain.Pageable
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping(path = ["index"])
class IndexController {

    // 例1
    @GetMapping
    fun index(pageable: Pageable): String {
        return pageable.toString()
    }

    // 例2
    @GetMapping(path = ["2"])
    fun index2(@Qualifier("foo") foo: Pageable
             , @Qualifier("bar") bar: Pageable): String {
        println("foo: ${foo}")
        println("bar: ${bar}")
        return "OK"
    }
}

例1

http://localhost:9000/app/index

   ↓

Page request [number: 0, size 20, sort: UNSORTED]
http://localhost:9000/app/index?page=1&size=30&sort=id,desc&sort=name,asc

   ↓

Page request [number: 1, size 30, sort: id: DESC,name: ASC]

例2

http://localhost:9000/app/index/2?foo_page=1&foo_size=10&bar_page=3&bar_size=10

   ↓

foo: Page request [number: 1, size 10, sort: UNSORTED]
bar: Page request [number: 3, size 10, sort: UNSORTED]

注意

Spring Data Restに似たパラメータがあるので注意が必要です。

# DATA REST (RepositoryRestProperties)
spring.data.rest.default-page-size= # Default size of pages.
spring.data.rest.limit-param-name= # Name of the URL query string parameter that indicates how many results to return at once.
spring.data.rest.max-page-size= # Maximum size of pages.
spring.data.rest.page-param-name= # Name of the URL query string parameter that indicates what page to return.
spring.data.rest.sort-param-name= # Name of the URL query string parameter that indicates what direction to sort results.

JDBCTemplate configuration

JDBCTemplateの設定がapplication.propertiesで出来るようになっています。

example
spring.jdbc.template.fetch-size= -1
spring.jdbc.template.max-rows= -1
spring.jdbc.template.query-timeout=

このプロパティはClass JdbcPropertiesに反映されます。

Cassandraのプーリングオプションのconfiguration

プーリングオプションの設定がapplication.propertiesで出来るようになっています。

example
spring.data.cassandra.pool.heartbeat-interval= 30s
spring.data.cassandra.pool.idle-timeout= 120s
spring.data.cassandra.pool.max-queue-size= 256
spring.data.cassandra.pool.pool-timeout= 5000ms

このプロパティはClass CassandraPropertiesに反映されます。

★spring-boot-starter-jsonの追加

jackson-databindの代わりになるspring-boot-starter-jsonが追加されています。

Maven

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

Gradle

compile("org.springframework.boot:spring-boot-starter-json")

また、以下の依存関係を別途追加しなくてもよくなりました。

  • jackson-datatype-jdk8
  • jackson-datatype-jsr310
  • jackson-module-parameter-names

spring-boot-starter-thymeleafの変更

spring-boot-starter-thymeleafにthymeleaf-extras-java8timeへの推移的依存関係が含まれるようになったので依存関係を別途追加しなくてもよくなりました。

Mongo clientのconfiguration

MongoClientSettingsBuilderCustomizerでMongoクライアントの設定ができるようになっています。

Test

Unit Test用のアノテーションが追加されています。

  • JooqTest
  • DataRedisTest

Spring-Boot-2.0.0-M3-Release-Notes

★Maven Pluginの変更

ゴールにパラメータを指定する場合、パラメータ名のprefixに"spring-boot"を付加するようになっています。

profileを指定するパラメータは、1.5まではrun.profilesでしたが

old
mvn spring-boot:run -Drun.profiles=foo

2.0以降はspring-boot.run.profilesになります。

new
mvn spring-boot:run -Dspring-boot.run.profiles=foo

DevToolsの変更

HTTP経由のリモートデバッグがサポートされなくなりました。

Spring-Boot-2.0.0-M4-Release-Notes

application.propertiesの名前空間の変更

Liquibase and Flyway

old new
liquibase.* spring.liquibase.*
flyway.* spring.flyway.*

Database initializer

Spring Batchが使用するデータベースの初期化をコントロールするプロパティが変わっています。
また、Spring Integration、Spring Session、QUARTZ SCHEDULERも同様の仕組みで初期化ができるようになりました。

指定できる値はDataSourceInitializationModeで定義されている次の3種類です。

  • always
    • Always initialize the datasource.
  • embedded
    • Only initialize an embedded datasource.
  • never
    • Do not initialize the datasource.

Spring Batch

deprecated
spring.batch.initializer.enabled= true
new
spring.batch.initialize-schema= embedded

このプロパティはClass BatchPropertiesに反映されます。

Spring Integration

new
spring.integration.jdbc.initialize-schema= embedded

このプロパティはClass IntegrationPropertiesに反映されます。

Spring Session

new
spring.session.jdbc.initialize-schema=embedded

このプロパティはClass JdbcSessionPropertiesに反映されます。

QUARTZ SCHEDULER

new
spring.quartz.jdbc.initialize-schema= embedded

このプロパティはClass QuartzPropertiesに反映されます。

compile flag

spring-boot-starter-parentを使うMavenプロジェクトでは-parametersコンパイルフラグがデフォルトで有効になります。

javac

Reflection APIのメソッドjava.lang.reflect.Executable.getParametersが取得できるように、コンストラクタやメソッドの仮パラメータ名を、生成されたクラス・ファイルに格納します。

★Support

  • Java9

Spring-Boot-2.0.0-M5-Release-Notes

Maven Surefire pluginのバージョンアップ

Maven Surefire Pluginのinclude/excludeの設定が変更されています。
下記は1.5までのデフォルトの設定です。2.0では利用するSurefire Pluginのバージョンが上がっていてデフォルトの設定が変わっています。

old
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <includes>
             <include>**/*Tests.java</include>
             <include>**/*Test.java</include>
        </includes>
        <excludes>
            <exclude>**/Abstract*.java</exclude>
        </excludes>
    </configuration>
</plugin>

また、JUnit5を利用する場合は使用するMaven Surefire pluginのバージョンを2.19.1に下げる必要があるようです。

プラグインのバージョン状況

デフォルトで利用するプラグインのバージョンをまとめました。

plugin 1.5.10 2.0.0.RC2
clean 2.6.1 3.0.0 :arrow_up:
compiler 3.1 3.7.0 :arrow_up:
deploy 2.8.2 2.8.2
install 2.5.2 2.5.2
jar 2.6 3.0.2 :arrow_up:
resources 2.6 3.0.1 :arrow_up:
site 3.5.1 3.6 :arrow_up:
surefire 2.18.1 2.20.1 :arrow_up:
spring-boot 1.5.10 2.0.0 :arrow_up:
build-helper 1.10 -
help 2.2 -
xml 1.0 -

Gradle pluginの変更

bootRunタスクでアプリケーションの引数(args)とJVM引数(jvmArgs)を指定できるようになっています。

bootRun {
    args += ['-X', '-F']
    jvmArgs += ['-Dspring.profiles.active=dev']
}

Redisドライバーの変更

spring-boot-starter-redisで組み込むRedisドライバーのデフォルトがJedisからLettuceへ変わっています。

Mockito 1.x

Mockito 1.xでは@MockBean@SpyBeanを使ってオブジェクトの注入ができません。
spring-boot-starter-testを使っていない場合は、手動でMockito 2.xへアップグレードする必要があります。

Support

★Spring Framework 5.0 GA

Spring Framework 5.0 goes GA

このマイルストーンから、Spring Framework 5.0 GA版が使用されています。

Spring-Boot-2.0.0-M6-Release-Notes

Gradle pluginの変更

BootRun、BootJar、BootWarタスクで、mainClassNameプロパティを利用するようになりました。
それまではそれぞれのタスクのプロパティで設定していました。

JPAのopen-in-viewプロパティの警告

デフォルト値はtrueですが、このプロパティの値を明示的にtrueに設定しないと起動時に警告メッセージが表示されます。

spring.jpa.open-in-view= true

警告メッセージ

spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning

Configuration location

2.0ではapplication.propertiesの検索パスを設定方法が変わっています。

1.5まではspring.config.locationに設定するパスはデフォルトの検索パスに追加されていました。
2.0からはspring.config.locationに設定するパスはデフォルトの検索パスを置き換えます。検索パスを追加したい場合はadditional-locationという新しいプロパティを使用します。

new
spring.config.additional-location= # Config file locations used in addition to the defaults.

ConfigurationPropertiesの検証

ConfigurationPropertiesアノテーションを付けたオブジェクトのバリデーションを行うにはValidatedアノテーションが必要です。

次のようなプロパティをFooConfigクラスへバインドする場合

my-app.my-module.foo.first-name= ruby
#my-app.my-module.foo.last-name= tomato
#my-app.my-module.foo.age= 77
my-app.my-module.foo.bar-settings.max-height= 999
my-app.my-module.foo.bar-settings.min-height= 100
@Component
@ConfigurationProperties(prefix = "my-app.my-module.foo")
@Validated
class FooConfig {

    lateinit var firstName: String

    lateinit var lastName: String

    var age: Int? = null

    var barSettings: BarConfig = BarConfig()

    class BarConfig {
        lateinit var maxHeight: Integer
        lateinit var minHeight: Integer
        override fun toString(): String {
            return "BarConfig(maxHeight=$maxHeight, minHeight=$minHeight)"
        }
    }

    override fun toString(): String {
        return "FooConfig(firstName='$firstName', lastName='$lastName', age=$age, barSettings=$barSettings)"
    }

}

Validatedアノテーションが付いているとアプリケーション起動時に検証が行われ、NotNullなプロパティが初期化できなかった場合、エラーを出力して終了します。

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to bind properties under 'my-app.my-module.foo.last-name' to java.lang.String:

    Reason: Unable to get value for property last-name

Action:

Update your application's configuration

Validatedアノテーションが付いていない場合、アプリケーションは起動しますがFooConfigのプロパティに最初にアクセスしたタイミングでエラーが発生します。

kotlin.UninitializedPropertyAccessException: lateinit property lastName has not been initialized
Property Origins

プロパティがバインドできなかった場合(BindExceptionがスローされるなどの理由)、そのプロパティのソース(property source)が表示されるようになっています。

下記はコマンドラインから与えたプロパティ"-Dmy-app.my-module.foo.color=white"が、my-app.my-module.foo.colorにバインドできなかったときに表示されたメッセージです。
指定された"white"という値がEnum Color型のフィールドにバインドできなかったためですが、この値が"SystemProperties"から指定されたことがメッセージの"Origin"の行から確認できます。

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to bind properties under 'my-app.my-module.foo.color' to com.example.demomavenspring2.type.Color:

    Property: my-app.my-module.foo.color
    Value: white
    Origin: "my-app.my-module.foo.color" from property source "systemProperties"
    Reason: Failed to convert from type [java.lang.String] to type [@javax.validation.constraints.NotNull com.example.demomavenspring2.type.Color] for value 'white'; nested exception is java.lang.IllegalArgumentException: No en
um constant com.example.demomavenspring2.type.Color.white

Action:

Update your application's configuration. The following values are valid:

    BLUE
    CYAN
    GREEN
    MAGENTA
    RED
    YELLOW

★DataSource Initialization

データベースの初期化をコントロールするプロパティが変更されています。

deprecated
spring.datasource.initialize= true

新しいプロパティは、デフォルトでは組み込みデータベース(H2、HSQL、Derby)に対してのみ機能します。

new
spring.datasource.initialization-mode= embedded

指定できる値はDataSourceInitializationModeで定義されている次の3種類です。
alwaysを指定すると組み込みデータベースでなくても機能してしまうので注意が必要です。

  • embedded
    • Only initialize an embedded datasource.
  • always
    • always initialize the datasource.
  • never
    • Do not initialize the datasource.

JPA mapping resources

マッピングリソースファイルを指定できるようになっています。

new
spring.jpa.mapping-resources= # Mapping resources (equivalent to "mapping-file" entries in persistence.xml).

HTTP/2

HTTP/2の有効/無効化をプロパティで設定できるようになっています。
このマイルストーンではTomcatとUndertowが対応します。(JettyはRC1で対応)

new
server.http2.enabled= true

HTTP/2が有効になったか確認するにはsslを有効にしてhttpsでアクセスする必要があります。

自動コンフィグレーションと依存関係の管理から除外

プロジェクトで利用する場合はバージョンを明示する必要があります。

Kotlin

runApplicationというインライン関数が用意されています。

1.5まで

fun main(args: Array<String>) {
    SpringApplication.run(Application::class.java, *args)
}

2.0以降は次のようにも記述できます。

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

または

fun main(args: Array<String>) {
    runApplication<Application>(*args) {
        // initialize code
    }
}

Spring-Boot-2.0.0-M7-Release-Notes

Jacksonのシリアライズのデフォルト値の変更

serialization.write-dates-as-timestampsプロパティのデフォルト値がfalseに変わっています。

サンプルコード

次のデータクラスのインスタンスがどのようにシリアライズされるか確認しました。

data class DateTimeDemo(
    var lt: LocalTime = LocalTime.now(),
    var ld: LocalDate = LocalDate.now(),
    var dt: LocalDateTime = LocalDateTime.now(),
    var ym: YearMonth = YearMonth.now(),
    var md: MonthDay = MonthDay.now(),
    var dd: Duration = Duration.of(1, ChronoUnit.HOURS),
    var pp: Period = Period.ofDays(1),
    var ii: Instant = Instant.now()
) : Serializable

2.0のデフォルトの場合

spring.jackson.serialization.write-dates-as-timestamps= false
spring.jackson.serialization.write-durations-as-timestamps= true

   ↓

{
    lt: "14:06:20.9023938",
    ld: "2018-02-16",
    dt: "2018-02-16T14:06:20.9023938",
    ym: "2018-02",
    md: "--02-16",
    dd: "PT1H",
    pp: "P1D",
    ii: "2018-02-16T05:06:20.904893500Z"
}

1.5のデフォルトの場合(jacksonのデフォルトでもあります)

spring.jackson.serialization.write-dates-as-timestamps= true
spring.jackson.serialization.write-durations-as-timestamps= true

   ↓

{
    lt: [
        14,
        7,
        43,
        61545000
    ],
    ld: [
        2018,
        2,
        16
    ],
    dt: [
        2018,
        2,
        16,
        14,
        7,
        43,
        61545000
    ],
    ym: [
        2018,
        2
    ],
    md: "--02-16",
    dd: 3600,
    pp: "P1D",
    ii: 1518757663.0645459
}

Kotlin

spring-boot-starter-jsonからjackson-module-kotlinの推移的依存関係が削除されました。
Kotlinでjacksonを利用している場合は手動で依存関係を追加する必要があります。

Maven

<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-kotlin</artifactId>
</dependency>

Gradle

compile("com.fasterxml.jackson.module:jackson-module-kotlin")

Spring Data repositories enablement

reactive対応のData Repositoryの追加により、リポジトリの有効/無効化のプロパティが変わっています。

従来のプロパティではtrue/falseで設定します。

deprecated
spring.data.cassandra.repositories.enabled=
spring.data.mongodb.repositories.enabled=
spring.data.redis.repositories.enabled= 
spring.data.couchbase.repositories.enabled=

Redis以外は新しいプロパティで、auto(デフォルト)、imperative、reactive、noneから選択します。
Redisは従来通りで変更はないようです。

new
spring.data.cassandra.repositories.type= auto
spring.data.mongodb.repositories.type= auto
spring.data.redis.repositories.enabled= true
spring.data.couchbase.repositories.type= auto
  • auto
    • enables all repository types automatically based on their availability
  • imperative
    • enables imperative repisitories
  • reactive
    • enables reactive repositories
  • none
    • enables no repositories

Redisだけtypeプロパティが無い

それぞれのstarter(spring-boot-starter-data-redis、spring-boot-starter-data-redis-reactive)のpom.xmlを確認すると、reactiveは従来版のエイリアスのような設定で違いはないようです。

自動コンフィグレーションと依存関係の管理から除外

プロジェクトで利用する場合はバージョンを明示する必要があります。

Conditions evaluation delta on restart

DevToolsを有効にしたアプリケーションは、起動中にクラスパスが更新されると自動的に再起動(automatic restart)します。
クラスパスを更新させる方法はIDEによって異なります。Eclipseでは変更したファイルを保存したとき、IntelliJ IDEAではプロジェクトのビルド(Ctrol + F9)、ファイルの再コンパイル(Ctrl +
Shift + F9
)でも同じ効果があります。

再起動を制御するには次のプロパティを設定します。

spring.devtools.restart.enabled= false

再起動時にアプリケーションの自動構成の変更が検出されると、その変更内容が「CONDITION EVALUATION DELTA」というレポートに出力されます。
(あくまでもautomatic restart時なので、停止&起動ではレポートは出力されません)
レポートの出力を制御するには次のプロパティを設定します。

spring.devtools.restart.log-condition-evaluation-delta= false

application.propertiesのDuration型

プロパティがJava Duration型の場合、"1h"のようにsuffixに単位を付けて指定することができるようになりました。

example
server.session.cookie.max-age= 1h

suffixを付けない場合は従来通りの単位になります。

# Maximum age of the session cookie. If a duration suffix is not specified, seconds will be used.
server.session.cookie.max-age= 3600

ただし、時間の長さを指定するプロパティでもDuration型を取らないパラメータもありますので注意が必要です。
たとえば次のmax-ageはLong型なのでsuffixを付けるとエラーになります。

NG
spring.datasource.tomcat.max-age= 30s

キャッシュにRedisを使ったときのデフォルト設定

キャッシュにredisを使用した場合のデフォルト値をapllication.propetiesで設定できるようになっています。

キャッシュにRedisを選択

spring.cache.type= redis

次のプロパティでデフォルトを設定できます。

example
spring.cache.redis.time-to-live= 120s
spring.cache.redis.cache-null-values= true
spring.cache.redis.use-key-prefix= true
spring.cache.redis.key-prefix= myCache_

このプロパティはClass CachePropertiesに反映されます。

cache-null-valuesプロパティをfalseにした場合、nullをキャッシュしようとするとIllegalArgumentExceptionがスローされます。
この場合は次のようにnullを除外するように設定します。

@Cacheable(cacheNames = ["myCache"], unless="#result == null")

spring-boot-starter-data-couchbase-reactiveの追加

Reactive対応のData Repositoryにcouchbaseが追加されています。
(MongoDB、Redis、CassandraはM1で対応)

  • spring-boot-starter-data-couchbase
  • spring-boot-starter-data-couchbase-reactive

Spring Boot 2.0.0 RC1 Release Notes

spring-boot-starter-webへの依存関係の削除

spring-boot-starter-freemarkerからspring-boot-starter-webへの依存関係が削除されています。
(spring-boot-starter-mustache、spring-boot-starter-thymeleafはM1にて対応)

Spring MVC path matchingの変更

suffixパスマッチングのデフォルトの挙動が変わっています。
これに伴い新しいパラメータが追加されています。

new
spring.mvc.content-negotiation.favor-parameter= false
spring.mvc.content-negotiation.favor-path-extension= false
spring.mvc.content-negotiation.parameter-name= 

1.5まで、たとえばこれらのリクエストはすべて

localhost://app/product
localhost://app/product.json
localhost://app/product.xml

次のハンドラメソッドにマッピングされていましたが、2.0以降はデフォルトではマッピングされなくなります。(404のNot Foundになります)

@GetMapping(path = ["/app/product"])
fun product(): String {

    //... 省略

}

application.propertiesの名前空間の変更

old new
banner.* spring.banner.*
spring.metrics.* management.metrics.*
server.* server.servlet.*

★Legacy properties migration

プロパティファイルの構成が大きく変わっているのでspring-boot-properties-migratorというプロパティファイルの移行モジュールが用意されています。
このモジュールを組み込むと、アプリケーション起動時に古いプロパティがあるとワーニングを表示し、且つ新しい構成に一時的に変換して起動してくれます。
なお、application.propertiesファイルをコンバートする訳ではないので修正作業は手動で行う必要があります。

Maven

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-properties-migrator</artifactId>
</dependency>

Gradle

runtime("org.springframework.boot:spring-boot-properties-migrator")

次のような古いフォーマットの記述があった場合

古いフォーマット
banner.charset: UTF-8 # Banner file encoding.

アプリケーション起動時にワーニングを表示してくれます。

WARN 7460 --- [  restartedMain] o.s.b.c.p.m.PropertiesMigrationListener  : 
The use of configuration keys that have been renamed was found in the environment:

Property source 'applicationConfig: [classpath:/application.properties]':
    Key: banner.charset
        Line: 2
        Replacement: spring.banner.charset


Each configuration key has been temporarily mapped to its replacement for your convenience. To silence this warning, please update your configuration to use the new keys.

Hibernateプロパティのconfiguration

hibernateのプロパティをカスタマイズできます。

サンプルコード

@Bean
fun customizer() = HibernatePropertiesCustomizer { props ->
    props["hibernate.show_sql"] = true
    props["hibernate.format_sql"] = true
    props["hibernate.use_sql_comments"] = true
    props["hibernate.generate_statistics"] = true
}

application.propertiesでは次のプロパティでカスタマイズできます。

spring.jpa.properties.hibernate.*

Redisのconfiguration

RedisCacheConfigurationでRedisCacheManagerの設定ができるようになっています。

@Bean
fun redisCacheConfig() = RedisCacheConfiguration.defaultCacheConfig()
    .entryTtl(Duration.ofMinutes(2))
    .disableCachingNullValues()

application.propertiesでは次のプロパティでカスタマイズできます。

spring.cache.redis.*

Support

  • GSon
  • HTTP/2 (Jetty)

Spring Boot 2.0.0 RC2 Release Notes

application.propertiesの変更

次のプロパティが再配置されています。(RC2で見直しされたようです)

ENDPOINTS WEB CONFIGURATION

management.endpoints.web.expose

   ↓

management.endpoints.web.exposure.include
management.endpoints.web.exposure.exclude

ENDPOINTS JMX CONFIGURATION

management.endpoints.jmx.expose

   ↓

management.endpoints.jmx.exposure.include
management.endpoints.jmx.exposure.exclude

Webjars Locatorの依存管理の変更

webjars-locatorの依存関係管理がwebjars-locator-coreに変更されています。

2.0より

Maven

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>webjars-locator-core</artifactId>
    <version>0.35</version>
</dependency>

Gradle

compile("org.webjars:webjars-locator-core:0.35")

Kotlin

Spring Bootリファレンスのセクション47にKotlin supportが追加されています。

また、Spring FrameworkのリファレンスにもKotlinのセクションがあります。

Custom binding delimiter

以前からプロパティのタイプがコレクションで、バインドする値(例えばapplication.propertiesで定義する)がカンマ区切りのとき、自動的にコレクションへバインドしてくれていました。

my-app.my-module.foo.hobbies= shopping,trekking,movies
@Component
@ConfigurationProperties(prefix = "my-app.my-module.foo")
data class FooConfig(
    var hobbies: List<String> = mutableListOf()
)

2.0よりDelimiterアノテーションを使って区切り文字を指定することができるようになっています。

my-app.my-module.foo.hobbies= shopping; trekking; movies
@field:Delimiter(value = ";")
var hobbies: List<String> = mutableListOf()

逆にカンマ区切りで分割したくない場合は次のように指定します。

@field:Delimiter(value = Delimiter.NONE)

Support

JOOQのSQLDialectの検出

デフォルトで自動検出が有効なので、通常はapplication.propertiesで明示する必要はありません。

spring.jooq.sql-dialect= # SQL dialect to use. Auto-detected by default.

リリースノートの補足

Spring Boot 2.0 Migration Guideには、基本的にM1からRC2の内容がまとめられていますが、Migration Guideにだけ記載されている事項がありましたので補足します。

spring.jpa.hibernate.ddl-autoのデフォルトの挙動の変更

1.5までのデフォルトは、組み込みデータベース使用時にcreate-drop、それ以外はnone

2.0以降のデフォルトは、組み込みデータベース使用時で且つflyway/Liquibaseを使用していない時にcreate-drop、それ以外はnone

このプロパティに設定できる値のその内容については変更はないようです。

  • create-drop
    • Create and then destroy the schema at the end of session
  • create
    • Create the schema and destroy previous data
  • none
    • Disable DDL handling
  • update
    • Update the schema if necessary
  • validate
    • Validate the schema, make no changes to the database

Spring Dataのバージョンアップの影響

Spring Data CommonsおよびJpaもバージョンアップしています。
このバージョンアップによりインターフェースの変更が行われているため実装コードの修正が必要な場合があります。

例えばCrudRepositoryインターフェースについては、次の違いがあります。

jpa_1.11.10
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID>
jpa_2.0.4
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID>
signature JPA 1.11.10 JPA 2.0.4
同じ <S extends T> S save(S entity) <S extends T> S save(S entity)
違う <S extends T> Iterable<S> save(Iterable<S> entities) <S extends T> Iterable<S> saveAll(Iterable<S> entities)
違う T findOne(ID id) Optional<T> findById(ID id)
違う boolean exists(ID id) boolean existsById(ID id)
同じ Iterable<T> findAll() Iterable<T> findAll()
違う Iterable<T> findAll(Iterable<ID> ids) Iterable<T> findAllById(Iterable<ID> ids)
同じ long count() long count()
違う void delete(ID id) void deleteById(ID id)
同じ void delete(T entity) void delete(T entity)
違う void delete(Iterable<? extends T> entities) void deleteAll(Iterable<? extends T> entities)
同じ void deleteAll() void deleteAll()
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした