しおしお

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

IntelliJ IDEA 2024.1から消えてしまったSearch Everywhereのdatabase検索タブを復活する方法

IntelliJ IDEAを2024.1のEAPにしてから消えてしまって困っていたSearch Everywhereのdatabase検索タブ(下の画像の赤枠のやつ)を復活する方法になります。

復活させるには下の画像のように設定画面のAdvanced SettingsにあるShow database tab in Search Everywhereのチェックをオンにしてあげます。 これで、以前と同じようにdatabase検索タブからシュッとデータベースのオブジェクトが検索できて移動などが簡単にできますね!

おわり。

IntelliJ IDEA2024.1 EAPで変わってしまったTerminalをもとに戻す方法

IntelliJ IDEA2024.1 EAPですっかり変わってしまったTerminal機能を従来の普通のTerminalに戻す方法になります。

戻す方法

設定画面のTools->Terminalにある、Shell integrationのチェックを外すことで、今まで通りの普通のTerminalに戻ってくれます

おわり。

LogbackのLogstashEncoderを使用しつつメッセージをマスクしてみたお話

LogbackでログメッセージをJsonで出力(logstash-logback-encoderを使用)する際に、メッセージの内容を一部マスクしてみました。

マスクの方法は、logback.xmlの指定だけで特定フィールドの内容をまるっと置き換えたり、正規表現を使用して柔軟に置き換えたりできるようです。 また、カスタムな実装を用意することで設定ファイルだけでは実現できないような柔軟な置き換えもできるみたいなので色々と試してみます。*1

フィールド名を指定してのマスキング

デフォルトのマスク定義でのマスキング

これは、特定のフィールドの内容をデフォルト定義に従い置き換える方法になります。

logback.xmlの内容

LogstashEncoderMaskingJsonGeneratorDecoratorを指定することで、マスキングの指定ができるようです。 この例ではmessageフィールドはすべて「これに置き換わるよ」と出力されます。

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <jsonGeneratorDecorator class="net.logstash.logback.mask.MaskingJsonGeneratorDecorator">
                <defaultMask>これに置き換わるよ</defaultMask>
                <path>message</path>
            </jsonGeneratorDecorator>
        </encoder>
    </appender>

実行結果

実行してみると、testとログメッセージを出力したのにlogback.xmlに従い「これに置き換わるよ」と出力されていることが確認できます。

フィールドごとにマスク定義を変えてのマスキング

これは、フィールドごとにマスク内容を定義して置き換える方法になります。

logback.xmlの内容

この例ではmessageフィールドはdefaultMaskの定義が適用されますが、pathMask配下で定義されているcustomフィールドは「customはこれにかわるよ」に置き換えられ出力されます。

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <jsonGeneratorDecorator class="net.logstash.logback.mask.MaskingJsonGeneratorDecorator">
                <defaultMask>これに置き換わるよ</defaultMask>
                <path>message</path>
                <pathMask>
                    <path>custom</path>
                    <mask>customはこれにかわるよ</mask>
                </pathMask>
            </jsonGeneratorDecorator>
        </encoder>
    </appender>

実行結果

実行してみると、messagecustomではlogback.xmlの定義に従い異なる置き換えが行われているのが確認できます。

出力される値をハンドリングしてのマスキング

値をベースのマスキングはすべてのフィールドに対して行われていくので、フィールド名を指定してのマスキングよりもパフォーマンスに与える影響は大ききなってしまいます。

正規表現を使ってのマスキング

これは、出力されるログメッセージの中で指定した正規表現にマッチした部分のみマスキングする方法になります。

loback.xmlの内容

defaultMaskはフィールド指定と同じになっていて、 valueに対して正規表現などを指定してマッチした部分を置き換えることができます。 なお、valueMaskを使うとフィールド名指定のpathMaskと同じようにマスク定義をそれぞれ定義できるようになります。

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <jsonGeneratorDecorator class="net.logstash.logback.mask.MaskingJsonGeneratorDecorator">
                <defaultMask>*****</defaultMask>
                <value>(sio){2}</value>
            </jsonGeneratorDecorator>
        </encoder>
    </appender>

