Humanity

Edit the world by your favorite way

「ひなた先生が教える デバッグが256倍速くなるテクニック」を読み返した

ひなた先生が教えるデバッグが256倍速くなるテクニック (Software Design Books)

ひなた先生が教えるデバッグが256倍速くなるテクニック (Software Design Books)

大学生当時に読んだ時もとても影響を受けた本だったけど、改めて読み返してみたらデバッグに限らず様々な話に展開されていて、やっぱりこの人は本当に凄い人だと思った(小並)。 まぁでも確かに Amazon のレビューの通り、この本でデバッグの仕方を学ぶのは難しいかもしれない… デバッグ中の思考の流れとかブレークポイントの取り方とかは参考になった。と思う。

C の for 文のイディオム(for (i = 0; i < N; i++))を深く認識できたのもこの本のおかげだし、 疎行列の存在を知ったのもこの本だった。 ド・モルガン則ってこんな便利なものだったのか、って認識できたのもこの本のおかげ。

将来の Visual Studio の機能についてもかなり面白い。 non-null 制約の(言語)機能なんかは一足早く実現してしまった感がある。

例外処理も自分にとって興味のある話題。 というか、この本を読んで興味の方向性を決められたのかもしれないと思うほど、自分の趣味嗜好とマッチしていた。

関数の API デザインについての記述があったのは思い出せたけど、 メソッドチェイン、閉方性についての記述があったのは読み返して初めて知った。

まぁ、そんな感じで自分にとってはとても印象深い本。

GitLab 運用して起こったエラー対処法

tyru.hatenablog.com

tyru.hatenablog.com

の後日録です。

あと、今日ちょうど GitLab の新バージョンが出たようです。

GitLab 8.13 Released with Multiple Issue Boards and Merge Conflict Editor | GitLab

同時に git push した場合のエラー

gitlab.com

自宅サーバをジョブで一斉に yum check-update; yum upgrade -y *1 した後、GitLab の Activity を見てみると設定が push されていないノードがあった。 その際の出力には下記のようなエラーが出ていた。

GitLab: Failed to authorize your Git request: internal API unreachable
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

同時に push されるとよく起こるっぽい。 現在6台のノードを管理していて、その内3台の git push が失敗していた。

ということで冒頭のチケット通りに /etc/gitlab/gitlab.rb を以下のように変更して対処した。

unicorn['worker_processes'] = 2

/etc/gitlab/gitlab.rb を変更した後は gitlab-ctl reconfigure を忘れずに(Omnibus package 版の場合)。

バックアップスクリプトがエラーになる問題

gitlab.com

これは結構以前からエラーになっていた。 チケットの以前の締切は 9/22 だったけど、それが 10/22 (今日)に変更された。 私は上記チケットにもコメントした通り対処したので今でも起きるかは確認してないけど、 チケットがクローズされていないので直ってないはず。

このエラーが起きる条件として、バックアップ先を WebDAV でマウントしたパスに設定したことだった。

gitlab_rails['backup_path'] =  "<WebDAV(davfs2) mounted path>"

