HackToTech

Hack To Technology

@Configurationで@Beanを宣言した際に誰がどう登録しているのかを見てたのでメモ

詳しく書いてあるところが見当たらなかったので単にコードを追った
完全に個人的なメモ

tl;dr

最終的に DefaultSingletonBeanRegistry で持ってそう spring.pleiades.io

バージョン

  • Spring Boot 3.1.5
  • Spring Framework 6.0.13

コード

data class Example(val value: String)

@Configuration
class ExampleConfig {
    @Bean
    fun exampleA(): Example {
        return Example(
            value = "ExampleA",
        )
    }
}

メモ

PGAuditのログを減らしたくてバージョンごとの挙動の違いを見てたのでメモ

github.com

  • PGAuditのissueとcommitを眺めていて、 個人的に悩みだった下記の問題が解決してそうだったのでバージョンごとの違いを確認する
    • INSERT時の外部キーの監査ログがWRITEとして出てしまう問題
      • 正直あまり意味のあるログではないので、出来る限り取りたくない
    • SELECT FOR UPDATEがWRITEのログとして出てしまう問題
      • WRITEだけログに出したいのに、キューとして使用しているテーブルのポーリングログ(SELECT FOR UPDATE)が出て邪魔になったりしている

検証結果

  • PostgreSQLの13以降を使用していて、且つ pgaudit.logall とか read じゃなければ、
    オブジェクト監査ログで SELECT を取得しているテーブル以外は、これらのログを取得しなくても済むようになりそう

以下は、それぞれ検証したPostgreSQLのバージョン(PGAuditのバージョン)

12.17(1.4.3)

  • INSERTの外部キー監査ログ
2023-11-28 16:38:34.149 UTC [75] LOG:  AUDIT: SESSION,2,1,WRITE,INSERT,TABLE,public.ref,"INSERT INTO ref values(1, 'insert');",<none>
2023-11-28 16:38:34.150 UTC [75] LOG:  AUDIT: SESSION,2,2,WRITE,UPDATE,TABLE,public.main,"SELECT 1 FROM ONLY ""public"".""main"" x WHERE ""id"" OPERATOR(pg_catalog.=) $1 FOR KEY SHARE OF x",1
  • SELECT FOR UPDATEのログ
2023-11-28 16:42:18.902 UTC [75] LOG:  AUDIT: SESSION,5,1,MISC,BEGIN,,,BEGIN;,<none>
2023-11-28 16:42:22.407 UTC [75] LOG:  AUDIT: SESSION,6,1,WRITE,UPDATE,TABLE,public.ref,SELECT * FROM ref WHERE main_id = 1 FOR UPDATE;,<none>
2023-11-28 16:42:35.654 UTC [75] LOG:  AUDIT: SESSION,7,1,WRITE,UPDATE,TABLE,public.ref,UPDATE ref SET memo = 'update' WHERE main_id = 1;,<none>
2023-11-28 16:42:39.230 UTC [75] LOG:  AUDIT: SESSION,8,1,MISC,COMMIT,,,COMMIT;,<none>

13.13(1.5.2)

  • INSERTの外部キー監査ログ
2023-11-28 17:01:04.890 UTC [42] LOG:  AUDIT: SESSION,4,1,WRITE,INSERT,TABLE,public.ref,"INSERT INTO ref values(1, 'insert');",<none>
2023-11-28 17:01:04.890 UTC [42] LOG:  AUDIT: SESSION,4,2,READ,SELECT,TABLE,public.main,"SELECT 1 FROM ONLY ""public"".""main"" x WHERE ""id"" OPERATOR(pg_catalog.=) $1 FOR KEY SHARE OF x",1
  • SELECT FOR UPDATEのログ
2023-11-28 17:02:49.548 UTC [42] LOG:  AUDIT: SESSION,5,1,MISC,BEGIN,,,BEGIN;,<none>
2023-11-28 17:02:49.548 UTC [42] LOG:  AUDIT: SESSION,6,1,READ,SELECT,TABLE,public.ref,SELECT * FROM ref WHERE main_id = 1 FOR UPDATE;,<none>
2023-11-28 17:02:49.549 UTC [42] LOG:  AUDIT: SESSION,7,1,WRITE,UPDATE,TABLE,public.ref,UPDATE ref SET memo = 'update' WHERE main_id = 1;,<none>
2023-11-28 17:02:49.549 UTC [42] LOG:  AUDIT: SESSION,8,1,MISC,COMMIT,,,COMMIT;,<none>