実行結果

実行してみるといい感じに正規表現にマッチしたいる部分がマスキングできているのがわかりますね。 複数箇所マッチすれば全て置き換えられているのも確認できます。

カスタム実装を使用してマスキングしてみる

カスタム実装を使用すると、特定フィールドの値が特定の正規表現にマッチしたら置き換えなんてことが柔軟にできるようになります。 例えば、messageフィールド内に出力される可能性のあるメールアドレスなどをマスキングするなんてこともできたりします。

カスタム実装

この例では、出力される値のマスキングを行うインタフェースのValueMaskerを使ってログに出力される値のマスキングをしています。 また、余計なフィールドに対しては何も行わないようmessageフィールドのみを対象にしています。

class ValueMaskerExample: ValueMasker {
    override fun mask(context: JsonStreamContext, value: Any?): Any? {
        return if (value != null && context.currentName == "message") {
            value.toString().replace("siosio", "***")
        } else {
            value
        }
    }
}

logback.xmlの内容

valueMaskerに、カスタム実装のクラスの完全修飾名を指定してあげます。 これで、すべてのフィールドに対してカスタム実装のマスク処理が呼び出されるようになります。

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <jsonGeneratorDecorator class="net.logstash.logback.mask.MaskingJsonGeneratorDecorator">
                <valueMasker class="ValueMaskerExample" />
            </jsonGeneratorDecorator>
        </encoder>
    </appender>

実行結果

実行してみるとmessageフィールドのみマスキングが行われていることが確認できます。 マスキング対象外のcustomフィールドはマスキングが行われていないことが確認できます。

Ktorのルーティング設定で正規表現を使ってみる

Ktorの2.3.0からルーティング設定で正規表現が使えるようになったらしいので試してみたよ。

ルーティング設定で正規表現を使ってみる

お試しコード

ルーティング設定での正規表現の指定方法は、文字列ではなくkotlin.text.Regexを指定してあげる形になります。 あとは、普段使い慣れた正規表現でマッチさせたいパスを表現してあげればOKです。

routing {
    get(Regex("[a-z]+")) {
        call.respondText("uri: ${call.request.uri}")
    }
}

実行結果

正規表現にマッチするパスを指定すると、実装通りの結果がレスポンスで返って来ています。

curl -i http://localhost:8080/aaaaa
HTTP/1.1 200 OK
Content-Length: 11
Content-Type: text/plain; charset=UTF-8

uri: /aaaaa%    

正規表現にマッチしないパスを指定すると、ルーティングの設定にマッチしないので404が返ってくることが確認できます。

curl -i http://localhost:8080/aaA
HTTP/1.1 404 Not Found
Content-Length: 0

パスパラメータで正規表現を使ってみる

お試しコード

パスパラメータとして正規表現を使いたい場合には、(?<パスパラメータの名前>正規表現)kotlin.text.Regexに指定します。 下のコードの場合には、idという名前で数値のみに限定したパスパラメータを定義しています。

routing {
    get(Regex("""(?<id>\d+)""")) {
        call.respondText("id: ${call.parameters["id"]}")
    }
}

実行結果

パスパラメータにマッチする数字のみを指定すると、実装通りの結果がレスポンスで返って来ています。 正規表現を使った場合でも、パスパラメータの値を取得できていることもこれで確認できますね。

curl -i http://localhost:8080/123
HTTP/1.1 200 OK
Content-Length: 7
Content-Type: text/plain; charset=UTF-8

id: 123%  

パスパラメータの場合も、正規表現にマッチしないパスを指定すると404が返ってくることが確認できます。

curl -i http://localhost:8080/123a
HTTP/1.1 404 Not Found
Content-Length: 0

