HackToTech

Hack To Technology

whiptailでlsしてファイル選択した結果を取得する

スクリプト書いてるとたまーに欲しくなるやつ
検索してもなんかぱっといい感じのやつが出てこないので(長すぎてコピペするのに躊躇うやつとか)、主に自分のローカルで使うように書いた

↓は /sbin で試したやつ

macで使うなら brew install coreutils して、 gls 使わないと動かないので注意

相変わらず bash 使ってるけど、いい加減他のシェルにしたほうが良いか悩ましい

LocalVariableTableParameterNameDiscovererが削除されてAutowireに失敗するようになった話

github.com

SpringBootを3.2.0に上げた際に、
同じ型のBeanを定義していて、名前による(Qualifierではない)Autowireが失敗するようになってしまったので、それの備忘録
とりあえず複数のBeanがマッチするところまでの話は以下の記事に書いてるので端折る

www.hacktotech.net

DependencyDescriptor#getDependencyName が呼ばれる
https://github.com/spring-projects/spring-framework/blob/v6.1.2/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java#L1728

MethodParameter#getParameterName が呼ばれる
https://github.com/spring-projects/spring-framework/blob/v6.1.2/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java#L356

ここでパラメータ名を解決するわけだが、
参照している parameterNameDiscoverer
KotlinReflectionParameterNameDiscoverer
StandardReflectionParameterNameDiscoverer
LocalVariableTableParameterNameDiscoverer
の順番でパラメータ名の解決を行うようになっていたのが、削除されたことによって失敗するようになっていた
(元のバージョンでも上2つのdiscovererは最終的にnullを返していたので、そのままでは解決できなくなった)
https://github.com/spring-projects/spring-framework/blob/v6.1.2/spring-core/src/main/java/org/springframework/core/MethodParameter.java#L714

で、困ったことに自分たちのコードはKotlinで書いているが、
外部ライブラリのstarterがJavaで書かれており、wikiの対応がまだされていなかったので、自分たちのSpringBootのバージョンをあげたら壊れてしまったという話だった
(自分たちのコードで、Kotlin内で同じ型のBeanが複数構成されていた場合の名前による解決はうまく動いたので、少なくともこのパターンだけは対応しないとうまく動かないとかな気がする)

↓はPoC

github.com

jmodule は外部ライブラリをイメージして書いていて、
src 配下は自分たちのコードをイメージして書いている

org.springframework.boot3.1.7 であれば
jmodule 側の build.gradle に↓を書かなくても問題なく動くが、

tasks.withType(JavaCompile).configureEach {
    options.compilerArgs.add("-parameters")
}

3.2.1 など spring-core6.1 系になると失敗するようになる
最初出会したときは、自分たちの build.gradle.kts だけ対応すればいけるのかと思っていたが、
そんなことはなかったのでバージョンを上げる際は注意したほうが良さそう

一応コンパイルオプションつけたときの ResolverAutoConfiguration.java のclassファイルの違いもみてみた

無効