14.10(1.6.2)

  • INSERTの外部キー監査ログ
2023-11-28 17:13:31.837 UTC [41] LOG:  AUDIT: SESSION,4,1,WRITE,INSERT,TABLE,public.ref,"INSERT INTO ref values(1, 'insert');",<none>
2023-11-28 17:13:31.838 UTC [41] LOG:  AUDIT: SESSION,4,2,READ,SELECT,TABLE,public.main,"SELECT 1 FROM ONLY ""public"".""main"" x WHERE ""id"" OPERATOR(pg_catalog.=) $1 FOR KEY SHARE OF x",1
  • SELECT FOR UPDATEのログ
2023-11-28 17:13:57.347 UTC [41] LOG:  AUDIT: SESSION,5,1,MISC,BEGIN,,,BEGIN;,<none>
2023-11-28 17:13:57.347 UTC [41] LOG:  AUDIT: SESSION,6,1,READ,SELECT,TABLE,public.ref,SELECT * FROM ref WHERE main_id = 1 FOR UPDATE;,<none>
2023-11-28 17:13:57.347 UTC [41] LOG:  AUDIT: SESSION,7,1,WRITE,UPDATE,TABLE,public.ref,UPDATE ref SET memo = 'update' WHERE main_id = 1;,<none>
2023-11-28 17:13:57.347 UTC [41] LOG:  AUDIT: SESSION,8,1,MISC,COMMIT,,,COMMIT;,<none>

検証環境構築

バージョン違うだけでほぼ同じなので12のパターンだけ

docker run --name pg12.17 -e POSTGRES_PASSWORD=password -d postgres:12.17
docker exec -it pg12.17 /bin/bash

# PGAuditのbuildに必要なものとかを入れる
apt update &&\
apt install -y build-essential curl libssl-dev libkrb5-dev postgresql-server-dev-12

cd tmp &&\
curl -L https://github.com/pgaudit/pgaudit/archive/refs/tags/1.4.3.tar.gz | tar xz --strip 1 &&\
make install USE_PGXS=1
  • shared_preload_libraries を設定する
psql -Upostgres
ALTER SYSTEM SET shared_preload_libraries TO 'pgaudit';
  • 読み込ませる為にコンテナの再起動をする
docker restart pg12.17
  • 再起動後に、PGAuditの設定をする
CREATE EXTENSION pgaudit;
CREATE ROLE auditor;
ALTER SYSTEM SET pgaudit.role = 'auditor';
ALTER SYSTEM SET pgaudit.log = 'all';
ALTER SYSTEM SET pgaudit.log_catalog = off;
ALTER SYSTEM SET pgaudit.log_level = 'log';
ALTER SYSTEM SET pgaudit.log_parameter = on;
ALTER SYSTEM SET pgaudit.log_relation = on;
ALTER SYSTEM SET pgaudit.log_statement_once = on;
SELECT pg_reload_conf();
  • SQLを流してログを確かめる
CREATE TABLE main (
   id SERIAL PRIMARY KEY
);

CREATE TABLE ref (
   main_id SERIAL REFERENCES main,
   memo    TEXT
);

INSERT INTO main values(1);
INSERT INTO ref values(1, 'insert');

BEGIN;
SELECT * FROM ref WHERE main_id = 1 FOR UPDATE;
UPDATE ref SET memo = 'update' WHERE main_id = 1;
COMMIT;

PGAuditのPR

SpringBoot + KotlinでListの要素にConstraintsをかけたかった話

tl;dr コンパイラオプションに -Xemit-jvm-type-annotationsを指定する必要がある
あとの文章は個人的なメモ

リストの要素にConstraintsをかけたかったが、
なんか一見正しく見えるのに正しく動かなかった

package com.example.exampleboot3

import org.hibernate.validator.constraints.Length

data class Request(val values: List<@Length(min = 1) String>)
package com.example.exampleboot3

import jakarta.validation.Valid
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController("/")
class SampleController {
    @PostMapping
    fun post(@RequestBody @Valid r: Request): String {
        return "ok"
    }
}

そのまま投げると、通ってしまう

curl -XPOST localhost:8080 -H "Content-Type: application/json" -d '{"values": ["test", ""] }'
ok 

でまあ、公式を見て -Xemit-jvm-type-annotations をコンパイルオプションに付与すればいいという話だった

kotlinlang.org

# build.gradle.kts
tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf(
            "-Xjsr305=strict",
            "-Xemit-jvm-type-annotations",
        )
        jvmTarget = "17"
    }
}

で、期待した通りに動くようにはなった