おわり。

JUnit Pioneerを使ってJUnit5のテスト実行時に環境変数を設定する

JUnit5なテスト実行時に環境変数を設定(クリア)できる、拡張のGitHub - junit-pioneer/junit-pioneer: JUnit 5 Extension Packを試してみました。

テスト実行時に環境変数を設定する

環境変数の設定は@SetEnvironmentVariableアノテーションで行います。 テストクラス、テストメソッド両方に指定ができるので、どのようなスコープで設定するかで使い分けることができるようになっています。

@SetEnvironmentVariable(key = "k1", value = "v1")
class HogeTest {

    @Test
    @SetEnvironmentVariable(key = "k2", value = "v2")
    fun 環境変数を設定してみる() {
        println("System.getenv(\"k1\") = ${System.getenv("k1")}")
        println("System.getenv(\"k2\") = ${System.getenv("k2")}")
    }
}

実行結果

設定した環境変数が設定され、参照できているのが確認できます。

System.getenv("k1") = v1
System.getenv("k2") = v2

テスト実行後に環境変数をクリアする

環境変数のクリアは@ClearEnvironmentVariableアノテーションで行います。 @SetEnvironmentVariableと同じように、テストクラスとテストメソッドの両方に指定が可能となっています。

    @Test
    @SetEnvironmentVariable(key = "k2", value = "v2")
    @ClearEnvironmentVariable(key = "k2")
    fun 環境変数を設定してみる() {
        println("System.getenv(\"k1\") = ${System.getenv("k1")}")
        println("System.getenv(\"k2\") = ${System.getenv("k2")}")
    }

おわり

Vue.js3でのv-modelのメモ

Vue.jsのバージョン3では、v-modelの仕様が結構変わっているのでそれのメモ。

サンプルコード

シンプルなパターン

modelValueプロパティを使った値の受け渡しになっていて、値の変更は、update:modelValueイベントを発火することで親コンポーネントに通知できるようになっています。
※バージョン2の頃の、valueプロパティとinputイベントの発火からは変更になっています。

子供側コンポーネントのコード

export default {
  name: 'ChildComponent',
  props: {
    modelValue: String,
  },
  data: () => {
    return {
      i: 1
    }
  },
  methods: {
    change: function () {
      const count = this.i++;
      this.$emit('update:modelValue', `${this.modelValue.replace(/:.+$/, '')}: ${count}`)
    }
  }
}

親側のコンポーネント

<template>
  <child v-model="str" />
</template>

名前付きのv-modelを使うパターン

v-modelの引数に名前を指定することで、その名前のプロパティを使った受け渡しが可能になります。 子供のコンポーネントでは、任意の名前でプロパティを定義して、値の変更はupdate:プロパティ名で発火することで親コンポーネントに通知できるようになっています。

子供側のコンポーネント

export default {
  name: 'ChildComponent',
  props: {
    name: String
  },
  methods: {
    change() {
      this.$emit('update:name', '変更されたよ')
    }
  }
}

親側のコンポーネント

<template>
  <child v-model:name="str" />
</template>

KotestのSpring拡張を試してみる

KotestSpring拡張を使う方法になります。

KotestのSpring拡張をdependenciesに追加

io.kotest:kotest-runner-junit5-jvmだけではなく、Spring拡張のio.kotest.extensions:kotest-extensions-springを追加します。

Mockkを使うのに便利なcom.ninja-squad:springmockkもセットで追加しておきます。

testImplementation("io.kotest:kotest-runner-junit5-jvm:5.1.0")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.0")

testImplementation("com.ninja-squad:springmockk:3.1.0")

テストを書いてみる

コンポーネントのテスト

テスト対象のコード

テスト対象は、Repositoryを呼び出して結果を詰め替えして返すだけのシンプルな実装としています。

