しおしお

IntelliJ IDEAのことなんかを書いてます

Spring Cloud AWSのSecrets Manager機能を試してみた

昔自分で作った、AWS Secrets Managerの値をSpring Bootでいい感じに使えるようにした - しおしおと同じようにAWSのSecrets Managerの値をSpringの設定値として使えるやつがSpring Cloud AWSに追加されていたので試してみました。*1

ライブラリの追加

build.gradle を以下のようにしています。 CloudFormationは使っていないので、CloudFormation関連のBeanが作成されないようにaws-java-sdk-cloudformationは除外しています。*2

ext {
  set('springCloudVersion', 'Greenwich.RELEASE')
}

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
  }
}

configurations {
  implementation {
    exclude group: 'com.amazonaws', module: 'aws-java-sdk-cloudformation'
  }
}

dependencies {
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
  implementation "org.jetbrains.kotlin:kotlin-reflect"
  implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
  implementation 'org.springframework.cloud:spring-cloud-starter-aws'
  implementation 'org.springframework.cloud:spring-cloud-starter-aws-secrets-manager-config'
}

確認用のクラス

PropsコンポーネントsecretIdプロパティにSecrets Managerのhoge.secretIdの値が設定されるようにしています。 RestControllerでは、確認用に設定されている値を返しています。

@RestController
class HelloController(
        private val props: Props
) {

    @GetMapping("/hello")
    fun hello(): Mono<String> {
        return Mono.just("secretId: ${props.secretId}")
    }
}

@Component
@ConfigurationProperties(prefix = "hoge")
class Props {
    lateinit var secretId: String
}

シークレットを作成する

デフォルトでは、 /secret/application とうい名前のシークレットから値を取得するようなので、/secret/applictionという名前で以下のシークレットを作成します。

f:id:sioiri:20190209064644p:plain

アプリケーションを実行する

作成したシークレットが、設定されアプリケーションで参照できていることが確認できます。

curl http://localhost:8080/hello
secretId: application_hoge

アプリケーション毎にシークレットを作成する

application.propertiesspring.application.nameを設定し、アプリケーションごとにシークレットを用意できます。 下のようにアプリケーション名をsiosioとした場合は、シークレット名を/secret/siosioとして作成します。

spring.application.name=siosio

確認用に以下のシークレットを作成します。

f:id:sioiri:20190209065439p:plain

アプリケーションを実行する

/secret/siosioのシークレットの値を参照できていることが確認できます。

curl http://localhost:8080/hello
secretId: siosio_hoge

プロファイル毎にシークレットを作成する

シークレット名の最後に_プロファイル名を付加すると、プロファイル専用のシークレットを用意できます。 例えば、prod専用のシークレットの場合/secret/siosio_prodのように作成します。

確認用に以下のシークレットを作成します。

f:id:sioiri:20190209070023p:plain

アプリケーションを実行する

指定したプロファイル用のシークレットの値を参照できていることが確認できます。

curl http://localhost:8080/hello
secretId: prod_hoge

シークレットの参照順

シークレットは以下の順で参照されます。

  1. プロファイルに対応するシークレット
  2. アプリケーション名に対応するシークレット
  3. 共通(/secret/application)のシークレット

おわり。

*1:ドキュメント→Spring Cloud AWS

*2:application.propertiesにcloud.aws.stack.auto=falseでもOKだと思います

IntelliJ IDEAのHTTP Clientでレスポンス内容を他のリクエストで使用する方法

やりたいこと

APIから返ってきたレスポンスの内容を他のリクエストのヘッダなどで使用したい。 例えば、レスポンス内に含まれるアクセストークンを他のリクエストのヘッダに設定したいなんてことをしたい。

試した環境

サーバ側のコード

サーバ側の実装です。

  • GETでヘッダーに値を設定して、ボディにはjsonを返しています
  • POSTではjsonをdata classで受け取って標準出力に出力しています