curl -XPOST localhost:8080 -H "Content-Type: application/json" -d '{"values": ["test", ""] }'
{"timestamp":"2023-10-28T17:51:52.475+00:00","status":400,"error":"Bad Request","path":"/"}

で、なんでそもそもJava8以降をターゲットとしてコンパイルする必要があるんだっけと思ったら

JSR 308 Explained: Java Type Annotations

JSR 308, Annotations on Java Types, has been incorporated as part of Java SE 8. This JSR builds upon the existing annotation framework, allowing type annotations to become part of the language. Beginning in Java SE 8, annotations can be applied to types in addition to all of their existing uses within Java declarations. This means annotations can now be applied anywhere a type is specified, including during class instance creation, type casting, the implementation of interfaces, and the specification of throws clauses. This allows developers to apply the benefits of annotations in even more places.

そもそもJSR308がJava8からの話だった(この辺よく理解してなかった)
1.8 よりも前を指定したらコンパイルコケるのか試したかったが、 1.6 指定したら Unknown Kotlin JVM target: 1.6 とか言われたので、古い環境用意するのも面倒になって諦めた

最近の環境ならデフォルトで有効でも良いんじゃないか?と思ってissueを眺めてたが、
基本的な使い方をする分には困らないけど、反変/共変での射影とかまだ色々と問題を抱えているから各自で有効にしてくれという話だと理解した https://youtrack.jetbrains.com/issue/KT-35843

There are a bunch of open questions that should be carefully discussed and full support of type annotations in JVM bytecode can't be fully implemented without discussing them.

でまあ一応中身の違いがどうなっているのかも気になったので、オプションの無効/有効で生成したclassファイルもみてみた

無効

public com.example.exampleboot3.Request(java.util.List<java.lang.String>);
descriptor: (Ljava/util/List;)V
flags: (0x0001) ACC_PUBLIC
Code:
  stack=2, locals=2, args_size=2
     0: aload_1
     1: ldc           #10                 // String values
     3: invokestatic  #16                 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V
     6: aload_0
     7: invokespecial #19                 // Method java/lang/Object."<init>":()V
    10: aload_0
    11: aload_1
    12: putfield      #22                 // Field values:Ljava/util/List;
    15: return
  LineNumberTable:
    line 5: 6
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0      16     0  this   Lcom/example/exampleboot3/Request;
        0      16     1 values   Ljava/util/List;
Signature: #7                           // (Ljava/util/List<Ljava/lang/String;>;)V
RuntimeInvisibleParameterAnnotations:
  parameter 0:
    0: #9()
      org.jetbrains.annotations.NotNull
MethodParameters:
  Name                           Flags
  values

有効

public com.example.exampleboot3.Request(java.util.List<java.lang.String>);
descriptor: (Ljava/util/List;)V
flags: (0x0001) ACC_PUBLIC
Code:
  stack=2, locals=2, args_size=2
     0: aload_1
     1: ldc           #13                 // String values
     3: invokestatic  #19                 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V
     6: aload_0
     7: invokespecial #22                 // Method java/lang/Object."<init>":()V
    10: aload_0
    11: aload_1
    12: putfield      #25                 // Field values:Ljava/util/List;
    15: return
  LineNumberTable:
    line 5: 6
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0      16     0  this   Lcom/example/exampleboot3/Request;
        0      16     1 values   Ljava/util/List;
Signature: #7                           // (Ljava/util/List<Ljava/lang/String;>;)V
RuntimeVisibleTypeAnnotations:
  0: #9(#10=I#11): METHOD_FORMAL_PARAMETER, param_index=0, location=[TYPE_ARGUMENT(0)]
    org.hibernate.validator.constraints.Length(
      min=1
    )
RuntimeInvisibleParameterAnnotations:
  parameter 0:
    0: #12()
      org.jetbrains.annotations.NotNull
MethodParameters:
  Name                           Flags
  values

RuntimeVisibleTypeAnnotations を見れば分かる通り、オプションによって有無の違いが見て取れる

docs.oracle.com

-Xmsと-Xmxを指定しても必ずしもすぐに物理メモリ上で確保されるわけではないという話

最初から確保されるものだと思い込んでいたら、
そういうわけではなかったので自分用に備忘録

理由はStackOverflowで解説されているので、それを読むのが良い stackoverflow.com

どうやらAlwaysPreTouchを指定すると(その分起動は遅くなるが)、
JVMの初期化時に強制的にマッピングされるらしいのでSpringのdemoアプリケーションで試した

AlwaysPreTouchあり

