UnicodeエスケープされたJavaプロパティファイルをsedとnkfで読めるようにする

native2ascii無き現代に.propertiesの化石と向き合う

もう2022年なのでnative2asciiはJDKに同梱されなくなっているわけですが、一方で目の前にはUnicodeエスケープされた.propertiesファイルが。

手元にはnative2asciiなんぞ無いので、sednkfでなんとかしましょう。

環境

  • macOS Big Sur 11.6
    • brew install nkfnkfを入れてある

こうする

$ cat example.properties | sed -E 's/\\u([0-9A-z]{4})/\&#x\1;/g' | nkf --numchar-input

例えばこんな感じの example.properties なら…

$ cat example.properties
# example.properties
FOO=\u3075\u30FC
BAR=\u3070\u30FC

こうなる

$ cat example.properties | sed -E 's/\\u([0-9A-z]{4})/\&#x\1;/g' | nkf --numchar-input
# example.properties
FOO=ふー
BAR=ばー

ポイント

Unicodeエスケープされた文字列のデコードには、nkf--numchar-input オプションが使える。 しかしこのコマンドで受け付けるのは &#XXXX; のような形式。 Javaのプロパティファイルは \uXXXX の形式でエンコードされているので、sed\uXXXX&#xXXXX; に置換してから nkf に渡す。

混在する改行コードをBSD版sedで変換する

環境

CR+LF, CR, LF を全て LF に統一したい

改行コード CR+LF, CR, LF が混在した邪悪なファイル file.txt がある。

$ echo -n 'あいうえお\r\nかきくけこ\rさしすせそ\n' > file.txt
# file.txt
あいうえお<CR+LF>
かきくけこ<CR>
さしすせそ<LF>

これをmacOS標準のBSDsed コマンドで LF に統一したい。

こうする。

$ sed -e ':L' -e 'N' -e '$!bL' -e 's/\r\n/\n/g' -e 's/\r/\n/g' file.txt
あいうえお
かきくけこ
さしすせそ

file.txt 全体を読み込んでから CR+LFLF に置換し、最後に残った CRLF に置換する。ファイル全体を読み込むので、ファイルサイズが大きい場合は注意。

仕組み

処理の流れは下記のとおり。

  1. :L: ラベル L を定義する。
  2. N: ファイルから次の行を読み込み、パターンスペース(sedが読み込んだ行を保存する領域)に追加する。
  3. $!bL: ファイル最終行 $ でない ! 場合は分岐(ジャンプ) b する。分岐先はラベル L*1
  4. s/\r\n/\n/g: パターンスペース内で \r\n\n に置換する。
  5. s/\r/\n/g: パターンスペース内で \r\n に置換する。

1.〜3.のループにより、ファイル全体がパターンスペースに保存される。 4.〜5.では、パターンスペースに保存されたファイル全体の文字列に対して、それぞれ CR+LFLF, CRLF を行っている。

後述のとおり、通常sedは1行ずつの読み込み時に改行コード LF が含まれないため、CR+LFの行が /\r\n/ にはマッチしない。 しかし2.のようにNコマンドで読み込むことで、改行コード LF も含めてパターンスペースに保存されるため /\r\n/CR+LF の行にマッチするようになり、CR+LFLF の置換ができる。

※後から気づいたんだけどこっちのが楽

$ sed -e 's/\r$//g' -e 's/\r/\n/g' file.txt
あいうえお
かきくけこ
さしすせそ

CR+行末(つまり CR+LF での改行)の CR を削除してから、残りの CRLF に置換する。

後述のとおり、sedの出力時にはもともと行末についていた LF も出てくるので、行末の CR を削除してしまえば自動的に LF での改行として出力される。 あとは CR で改行されているものだけ LF に置換すればよい。

うまくいかない例

単純に CR を削除する

$ sed -e 's/\r//g' file.txt
あいうえお
かきくけこさしすせそ

よくあるやつ。CR+LFLF に変換するだけであればOKだが、CR のみの場合は改行コードが消えてしまうので今回の条件ではNG。

行ごとに CR+LF でマッチさせようとする

$ sed -e 's/\r\n/\n/g' -e 's/\r/\n/g' file.txt
あいうえお

かきくけこ
さしすせそ