fun Application.module(testing: Boolean = false) {
    install(ContentNegotiation) {
        jackson {
            enable(SerializationFeature.INDENT_OUTPUT)
        }
    }

    routing {
        get("/") {
            call.response.header("X-HOGE", "fuga")
            call.respond(mapOf("hello" to "world"))
        }

        post("/") {
            val hoge = call.receive<Hoge>()
            println("hoge = ${hoge}")
        }
    }
}

data class Hoge(val hello: String, val hoge: String)

HTTP Clientを使ってレスポンスの内容を他のリクエストで使ってみる

ドキュメント(Response Handling Examples - Help | IntelliJ IDEA)を参考に、 .http ファイルを作ってみます。

  • GETを呼び出した後のレスポンスの内容を client.global.set を使って設定することで次のリクエスでその値を使えるようになります
  • レスポンスヘッダを使いたい場合には、 response.headers.valuesOf を使ってヘッダの値を取得します
  • ボディのjsonの値を使いたい場合には、 response.body を使って取得します
  • client.global.set を使って設定した値は、 {{変数名}} で参照できます
GET http://localhost:8080

> {% 
  client.global.set("hoge", response.headers.valuesOf("X-HOGE")[0]);
  client.global.set("hello", response.body.hello);
%}

###
POST http://localhost:8080
Content-Type: application/json

{
  "hello": "{{hello}}",
  "hoge": "{{hoge}}"
}

実行結果

POST処理のjsonには、最初のGETで取得した値が設定されていますね。

hoge = Hoge(hello=world, hoge=fuga)

KDocを生成するIntelliJ IDEAのプラグイン作ったよ

プラグインでできること

