読者です 読者をやめる 読者になる 読者になる

理系学生日記

おまえはいつまで学生気分なのか

忍者TOOLS

Stubby4JにおけるJSONでの正規表現、あるいはリクエスト中の特定のフィールドでレスポンスを変更する

technology java

Stubby4J を使用したテストにおいて、API リクエストとして送る JSON の中の値によって、レスポンスを切り替えたい場合がある。 もうちょっと具体的にいうと、

  1. リクエストの JSON の中の特定のフィールドの値によってのみ、レスポンスを切り替える
  2. 他のフィールドの値は実行時まで予想できない

こういうケースでレスポンスを切り替えるのは Stubby4J (v3.3.0) ではできないということが分かったので、その原因と、何とかする方法を考えてみる。

実装上できない理由

Stubby4J では、実際に送られてきた HTTP リクエストと、設定ファイル(YAML) 上に記述されたリクエストを StubRequest#equals(Object) で突合し、合致する場合にのみ、YAML ファイル上に記述されたレスポンスを返却するという動作になっている。 この「突合」(equals) の実装は、実際の HTTP リクエストの Content-Type によって挙動を替えるようになっている。application/json の場合は、「送信されてきた JSON」と「YAML ファイル上に記述された JSON」が、JSON のフィールド順を除いて完全一致すること が条件になっている。 以下に実装を示すけど、JSONCompare を使用しているということは、HTTP リクエスト上の JSON はもちろん、YAML 上に記述した JSON も valid である必要があるので、何らかの細工 (正規表現を記述するなど) もできない。

   private boolean postBodiesMatch(final boolean isDataStorePostStubbed, final String dataStorePostBody, final String thisAssertingPostBody) {
         // snip

         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_JSON)) {
            try {
               return JSONCompare.compareJSON(dataStorePostBody, thisAssertingPostBody, JSONCompareMode.NON_EXTENSIBLE).passed();
            } catch (JSONException e) {
               return false;
            }
         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_XML)) {

         // snip
   }

設計思想によってできない理由

Json request body matching by vishalmanohar · Pull Request #19 · azagniotov/stubby4j · GitHub で、JSON の中の値でレスポンスを切り替えられるような Pull Request があった。 これを利用すると、以下のようにリクエストの JSON に依存したレスポンスの設定がかけるようになる。

-  request:
      method: POST
      url: /templatize/response
   response:
      -  status: 200
         headers:
            content-type: application/json
         body: >
               <#assign requestBodyJson = requestBody?eval>
               {"foo": "${requestBodyJson.foo}"}

のだけれど、この Pull Request に対しては「HTTP リクエストと YAML 上のリクエストの突合は HTTP 1.1 レベルでしか実施しない」という設計思想が語られており、Merge されないままクローズされた。 実はこのあとに Content-Type に依存した突合の実装が取り込まれていることから鑑みるに、設計思想が変化しているみたいなのだけれど…。

何とかする方法

上記の PR と同じような修正を行ったり、あるいは現行の実装で使用している JSONCompare での比較ではなく、もっと柔軟な比較 (正規表現を許容する、jsonpath で比較すべきフィールドを指定する、JSON Schema を書けるようにする、等) をできるようにする、などはできるのだけど、とりあえず実現するだけなら以下で可能になる。