通常 sed は1行ごとに文字列を処理していくが、そのときの処理対象の文字列には改行コード LF は含まれないため、's/\r\n/\n/g' では CR+LF にマッチしない。結局 's/\r/\n/g' しか処理されないので、CR+LFLF が二重になってしまう。

(例えば あいうえお\r\n という文字列は、sed コマンドで普通に読み込まれると あいうえお\r に見えており、\n が含まれない)

行ごとに CR+行末 でマッチさせようとする

$ sed -e 's/\r$/\n/g' -e 's/\r/\n/g' file.txt
あいうえお

かきくけこ
さしすせそ

\r$CR+LF の行にマッチするものの、出力時には置換後の \n に加えて、もともと行末についていた \n も出てくるため、改行コードが二重になってしまう。

*1:つまりファイル最終行に到達するまで、1.に戻って、2.で次の行を読み込んで、…を繰り返す

Spring Bootでプロパティを環境変数から読むときの変換ルール

これは完全に自分用メモなのですが

Spring Bootでプロパティの値を環境変数から読むときの変数名の変換ルールは下記のとおり。

To convert a property name in the canonical-form to an environment variable name you can follow these rules:

  • Replace dots (.) with underscores (_).
  • Remove any dashes (-).
  • Convert to uppercase.

For example, the configuration property spring.main.log-startup-info would be an environment variable named SPRING_MAIN_LOGSTARTUPINFO.

Environment variables can also be used when binding to object lists. To bind to a List, the element number should be surrounded with underscores in the variable name.

For example, the configuration property my.service[0].other would use an environment variable named MY_SERVICE_0_OTHER.

docs.spring.io

tr コマンドで tr 'a-z.[' 'A-Z__' | tr -d ']-' とすれば変換できる。

$ echo 'spring.main.log-startup-info' | tr 'a-z.[' 'A-Z__' | tr -d ']-'
SPRING_MAIN_LOGSTARTUPINFO
$ echo 'my.service[0].other' | tr 'a-z.[' 'A-Z__' | tr -d ']-'
MY_SERVICE_0_OTHER

環境

  • Spring Boot 2.6.2

findコマンドの-depthオプションのGNU版とBSD版の違いでハマったという話

なにごと?

find コマンドで「1階層目のファイル/ディレクトリだけを列挙したい」となり*1macOS$ man find したら -depth オプションを使えばよさそうだったので、こんなふうにしてたわけです。

$ find . -depth 1

で、これをGitHub ActionsでやっているCI(ubuntu-latestなイメージ)に組み込んだら、こんなエラーが。