下のようにfunctionclassinterface でKDocの開始コメント(/**)を入力してEnterでKDocの雛形が生成されます。 https://raw.githubusercontent.com/siosio/kdoc-generator-intellij-plugin/master/images/gen-kdoc.gif

なんで作ったの?

KT-11079にあるように、公式的にはJavadoc形式の書き方は推奨していないようです。*1
ただ、ちょいちょいJavadoc的なドキュメンテーションを要求されるから、Javadocと同じようにテンプレート生成されるといいんだけどなぁ的なツィートを見かけたので作りました。

インストール方法

公式サイトにアップしてあるので、IntelliJ IDEAのプラグインインストール画面からインストールできます。 IntelliJのバージョンによっては、 kdoc-generator ではなく kodkod という名前で登録されています。
公開は一年前にしていたけど、雑につけていたプラグイン名の変更と、classinterface にも対応するように最近がんばりました。

*1:ちょいちょいYouTrackに同じようなチケットが上がるのですが、全て同じ理由で対応しませんで終わっています。

KtorでHello World

Ktor触ったことなかったので、IntelliJ IDEAのプラグイン使ってさくっと動かしてみた。

準備

IntelliJ IDEA(使ったバージョン2018.3)にKtor - Plugins | JetBrainsをインストールします。

プラグイン使って雛形プロジェクトの作成

IntelliJ IDEA - Quick Start - Ktorを参考にプロジェクトを作成します。
画面にHello World表示するのに必要そうなFreemarkerにチェックをします。
f:id:sioiri:20190120062943p:plain
プロジェクトはこんな感じに生成されます。ディレクトリの構成はGradleの構成とはちょっと変えてるみたいですね。
f:id:sioiri:20190120063454p:plain

出力されたソースコード

HELLO WORLDをtext/plainで返すルートと画面を返すルートが定義されています。

    routing {
        get("/") {
            call.respondText("HELLO WORLD!", contentType = ContentType.Text.Plain)
        }

        get("/html-freemarker") {
            call.respond(FreeMarkerContent("index.ftl", mapOf("data" to IndexData(listOf(1, 2, 3))), ""))
        }
    }

動かしてみる

アプリケーションを起動してみます。ログから8080ポートでアプリケーションが起動されたことが確認できます。

2019-01-20 06:40:16.468 [main] INFO  Application - No ktor.deployment.watch patterns specified, automatic reload is not active
2019-01-20 06:40:17.795 [main] INFO  Application - Responding at http://0.0.0.0:8080

ブラウザでアクセスすると、HELLO WORLDが表示されます。
f:id:sioiri:20190120064218p:plain

View(FreeMarker)でHello Worldしてみる

メッセージを保持するクラスを追加します。

data class Message(val text: String)

ルーティングのFreeMarkerの実装を変更し、Hello Worldメッセージを保持するオブジェクトをmodelに設定します。

    routing {
        get("/") {
            call.respondText("HELLO WORLD!", contentType = ContentType.Text.Plain)
        }

        get("/hello") {
            call.respond(FreeMarkerContent("index.ftl", mapOf("message" to Message("Hello World!!!"))))
        }
    }

最後に、レスポンスで指定しているindex.ftl(resources/templates/index.ftl)をメッセージを表示するように変更します。

<#-- @ftlvariable name="message" type="siosio.Message" -->
<html>
    <body>
    メッセージ:${message.text}
    </body>
</html>

動かしてみる

ブラウザでアクセスすると、メッセージとしてHello Worldが表示されました。ちゃんと動きましたね。
f:id:sioiri:20190120065314p:plain

Hello Worldを返すAPIを追加してみる

今までは画面だったので、最後にJsonHello Worldを返すAPIを追加してみます。
HTTP API - Quick Start - Ktorを参考にするのとイケそうですね。

Jsonを返すためにJackson機能を有効にする

ktor-jacksonをbuild.gradleのdependenciesに追加します。

compile "io.ktor:ktor-jackson:$ktor_version"

Application.ktに以下を追加して、jacksonをアプリケーションにインストールします。

fun Application.module(testing: Boolean = false) {
    // 省略
    install(ContentNegotiation) {
        jackson {
            // 確認しやすくするためにインデントして
            enable(SerializationFeature.INDENT_OUTPUT)
        }
    }
    // 省略
}

ルートを追加する

hello_jsonにアクセスするとJsonHello Worldを返すようにしてみます。

        get("/hello_json") {
            call.respond(Message("Hello World!"))
        }

動かしてみると

Jsonでちゃんと返してきてくれましたね。
f:id:sioiri:20190120071400p:plain

Spring Session & RedisでJacksonを使ったシリアライズを試してみる

Spring SessionでRedisにセッション情報を格納する際に、Jacksonを使ってシリアライズをできるみたいなので試してみました。*1

ライブラリのバージョンなど

build.gradleを参照ください。

Sessionの格納先をRedisにする

application.propertiesに以下を追加します。

spring.session.store-type=redis

Jackson2JsonRedisSerializerをBean定義する

RedisHttpSessionConfigurationを見ると、こんな実装がるのでBean名にspringSessionDefaultRedisSerializerを指定してJacksonのシリアライザをBean定義すれば良さそうですね。

	@Autowired(required = false)
	@Qualifier("springSessionDefaultRedisSerializer")
	public void setDefaultRedisSerializer(
			RedisSerializer<Object> defaultRedisSerializer) {
		this.defaultRedisSerializer = defaultRedisSerializer;
	}

実際のBean定義です。Jacksonでシリアライザを行うJackson2JsonRedisSerializerを返します。
@Qualifierアノテーションを使ってBean名にspringSessionDefaultRedisSerializerを指定します。
JacksonのObjectMapperには、セッション情報をシリアライズする際に必要となるためSecurityJackson2Modulesを使って、必要なModuleを登録してあげます。

    @Bean
    @Qualifier("springSessionDefaultRedisSerializer")
    fun redisSerializer(): RedisSerializer<Any> {
        return Jackson2JsonRedisSerializer(Any::class.java).apply {
            val objectMapper = ObjectMapper()
            objectMapper.registerModules(SecurityJackson2Modules.getModules(this.javaClass.classLoader))
            setObjectMapper(objectMapper)
        }
    }

ログインを行いRedisの中身を確認する

ログインを行ってセッションの中身を確認してみると、デフォルトで使われるUserクラスの情報がRedisに登録されていることが確認できます。

127.0.0.1:6379> hgetall spring:session:sessions:6ff0be42-853f-4184-a874-1e4881a822f7
creationTime
1547242900218
sessionAttr:SPRING_SECURITY_SAVED_REQUEST

sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN

maxInactiveInterval
1800
lastAccessedTime
1547242908403
sessionAttr:SPRING_SECURITY_LAST_EXCEPTION

sessionAttr:SPRING_SECURITY_CONTEXT
{"@class":"org.springframework.security.core.context.SecurityContextImpl","authentication":{"@class":"org.springframework.security.authentication.UsernamePasswordAuthenticationToken","authorities":["java.util.Collections$UnmodifiableRandomAccessList",[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"ROLE_USER"}]],"details":{"@class":"org.springframework.security.web.authentication.WebAuthenticationDetails","remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"272fd950-13c5-4d79-a3dc-54010be92ad7"},"authenticated":true,"principal":{"@class":"org.springframework.security.core.userdetails.User","password":null,"username":"test","authorities":["java.util.Collections$UnmodifiableSet",[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"ROLE_USER"}]],"accountNonExpired":true,"accountNonLocked":true,"credentialsNonExpired":true,"enabled":true},"credentials":null}}

UserDetailsの実装クラスを変えてみる

デフォルトで使われるorg.springframework.security.core.userdetails.Userではなく、独自のUserDetails実装を使った場合の動きを確認してみます。

UserDetails実装はSampleUserとしてデフォルトの情報以外に、ログインしたユーザの名前を保持するようにしています。

data class SampleUser(private val username: String,
                      private val password: String,
                      private val authorities: List<GrantedAuthority>,
                      val loginUserName: String) : UserDetails {

    override fun getAuthorities(): List<GrantedAuthority> = authorities

    override fun isEnabled(): Boolean = true

    override fun getUsername(): String = username

    override fun isCredentialsNonExpired(): Boolean = true

    override fun getPassword(): String = password

    override fun isAccountNonExpired(): Boolean = true

    override fun isAccountNonLocked(): Boolean = true
}

独自Userでも動作するか確認してみます

残念ながらエラーになってしまいました。デシリアライズ処理ができなかったようです。

org.springframework.data.redis.serializer.SerializationException: Could not read JSON: The class with siosio.springsessionredisexample.config.SampleUser and name of siosio.springsessionredisexample.config.SampleUser is not whitelisted. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github.com/spring-projects/spring-security/issues/4370 for details (through reference chain: org.springframework.security.core.context.SecurityContextImpl["authentication"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: The class with siosio.springsessionredisexample.config.SampleUser and name of siosio.springsessionredisexample.config.SampleUser is not whitelisted. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github.com/spring-projects/spring-security/issues/4370 for details (through reference chain: org.springframework.security.core.context.SecurityContextImpl["authentication"])
	at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.deserialize(Jackson2JsonRedisSerializer.java:75) ~[spring-data-redis-2.1.3.RELEASE.jar:2.1.3.RELEASE]
	at org.springframework.data.redis.core.AbstractOperations.deserializeHashValue(AbstractOperations.java:354) ~[spring-data-redis-2.1.3.RELEASE.jar:2.1.3.RELEASE]
	at org.springframework.data.redis.core.AbstractOperations.deserializeHashMap(AbstractOperations.java:298) ~[spring-data-redis-2.1.3.RELEASE.jar:2.1.3.RELEASE]
	at org.springframework.data.redis.core.DefaultHashOperations.entries(DefaultHashOperations.java:247) ~[spring-data-redis-2.1.3.RELEASE.jar:2.1.3.RELEASE]
	at org.springframework.data.redis.core.DefaultBoundHashOperations.entries(DefaultBoundHashOperations.java:183) ~[spring-data-redis-2.1.3.RELEASE.jar:2.1.3.RELEASE]
	at org.springframework.session.data.redis.RedisOperationsSessionRepository.getSession(RedisOperationsSessionRepository.java:466) ~[spring-session-data-redis-2.1.2.RELEASE.jar:2.1.2.RELEASE]
	at org.springframework.session.data.redis.RedisOperationsSessionRepository.findById(RedisOperationsSessionRepository.java:435) ~[spring-session-data-redis-2.1.2.RELEASE.jar:2.1.2.RELEASE]

なお、Redisの内容を確認してみると、想定通りSampleUserの情報が格納されているのでシリアライズは問題なさそうですね。

sessionAttr:SPRING_SECURITY_CONTEXT
{"@class":"org.springframework.security.core.context.SecurityContextImpl","authentication":{"@class":"org.springframework.security.authentication.UsernamePasswordAuthenticationToken","authorities":["java.util.Collections$UnmodifiableRandomAccessList",[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"ROLE_USER"}]],"details":{"@class":"org.springframework.security.web.authentication.WebAuthenticationDetails","remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"0f181292-9a16-4622-b820-83adcc8ae33c"},"authenticated":true,"principal":{"@class":"siosio.springsessionredisexample.config.SampleUser","username":"test","password":"$2a$10$AXwQIZKK8yaNk2tFN1JwaOCxcn7DYuCwK5d6wvY.tX.ZGQUWeA2sy","authorities":["java.util.Collections$SingletonList",[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"ROLE_USER"}]],"loginUserName":"テストユーザ","enabled":true,"accounpired":true,"credentialsNonExpired":true,"accountNonLocked":true},"credentials":null}

シリアライズ処理がうまく動くようMixInを追加する

デフォルトのUserクラス用のMixIn(org.springframework.security.jackson2.UserMixin)を参考にしてデシリアライズ処理を行うクラスなどを追加します。
SampleUserMixinでは、デシリアライズを行うクラスの指定や、フィールドをシリアライズ対象にするなどの設定をしています。
シリアライズを行うSampleUserDeserializerは、SampleUserを生成して返します。

data class SampleUser(private val username: String,
                      private val password: String,
                      private val authorities: List<GrantedAuthority>,
                      val loginUserName: String) : UserDetails {

    override fun getAuthorities(): List<GrantedAuthority> = authorities

    override fun isEnabled(): Boolean = true

    override fun getUsername(): String = username

    override fun isCredentialsNonExpired(): Boolean = true

    override fun getPassword(): String = password

    override fun isAccountNonExpired(): Boolean = true

    override fun isAccountNonLocked(): Boolean = true
}

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonDeserialize(using = SampleUserDeserializer::class)
@JsonAutoDetect(
        fieldVisibility = JsonAutoDetect.Visibility.ANY,
        getterVisibility = JsonAutoDetect.Visibility.NONE,
        isGetterVisibility = JsonAutoDetect.Visibility.NONE,
        creatorVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonSubTypes
internal abstract class SampleUserMixin

class SampleUserDeserializer : JsonDeserializer<SampleUser>() {
    override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): SampleUser {
        val mapper = jp.codec as ObjectMapper
        val jsonNode: JsonNode = mapper.readTree(jp)
        val authorities = mapper.convertValue<List<SimpleGrantedAuthority>>(jsonNode.get("authorities"))

        val password = jsonNode.readText("password")
        return SampleUser(
                jsonNode.readText("username"),
                password,
                authorities.toList(),
                jsonNode.readText("loginUserName")
        )
    }

    fun JsonNode.readText(field: String, defaultValue: String = ""): String {
        return when {
            has(field) -> get(field).asText(defaultValue)
            else -> defaultValue
        }
    }
}

最後に、JacksonのシリアライザのBean生成を行っているところで、作成したMixInをJacksonに登録します。

    @Bean
    @Qualifier("springSessionDefaultRedisSerializer")
    fun redisSerializer(): RedisSerializer<Any> {
        return Jackson2JsonRedisSerializer(Any::class.java).apply {
            val objectMapper = ObjectMapper()
            objectMapper.registerModules(SecurityJackson2Modules.getModules(this.javaClass.classLoader))
            objectMapper.addMixIn(SampleUser::class.java, SampleUserMixin::class.java)
            setObjectMapper(objectMapper)
        }
    }

MixInの確認

ログイン後に呼び出されるコントローラで下のようにUser情報を受けとり確認してみます。

    @GetMapping
    fun menu(@AuthenticationPrincipal user: SampleUser): String {
        println("user = ${user}")
        return "menu"
    }

ログインしてみると標準出力にユーザ情報が出力されるのでJacksonでのシリアライズ&デシリアライズが動いていることがわかります。
f:id:sioiri:20190112075143p:plain

最後に…

ソース全体はこちら→GitHub - siosio/spring-session-redis-example

*1:デフォルトでは、ObjectOutputStreamを使ったシリアライズが行われます。

IntelliJのDocker integrationプラグインからExecした時に保存されるコマンド履歴を削除する方法

IntelliJさんのDocker Integrationプラグインを使うとIDE上からdocker execできて便利ですよね。

でも、コマンド名間違えちゃった場合にそれが記憶されて、次にExecしようとした時に誤ったコマンド名が候補に出てくるの邪魔ですね…
f:id:sioiri:20190107143536p:plain

そんなときは設定ファイルを直接いじって削除してあげましょう。

.idea/workspace.xmlを開いて、DockerExecCommandを検索します。
下のように誤ったコマンドの設定がDockerExecCommandのcomponent内にあるので、それを削除してあげます。

    <ExecCommand>
      <option name="command" value="hoge" />
      <option name="imageId" value="redis:latest" />
    </ExecCommand>

Gradle 5.0でMavenのBOMを使おう

Managing Transitive Dependenciesあたりをお試ししてみました。

implementation platformを使ってみる

build.gradle

BOMをimplementation platformに指定します。
BOMで定義されているバージョンを使用するので、個別のライブラリ(この例ではjackson-databind)にはバージョン指定は不要となります。

dependencies {
  implementation platform('com.fasterxml.jackson:jackson-bom:2.9.7')
  implementation 'com.fasterxml.jackson.core:jackson-databind'
}

dependenciesの結果

バージョンを指定しなくても、BOMのバージョンが適用されていますね

+--- com.fasterxml.jackson:jackson-bom:2.9.7
|    +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
|    +--- com.fasterxml.jackson.core:jackson-core:2.9.7
|    \--- com.fasterxml.jackson.core:jackson-databind:2.9.7
|         +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
|         \--- com.fasterxml.jackson.core:jackson-core:2.9.7
\--- com.fasterxml.jackson.core:jackson-databind -> 2.9.7 (*)

個別のライブラリに明示的にバージョンを指定してみると

明示的に新しいバージョンを指定してみます。

dependencies {
  implementation platform('com.fasterxml.jackson:jackson-bom:2.9.7')
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
}

dependenciesを見てみると、明示的に指定したバージョンが適用されています。
このように、明示的にバージョンが指定された場合でもBOMバージョンを強制したい場合には、enforcedPlatformを使う必要があります。

+--- com.fasterxml.jackson:jackson-bom:2.9.7
|    +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
|    +--- com.fasterxml.jackson.core:jackson-core:2.9.7 -> 2.9.8
|    \--- com.fasterxml.jackson.core:jackson-databind:2.9.7 -> 2.9.8
|         +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
|         \--- com.fasterxml.jackson.core:jackson-core:2.9.8
\--- com.fasterxml.jackson.core:jackson-databind:2.9.8 (*)

implementation enforcedPlatformを使ってみる

build.gradle

platformenforcedPlatformにBOMを指定します。

dependencies {
  implementation enforcedPlatform('com.fasterxml.jackson:jackson-bom:2.9.7')
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
}

dependenciesの結果

個別指定のバージョンが、BOMに定義されているバージョンで上書きされるようになしました。

+--- com.fasterxml.jackson:jackson-bom:2.9.7
|    +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
|    +--- com.fasterxml.jackson.core:jackson-core:2.9.7
|    \--- com.fasterxml.jackson.core:jackson-databind:2.9.7
|         +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
|         \--- com.fasterxml.jackson.core:jackson-core:2.9.7
\--- com.fasterxml.jackson.core:jackson-databind:2.9.8 -> 2.9.7 (*)

Gradle4.6-RC1でお試し追加されたBOM対応を動かしてみると

Gradle4.6 RC1で追加されたお試し版は動かないですね!

+--- com.fasterxml.jackson:jackson-bom:2.9.7
\--- com.fasterxml.jackson.core:jackson-databind FAILED