$ git diff  main/java/by/stub/yaml/stubs/StubRequest.java
diff --git a/main/java/by/stub/yaml/stubs/StubRequest.java b/main/java/by/stub/yaml/stubs/StubRequest.java
index 7e6a8a8..d76d6de 100644
--- a/main/java/by/stub/yaml/stubs/StubRequest.java
+++ b/main/java/by/stub/yaml/stubs/StubRequest.java
@@ -252,12 +252,6 @@ public class StubRequest {
          final boolean isAssertingValueSet = isSet(thisAssertingPostBody);
          if (!isAssertingValueSet) {
             return false;
-         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_JSON)) {
-            try {
-               return JSONCompare.compareJSON(dataStorePostBody, thisAssertingPostBody, JSONCompareMode.NON_EXTENSIBLE).passed();
-            }

これはかなり強引な方法で、いってみれば「JSON とか知るか!! 文字列として扱うんや!!! 」という方法である。 つまり、リクエストが valid JSON でなくても、XML や Form であってもマッチしてしまうので、YAML 上でリクエストを記述するときにかなりの注意を払わなければならない。

上記修正を行った後で、gradle build -x test でビルドして実験し、以下のような YAML で Stubby4J を立ち上げる。

- request:
    method: POST
    url: /hello-world
    post: >
      \{\s*"hello"\s*:\s*"(.*?)"\s*.*\}

  response:
    status: 200
    headers:
      content-type: application/json
    body: Hello, <% post.1 %>

これに対して、リクエストを送信すると、きちんとレスポンスを切り替えられるようになる

$ curl -H'content-type: application/json' -d "{\"hello\":\"kiririmode\", \"hoge\":\"fuga\" }" http://localhost:8882/hello-world
Hello, kiririmode
$ curl -H'content-type: application/json' -d "{\"hello\":\"world\", \"var\":\"val\" }" http://localhost:8882/hello-world
Hello, world

15分でWebSphere MQの環境を構築しメッセージの読み書きを行う

middleware technology

WebSphere MQ の実験するんだったら Docker 使うのが一番楽だと思います。 調べてたら、IBM が公式に WebSphere MQ v8.0 の Docker Image を作ってくれてて、これあったら 15 分くらいで WebSphere MQ が使える環境が整います。便利な世の中ですね。

ただし、上記イメージには、いわゆる WebSphere MQ のサンプルアプリが入っていないので、すぐ QUEUE にメッセージを PUT してみたい、みたいなことができない。 そういうわけで、fork してサンプルアプリもインストールするように Dockerfile を書き換えた。

それでは、これを使って 15 分くらいで WebSphere MQ が使える環境を整えましょう。

file-max を増やす

WebSphere MQ は、インストール時に fs.file-max を 524288 以上かどうかのチェックが入っており、デフォルト設定だと docker container の起動でコケる。 これは docker host 側の設定になるので、ここでは以下のコマンドで当該の制限値を変更しておく。

$ docker-machine ssh default sudo 'sysctl -w fs.file-max=524288'

Docker Container を立ち上げる

まずはリポジトリを clone して、Dockerfile から Docker Container を作成。

$ git clone https://github.com/kiririmode/mq-docker
$ cd mq-docker
$ docker build --tag mq ./8.0.0/

その後は、Container を立ち上げるだけです。

$ mkdir ~/work && cd ~/work
$ docker run --env LICENSE=accept --env MQ_QMGR_NAME=QM1 --publish 1414:1414 --detach mq
$ docker start <Container ID>
$ docker exec -it <Container ID> /bin/bash
$ docker exec -it <Container ID> /bin/bash
(mq:8.0)root@1a980fdb7590:/# su - mqm
$

コンテナを立ち上げた段階でキューマネージャ QM1 が作成されていますが、これは docker run 実行時の MQ_QMGR_NAME 指定によるものです。 dspmq コマンドで確認できます。

$ dspmq
QMNAME(QM1)                                               STATUS(Running)

キューマネージャを作成する

新しくキューマネージャを作成するのは crtmqm コマンド。-q オプションは、作成したキューマネージャをデフォルトキューマネージャに指定するものです。

$ crtmqm -q host1/qm1
WebSphere MQ queue manager created.
Directory '/var/mqm/qmgrs/host1&qm1' created.
The queue manager is associated with installation 'Installation1'.
Creating or replacing default objects for queue manager 'host1/qm1'.
Default objects statistics : 79 created. 0 replaced. 0 failed.
Completing setup.
Setup completed.

デフォルトキューマネージャの情報は、/var/mqm/mqs.ini に格納されています。

$ grep -A1 DefaultQueueManager /var/mqm/mqs.ini
DefaultQueueManager:
   Name=host1/qm1

キューマネージャを起動する

キューマネージャの起動は strmqm コマンドで実行します。引数にはキューマネージャ名を取るけど、デフォルトキューマネージャに対する操作の場合は省略可能です。

$ strmqm host1/qm1
WebSphere MQ queue manager 'host1/qm1' starting.
The queue manager is associated with installation 'Installation1'.
5 log records accessed on queue manager 'host1/qm1' during the log replay phase.
Log replay for queue manager 'host1/qm1' complete.
Transaction manager state recovered for queue manager 'host1/qm1'.
WebSphere MQ queue manager 'host1/qm1' started using V8.0.0.4.

ローカルキューを作成する

ローカルキューの作成は runmqsc コマンド経由でプロンプトを取って行います。実際にローカルキューを作成するのは DEFINE QLOCAL

$ runmqsc host1/qm1
(snip)
DEFINE QLOCAL('queue1') DESCR('newly defined')
     1 : DEFINE QLOCAL('queue1') DESCR('newly defined')
AMQ8006: WebSphere MQ queue created.

この確認には、DISPLAY QLOCAL を使用します。DISPLAY QLOCAL(<QNAME>) ALL を使用すると情報が多すぎるので、特定の情報だけ見たい場合は見たい属性の名前を明示的に指定してやる方が良いでしょう。

# 情報量が多い
DISPLAY QLOCAL('queue1')
     1 : DISPLAY QLOCAL('queue1')
AMQ8409: Display Queue details.
   QUEUE(queue1)                           TYPE(QLOCAL)
   ACCTQ(QMGR)                             ALTDATE(2016-05-04)
   ALTTIME(09.34.30)                       BOQNAME( )
(snip)

# 情報量を絞る
DISPLAY QLOCAL('queue1') CURDEPTH DESCR
     3 : DISPLAY QLOCAL('queue1') CURDEPTH DESCR
AMQ8409: Display Queue details.
   QUEUE(queue1)                           TYPE(QLOCAL)
   CURDEPTH(0)                             DESCR(newly defined)

ローカルキューの属性を変更する

ローカルキューの属性を変更するのは ALTER QLOCAL コマンド。

# 変更
ALTER QLOCAL('queue1') DESCR('kiririmode')
     4 : ALTER QLOCAL('queue1') DESCR('kiririmode')
AMQ8008: WebSphere MQ queue changed.

# 確認
DISPLAY QLOCAL('queue1') DESCR
     5 : DISPLAY QLOCAL('queue1') DESCR
AMQ8409: Display Queue details.
   QUEUE(queue1)                           TYPE(QLOCAL)
   DESCR(kiririmode)

メッセージを PUT してみる

WebSphere MQ にはデフォルトでサンプルプログラム群が含まれており、メッセージを PUT するサンプルとして amqsput が提供されている。 コマンドの引数にローカルキュー名を渡して起動すると、標準入力でメッセージ入力を行えるようになる。ここでは hello world! と入力してみた。入力の終了は空行で判断されるらしい。

$ amqsput queue1
Sample AMQSPUT0 start
target queue is queue1
hello world!

Sample AMQSPUT0 end

この後で、queue1 の中に入っているメッセージ長(CURDEPTH) を表示すると、1 に増えていることが確認できる。

DISPLAY QLOCAL('queue1') CURDEPTH
     2 : DISPLAY QLOCAL('queue1') CURDEPTH
AMQ8409: Display Queue details.
   QUEUE(queue1)                           TYPE(QLOCAL)
   CURDEPTH(1)

キューの中のメッセージを表示してみる

キューの中を覗くサンプルプログラムは amqsbcg です。

詳細は、キュー・マネージャーの独り言: 第5回 使って便利なサンプル・プログラム に記載があります。

amqsbcgは、指定されたキューからメッセージを読み込み、そのヘッダ情報とデータ本体を標準出力へ表示するプログラムです。ただし、amqsgetとは異なりbrowseするだけですので、メッセージ自体はキューに残ります。

amqsbcg では、メッセージの MQMD まで見ることができるのがメリットです。

$ amqsbcg queue1

AMQSBCG0 - starts here
**********************

 MQOPEN - 'queue1'


 MQGET of message number 1, CompCode:0 Reason:0
****Message descriptor****

  StrucId  : 'MD  '  Version : 2
  Report   : 0  MsgType : 8
  Expiry   : -1  Feedback : 0
  Encoding : 546  CodedCharSetId : 819
  Format : 'MQSTR   '
  Priority : 0  Persistence : 0
  MsgId : X'414D5120686F7374312F716D31202020D7E3295704220020'
  CorrelId : X'000000000000000000000000000000000000000000000000'
  BackoutCount : 0
  ReplyToQ       : '                                                '
  ReplyToQMgr    : 'host1/qm1                                       '
  ** Identity Context
  UserIdentifier : 'mqm         '
  AccountingToken :
   X'0431303030000000000000000000000000000000000000000000000000000006'
  ApplIdentityData : '                                '
  ** Origin Context
  PutApplType    : '6'
  PutApplName    : 'amqsput                     '
  PutDate  : '20160504'    PutTime  : '11590590'
  ApplOriginData : '    '

  GroupId : X'000000000000000000000000000000000000000000000000'
  MsgSeqNumber   : '1'
  Offset         : '0'
  MsgFlags       : '0'
  OriginalLength : '-1'

****   Message      ****

 length - 12 of 12 bytes

00000000:  6865 6C6C 6F20 776F 726C 6421           'hello world!    '



 No more messages
 MQCLOSE
 MQDISC

というわけで、基本的なキューへの読み書きが実現できました。

MacでIMEを切り替えるキーバインド

environment

Mac の IME として AquaSKK を使っているんですが、AquaSKK は Eclipse などとの相性が非常に悪くて、特定の文字が表示されないとか、Ctrl+J でモードを切り替えようとしたら改行コードが入力されてしまうとかで、ぼくは日に日にストレスを溜め込んでいます。 そういうわけですから、Eclipse のお供には専ら Google 日本語入力 を使用しているわけですが、この AquaSKK と Google 日本語入力が、意図せず切り替わってしまうことが多く、ぼくはメニューバーのアイコンをクリックして丁寧に IME を切り替えるという優しい運用をしていたんだけれど、そうこうしているうちにぼくのストレスは指数関数的に増えていってしまいました。

いちいちクリックするのがダルすぎるからショートカットでもないのかなと思ってネットを彷徨ってみると、Ctrl+Space で IME を切り替えられると書いている。

しかしですね、Ctrl+Space というのは Emacs を使っている人間からすると使用頻度の高いキーバインディングでして、これが使えなくなるというのは死に直結する。 Mac は Emacs キーバインドをそこかしこで採用しているくせに、なぜか Ctrl+Space を OS で奪いたがる。Spotlight でも同じ罪を犯してたぞ、おい。

これはもうダメですねと思っていたら、この Ctrl+Space って変更できるんですね。システム環境設定-キーボード-ショットカット-入力ソースで設定を変更できた。

f:id:kiririmode:20160503151204p:plain

Mac 界の方々におかれましては、大変申し訳ございませんでした。今後のご多幸をお祈りしております。

MQMDおよびMQIの整理

middleware technology

WebSphere MQ なんて聞いたこともなかったんだけど、なんかまぁ特定エリアではよく使われているらしい。 以下、WebSphere MQ と RabbitMQ とかと比較するのは比較軸としてふさわしくない気がしないでもないけど、Google Trends の結果としては以下のようなかんじ。

まぁそんな製品を勉強しなければならない状況は変わっていないので、いろいろと調べてみてる。 今読んでいるのは WebSphere MQ V6 Fundamentals。これを読んだら、全体感は掴めそうな気がする。

MQMD

個々の MQ メッセージに付与されるヘッダ。WebSphere MQ Message Descriptor の略。 代表的なヘッダフィールドには以下のようなものがある。

  • MsgType: レスポンスを不要とするメッセージであることを示すdatagramや、リクエストとそれに対する返信を示す requestreply、あるいは report など、メッセージの種別を示す。
  • Report: 通信先のシステム (キューマネージャ、あるいは、アプリケーション) に report の返信を要求することを示す。
  • ReplyToQ: 通信先のシステムに対し、「どのキューに」reply あるいは report を返信してほしいのかを示す。
  • ReplyToQMgr: ReplyToQ をホストしているキューマネージャ名を示す。通常、WebSphere MQ は、MQ メッセージが最初に PUT されたキューマネージャ名をこのフィールドに自動で書き込む。アプリケーションでの書き換えは可能。
  • MsgId: 名前のとおり、MQ メッセージの ID。アプリケーションが明示的に指定することも、WebSphere MQ に自動で生成させることも可能。
  • CorrelId: メッセージとメッセージの関連付けるフィールド。通常は、reply MQ メッセージの CorrelId フィールドに request MQ メッセージのメッセージ ID を格納することで、request-reply の対を関連付ける。
  • Persistence: MQ メッセージの永続性を示す。
  • CodedCharSetId: MQ の文脈では CCSID とも呼ばれ、文字コードセットの ID を示す。Web Sphere MQ はこの値を元にして、データを文字に変換することが「できる」 (変換可否は Format に依存し、例えばバイナリデータは変換不可)
  • Encoding: 数値データのエンディアンを示す
  • Expiry: メッセージの生存時間の指定で、1/10 秒単位で指定する。これを超えた MQ メッセージは、次に WebSphere MQ 内で参照 (原文的には "when an attempt is made to retrive or view them by an application") されたタイミングで削除される。WebSphere MQ V6.0 からは "expiry task" というのがあり、定期的にこういうメッセージを削除してくれるらしい。

Expiry については、後の参考文献に示すとおり When are expired WebSphere MQ messages removed from a queue? (Application Integration Middleware Support Blog) に気になる情報がある。Web Sphere MQ V6.0 以上の場合は内部に expiry task が存在しており、5 分毎に消してくれるらしい。コメント欄には以下の既述がある。

By default, the internal expirer task runs every 300 seconds (5 minutes). Lowering the frequency to every second could impact performance depending on your overall configuration (number of queues, etc).

MQI

MQI っていうのは、WebSphere MQ を使用した手続型のメッセージングインタフェースのことで、Message Queue Interface の略。全部で 13 の関数が定義されている。 Java や C++ のようなオブジェクト指向言語で直接使用するわけではないが、JMS 等の実装の中ではこの MQI が使われている。

MQCONN/MQCONNX

WebSphere MQ のキューマネージャとのコネクションを確立するためのインタフェース。MQCONNX は、単純に MQCONN よりも渡せる引数が多くなっているだけ。 成功すると、HConn と呼ばれるコネクションのハンドルを返却する。

MQOPEN

"Connected" な状態になったキューマネージャ上のオブジェクト (Queue を含む) にアクセスするためのインタフェース。ちなみに、キューを開く場合は、Object Descriptor (MQOD) とキュー名とともに、キューマネージャも渡すことで、ローカルキューマネージャが知っている別のキューマネージャに送ることもできる。 MQOPEN が成功した場合は、Hobj と呼ばれるオブジェクトハンドルを返却する。

MQCLOSE

まぁ閉じるんでしょう。あれを。

MQPUT

Open したキューに対し、名前の通りメッセージを PUT する。この MQPUT は、アプリケーションが接続しているキューマネージャ上のキューにメッセージを PUT できるまでブロックする。 ちなみに、Open したキューが「別のキューマネージャ上のキュー」である場合は、ローカルキューマネージャ上のトランスミッションキューに PUT できた時点で制御が戻ることになる。

MQPUT には MQMD とともに、MQPUT の振舞を定める MQPMO (PUT Message Options Structure) を渡すことができる。典型的には、syncpoint (MQ の文脈でいうトランザクション) を使うか否かを制御できる。

MQPUT1

クソみたいに名前がわかりにくいけど、このインタフェースは既述の MQOPENMQPUTMQCLOSE を行う。便利なだけでなく、効率も良い。

MQGET

これは MQGET とは逆に、Open したキューから、名前の通りメッセージを GET する。MQGET の振舞いは MQGMO (Get Message Options Structure) で指定でき、GET したメッセージをキューから消すか残すか、どういう基準でメッセージを選択するか (CorrelId 指定とか) などを既述できる。

MQBEGIN/MQCMIT/MQBACK

WebSphere MQ には、DBMS 等の外部リソースを巻き込んだトランザクションの概念として global unit of work がある。このトランザクションを開始するのが MQBEGINMQCMIT、あるいは MQBACK で終了する。

MQINC/MQSET

あんまり使わないかと思うので今のところ興味がないんだけど、open したオブジェクトの属性を参照するのが MQINC、設定するのが MQSET

MQDISK

キューマネージャとアプリケーションとの接続を切断する。MQCONN あるいは MQCONNX の逆操作。 MQDISK が呼び出されないままアプリケーションが終了し、それをキューマネージャが検知すると、キューマネージャは MQBACK をコールしてロールバックを行い、コネクションを切断する。

参考文献

GitbucketのDBからレビュー指摘を抽出する

environment technology db

GitBucket 上で Pull Req とかやりとりしているんだけど、いろいろと評価をするために、これらのレビュー情報を定量化する必要に駆られました。 こういうのを手で数えたりしていると日が暮れるし、何が生産性だ腹を切って死ねということになる。GitBucket でも当然のように DB 管理をしているので、それを取り出せれば良いでしょう。

環境を構築しよう

とりあえず試すためには GitBucket を構築して実際の DB の中身を見れば良い。こういう環境を一から構築していると日が暮れてしまって貴重な休日を浪費する。GitBucket には docker image があるので、docker で構築すれば良いでしょう。

$ docker-machine start default
$ eval $(docker-machine env)
$ docker run -d -p 8080:8080 -p 29418:29418 -v $(pwd)/data:/gitbucket takezoe/gitbucket

上のようなコマンドを実行すると、http://$(docker-machine ip):8080 にアクセスするとブラウザで gitbucket が立ち上がるし、カレントディレクトリ直下の data ディレクトリに gitbucket の DB (h2 database) である data.mv.db が出来上がる。

% ls data
data.h2.db  data.mv.db  data.trace.db  repositories  test.mv.db  tmp  version

h2 database に接続しよう

Gitbucket は Administrator の権限が付与されたユーザでログインすると、下記の画面のように「H2 Console」のメニューがでる。

f:id:kiririmode:20160501114138p:plain

このメニューをクリックすると、H2 接続の Web インタフェースが表示される。

f:id:kiririmode:20160501114358p:plain

JDBC URL に指定する場所は、Dockerfile (gitbucket-docker/Dockerfile at master · takezoe/gitbucket-docker · GitHub) を見れば分かるとおり jdbc:h2:/root/.gitbucket/data を指定すれば良い。 User NamePassword には sa と入れておけばいいです。この sa って何を意味しているんだろ。

そしたらレビュー指摘を抽出してみましょう

運用上、レビュー指摘は Pull Request に対して行われているんだけど、Pull Request に表示されるファイルに対するコメントとしての指摘と、Pull Request 自体に対する指摘 (ISSUE の Conversation としての指摘) で、コメントを格納されるテーブルが異なっている。

Pull Req 自体に対するレビュー指摘

Pull Req に対する指摘は ISSUE_COMMENT テーブルに対して格納される。 Pull Request を軸にして抽出するなら以下のようなかんじかな。

select pr.request_user_name, pr.request_repository_name, i.title, comment.commented_user_name, comment.content, comment.registered_date
from pull_request pr
  inner join issue i on  (pr.repository_name = i.repository_name and pr.issue_id = i.issue_id)
  inner join issue_comment comment on (comment.repository_name = pr.repository_name and comment.issue_id = pr.issue_id)
where comment.action = 'comment'
order by pr.request_user_name, pr.request_repository_name, comment.registered_date

f:id:kiririmode:20160501115801p:plain

ファイルに付与したレビュー指摘

ファイルに付与したレビュー指摘は、実体として「特定 commit の特定ファイルに対するコメント」として COMMIT_COMMENT テーブルに格納される。 これを利用すると、以下のような SQL で抽出できそう。

select cm.user_name, cm.repository_name, cm.commented_user_name, cm.content, cm.registered_date
from commit_comment cm
  inner join issue iss on  (cm.user_name = iss.user_name
                       and  cm.repository_name = iss.repository_name                                                   
                       and  cm.issue_id = iss.issue_id)
order by cm.user_name, cm.repository_name, registered_date

f:id:kiririmode:20160501120105p:plain

Maven Assembly Plugin でできるファイル名がクソダサかった話

java technology

Maven Assembly Plugin でできるファイル名がクソださかったのでなんとかしたかった話。

descriptorRef 要素で jar-with-dependency 指定してたら、hoge-0.0.1-SNAPSHOT-jar-with-dependencies.jar とかいうフザけた名前になってて、あまりの衝撃に手足が震えはじめ唇が紫に変色した。

いきなし結論から書くと、まず pom.xml<configuration> に対して、<finalName> を指定する。

<finalName>vault-${project.version}</finalName>

これだけだと、<finalName> で指定した名前に対して、assemblyId が付与されてしまうので、さらに <appendAssemblyId>false</appendAssemblyId> を付与する。
なので、pom.xml の指定としては以下のようになる。

<finalName>vault-${project.version}</finalName>
<appendAssemblyId>false</appendAssemblyId>

こうすることで、Assembly Plugin で生成されるファイル名を vault-0.0.1-SNAPSHOT.jar とかにできる。これにより手の震えが治まる。

Maven Assembly Pluginで実行可能jarをつくる

java technology

maven assembly plugin

maven 力も Java 力も貧弱なのだけれど、依存性を 1 つにまとめた実行可能 jar を作る必要性に駆られたので、ちょっと調べてた。 maven の plugin で実行可能 jar を作ろうとすると、以下の 2 つがメジャー。

  1. Maven Assembly Plugin
  2. Maven Shade Plugin

両方ともに、いわゆる uber-jar とか Fat Jar とか言われている、依存関係をすべて 1 つにまとめた Jar を作成することはできるのだけれど、それを主目的と考えると後者の Maven Shade Plugin の方が合致する。これは主に以下のような機能によるものらしいが、bytecode を置き換えるとかいうヤバげな文字も見えたので、そっ閉じした。

というわけで、Maven Assembly Plugin をまずは使いましょう。

assembly とは何か

Maven Assembly Plugin の言う "assembly" というのは、ファイルやディレクトリ、モジュールが依存している jar などを含めたアーカイブのことで、これを作り出すことが maven assembly plugin の目的。

"assembly" の形式

plugin に予め定義されている assembly の形式としては以下のような 4 つがあって、maven の descriptorRef で記述することができる。

  • bin
    • mvn package で作成される jar ファイルと、README、LICENSE、NOTICE といったファイルをアーカイブしてくれる。フォーマットとしては、tar.gztar.bz2zip が選択できる。
      もうちょっと具体的に言うと、${project.basedir} 配下の README*LICENSE*NOTICE* と、${project.build.directory} 配下の jar ファイルがアーカイブされる。
  • jar-with-dependencies
    • 依存している jar ファイルを含めた "uber-jar" を作成してくれる。uber-jar については後述するけど、いわゆる fat jar のこと。
  • src
    • こっちはその名の通り、pom のプロジェクトのソースファイル群をアーカイブしてくれる。
      もうちょっと具体的に言うと、${project.basedir} 配下の README*LICENSE*NOTICE*pom.xml${project.basedir}/src 配下のファイルが格納される。
  • project
    • pom で管理しているプロジェクトをそのまま assembly にする。
    <configuration>
      <descriptorRefs>
        <descriptorRef>jar-with-dependencies</descriptorRef>
      </descriptorRefs>
    </configuration>

もちろん、今回の目的に叶うのは jar-with-dependencies ではあるのだけれど、この「予め定義されている」descriptor だと今後の細かなカスタマイズができない。 そういう場合は、Assembly Descriptor を自作すれば良い。

依存関係をすべて含めた Assembly Descriptor

Assembly Descriptor はその名前のとおりで "Assembly" がどういうものかを記述するもので、基本的には「どのファイルを」「どこに配置する」Assembly なのかを記述していけば良い。 ぼくの用途はいまのところ jar-with-dependencies とほとんど変わらず、

  1. runtime スコープにある依存 jar をすべて含む

ということくらいしかやることないので、以下のようなファイルをつくって、vault-assembly.xml とかで保存して src/assembly 配下に保存してやった。

<assembly
    xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
    <id>vault</id>
    <formats>
        <format>jar</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <dependencySets>
        <dependencySet>
            <outputDirectory>/</outputDirectory>
            <useProjectArtifact>true</useProjectArtifact>
            <unpack>true</unpack>
            <scope>runtime</scope>
        </dependencySet>
    </dependencySets>
</assembly>

Manifest ファイル

実行可能 jar をつくるためには Manifest ファイルの Main-Class エントリを作成する必要があるけど、Maven Assembly Plugin には archive の設定でこれを記述することができる。

なので、Assembly Descriptor を含め pom.xml に指定してやれば良い。

<configuration>
    <finalName>vault-${project.version}</finalName>
    <appendAssemblyId>false</appendAssemblyId>
    <descriptors>
        <descriptor>src/assembly/vault-assembly.xml</descriptor>
    </descriptors>
    <archive>
        <manifest>
            <mainClass>com.kiririmode.vault.util.Main</mainClass>
        </manifest>
    </archive>
</configuration>
<executions>
    <execution>
        <id>make-assembly</id>
        <phase>package</phase>
        <goals>
            <goal>single</goal>
        </goals>
    </execution>
</executions>

参考文献

Stubby4JをJUnitから利用する

java

Stubby4J を Junit で起動できればだいたいの HTTP 要求に関するテストはできる。
だいたいこんな感じでテストを書きたい。

public class Stubby4jTest {
    
    @ClassRule
    public static Stubby4jServer server 
        = Stubby4jServer.fromResource("com/kiririmode/blog/http/client/stubby4j.yaml");

    @Test
    public void test() throws Exception {
        ClientResponse<?> response = new ClientRequest("http://localhost:8882/hello-world").get();
        assertThat(response.getEntity(String.class), is("Hello World!"));
    }
}

よくよく考えると Java で書かれていて Jetty 組み込みのサーバなんだし悩むことなかった。 Stubby4J の API 見ると Jetty 起動する API も既に用意されてた。

public class Stubby4jServer extends ExternalResource {

    private String yamlConfigurationFileName;
    private StubbyClient stubbyClient;

    public static Stubby4jServer fromResource(String resource, boolean isMute) {
        String yamlConfigurationFileName = Stubby4jServer.class.getClassLoader().getResource(resource).getPath();
        return new Stubby4jServer(yamlConfigurationFileName, isMute);
    }

    public static Stubby4jServer fromResource(String resource) {
        return fromResource(resource, true);
    }

    private Stubby4jServer(String yamlConfigrationFileName, boolean isMute) {
        this.yamlConfigurationFileName = yamlConfigrationFileName;
        ANSITerminal.muteConsole(isMute);
    }

    @Override
    protected void before() {
        try {
            stubbyClient = new StubbyClient();
            stubbyClient.startJetty(yamlConfigurationFileName);
        } catch (Exception e) {
            throw new RuntimeException(
                    String.format("stubby4J start failed.  conf=[%s]", yamlConfigurationFileName),
                        e);
        }
    }

    @Override
    protected void after() {
        try {
            stubbyClient.stopJetty();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

}

RestEasyとHttpComponents、そしてConnectionManager

java technology

RestEasy client を使用した HTTP 要求を行うと、TCP コネクションが長時間にわたり残存する問題が発生することがあります。 RestEasy の公式ドキュメントを読めば「HTTP コネクションは適切に release し、適切に close しなければならない」ということは分かるのですが、RestEasy にてどのように HTTP コネクションが管理されているのか良く分からなかったので、そのあたりを調べてみました。

RestEasy と HttpComponents

RestEasy が HTTP 要求を行う際、デフォルトでは、HttpComponents を内部で使用することになっています。 このあたりは、RestEasy における HTTP 要求を示すクラスである ClientRequest の実装を見れば分かりやすいでしょう。 何も意識しないと、以下のコンストラクタを使うことになりますが、ここではデフォルトの ClientExecutorgetDefaultExecutor() で生成しています。

   public ClientRequest(String uriTemplate)
   {
      this(uriTemplate, getDefaultExecutor());
   }

では、その実装はというと、以下のように、ApacheHttpClient4Executor を使用するようになっていることがわかります。

   private static String defaultExecutorClasss = "org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor";
   
   public static ClientExecutor getDefaultExecutor()
   {
      try
      {
         Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(defaultExecutorClasss);
         return (ClientExecutor) clazz.newInstance();
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }

あとはこの ApacheHttpClient4Executor の実装を見れば、httpClient に代表される httpComponents のクラスが山積みです。

httpComponent における HTTP コネクション管理

HTTP での通信を行う際は当然ながら HTTP コネクションを管理する必要があります。 httpComponent においては、この責務は ClientConnectionManager Interface の実装クラスが負うということになっています。具体的には、ClientConnectionManager

  • HTTP コネクションの作成
  • Keep Alive な HTTP コネクションの管理
  • 個々のスレッドに対してどの HTTP コネクションを使わせるのかの制御

を行うっていう Interface となり、その実装クラスとして、後述する BasicClientConnectionManagerPoolingClientConnectionManager なんてものが存在しています。

RestEasy のデフォルト

さて、RestEasy に戻りますが、何も意識しないで RestEasy を使用すると、ClientExecutor としては先述のように ApacheHttpClient4Executor が使用されます。 そして、このコンストラクタでは、以下のように httpClient として DefaultHttpClient が生成されます。

   public ApacheHttpClient4Executor()
   {
      this.httpClient = new DefaultHttpClient();
      this.createdHttpClient  = true;
      checkClientExceptionMapper();
   }

DefaultHttpClient は、HTTP コネクションを管理する ClientConnectionManager として、 BasicClientConnectionManager を使う実装になっています。 BasicClientConnectionManager は HTTP コネクションを 1 本だけ管理するという ConnectionManager であって、シングルスレッドのみで使われることを意図したものになっています。

これまでのまとめ

これまでの RestEasy の挙動を簡単にまとめると、

  1. ClientRequest を直接使用して HTTP 要求を行う際、(ApacheHttpClient4Executor が都度生成される結果として) 都度 httpClient が作成され、
  2. その中で、BasicClientConnectionManager が作られる

という振舞いになります。

実装1: BasicClientConnectionManager で close/release しない

たとえば以下のようなコードで 20 スレッドで HTTP 要求を行うと、HTTP コネクションが 20 本作成されるとともに、個々の HTTP コネクションが keep-alive のタイムアウト時間が経過するまで開放されない(5 秒間の間、20 本の ESTABLISHED な TCP コネクションが作成され、それが 5 秒後に CLOSE_WAIT に遷移し、当分の間残存する。

   public static void main(String... args) throws Exception {
        final int threadNum = 20;
        final String uri = "http://192.168.99.100:8080"; // Docker コンテナ上の Apache Listen ポート

        ExecutorService service = Executors.newFixedThreadPool(threadNum);
        for (int i = 0; i < threadNum; ++i) {
            service.submit(new GetRunnable(uri));
        }
        service.shutdown();

        // netstat 観測用の時間
        Thread.sleep(10000L);
    }

    public static class GetRunnable implements Runnable {

        private String uri;

        public GetRunnable(String uri) {
            this.uri = uri;
        }

        public void run() {
            try {
                new ClientRequest(uri).get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

実装2: PoolingClientConnectionManager で close/release しない

次に PoolingClientConnectionManager を使用してみる。この ConnectionManager は、その名前の通り、HTTP コネクションをプールとして管理する。 RestEasy が (正しくは httpComponent が)デフォルトで使用する BasicClientConnectionManager との違いは、BasicClientConnectionManager は個々の実行スレッドがそれぞれインスタンスを保持することを意図している一方で、PoolingClientConnectionManager はマルチスレッドで共有されることを(通常は)意図している点となる。 PoolingClientConnectionManager を使用するためには、次のように書けば良い。

   public static void main(String... args) throws Exception {
        final int threadNum = 20;
        final String uri = "http://192.168.99.100:8080";
        
        ClientConnectionManager connManager = new PoolingClientConnectionManager();
        HttpClient client = new DefaultHttpClient(connManager);

        ExecutorService service = Executors.newFixedThreadPool(threadNum);
        for (int i = 0; i < threadNum; ++i) {
            service.submit(new GetRunnable(uri, client));
        }

        service.shutdown();
    }

    public static class GetRunnable implements Runnable {

        private String uri;
        private HttpClient client;

        public GetRunnable(String uri, HttpClient client) {
            this.uri = uri;
            this.client = client;
        }

        public void run() {
            ClientExecutor executor = new ApacheHttpClient4Executor(client);
            try {
                new ClientRequest(uri, executor).get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

実際にこれを実行すると、非常に面白いことが起こる。 20 本のスレッドを同時実行しているはずなのに、Apache のアクセスログに記録されるのは 2 リクエストのみである。おい残りの 18 本はどこに行った。

192.168.99.1 - - [23/Apr/2016:13:03:52 +0000] "GET / HTTP/1.1" 200 12
192.168.99.1 - - [23/Apr/2016:13:03:52 +0000] "GET / HTTP/1.1" 200 12

また、この後で netstat を見ると、以下のように 2 本のコネクションが CLOSE_WAIT 状態のままとなっている。

tcp4       0      0  192.168.99.1.64268     192.168.99.100.8081    CLOSE_WAIT
tcp4       0      0  192.168.99.1.64267     192.168.99.100.8081    CLOSE_WAIT

種明かしをすると、以下の 2 つに起因して、残りの 18 スレッドは HTTP コネクション待ちの状態になっている。

  1. コネクションを適切に release していない
  2. PoolingClientConnectionManager のプール数の上限は、デフォルトだとトータルで 20 本、かつ、1 つの宛先ホスト毎に 2 本という制限がある

1 つ目の問題を解決するためには、ClientResponse#releaseConnection を呼び出せば良い。
releaseConnection は、HTTP コネクションをプールに返却するメソッドである。これにより HTTP コネクションが他のスレッドから使用できるようになるため、先ほどのような問題は解消される。

diff --git a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
index 34198f6..e54889a 100644
--- a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
+++ b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
@@ -9,6 +9,7 @@ import org.apache.http.impl.client.DefaultHttpClient;
 import org.apache.http.impl.conn.PoolingClientConnectionManager;
 import org.jboss.resteasy.client.ClientExecutor;
 import org.jboss.resteasy.client.ClientRequest;
+import org.jboss.resteasy.client.ClientResponse;
 import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;

 public class ResteasyClient {
@@ -41,10 +42,15 @@ public class ResteasyClient {

                public void run() {
                        ClientExecutor executor = new ApacheHttpClient4Executor(client);
+                       ClientResponse<?> response = null;
                        try {
-                               new ClientRequest(uri, executor).get();
+                               response = new ClientRequest(uri, executor).get();
                        } catch (Exception e) {
                                e.printStackTrace();
+                       } finally {
+                               if (response != null) {
+                                       response.releaseConnection();
+                               }
                        }
                }

  

2 つ目の問題に対しては、単純にプールの設定を変更してやれば良い。

diff --git a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
index e54889a..14ae85b 100644
--- a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
+++ b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
@@ -18,7 +18,9 @@ public class ResteasyClient {
                final int threadNum = 20;
                final String uri = "http://192.168.99.100:8081";

-               ClientConnectionManager connManager = new PoolingClientConnectionManager();
+               PoolingClientConnectionManager connManager = new PoolingClientConnectionManager();
+               connManager.setDefaultMaxPerRoute(10);
+               connManager.setMaxTotal(100);
                HttpClient client = new DefaultHttpClient(connManager);

                ExecutorService service = Executors.newFixedThreadPool(threadNum);

実装3: PoolingClientConnectionManager で close/release する

残りの問題は、CLOSE_WAIT が残り続ける問題なのだけれど、これは単純に ConnectionManager に対して HTTP コネクションの close を指示すれば良い。 これを行うのは ClientConnectionManager#shutdown メソッドである。 以下のように呼び出すことにより、TCP コネクションが CLOSE_WAIT 状態で残存することはなくなった。

diff --git a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
index 14811f9..b229cde 100644
--- a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
+++ b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
@@ -2,6 +2,7 @@ package com.kiririmode.blog.http.client;

 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;

 import org.apache.http.client.HttpClient;
 import org.apache.http.impl.client.DefaultHttpClient;
@@ -28,7 +29,9 @@ public class ResteasyClient {
                }

                service.shutdown();
-               Thread.sleep(10000L);
+               service.awaitTermination(10L, TimeUnit.SECONDS);
+
+               client.getConnectionManager().shutdown();
        }

        public static class GetRunnable implements Runnable {

MacBook AirでもRetinaディスプレイ並の高解像度を実現したい

environment software

むかしは一年に一台ほど Mac 買ってたけど、2012 年以降はまったく買ってなくて、ひさしぶりに Mac 系の PC 買った。MacBook Air の 13 inch 買った。 これまでは MacBook Pro 15 inch の Retina で、Air もまぁ Retina だとタカをくくっていたのだけれど、MacBook Air が Retina に対応してないの覚えてなくて、買って画面を開いてから一しきり泣いた。

Retina でなくて何が悲しいかというと単純に作業スペースの問題で、とにかく画面に映すことができる情報量が少なすぎる。小さい画面だからこそ、その画面にそれなりの情報量を映し出せないと効率おちまくるから、なんとかしてこの 13 inch のディスプレイにより多くの情報を映し出せないと、今以上に泣くはめになる。 そういうわけで何とかせねばとインターネットを彷徨っていたら、QuickRes というソフトウェアの存在を知った。

このソフト、どういうワケだか知らないけれど、ディスプレイの解像度を超えた解像度を設定できるソフトウェアで、たとえば MacBook Air 13 inch であっても 2560 x 1600 まで設定できるようになる。もちろんディスプレイの解像度はそこまで出ないから、2560 x 1600 に設定しても文字がクソみたいにボヤけるし、そもそも小さすぎて作業効率が上がるとか上がらない以前に文字が読めない。 でも、1680 x 1050 くらいに設定すると、文字は多少ぼやけるけど情報量がかなり増えて、Twitter と Emacs を同時に立ち上げて、Twitter をフムフムと見ながらプログラムが書けたりもする。なんてすばらしいソフトウェアなんでしょう。

まぁ、Twitter を見ながらプログラム書くとか言ってるヤツがそもそも作業効率とか言っているのがおかしくて、作業効率を大切にしたい人は twitter.com を名前解決できないようにしたほうが絶対にいい。