find: paths must precede expression: `1'

よく見てみると、findコマンドのオプションがBSD版とGNU版で異なるためにエラーになっている様子。 というわけで、ちゃんとマニュアルを読んでみる。

環境

BSD版の -depth

-depth n

True if the depth of the file relative to the starting point of the traversal is n.

https://www.freebsd.org/cgi/man.cgi?find(1)

つまり、指定したパスの開始点から深さ n のファイルやディレクトリが列挙される。

ちなみに引数 n を取らない -depth オプションもある。

-depth

Always true; same as the non-portable -d option. Cause find to perform a depth-first traversal, i.e., directories are visited in post-order and all entries in a directory will be acted on before the directory itself. By default, find visits directories in pre-order, i.e., before their contents. Note, the default is not a breadth-first traversal.

https://www.freebsd.org/cgi/man.cgi?find(1)

こっちは深さ優先探索にするオプションらしい。

GNU版の -depth

-depth

ディレクトリそのものより先に、ディレクトリの中身を処理する。 アクション -delete を使用すると、-depth オプションも自動的に設定される。

https://linuxjm.osdn.jp/html/GNU_findutils/man1/find.1.html

つまり、BSD版の -depth (引数を取らないほう)と同じく深さ優先探索になる。 引数を取る -depth n は無い。このため、前述のエラーになっていたということらしい。

GNU版でもBSD版の -depth n みたいに特定の階層のみ列挙したい場合は?

-mindepth-maxdepth を組み合わせればよい。 下記のようにすれば、1階層目のファイル/ディレクトリだけが列挙される。

$ find . -mindepth 1 -maxdepth 1

*1:これだけだと「lsでいいじゃん」ってなるのですが、実際にはもうちょっと細かい条件があって、findが適切だったのです

jshellコマンドを使って.javaファイル無しでJavaプログラムを実行する

環境

$ jshell -version
jshell 17

本題

小ネタ。

jshellコマンドは標準入力からソースコードを流し込めるので、シェルのヒアドキュメントを使うことでソースコードのファイルを作らずにJavaプログラムを実行できる。

例えば「CIのときにあらかじめcreate databaseしておきたい!けどpsqlコマンドを使うためだけにapt-get install postgresqlするのもなんかやだ!!手元にJDBCドライバはある!!!」みたいなレアな状況で役に立つ。

$ jshell -s --class-path ~/local/lib/postgresql-42.3.1.jar <<EOS
import java.sql.DriverManager;
try (var c = DriverManager.getConnection("jdbc:postgresql://localhost:5432/", "postgres", "postgres");
    var s = c.createStatement()) {
    s.execute("create database testdb");
}
EOS

CI以外でも、ちょっとしたJavaプログラムをシェルスクリプト内で実行したいときなどに使えるはず。

mvnコマンドに-Dオプションを渡せばpom.xmlのプロパティが指定できる

はじめに

実は知らなかったシリーズ。ドキュメントちゃんと読んでないのがバレる。

環境

本題

例えばこんな感じで pom.xml を書いて…

<properties>
    <someOption>${someValue}</someOption>
</properties>

こんな感じで -D オプション付きで実行する。

$ mvn -DsomeValue=SOME_VALUE

そうすると、pom.xml${someValue}SOME_VALUE がセットされる。コマンドで動作を切り替えたいときとかに使える。

-Dシステムプロパティを定義するオプション。 -Dシステムプロパティを定義して ${ } で参照するという、至極当たり前の話ではある。 当然、<properties> 以外のタグ内でも使える。

ちなみに ${env.XXX}環境変数 XXX の値も参照できるらしい。${env.PATH} とか。

maven.apache.org

参考

SpringアプリのテストでHikariDataSource has been closedなどと言われたのでなんとかした

TL; DR

  • Springのテスト時に使われるコンテキストキャッシュ DefaultContextCache はデフォルトの保持数が32で、それを超えると古いコンテキストが破棄される。そのときにDBのコネクションプールも破棄されてしまって困る。
  • キャッシュの保持数はSpringのプロパティspring.test.context.cache.maxSize で設定できる。

環境

本題

なにごと?

JUnitでSpring Bootアプリのテストをゴリゴリ追加していたところ、いきなりエラーが。

logger:o.s.b.w.embedded.tomcat.TomcatWebServer  LEVEL:INFO  thread:main msg:Tomcat started on port(s): 52394 (http) with context path ''
logger:xxx.xxx.XxxTest                          LEVEL:INFO  thread:main msg:Started XxxTest in 1.134 seconds (JVM running for 65.714)
logger:o.s.s.concurrent.ThreadPoolTaskExecutor  LEVEL:INFO  thread:main msg:Shutting down ExecutorService 'applicationTaskExecutor'
logger:com.zaxxer.hikari.HikariDataSource       LEVEL:INFO  thread:main msg:HikariPool-1 - Shutdown initiated...
logger:com.zaxxer.hikari.HikariDataSource       LEVEL:INFO  thread:main msg:HikariPool-1 - Shutdown completed.
logger:o.s.test.context.TestContextManager      LEVEL:WARN  thread:main msg:Caught exception while invoking 'beforeTestMethod' callback on TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener@347d1586] for test method [com.intuit.karate.junit5.Karate xxx.xxx.XxxTest.testXxx()] and test instance [xxx.xxx.XxxTest.testXxx@5e462cc]
org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction; nested exception is java.sql.SQLException: HikariDataSource HikariDataSource (HikariPool-1) has been closed.
        at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:309)
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:400)
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373)

下記の行に注目。DBへのコネクションを開始しようとしたのにDataSourceが閉じられてしまっているのでエラー、ということらしい。

Could not open JDBC Connection for transaction; nested exception is java.sql.SQLException: HikariDataSource HikariDataSource (HikariPool-1) has been closed.

DataSourceが勝手に閉じられるのはよろしくないので、なぜ閉じられているのかを調べることにする。

調べてみる

HikariCPで何か起こっているということはわかったので、HikariCPのログレベルを debug に変更し、ログの情報量を増やしてみる。

application.yamlを下記のように設定して再度テストを実行。

logging:
  level:
    com.zaxxer.hikari: debug

そうすると、テスト開始時にDBとのコネクションが閉じられている様子が表示される。(connection evicted) ですって。

logger:o.s.b.w.embedded.tomcat.TomcatWebServer  LEVEL:INFO  thread:main msg:Tomcat started on port(s): 54354 (http) with context path ''
logger:xxx.xxx.XxxTest    LEVEL:INFO  thread:main msg:Started XxxTest in 1.068 seconds (JVM running for 64.084)
logger:o.s.s.concurrent.ThreadPoolTaskExecutor  LEVEL:INFO  thread:main msg:Shutting down ExecutorService 'applicationTaskExecutor'
logger:com.zaxxer.hikari.HikariDataSource       LEVEL:INFO  thread:main msg:HikariPool-1 - Shutdown initiated...
logger:com.zaxxer.hikari.pool.HikariPool        LEVEL:DEBUG thread:main msg:HikariPool-1 - Before shutdown stats (total=10, active=0, idle=10, waiting=0)
logger:com.zaxxer.hikari.pool.PoolBase          LEVEL:DEBUG thread:HikariPool-1 connection closer msg:HikariPool-1 - Closing connection org.postgresql.jdbc.PgConnection@36d7a68a: (connection evicted)
logger:com.zaxxer.hikari.pool.PoolBase          LEVEL:DEBUG thread:HikariPool-1 connection closer msg:HikariPool-1 - Closing connection org.postgresql.jdbc.PgConnection@1eeacb6e: (connection evicted)
logger:com.zaxxer.hikari.pool.PoolBase          LEVEL:DEBUG thread:HikariPool-1 connection closer msg:HikariPool-1 - Closing connection org.postgresql.jdbc.PgConnection@102d26c3: (connection evicted)
logger:com.zaxxer.hikari.pool.PoolBase          LEVEL:DEBUG thread:HikariPool-1 connection closer msg:HikariPool-1 - Closing connection org.postgresql.jdbc.PgConnection@1412a01e: (connection evicted)
logger:com.zaxxer.hikari.pool.PoolBase          LEVEL:DEBUG thread:HikariPool-1 connection closer msg:HikariPool-1 - Closing connection org.postgresql.jdbc.PgConnection@51102cf8: (connection evicted)
logger:com.zaxxer.hikari.pool.PoolBase          LEVEL:DEBUG thread:HikariPool-1 connection closer msg:HikariPool-1 - Closing connection org.postgresql.jdbc.PgConnection@19692c0f: (connection evicted)
logger:com.zaxxer.hikari.pool.PoolBase          LEVEL:DEBUG thread:HikariPool-1 connection closer msg:HikariPool-1 - Closing connection org.postgresql.jdbc.PgConnection@600b1adf: (connection evicted)
logger:com.zaxxer.hikari.pool.PoolBase          LEVEL:DEBUG thread:HikariPool-1 connection closer msg:HikariPool-1 - Closing connection org.postgresql.jdbc.PgConnection@61984490: (connection evicted)
logger:com.zaxxer.hikari.pool.PoolBase          LEVEL:DEBUG thread:HikariPool-1 connection closer msg:HikariPool-1 - Closing connection org.postgresql.jdbc.PgConnection@313fd3b7: (connection evicted)
logger:com.zaxxer.hikari.pool.PoolBase          LEVEL:DEBUG thread:HikariPool-1 connection closer msg:HikariPool-1 - Closing connection org.postgresql.jdbc.PgConnection@61e698cb: (connection evicted)
logger:com.zaxxer.hikari.pool.HikariPool        LEVEL:DEBUG thread:main msg:HikariPool-1 - After shutdown stats (total=0, active=0, idle=0, waiting=0)
logger:com.zaxxer.hikari.HikariDataSource       LEVEL:INFO  thread:main msg:HikariPool-1 - Shutdown completed.
logger:o.s.test.context.TestContextManager      LEVEL:WARN  thread:main msg:Caught exception while invoking 'beforeTestMethod' callback on TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExe
cutionListener@7ef7161d] for test method [com.intuit.karate.junit5.Karate xxx.xxx.XxxTest.testXxx()] and test instance [xxx.xxx.XxxTest@55b5625d]
org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction; nested exception is java.sql.SQLException: HikariDataSource HikariDataSource (HikariPool-1) has been closed.
        at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:309)
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:400)

HikariCPのソースコード内を (connection evicted) で検索すると、HikariPool の下記のメソッドが見つかる。

public final class HikariPool extends PoolBase implements HikariPoolMXBean, IBagStateListener
{
   // 中略
   /** {@inheritDoc} */
   @Override
   public void softEvictConnections()
   {
      connectionBag.values().forEach(poolEntry -> softEvictConnection(poolEntry, "(connection evicted)", false /* not owner */));
   }

https://github.com/brettwooldridge/HikariCP/blob/ed2da5f1f4ef19f871fac12effc0b199706905dc/src/main/java/com/zaxxer/hikari/pool/HikariPool.java#L375

どうやらDataSourceが閉じられる際には、ここを通っているらしい。 IntelliJ IDEAで上記メソッドにブレークポイントを仕掛けて、コールスタックを追ってみる。

f:id:ser1zw:20210806023511p:plain

最初の HikariPool#shutdown, HikariDataSource#close は正常なコネクションプール終了処理であり、特に気になる部分は無し。

そのまま順番に追っていくと、DefaultContextCache.LruCache#removeEldestEntryDefaultContextCache.this.remove(〜) しているところが見つかる。

f:id:ser1zw:20210806022737p:plain

public class DefaultContextCache implements ContextCache {
    // 中略
    private class LruCache extends LinkedHashMap<MergedContextConfiguration, ApplicationContext> {
        LruCache(int initialCapacity, float loadFactor) {
            super(initialCapacity, loadFactor, true);
        }

        protected boolean removeEldestEntry(Entry<MergedContextConfiguration, ApplicationContext> eldest) {
            if (this.size() > DefaultContextCache.this.getMaxSize()) {
                DefaultContextCache.this.remove((MergedContextConfiguration)eldest.getKey(), HierarchyMode.CURRENT_LEVEL); // ←これ
            }

https://github.com/spring-projects/spring-framework/blob/7c2a72c9b43d066ae9e71d4f39d7bab8f6d9c2ff/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java#L337

DefaultContextCacheはspring-testでのテストの際にアプリケーションコンテキストのキャッシュを保持するクラスで、LruCacheはその内部クラス。 LRUっていうぐらいだし、キャッシュがいっぱいになったら古いものを削除するんだろうなと想像できる。実際、削除処理の条件も if (this.size() > DefaultContextCache.this.getMaxSize()) だし。

キャッシュ保持数はデフォルトで32の様子*1。 このことから、以下のような事象が起こっていると考えられる。

  1. テスト実行時に、アプリケーションコンテキストが DefaultContextCache に格納される。32番目のテストまでは、問題なく実行される。
  2. 33番目のテストの際、アプリケーションコンテキストを DefaultContextCache に格納しようとすると、キャッシュ保持数の最大値を超えているため、古いアプリケーションコンテキストが削除される。
  3. アプリケーションコンテキストが削除されると、それに紐づくDBのコネクションプールがクローズされる。
  4. 33番目のテストでDBに接続しようとすると、3.で閉じられたコネクションプールが使われてしまい、エラーとなる。

キャッシュから削除されないようにキャッシュ保持数の最大値を大きくすれば、ひとまずは解消できそう。 DefaultContextCache のドキュメントによると、この最大値はプロパティ spring.test.context.cache.maxSize で変更できるらしい。

最大サイズは、コンストラクターの引数として指定するか、システムプロパティまたは spring.test.context.cache.maxSize という Spring プロパティを介して設定できます。

spring.pleiades.io

application.yaml で設定したり、実行時引数で指定したりすればOK。

# application.yaml
spring.test.context.cache.maxSize: 128
$ mvn test -Dspring.test.context.cache.maxSize=128

とはいえ、これだとコンテキストが破棄されないのでリソース消費が気になる…。本当はもうちょっとまともな対応方法がほしいところではあるけど、まぁテストだしこれでいいことにする。

まとめ

つかれた。

*1:DefaultContextCacheのコンストラクタにブレークポイントを仕掛けてmaxSizeの値を確認した