UnicodeエスケープされたJavaプロパティファイルをsedとnkfで読めるようにする
native2ascii無き現代に.propertiesの化石と向き合う
もう2022年なのでnative2asciiはJDKに同梱されなくなっているわけですが、一方で目の前にはUnicodeエスケープされた.properties
ファイルが。
手元にはnative2asciiなんぞ無いので、sedとnkfでなんとかしましょう。
環境
こうする
$ 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標準のBSD版 sed
コマンドで LF
に統一したい。
こうする。
$ sed -e ':L' -e 'N' -e '$!bL' -e 's/\r\n/\n/g' -e 's/\r/\n/g' file.txt あいうえお かきくけこ さしすせそ
file.txt
全体を読み込んでから CR+LF
を LF
に置換し、最後に残った CR
を LF
に置換する。ファイル全体を読み込むので、ファイルサイズが大きい場合は注意。
仕組み
処理の流れは下記のとおり。
:L
: ラベルL
を定義する。N
: ファイルから次の行を読み込み、パターンスペース(sed
が読み込んだ行を保存する領域)に追加する。$!bL
: ファイル最終行$
でない!
場合は分岐(ジャンプ)b
する。分岐先はラベルL
。*1s/\r\n/\n/g
: パターンスペース内で\r\n
を\n
に置換する。s/\r/\n/g
: パターンスペース内で\r
を\n
に置換する。
1.〜3.のループにより、ファイル全体がパターンスペースに保存される。
4.〜5.では、パターンスペースに保存されたファイル全体の文字列に対して、それぞれ CR+LF
→LF
, CR
→LF
を行っている。
後述のとおり、通常sed
は1行ずつの読み込み時に改行コード LF
が含まれないため、CR+LF
の行が /\r\n/
にはマッチしない。
しかし2.のようにN
コマンドで読み込むことで、改行コード LF
も含めてパターンスペースに保存されるため /\r\n/
で CR+LF
の行にマッチするようになり、CR+LF
→ LF
の置換ができる。
※後から気づいたんだけどこっちのが楽
$ sed -e 's/\r$//g' -e 's/\r/\n/g' file.txt あいうえお かきくけこ さしすせそ
CR+行末
(つまり CR+LF
での改行)の CR
を削除してから、残りの CR
を LF
に置換する。
後述のとおり、sed
の出力時にはもともと行末についていた LF
も出てくるので、行末の CR
を削除してしまえば自動的に LF
での改行として出力される。
あとは CR
で改行されているものだけ LF
に置換すればよい。
うまくいかない例
単純に CR
を削除する
$ sed -e 's/\r//g' file.txt あいうえお かきくけこさしすせそ
よくあるやつ。CR+LF
を LF
に変換するだけであれば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+LF
は LF
が二重になってしまう。
(例えば あいうえお\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 namedSPRING_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 namedMY_SERVICE_0_OTHER
.
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階層目のファイル/ディレクトリだけを列挙したい」となり*1、macOSで $ 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
.
つまり、指定したパスの開始点から深さ 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.
こっちは深さ優先探索にするオプションらしい。
GNU版の -depth
-depth
ディレクトリそのものより先に、ディレクトリの中身を処理する。 アクション -delete を使用すると、-depth オプションも自動的に設定される。
つまり、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
mvnコマンドに-Dオプションを渡せばpom.xmlのプロパティが指定できる
はじめに
実は知らなかったシリーズ。ドキュメントちゃんと読んでないのがバレる。
環境
- Maven 3.8.2
本題
例えばこんな感じで 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}
とか。
参考
SpringアプリのテストでHikariDataSource has been closedなどと言われたのでなんとかした
TL; DR
- Springのテスト時に使われるコンテキストキャッシュ
DefaultContextCache
はデフォルトの保持数が32で、それを超えると古いコンテキストが破棄される。そのときにDBのコネクションプールも破棄されてしまって困る。 - キャッシュの保持数はSpringのプロパティ
spring.test.context.cache.maxSize
で設定できる。
環境
- Spring Boot (spring-boot-starter 2.4.4)
- spring-framework 5.3.5
- HikariCP 3.4.5
- AdoptOpenJDK 16.0.1
- IntelliJ IDEA 2021.2 (Ultimate)
本題
なにごと?
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 */)); }
どうやらDataSourceが閉じられる際には、ここを通っているらしい。 IntelliJ IDEAで上記メソッドにブレークポイントを仕掛けて、コールスタックを追ってみる。
最初の HikariPool#shutdown
, HikariDataSource#close
は正常なコネクションプール終了処理であり、特に気になる部分は無し。
そのまま順番に追っていくと、DefaultContextCache.LruCache#removeEldestEntry
で DefaultContextCache.this.remove(〜)
しているところが見つかる。
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); // ←これ }
DefaultContextCacheはspring-testでのテストの際にアプリケーションコンテキストのキャッシュを保持するクラスで、LruCacheはその内部クラス。
LRUっていうぐらいだし、キャッシュがいっぱいになったら古いものを削除するんだろうなと想像できる。実際、削除処理の条件も if (this.size() > DefaultContextCache.this.getMaxSize())
だし。
キャッシュ保持数はデフォルトで32の様子*1。 このことから、以下のような事象が起こっていると考えられる。
- テスト実行時に、アプリケーションコンテキストが
DefaultContextCache
に格納される。32番目のテストまでは、問題なく実行される。 - 33番目のテストの際、アプリケーションコンテキストを
DefaultContextCache
に格納しようとすると、キャッシュ保持数の最大値を超えているため、古いアプリケーションコンテキストが削除される。 - アプリケーションコンテキストが削除されると、それに紐づくDBのコネクションプールがクローズされる。
- 33番目のテストでDBに接続しようとすると、3.で閉じられたコネクションプールが使われてしまい、エラーとなる。
キャッシュから削除されないようにキャッシュ保持数の最大値を大きくすれば、ひとまずは解消できそう。
DefaultContextCache
のドキュメントによると、この最大値はプロパティ spring.test.context.cache.maxSize
で変更できるらしい。
最大サイズは、コンストラクターの引数として指定するか、システムプロパティまたは spring.test.context.cache.maxSize という Spring プロパティを介して設定できます。
application.yaml
で設定したり、実行時引数で指定したりすればOK。
# application.yaml spring.test.context.cache.maxSize: 128
$ mvn test -Dspring.test.context.cache.maxSize=128
とはいえ、これだとコンテキストが破棄されないのでリソース消費が気になる…。本当はもうちょっとまともな対応方法がほしいところではあるけど、まぁテストだしこれでいいことにする。
まとめ
つかれた。