しおしお

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

Spring Bootなアプリケーションのキャッシュの内容を定期的にクリアしてみる

@Scheduled アノテーションを使ってタスクを定期実行し、キャッシュの値を定期的にクリアしています。

設定値(application.properties)

  • 簡単に確認出来るインメモリキャッシュを設定しています
spring.cache.type=simple

サンプルコード

  • @Scheduled で10秒毎にキャッシュをクリアするタスクを実行しています
  • 同時に@CacheEvictを設定して、削除対象のキャッシュ名とすべてのエントリを削除するようにしています
@SpringBootApplication
@EnableCaching
@EnableScheduling
class CacheDemoApplication

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

@RestController
@RequestMapping("/sample")
class SampleController(private val sampleService: SampleService) {
    val logger = LoggerFactory.getLogger(SampleController::class.java)
    @GetMapping
    fun toUpper(@RequestParam("value") value: String): String {
        logger.info("value: $value")
        return sampleService.toUpper(value)
    }

}

@Service
class SampleService {

    val logger = LoggerFactory.getLogger(SampleService::class.java)

    @Cacheable("toupper")
    fun toUpper(value: String): String {
        TimeUnit.SECONDS.sleep(1)
        val upper = value.toUpperCase()
        logger.info("$value --> $upper")
        return upper
    }
}

@Component
class SampleClearCache {

    val logger = LoggerFactory.getLogger(SampleClearCache::class.java)

    @Scheduled(fixedRateString = "PT10S", initialDelayString = "PT10S")
    @CacheEvict(value = ["toupper"], allEntries = true)
    fun clearCache(): Unit {
        logger.info("clear cache")
    }
}

実行結果

  • 初回はServiceが実行され、それ以降はServiceの実行が行われずキャッシュの値が使われていることがわかります
  • スケジュールなタスクでキャッシュのクリアが行われると、Serviceが実行されていることがわかります
  • スケジュールなタスクが指定された時間(10秒)毎に実行されていることがわかります
2019-03-23 07:28:07.930  INFO 26010 --- [nio-8080-exec-7] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:08.931  INFO 26010 --- [nio-8080-exec-7] siosio.cachedemo.SampleService           : a --> A
2019-03-23 07:28:09.220  INFO 26010 --- [nio-8080-exec-8] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:09.993  INFO 26010 --- [nio-8080-exec-9] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:10.665  INFO 26010 --- [io-8080-exec-10] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:11.680  INFO 26010 --- [nio-8080-exec-2] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:12.399  INFO 26010 --- [nio-8080-exec-1] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:13.220  INFO 26010 --- [nio-8080-exec-3] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:14.013  INFO 26010 --- [nio-8080-exec-4] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:14.820  INFO 26010 --- [nio-8080-exec-5] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:15.703  INFO 26010 --- [   scheduling-1] siosio.cachedemo.SampleClearCache        : clear cache
2019-03-23 07:28:15.715  INFO 26010 --- [nio-8080-exec-7] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:16.716  INFO 26010 --- [nio-8080-exec-7] siosio.cachedemo.SampleService           : a --> A
2019-03-23 07:28:17.837  INFO 26010 --- [nio-8080-exec-8] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:20.011  INFO 26010 --- [nio-8080-exec-9] siosio.cachedemo.SampleController        : value: a
2019-03-23 07:28:25.703  INFO 26010 --- [   scheduling-1] siosio.cachedemo.SampleClearCache        : clear cache

JUnit5でTempDirを使って一時ディレクトリを扱う

JUnit4のTemporaryFolder的なやつですね。5.4から追加されて、アノテーションEXPERIMENTAL とあるのでまだ実験的な機能のようです。

サンプル

テストメソッド単位に一時ディレクトリを使う

  • テストメソッドの引数として java.nio.file.Path を受け取り、 org.junit.jupiter.api.io.TempDir アノテーションを設定します
  • java.nio.file.Path は一時ディレクトリを示すパスになっています
@Test
internal fun sample1(@TempDir tempDir: Path) {
    val file = tempDir.resolve("test.txt").toFile()
    file.writeText("test")
    
    assertThat(file.readText())
            .isEqualTo("test")
}

テストクラス内のすべてのテストメソッドで一時ディレクトリを使う

class TempDirSample {
    
    @TempDir
    lateinit var tempDir: Path

    @Test
    internal fun sample1() {
        println("tempDir = ${tempDir}")
    }

    @Test
    internal fun sample2() {
        println("tempDir = ${tempDir}")
    }
}

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に同じようなチケットが上がるのですが、全て同じ理由で対応しませんで終わっています。