public class org.example.resolver.ResolverAutoConfiguration
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #12                         // org/example/resolver/ResolverAutoConfiguration
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 2
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // org/example/resolver/Resolver
   #8 = Utf8               org/example/resolver/Resolver
   #9 = Methodref          #7.#10         // org/example/resolver/Resolver."<init>":(Lorg/example/resolver/ParameterNameResolver;)V
  #10 = NameAndType        #5:#11         // "<init>":(Lorg/example/resolver/ParameterNameResolver;)V
  #11 = Utf8               (Lorg/example/resolver/ParameterNameResolver;)V
  #12 = Class              #13            // org/example/resolver/ResolverAutoConfiguration
  #13 = Utf8               org/example/resolver/ResolverAutoConfiguration
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               Lorg/example/resolver/ResolverAutoConfiguration;
  #19 = Utf8               resolver
  #20 = Utf8               (Lorg/example/resolver/ParameterNameResolver;)Lorg/example/resolver/Resolver;
  #21 = Utf8               parameterNameResolver
  #22 = Utf8               Lorg/example/resolver/ParameterNameResolver;
  #23 = Utf8               RuntimeVisibleAnnotations
  #24 = Utf8               Lorg/springframework/context/annotation/Bean;
  #25 = Utf8               SourceFile
  #26 = Utf8               ResolverAutoConfiguration.java
  #27 = Utf8               Lorg/springframework/context/annotation/Configuration;
{
  public org.example.resolver.ResolverAutoConfiguration();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lorg/example/resolver/ResolverAutoConfiguration;

  public org.example.resolver.Resolver resolver(org.example.resolver.ParameterNameResolver);
    descriptor: (Lorg/example/resolver/ParameterNameResolver;)Lorg/example/resolver/Resolver;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: new           #7                  // class org/example/resolver/Resolver
         3: dup
         4: aload_1
         5: invokespecial #9                  // Method org/example/resolver/Resolver."<init>":(Lorg/example/resolver/ParameterNameResolver;)V
         8: areturn
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lorg/example/resolver/ResolverAutoConfiguration;
            0       9     1 parameterNameResolver   Lorg/example/resolver/ParameterNameResolver;
    RuntimeVisibleAnnotations:
      0: #24()
        org.springframework.context.annotation.Bean
}
SourceFile: "ResolverAutoConfiguration.java"
RuntimeVisibleAnnotations:
  0: #27()
    org.springframework.context.annotation.Configuration

有効

public class org.example.resolver.ResolverAutoConfiguration
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #12                         // org/example/resolver/ResolverAutoConfiguration
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 2
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // org/example/resolver/Resolver
   #8 = Utf8               org/example/resolver/Resolver
   #9 = Methodref          #7.#10         // org/example/resolver/Resolver."<init>":(Lorg/example/resolver/ParameterNameResolver;)V
  #10 = NameAndType        #5:#11         // "<init>":(Lorg/example/resolver/ParameterNameResolver;)V
  #11 = Utf8               (Lorg/example/resolver/ParameterNameResolver;)V
  #12 = Class              #13            // org/example/resolver/ResolverAutoConfiguration
  #13 = Utf8               org/example/resolver/ResolverAutoConfiguration
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               Lorg/example/resolver/ResolverAutoConfiguration;
  #19 = Utf8               resolver
  #20 = Utf8               (Lorg/example/resolver/ParameterNameResolver;)Lorg/example/resolver/Resolver;
  #21 = Utf8               parameterNameResolver
  #22 = Utf8               Lorg/example/resolver/ParameterNameResolver;
  #23 = Utf8               MethodParameters
  #24 = Utf8               RuntimeVisibleAnnotations
  #25 = Utf8               Lorg/springframework/context/annotation/Bean;
  #26 = Utf8               SourceFile
  #27 = Utf8               ResolverAutoConfiguration.java
  #28 = Utf8               Lorg/springframework/context/annotation/Configuration;
{
  public org.example.resolver.ResolverAutoConfiguration();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lorg/example/resolver/ResolverAutoConfiguration;

  public org.example.resolver.Resolver resolver(org.example.resolver.ParameterNameResolver);
    descriptor: (Lorg/example/resolver/ParameterNameResolver;)Lorg/example/resolver/Resolver;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: new           #7                  // class org/example/resolver/Resolver
         3: dup
         4: aload_1
         5: invokespecial #9                  // Method org/example/resolver/Resolver."<init>":(Lorg/example/resolver/ParameterNameResolver;)V
         8: areturn
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lorg/example/resolver/ResolverAutoConfiguration;
            0       9     1 parameterNameResolver   Lorg/example/resolver/ParameterNameResolver;
    MethodParameters:
      Name                           Flags
      parameterNameResolver
    RuntimeVisibleAnnotations:
      0: #25()
        org.springframework.context.annotation.Bean
}
SourceFile: "ResolverAutoConfiguration.java"
RuntimeVisibleAnnotations:
  0: #28()
    org.springframework.context.annotation.Configuration

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

