しおしお

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

IntelliJ IDEAのDomaプラグイン(Doma Support)をKotlin対応したよ

こんなツィートを見かけたので、Doma SupportにKotlin対応追加してみました。

KotlinのDaoで出来ること

  • Daoのメソッドに対するSQLファイルの存在チェック
  • SQLファイルが存在しない場合、QuickFixでSQLファイルを作る機能
  • DaoからSQLファイルへの移動

JavaでDaoを作ったときと比べるとできることは少ないです

動作イメージ

f:id:sioiri:20190309072722g:plain

インストール方法

Kotlin対応版の1.0.2をインストールすることで利用できます。

プラグインが承認されるまでは、プラグインサイトからインストールできないのでRelease 1.0.2 · siosio/DomaSupport · GitHubからダウンロードしてインストールしていただければと。

KtorでDoma2を使ってデータベースアクセスしてみた

Ktor - asynchronous Web framework for KotlinでDoma2を使ってデータベースアクセスしてみました。 Ktorの公式サイト上にはデータベースアクセスする方法などが全く無いので、これが正解かどうかはわかりませんが…

build.gradle

  • データベースアクセスに必要となるライブラリを追加します
dependencies {
  kapt 'org.seasar.doma:doma:2.22.0'
  implementation 'org.seasar.doma:doma:2.22.0'
  runtime 'org.postgresql:postgresql:42.2.5'
  implementation 'com.zaxxer:HikariCP:3.3.1'
}
  • Doma2でkaptが必要になるので、kaptプラグインを追加します
apply plugin: 'kotlin-kapt'

データベースの接続先を定義する

  • resources/application.confにデータベースの接続先などを定義します
database {
    driverClass = org.postgresql.Driver
    url = "jdbc:postgresql://localhost:5432/siosio"
    user = siosio
    password = siosio
}

DomaのConfigクラスを追加する

  • トランザクション — Doma 2.0 ドキュメントを参考にシングルトンなコンフィグを作ります
    Kotlinなので、objectで作ってsingleton@JvmStaticにしています
  • initで、HikariCPのDataSourceを作成します
    接続先などの情報は、resources/application.conf からとってきます
@SingletonConfig
object DomaConfig : Config {
    
    private lateinit var ds: LocalTransactionDataSource
    
    fun init(environment: ApplicationEnvironment) {
        val hikariConfig = HikariConfig()
        hikariConfig.driverClassName = environment.config.property("database.driverClass").getString()
        hikariConfig.jdbcUrl = environment.config.property("database.url").getString()
        hikariConfig.username = environment.config.property("database.user").getString()
        hikariConfig.password = environment.config.property("database.password").getString()
        ds = LocalTransactionDataSource(HikariDataSource(hikariConfig))
    }

    override fun getDialect(): Dialect = PostgresDialect()

    override fun getDataSource(): DataSource {
        if (::ds.isInitialized.not()) {
            throw IllegalStateException("database setting is not initialized")
        }
        return ds
    }

    override fun getTransactionManager(): TransactionManager {
        return LocalTransactionManager(ds.getLocalTransaction(jdbcLogger))
    }

    @JvmStatic
    fun singleton(): DomaConfig = DomaConfig
}

Moduleの定義タイミングでDataSourceを作成する

  • DomaConfig#initを呼び出してDataSourceを作成します
fun Application.module(testing: Boolean = false) {
    // 省略

    DomaConfig.init(environment)
    
    routing {
        user()
    }
}

Domaを使いやすくするヘルパーメソッドを追加する

  • Daoの実装クラス(DaoImpl)を取得するメソッドを追加します
  • transactionを実行するためのメソッドを追加します
inline fun <reified T> dao(): T {
    return Thread.currentThread().contextClassLoader
            .loadClass("${T::class.qualifiedName}Impl")
            .newInstance() as T
}

fun <T> PipelineContext<out Any, out Any>.transaction(block: () -> T): T {
    return DomaConfig.transactionManager.required(block)
}

データベースアクセス処理を実装する

  • 先程定義したtransactionメソッドに渡したブロック内でデータベースアクセスを行います
  • 確認用に登録処理と、登録したデータを一括取得する処理を実装しています
fun Route.user(): Unit {
    get("/users") {
        val userList = transaction {
            dao<UserDao>().findAll()
        }
        call.respond(userList)
    }

    post("/users") {
        val user = call.receive(UserEntity::class)
        transaction {
            dao<UserDao>().insert(user)
        }
        call.respond(HttpStatusCode.Created)
    }
}

アプリケーションを実行して確認してみる

データの登録処理

登録処理を呼び出して、2件データを登録してみます

~ ❮❮❮ curl -H 'Content-Type: application/json' -d '{"name": "hoge"}' -D - http://localhost:8080/users
HTTP/1.1 201 Created
Content-Length: 0

~ ❯❯❯ curl -H 'Content-Type: application/json' -d '{"name": "fuga"}' -D - http://localhost:8080/users
HTTP/1.1 201 Created
Content-Length: 0

データの取得処理

一括取得を呼び出すと登録したデータが返されることが確認できます

~ ❯❯❯ curl  -D - http://localhost:8080/users
HTTP/1.1 200 OK
Content-Length: 72
Content-Type: application/json; charset=UTF-8

[ {
  "id" : 2,
  "name" : "hoge"
}, {
  "id" : 3,
  "name" : "fuga"
} ]

データベースの確認

登録した2件のデータが確認できます。ちゃんと動いていますね。

siosio=# select * from users;
 id | name 
----+------
  2 | hoge
  3 | fuga
(2 rows)

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を使ったシリアライズが行われます。