@Transactional(readOnly = true)
class FindUsersUseCase(private val userRepository: UserRepository) {
    fun findAllUser(): List<UserDto> {
        return userRepository.findAll()
                .map { UserDto(it.id, it.name) }
    }
}

data class UserDto(val id: Int, val name: String)

テストコード

JUnit と同じように@SpringBootTestアノテーションを使ってテスト対象のコンポーネントを指定します。 KotestのSpring拡張はこのアノテーションのみでコンストラクターインジェクションを使ってテストが実行できるようです。

テスト対象の中で依存しているRepositoryをモックに差し替えてテストしたいので、dependenciesに追加したcom.ninja-squad:springmockk@MockkBeanアノテーションを使用してモックを生成しています。

SpringExtensionextensionsに登録することで、Kotestのライフサイクルの中でTestExecutionListenerが動作するようになります。(例えばこの機能で、Mockkのモックのクリア処理がテスト実行後に自動的に行われるようになったりします。)

@SpringBootTest(classes = [FindUsersUseCase::class])
@ActiveProfiles("test")
class FindUsersUseCaseTest(
    private val sut: FindUsersUseCase,
    @MockkBean val mockUserRepository: UserRepository
) : FreeSpec({

    extensions(SpringExtension)

    "ユーザ一覧が取得できるよ" {
        every { mockUserRepository.findAll() } returns listOf(User(1, "name_1"), User(2, "name_2"), User(3, "name_3"))

        sut.findAllUser() shouldBe listOf(UserDto(1, "name_1"), UserDto(2, "name_2"), UserDto(3, "name_3"))

        verify { mockUserRepository.findAll() }
    }
})

SpringExtensionをすべてのテストクラスで登録するのはかなり面倒かつ漏れたりすると思うので、下のようなプロジェクト全体設定で登録しておくといい感じになります。

class ProjectConfig : AbstractProjectConfig() {
    override fun extensions() = listOf(SpringExtension)
}

テストの実行結果テストも期待通りに実行できています。

f:id:sioiri:20220211214250p:plain

MockMvcを使ったテスト

テストコード

基本的なテストの書き方は、コンポーネントのテストと同じになります。 MockMvcを利用するために、JUnitと同じように@AutoConfigureMockMvcアノテーションを設定します。

@SpringBootTest
@AutoConfigureMockMvc
internal class UserHandlerTest(
    val mockMvc: MockMvc,
    val dataSource: DataSource,
    val flyway: Flyway
) : FreeSpec({

    extension(SpringExtension)

    beforeTest {
        flyway.clean()
        flyway.migrate()
    }

    "ユーザが登録できるよ" {
        val table = Table(dataSource, "user")
        val changes = Changes(table)
        changes.setStartPointNow()

        mockMvc.post("/api/users") {
            contentType = MediaType.APPLICATION_JSON
            content = """{"name": "siosio"}"""
        }.andExpect {
            status { isOk() }
        }

        changes.setEndPointNow()
        Assertions.assertThat(changes)
            .hasNumberOfChanges(1)
            .changeOfCreation()
            .rowAtEndPoint().value("name").isEqualTo("siosio")
    }

    "ユーザ一覧が取得できるよ" {
        mockMvc.get("/api/users")
            .andExpect {
                status { isOk() }
                content { contentType(MediaType.APPLICATION_JSON) }
                jsonPath("$.users.length()") { value(3) }
                jsonPath("$.users[*].name") { value(contains("user_1", "user_2", "user_3")) }
                jsonPath("$.users[*].id") { value(contains(1, 2, 3)) }
            }
    }
}) {
    companion object {
        @Suppress("unused")
        val container = MySQLContainer<Nothing>("mysql:8.0.28").run {
            start()
            waitingFor(HostPortWaitStrategy())

            System.setProperty("spring.datasource.url", jdbcUrl)
            System.setProperty("spring.datasource.username", username)
            System.setProperty("spring.datasource.password", password)
        }
    }
}

おわり。