./gradlew bootRun -PjvmArgs="-Xms2g -Xmx2g -XX:+AlwaysPreTouch"

> Task :bootRun

2023-09-24 01:03:59.352  INFO 8070 --- [           main] com.example.demo.DemoApplicationKt       : Starting DemoApplicationKt using Java 11.0.20 on local with PID 8070 (/home/atr0phy/IdeaProjects/demo/build/classes/kotlin/main started by atr0phy in /home/atr0phy/IdeaProjects/demo)
<==========---> 83% EXECUTING [1m 33s]

AlwaysPreTouchなし

./gradlew bootRun -PjvmArgs="-Xms2g -Xmx2g"

> Task :bootRun

2023-09-24 01:05:51.778  INFO 8208 --- [           main] com.example.demo.DemoApplicationKt       : Starting DemoApplicationKt using Java 11.0.20 on local with PID 8208 (/home/atr0phy/IdeaProjects/demo/build/classes/kotlin/main started by atr0phy in /home/atr0phy/IdeaProjects/demo)
<==========---> 83% EXECUTING [3m 32s]

違いはRESを見れば分かる通り、2.2GBと284MBになっている

個人的にはXmsとXmxを同じ値で指定することが多いが、一応↓見た感じXmsのサイズが確保されるらしいとのことだったので、試した stackoverflow.com

一長一短あるので常に指定するのが良いとは限らないが、
起動後に少ししてから大してメモリをまだ使ってないにも関わらず、
GCがはしってCPUの使用率が高くなってレイテンシが悪化する(OS上のメモリの使用量がXmsに向けて増加しつつあったので恐らく裏側でページフォールトしてる)、みたいなことが解消できたので個人的には助かった

dependabotのgradleにKotlin DSLでpluginsとdependenciesで同じバージョンを指定してPRを作らせたい

残念ながら、gradleのバージョンが古くて(6.9系)version catalogが使えなかったので、その場合にしか役に立たない備忘録

おそらくgradleのバージョンが7.2以上なら多分version catalog使って、こんな面倒なことしなくても色々解決する気がしている developer.android.com Add support for Gradle Version Catalogs (Gradle 7.0) · Issue #3121 · dependabot/dependabot-core · GitHub

後は、最近GAされたdependabotの grouped version を使っても出来るような気もするが、個人的にはktsで完結している方が好みではある github.blog

以下は、通常であればそれぞれで指定する意味はないケースだが、今回は例としてあげる
できればバージョンを一箇所で指定して読み取るのがベストだが、
後述の理由によりそれができないので springBootVersion の変数をそれぞれ指定して作ることによって、dependabotのPRとしては1つ作成させるようにしている

  • build.gradle.kts
plugins {
  val springBootVersion = "3.1.1"
  id("org.springframework.boot") version springBootVersion 
}
  • module/build.gradle.kts
dependencies {
  val springBootVersion = "3.1.1"
  implementation("org.springframework.boot:spring-boot-starter:$springBootVersion")
}

そうすると↓みたいな感じで、変数に対してまとめてPRを出してくれる

で、なぜ一箇所で定義して読み取る方式ができないかというと、
古い gradle のージョンで恐らく出来ることといえば、
gradle.properties を使って pluginManagement 側で指定しつつ、 dependencies 側は inejct することになると思う

  • gradle.properties
springBootVersion=3.1.1
  • settings.gradle.kts
pluginManagement {
  plugins {
    val springBootVersion: String by settings
    id("org.springframework.boot") version springBootVersion
  }
}
  • build.gradle.kts
plugins {
  // pluginManagement 側で指定しているので特に指定しない
  id("org.springframework.boot")
}
  • module/build.gradle.kts
dependencies {
  val springBootVersion: String  by project
  implementation("org.springframework.boot:spring-boot-starter:$springBootVersion")
}

ただこれだと、gradleは正しく読み取って処理を行えるが、
dependabotは gradle.properties で指定して埋め込んだバージョンを理解できなくてPRを作ってくれないので、
最初に紹介した変数を複数定義する方式を今回は採用した

ちなみにこの変数を使う方式だと同じバージョンを複数のライブラリに使ってまとめてアップデートすることも可能だったりする

  • build.gradle.kts
dependencies {
  val awsSdkVersion = "2.20.135"
  implementation("software.amazon.awssdk:sts:$awsSdkVersion")
  implementation("software.amazon.awssdk:s3:$awsSdkVersion")
  implementation("software.amazon.awssdk:cognitoidentityprovider:$awsSdkVersion")
}