docs.oracle.com

SpringBootでAutowireする際に複数のBeanがマッチした際の挙動をみたのでメモ

雰囲気で挙動を理解していたのをちゃんと見たのでメモ

spring.pleiades.io

@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
        @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
        ...
        // Step 3b: direct bean matches, possibly direct beans of type Collection / Map
        Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor); // ここで型に対して一致するbeanの一覧を取得する
        ...

        // Step 4: determine single candidate
        if (matchingBeans.size() > 1) {
            autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor); // 複数のbeanが当てはまる場合はどれを使うかを決定する
            if (autowiredBeanName == null) {
                if (isRequired(descriptor) || !indicatesArrayCollectionOrMap(type)) {
                    // Raise exception if no clear match found for required injection point
                    return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
                }
                else {
                    // In case of an optional Collection/Map, silently ignore a non-unique case:
                    // possibly it was meant to be an empty collection of multiple regular beans
                    // (before 4.3 in particular when we didn't even look for collection beans).
                    return null;
                }
            }
            instanceCandidate = matchingBeans.get(autowiredBeanName);
        }
        ...
}
protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) {
    Class<?> requiredType = descriptor.getDependencyType();
    String primaryCandidate = determinePrimaryCandidate(candidates, requiredType); // @Primaryがないかを優先して探す
    if (primaryCandidate != null) {
        return primaryCandidate; // 見つかったら返す
    }
    String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType); // @Priorityで指定した中で最も優先度の高い(値の低い)ものを探す
    if (priorityCandidate != null) {
        return priorityCandidate; // 見つかったら返す
    }
    // Fallback: pick directly registered dependency or qualified bean name match
    for (Map.Entry<String, Object> entry : candidates.entrySet()) {
        String candidateName = entry.getKey();
        Object beanInstance = entry.getValue();
        if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) ||
                matchesBeanName(candidateName, descriptor.getDependencyName())) {
            return candidateName; // Autowireする際の変数名が一致するものが見つかったら返す
        }
    }
    return null;
}

@Priority でInjectするbeanを操作しようとした際に、↓は一見 @Priority が評価されそうに見えるが、
実際は評価されず exampleAexampleB どちらを使えば良いかわからずに終了する

import jakarta.annotation.Priority
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.Ordered


interface MarkerExample
data class ExampleImpl1(val value: String): MarkerExample
data class ExampleImpl2(val value: String): MarkerExample

@Configuration
class ExampleConfig {
    @Bean
    @Priority(Ordered.LOWEST_PRECEDENCE)
    fun exampleA(): MarkerExample {
        return ExampleImpl1(
            value = "ExampleA",
        )
    }

    @Bean
    @Priority(Ordered.HIGHEST_PRECEDENCE)
    fun exampleB(): MarkerExample {
        return ExampleImpl2(
            value = "ExampleB",
        )
    }

    @Bean
    fun exampleC(example: MarkerExample): String {
        return "ExampleC"
    }
}

逆に↓は動く(Bで解決される)

import jakarta.annotation.Priority
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.Ordered


interface MarkerExample
@Priority(Ordered.LOWEST_PRECEDENCE)
data class ExampleImpl1(val value: String): MarkerExample
@Priority(Ordered.HIGHEST_PRECEDENCE)
data class ExampleImpl2(val value: String): MarkerExample

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

    @Bean
    fun exampleB(): MarkerExample {
        return ExampleImpl2(
            value = "ExampleB",
        )
    }

    @Bean
    fun exampleC(example: MarkerExample): String {
        return "ExampleC"
    }
}

こういったパターンはほぼ @Primary があれば足りるし使うことはないが、
思っていた挙動と違ったので意外だった

@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