これを初期値のパスに戻して(変更した後は gitlab-ctl reconfigure を(ry)、

gitlab_rails['backup_path'] =  "/var/opt/gitlab/backups"

rsync で転送するようにした。

# root で実行
sudo -u git -H gitlab-rake gitlab:backup:create
rsync -rlv -delete /var/opt/gitlab/backups/ /path/to/remote/backup/path/

転送先のディスク容量が溢れないよう --delete を付けているけど、 安全のために転送先のファイルサーバには1か月前までのバックアップを残すとかしてもいいかも。 ただ GitLab を動かしてる VM にあまり容量がないので、ローカルには7日間しか残していない。

# root で実行
sudo -u git -H gitlab-rake gitlab:backup:create
rsync -rlv /var/opt/gitlab/backups/ /path/to/remote/backup/path/
find /path/to/remote/backup/path/ -type f -name '*.tar' -mtime +30 -delete

むしろ転送したらローカルには何も残さない方がいい気がする?(ファイルサーバには30日間残す)

# root で実行
sudo -u git -H gitlab-rake gitlab:backup:create
rsync -rlv /var/opt/gitlab/backups/ /path/to/remote/backup/path/ && rm -f /var/opt/gitlab/backups/*.tar
find /path/to/remote/backup/path/ -type f -name '*.tar' -mtime +30 -delete

*1:ちゃんと運用するなら検証環境をアップグレードした後、一定期間問題が無ければ本番環境をアップグレードした方がいいんだろうけど

Spring でリクエストボディの JSON に対してバリデーションを行う

問題

Spring でリクエストボディの JSON に対して共通のバリデーション処理を行うに当たって、まずは問題がある。

@ControllerAdvice を使うと、コントローラーが実行される前にバリデーションなどの共通処理を行うことができるが、 @ControllerAdvice で実行できるメソッドに渡ってくるのはリクエストボディのストリームである。 これは元の HttpServletRequest#getInputStream() がそうだから。 それの何が問題なのかと言うと、一度ストリームを読んでしまうと、Stream は通常元に戻せない *1 ため、 @ControllerAdvice のメソッドでバリデーションするために読み込んでしまうとコントローラーが実行されるタイミングで EOF (Stream closed) となってしまいます。

実はこの問題は Spring に限りません。 先程 HttpServletRequest#getInputStream() 由来の問題であると述べたように、Servlet の filter などで前後処理を行う際もこの問題は起き得ます。

解決策

では Spring はこれに対して対策をしていないのかというと、もちろんしています。 @ControllerAdvice アノテーションが付いていて、かつ RequestBodyAdvice インターフェースを実装したクラスは RequestBodyAdvice#beforeBodyRead() で新たな InputStream を含む HttpInputMessage クラスのインスタンスを返すことができます。 これにより、一度リクエストボディを文字列として読み込んでバリデーションなどの前処理を行ったあと、そのボディの文字列をまた InputStream に変換してやり、それを HttpInputMessage クラスのインスタンスに持たせて返してやればいいのです。

サンプルコード

  1. beforeBodyRead()
  2. initBinder()

の順番に呼ばれます。

github.com

ちなみに、上記クラスの使用方法は、プロジェクトの README.md の examples にも書いてあるように以下のように使います。

ちなみに

ちなみに RequestBodyAdvice の似たメソッド afterBodyRead() は何をするためのメソッドかと言うと、このメソッドの第1引数にはリクエストボディが変換された後の POJO クラスのインスタンスが渡される。

どういうことかというと、

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<HelloReqBean> addMsg(@RequestBody HelloReqBean hello) {
        service.addMsg(hello);
    }

コントローラーのクラスにこのようなエンドポイントのメソッドがあるとして、この @RequestBody が付いた引数 HelloReqBean クラスのインスタンスが上記で言った POJO クラスのインスタンスにあたる。 ちなみに @PostMapping@RequestMapping(method=RequestMethod.POST)エイリアス@RequestMapping(method=RequestMethod.POST, value="/path/to/resource") の代わりに @PostMapping("/path/to/resource") みたいにも書ける。

この変数は Object 型として渡ってくるが、あくまでメソッドシグニチャのためであり、HelloReqBean 型にキャストできる。

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) {
        HelloReqBean bean = (HelloReqBean) body;
        // ...
    }

また @InitBinder が付けられたメソッドで取得できる WebDataBinder クラスのインスタンスのメソッド getTarget() でもこの POJO クラスのインスタンスを取得することができるようだ。

    @InitBinder
    private void initBinder(WebDataBinder binder) throws IOException {
        HelloReqBean bean = (HelloReqBean) binder.getTarget();
        // ...
    }

afterBodyRead()@InitBinderPOJO クラスに対して共通の処理を行うなら便利そうだけど、今回はリクエストボディの JSON 文字列を取得しないといけなかったので見送った。

まとめ

  • HttpServletRequest#getInputStream() で2回読む事はできない(IOException: Stream closed と言われる)
  • リクエストボディを文字列で取得したければ @ControllerAdvice アノテーションを付け、かつ RequestBodyAdvice インターフェースを実装し、beforeBodyRead() メソッドでストリームからリクエストボディを読み込む
  • 読み込んだ後、後続のコントローラー等の処理もストリームを読めるようにするため、読み込んだバイト列なり文字列を InputStream にまた変換してやり、HttpInputMessage クラスのインスタンスに渡して返す
  • コントローラーに引数で渡すクラスのインスタンスに対して共通処理を行うには RequestBodyAdvice#afterBodyRead() または WebDataBinder#getTarget() が使える

関連リンク

Spring MVC(+Spring Boot)上でのリクエスト共通処理の実装方法を理解する - Qiita

@ControllerAdvice ではなく Spring AOPAspectJ)を使う場合のコードはこちら。 ただ、InputStream の問題は同様に存在します。

参考リンク

1と2のアーキテクチャ概要図がソースコードを追う時にすごく参考になった。

  1. 01.基本概念:全体的な処理フロー - soracane
    • 詳しすぎて何者感ある資料。
  2. 2.2. Spring MVCアーキテクチャ概要 — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.2.0.RELEASE documentation
  3. Spring Framework Reference Documentation
    • 公式資料だけど見づらい…

コードリーディングメモ

Spring のソースコード追っかけてた時の自分用メモ。

  • org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(HttpInputMessage, MethodParameter, Type)

でそれぞれの ControllerAdvice を呼んでいる。

  • org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage.EmptyBodyCheckingHttpInputMessage(HttpInputMessage)

PushBackStream 初めて知った。 そういや InputStream の mark も使ったことないな…

一応 JAX-RS、Spring MVC の他に素の HttpServletRequest 対応も考えてるので入れたい。 一応クラスは入れてあるものの、HttpServletRequst#getInputStream() を直接呼んでるので2回呼べない。 これじゃ前処理に使えない…

java-json-hyper-schema-validator/ServletJSONRequest.java at 1f4e78fccf60eaa615c1f2dfcb0b365513bb5dee · tyru/java-json-hyper-schema-validator · GitHub

色々調べたけど、どうやら元々の HttpServletRequst#getInputStream() を2回呼べない問題は単純に2回読んだ場合はどうしても解決できないっぽいので、Spring みたく Chain of Responsibility パターンで解決するか、他の何らかの方法で対策をする必要があるのか…

そういや JAX-RS の実装も ContainerRequestContext#getEntityStream() で InputStream 取得して中身読んでたけど普通にコントローラーちゃんと呼べてたなぁ… 対策してあるのか分からんけど、もしくは未定義動作…?

java-json-hyper-schema-validator/JaxrsJSONRequest.java at 1f4e78fccf60eaa615c1f2dfcb0b365513bb5dee · tyru/java-json-hyper-schema-validator · GitHub

  • org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(NativeWebRequest, ModelAndViewContainer, Object...)
    • DefaultParameterNameDiscoverer
      • Default implementation of the ParameterNameDiscoverer strategy interface, using the Java 8 standard reflection mechanism (if available)
    • HandlerMethodArgumentResolverComposite
      • Resolves method parameters by delegating to a list of registered HandlerMethodArgumentResolvers.

コントローラーや initBinder() の引数はこれでインスタンスを生成して渡しているっぽい。

上記メソッドにブレークポイントを置くと、コントローラーに inject する引数を生成するために再帰的に @InitBinder のメソッドを読んでいるのが分かる。

f:id:tyru:20161013121558p:plain f:id:tyru:20161013121601p:plain

まずコントローラーの引数を ServletInvocableHandlerMethod#getMethodArgumentValues(NativeWebRequest, ModelAndViewContainer, Object...) で解決しようとする。 次に InvocableHandlerMethod.java 161行目の

args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);

により @ControllerAdvice の @InitBinder が呼び出される。

これ Spring じゃないけど、HttpServletRequest をラップするとしたらこんな感じのコードがよさそう。 まぁこのクラスに HttpServletRequest のインスタンスを渡したらその時点で HttpServletRequest#getInputStream() が呼ばれて読み込まれるから、 元の HttpServletRequest は使えなくなるけど…

あくまで本記事のようにリクエストボディのみではなく HttpServletRequest クラスをラッパークラスに渡して、そのサブクラスとして扱いたい場合は、HttpServletRequest のインスタンスを上記のクラスのコンストラクタに渡して、以降はそのラッパークラスのインスタンスを使い回すのがいいかな?(細かく見てないので間違ってるかも)

*1:InputStream#markSupported() が true を返す実装クラスのインスタンスと PushBackStream は除く