失敗は一時の恥

パッケージソフト開発をしているプログラマが気の赴くままに何かを投稿するブログ.

Jar に含まれるリソースを検索する

概要

Java のコードから Jar に含まれるリソースファイルを取得する場合は,おそらく最も単純な方法だと ClassLoader#getResource(String) メソッドなどを使うことになると思います.

ですが,「指定した名前のリソースを取得」するというよりも,「名前が特定のパターンのリソースをすべて取得」したいといった状況もあるのではないでしょうか.

そのようなことはどうすればいいのか調べていたところ

といった解決策を見つけることができました.

Corn CPS

Corn Classpath Scanner (略して CPS) は,その名の通りクラスパスの探索を行うためのライブラリです.

使ってみる

pom.xml に Corn CPS を追加します:

<dependency>
  <groupId>net.sf.corn</groupId>
  <artifactId>corn-cps</artifactId>
  <version>1.1.10</version>
</dependency>

例えば "default_*.png" といったパターンの名前を持つリソースを見つけるには,次のようにします.

List<URL> urls = CPScanner.scanResources(new ResourceFilter().resourceName("default_*.png"));

ちなみに,これで返ってくる URL は jar:file:/... といった値になっています. こういった URL にアクセスするために,Java には JarURLConnection というクラスが用意されているということに初めて気がつきました.
(普段 ClassLoader#getResourceAsStream(String) でストリームを直接取っていた)

PathMatchingResourcePatternResolver

こちらは Spring Framework に含まれているクラスです.

すでに Spring を使っている環境ではこちらの方が導入の手間がなくて済むのかもしれません.
(逆に,リソースの検索のためだけに Spring のライブラリを入れるというのもどうかと思います)

使ってみる

pom.xml にライブラリを追加します:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-core</artifactId>
  <version>5.1.4.RELEASE</version>
</dependency>

先ほどの例のように "default_*.png" といったパターンの名前を持つリソースを見つけるには,次のようにします.

PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath*:/**/default_*.png");

ここで返ってくるのは org.springframework.core.io.Resource 型の値です.

リソースの内容は Resource#getInputStream() メソッドなどを使えば読み出すことができます.

リソース検索の仕組み

これらのライブラリの中身はどのようなことをしているのだろう?と思って,CPScanner のソースを見てみました. 見てみましたが,僕にはパッと見では何故これで求めているリソースが取得できるのか理解できませんでした……

おわり

Jar の中から特定パターンの名前を持つリソースを検索するライブラリとして Corn CPS と PathMatchingResourcePatternResolver を紹介しました. 便利なライブラリがあったものですが,使うだけでなく余裕があるときに中身も読んで理解していきたいものです.

Hello Golang!

How to Write Go Code の内容に沿ってコードを書いてみました.

概要

Go に興味があったのと,最近 Go で色々する必要が生じたので,とりあえずチュートリアルをやってみようといったところです.

僕自身 Go のプログラムを書いたことがほぼないという状態から,とりあえず Hello world 程度のものを動かすという内容です.

まだ勉強不足なので手を動かしながら疑問に思った点もひとまず書き散らしつつ,ひとまず読み進めた軌跡というような感じになっています.

最終的に書いたものは GitHub に置きました.

作業環境

大雑把に以下のような環境です:

Go のインストール

brew install go で入れます.

Go 自体のバージョン管理 (rbenv 的な) はどうするのかふと頭をよぎりますが,今の時点ではとりあえず気にしません.

ソースディレクトリを作る

適当に作業ディレクトリを決めて,ソース置き場を作ります.

How to Write Go Code に従えば,ディレクトリのつくりは

  • 作業場所を決める (そこを $GOPATH とする)
  • $GOPATH/src/github.com/username/project みたいなディレクトリにコードを置く
  • go コマンドによるビルド物は $GOPATH/bin に生成されるようになる

という風になっているようです.

$GOPATH は Go コマンドの動作に影響する環境変数で,デフォルトだと $HOME/go となるようなのですが, プロジェクト毎にライブラリなどを管理したい場合は都度都度指定した方がいいように想像しました.

Visual Studio Code の設定

テキストエディタだけでもできないことはないのでしょうが,作業効率的に IDE 的なものがあったほうがいいと思い,とりあえず VS Code を使っています. (Go 向けのもっといい IDE があるのかどうかは今の時点では気にしていません)

まず,Go の拡張機能を入れました.

GOPATH in the VS Code Go Extension の説明を参考に,GOPATH の推測を有効にするようにしました . (この設定をしないと VS Code がエラーを出したりして不安な気持ちになるのでとりあえずやっておいた方がいいと思います)

ワークスペースの設定 .vscode/settings.json に次のように記述します.

{
  "go.inferGopath": true
}

これを指定すると,

という動きをするようです.

コードを書く

前述の内容を実施しておけば,VS Code で作業ディレクトリ ($GOPATH となる場所) を開いて,How to Write Go Code の内容にしたがってコードを書いてコマンドを叩いて,という操作ができるようになっていると思います.

  • src/github.com/usrname/hello とか,
  • src/github.com/username/stringutil とか,

あたりに,言われた通りにとりあえずコードを写経しました.

パッケージ名について

Go のプログラムでは「パッケージ名」とは

  • プログラム全体でユニークである必要はない
  • ユニーク性が必要なのは Import path (e.g. github.com/username/project)
  • プログラム上のパッケージ名は Import path の最後の要素 (e.g. Import path がcrypto/rot13 ならパッケージ名は rot13)

という扱いのようです.

Java だとクラス名が衝突するときはプログラム上 FQCN で指定する (com.example.project.subproject.SomeClass みたいな) ことになっていたと思うのですが,Go でパッケージ名が衝突した時はどうなるのでしょう.Haskell みたいに import qualified できるといいな……

テスト

ユニットテスト書き方の説明に従って書いてみました.

テストを実行するときは,go test github.com/user/project といったコマンドを叩きます.

コード例 (テスト付き)

ふつうのテストとは別に,Example というコード例を記述する仕組みもあるみたいです. 関数名が Example で始まるものが go test コマンドによりコード例として認識されます.

今回のチュートリアルにはないのですが,次のような main 関数に対する Example を書いてみました.

package main

func ExampleMain() {
    main()
    // Output:
    // Hello world!
}

この // Output: に続けて書かれているコメント通りの文字列が標準出力に出るのかどうかが,go test コマンド実行時に検証されます

テストをまとめて実行

go test コマンドにパッケージ名のプレフィックス... を与えると,そのプレフィックスに一致するパッケージのテストがすべて実行されます.

こんな感じです:

$ go test github.com/snobutaka/hello-golang/...
ok      github.com/snobutaka/hello-golang/hello
ok      github.com/snobutaka/hello-golang/stringutil

リモートからのソース取得

go get github.com/golang/example/hello といったコマンドを叩くと,リモートからそのソースを取得して $GOPATH/src 以下に配置してくれます.

この go get をした時点で,$GOPATH/bin に生成される hello というバイナリが,自分で書いたサンプルコードをビルドしたものなのかリモートから取得してビルドしたものなのかよく分からないのですが,例としてどうなんでしょう……

ここまでやってみて

ひとまず,プロジェクトを構成して,コードを書いて,テストを書いて,実行して,という流れはイメージが持てたように思います.

まだまだ実践的なモノを作れそうなレベルにはないので,もっと色々調べなきゃですね.

さらに学ぶために

この辺のページも読んでみようと思いました:

あとはこのあたりの本も読んでみたい:

Kotlin/Native を試してみる

概要

Kotlin/Native という,Kotlin のコードをネイティブのバイナリにコンパイルするツールがリリースされたというニュースを目にし,気になったのでちょっとだけ試してみました.

Kotlin で書いたネイティブな Hello world! を実行できるようにしてみます.

基本的には,Kotlin Tutorials にある A Basic Kotlin/Native Application の通りにやっていけば,Hello world! できます.

下準備

Docker イメージ

今回は Kotlin/Native を動かす Docker コンテナを用意して試しました.

Java 8 と Kotlin 1.3 と Kotlin/Native 1.3 が必要なので, これらが使える環境を作成します.

次のような Dockerfile ファイルを書きました.

FROM openjdk:8

RUN wget -O /tmp/kotlin.zip https://github.com/JetBrains/kotlin/releases/download/v1.3.10/kotlin-compiler-1.3.10-linux-x64.zip && \
    unzip /tmp/kotlin.zip -d /usr/lib && \
    wget -O /tmp/kotlin-native.tar.gz https://github.com/JetBrains/kotlin/releases/download/v1.3.0/kotlin-native-linux-1.3.0.tar.gz && \
    tar -xf /tmp/kotlin-native.tar.gz -C /usr/lib && \
    rm /tmp/kotlin.zip /tmp/kotlin-native.tar.gz

ENV PATH $PATH:/usr/lib/kotlinc/bin:/usr/lib/kotlin-native-linux-1.3.0/bin

CMD [ "/bin/bash" ]

Java はベースイメージを openjdk:8 にして使えるようにしました. 他は適当にダウンロードしてきて展開し,パスを通しているだけです.

Dockerfile を作成したら,ビルドしておきましょう.

docker build -t kotlin-native .

ソース

Hello world! 的な Kotlin プログラムを書きます:

fun main(args: Array<String>) {
    println("Hello Kotlin!")
}

実行

Docker イメージを実行して,Kotlin のソースをネイティブコンパイルし,バイナリを実行してみましょう.

$ docker run -it kotlin-native
(以下,コンテナ内の世界)
$ kotlinc-native hello.kt
(program.kexe というファイルができる)
$ ./program.kexe
=> Hello Kotlin!

出力されるファイルが .kexe という見慣れない怪しげな拡張子になっていますが,これは Java バイトコードとかそんなんではなく,ネイティブに実行可能なバイナリです.

Hello world! 的なメッセージが得られて,めでたしめでたし!

やってて気にかかったこと

~/.konan に依存ライブラリが入る

Mac でも Tutorial に書いてある通りにやれば実行できることは確認したのですが, ~/.konan というディレクトリに Kotlin/Native の依存ライブラリなどがインストールされてなんとなく嫌だったので,Docker の使い捨て環境でやることにしました.

おわり

ごくごく簡単でしたが Kotlin/Native を試してみました.

まぁ,個人的に今後使う予定もいまのところないのですが,好奇心を満たせたのでよし!

Awaitility で非同期処理のテストをいい感じに書く

概要

Awaitility という,Java で非同期な処理に対するテストを書く際の便利ライブラリがあったのでその紹介です.

マルチスレッドで諸々が背後で動いているときに,テストが期待する状態になるのを待つコードをいい感じに実装できるというもので,Usage の例を挙げると

await().until(newUserIsAdded());

とか

await().atMost(5, SECONDS).until(newUserWasAdded());

みたいな書き方ができるようになります.いい感じっぽいですよね?!

使い方

ドキュメントを軽く読んでみた薄いまとめです.

ライブラリの入れ方

Maven を使っているならこんな感じで依存関係を追加します.

<dependency>
      <groupId>org.awaitility</groupId>
      <artifactId>awaitility</artifactId>
      <version>3.1.2</version>
      <scope>test</scope>
</dependency>

他にも Gradle や SBT の設定例が Getting started に載っています.

使い方

Usage に載っている簡単な例を 3 つほど試してみました.

最も単純な例

何かしらの非同期な処理でユーザーが追加される,というコードをテストするというシチュエーションです.

追加されるユーザーのデータを取得してアサーションをかける前に,こんな感じで待機します:

await().until(newUserIsAdded());

ここで newUserIsAdded() は,ユーザーが増えたことを判定する Callable です:

private Callable<Boolean> newUserIsAdded() {
      return new Callable<Boolean>() {
            public Boolean call() throws Exception {
                  return userRepository.size() == 1;
            }
      };
}

再利用性がより高い例

until() に,真理値を返す Callable を渡すのではなく,値のサプライヤとマッチャを渡すこともできます:

await().until( userRepositorySize(), equalTo(1) );

ここで userRepositorySize() は次のようなサプライヤです:

private Callable<Integer> userRepositorySize() {
      return new Callable<Integer>() {
            public Integer call() throws Exception {
                  return userRepository.size();
            }
      };
}

一方で equalTo()Hamcrest のマッチャです.普段から JUnit のアサートに使う人も多いと思います.

こうしておくことで,期待する状態というのがユーザーが 1 人追加された状態なのか 3 人追加された状態なのかと条件が変わっても,テストコードが書きやすくなりますね.

Proxy を使った条件

自分で都度 Callable を作るのではなく,AwaitilityClassProxy に定義されている to メソッドを使って次のような書き方もできます:

await().untilCall( to(userRepository).size(), equalTo(3) );

この Proxy を使うには,awaitility-proxy という依存関係を別途追加する必要があります.

その他

他にもサンプルがありますが僕が読んで試したのはここまでです. 興味が湧いた人はご自身で Usage あたりを読んでみてください.

仕組み

テストコードをいい感じに書けるわけですが,実施にやっていることは条件式のポーリングのようです.

初期値の説明 をみたところ,とりあえず何も設定せずに await() した場合は次のように動作するようです:

  • ポーリング開始の初期遅延は 100 ミリ秒
  • ポーリングの間隔は 100 ミリ秒
  • ポーリングのタイムアウトは 10 秒

所感

自分も最近非同期なコードに対するテストを書いていて,少なくとも自分の開発機で動かしている分には何の問題もないのですが,他のプロジェクトと同居しているビルドサーバーや,誰かが作業用に作った低スペックな VM だとタイミング問題でテストがこけるという事象がときどき起きてしまっています.

単純に Thread.sleep() を長めに見積もって設定しても無駄な待ち時間でテストやビルドが長引くので,じゃあポーリング的なことをすればいいかといっても適当に書くとコードが汚くなるし,いい感じのライブラリないかな〜と探していたら見つけたのが Awaitility でした.

ということで,ときどき失敗してしまうテストコードの修正にでも近々取り組みたいと思います……

アンチウイルスソフトと同居した PostgreSQL の奇行

概要

仕事で PostgreSQL を使っているのですが,奇妙な動作をすることが稀にあります. 奇妙な動作といっても,PostgreSQL 自体の不具合というではなく,アンチウイルスソフトとの競合で発生している事象だと思われる類のものです.

稀に発生すると,問題の切り分けに非常に労力を割かれて嫌な思いをしたりもするので,備忘録ついでにブログに書いておこうと思います.

観測された事象

大きく 2 つあります.

1. テーブルの主キーのインデックスが壊れる

主キーと適当なカラムを持つテーブルがありました:

id | value 
---+------
1  | 'one'
2  | 'two'

このテーブルに対して,values を条件に SELECT 文を発行すると意図した結果が得られるのですが,id を条件にすると意図せぬ結果が得られるということがありました.

postgres=# SELECT * FROM table WHERE value = 'one';
(1 行 SELECT される)
id | value 
---+------
1  | one
postgres=# SELECT * FROM table where id = 1;
(1 行も SELECT されない(!))

この状況を見るに,レコードは確かに存在しているが,条件に指定したときに SELECT できるカラムとできないカラムの違いとは何……と考えた結果として「インデックスがおかしい」という結論になりました.

この事象への対処としては,reindexdb コマンドを使ってインデックスを作り直すことで解決することができました.

このケースでは,主キーで SELECT もできないのですが,主キーで UPDATE もやはりできないので,「対象レコードは存在するのに操作しても値が変わらないのは……?マルチスレッドで FOR UPDATE したり SKIP LOCKED したりする実装にバグがあるの???」といった見当違いな予想 (実際にはそこには問題はなかった) から調査を開始して結論にたどり着くまでが一苦労でした.

2. 外部キー制約を犯した行が挿入される

tableB から tableA へ外部キー制約を張っているのですが,制約を犯した行が鎮座しているという事象がありました.

tableA:

id | value 
---+------
1  | 'one'
2  | 'two'

tableB:

id  | comment | foreign_key
----+---------+------------
100 | 'hello' | 1
101 | 'world' | 3 (こんな id は tableA にない(!))

この事象が起きたとき,外部キー制約を犯した行を DELETE しようとしたり,外部キーとして存在するはずの行を INSERT したりしようとしたのですが,どちらも「外部キー制約違反」となり押すも引くもできない状態になってしまいました. 最終的に,テーブルの DROP を強行するくらいしか取れる選択肢がありませんでした.

これももしかするとインデックスが壊れたことで発生した事象なのかもしれません.

事象が観測された環境

僕の経験の範囲ですが,以下の組み合わせて上述の事象を経験しました.

アンチウイルスソフト自体は他の製品でも問題になることはあるのかもしれません.

基本的には,業務で開発環境として使っている Windows マシン (会社の情報管理規定によりウイルスバスターのインストール必須) やテスト環境において起きており,DB が壊れてもデータ消して作り直せばいいでしょくらいのノリではありました (顧客環境で顧客のインストールしたアンチウイルスソフトで問題が起こることがなかったとは言ってない).

PostgreSQLアンチウイルスソフトに関する情報

PostgreSQL Wiki にはアンチウイルスソフトを同居させると意図せぬ挙動をするという旨の記載があるし,アンチウイルスソフトの説明にもデータベースソフトを監視するなと書いてある情報は見つかります. 使おうとするソフトの説明書きをよく読めということですね.

所感

DB があり得ない状態になっていると,コードを追っても理解できないようなエラーをアプリケーションが出すので,原因の特定に大変苦労します.

ふつうに過ごしていると DB が壊れているなんてことはなかなかないとは思うのですが,不可思議なエラーを目にしたら先入観を取っ払って普段は考えないようなところに問題があるのでは?と疑ってみるのも必要ですね.

計算の概念

計算の概念

過去に作成した,数学において「計算」というものをどう定式化するか?といった紹介をする資料を SlideShare にアップロードしたので,ここにもリンクを貼ってみます.

計算の概念 - SlideShare

(MacLaTeX で作成した PDF なのですが,オンラインでプレビューすると日本語部分が正しく表示されないようです.適宜ダウンロードしてプレビューしていただけると幸いです)

このスライドは,僕が大学院生の頃に「学部 1 年生向けに専門分野を紹介する」みたいな機会があって,その際に作成したものです.

大学院では数理論理学やラムダ計算の研究をしていた (というよりも,これを書いた当時はこの辺りの分野の研究ネタを見つけるために勉強していたという感じですが) ので,誰しもなんとなくイメージを持っている「計算」というものについて,数学的にはこんな形で現されるんだという紹介をしようと思ったわけです.

話す対象の学部 1 年生というのも,理学部の学生 (数学とか物理が得意な類の人々) だったので,そこそこ複雑な定義とか計算を見せつける初見の人には若干ハードな内容となってしまってはいますが……

# 久しぶりに少しくらい数学の勉強でもしたい気持ちが湧かないでもない今日この頃

マルチスレッド下で SimpleDateFormat をおかしくしてみる

概要

JavaSimpleDateFormat クラスはスレッドセーフでないことで知られていると思いますが,実際にマルチスレッド下で使ったらどうなるのかを試してみました.

実際に試すと,エラーになることもなくメチャクチャな値が得られるという悲惨な結果となりました.

背景

最近,職場のコードで SimpleDateFormat をマルチスレッドで使用している箇所が見つかり騒ぎになったので,実際にどういう挙動をするのか調べてみようと思った次第です.

「見つかり騒ぎになった」というのも,お客さんから製品の怪しい動作を指摘されて調査した結果発覚したという経緯になっていて,他のお客さんも含めたその後の対応もあり色々と辛い出来事でした.

実験

SimpleDateFormat をマルチスレッドで使うこんな感じのコードを書いてみました:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class BreakDateFormat {
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

    public static void main(String[] args) throws ParseException, InterruptedException {
        final int numThreads = 10;
        final int numLoops = 10;
        final String dateString = "2001-07-04T12:08:56.235-0700";
        final Set<Date> parsedDates = Collections.synchronizedSet(new HashSet<>());

        ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
        for (int i = 0; i < numThreads; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        for (int j = 0; j < numLoops; j++) {
                            parsedDates.add(dateFormat.parse(dateString));
                        }
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(5, TimeUnit.SECONDS);

        System.out.println("size=" + parsedDates.size());
        System.out.println("values:");
        for (Date date : parsedDates) {
            System.out.println(dateFormat.format(date));
        }
    }
}

やっていることのポイントは

  • static な SimpleDateFormat のインスタンスを作る: static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
  • executorService に上記のインスタンスを使って parsedDates.add(dateFormat.parse(dateString)); を実行するタスクを放り込んでマルチスレッドで動作させる

というところです.

どんな値が得られるのかは最終的に,Set parsedDates の中身を標準出力に出します.

結果

10 スレッドで 10 回ずつタスクを回してみて,手元では次のような結果が得られました.

size=6
values:
1669500-12-05T04:08:56.235+0900
0001-07-05T04:41:21.235+0900
2008-04-05T04:08:56.235+0900
2001-07-05T04:08:56.235+0900
2000-12-05T04:08:56.235+0900
2059-08-05T04:08:56.235+0900

かなりぶっ飛んだ値が得られました.

こんな値に従って動作したら,システムもさぞ問題のある挙動をすることでしょう……

この問題を防ぐにはどうすればよかったか

コードレビューをきちんとしていればさすがに誰かは気づいたのではないかというのと,ツールとして SpotBugs なんかを使えば,static な SimpleDateFormat インスタンスの使用箇所を検出してくれたりもして,防げたのではないかという印象です. 実際に,今回書いた不具合再現プログラムに対しても指摘してくれています.

f:id:snobutaka:20181029084521p:plain
spotbugs_find_static_date_format

所感

やってはいけないことというのを学んだり,やってはいけないことをコードレビューで見つけるなり,個人の知識や開発プロセスとして何かしらできることはあったはずだという無念な気持ちが湧きました.

特に今回のレベルの問題は SpotBugs で機械的に検知できるものでもあったので,後で見つかってから部門をまたがって協議したり緊急対応パッチを作ったりユーザ全体へ通達を出したりなんかするコストを考えると,仕組みで防げるものは潰していけるようにツールなりなんなり導入するのは十分投資として見合うものだな〜と思ったり.