TypoFixer というサービスを作ってみて感じたこと

初めに

こんにちは、 おれたま@AHA_oretamaです。 Software Engineer in Test(SET)として活動しているエンジニアです。

今回、GitHubのプルリクエストで、タイポ(打ち間違え)を指摘して自動で修正するアプリを作りました。 作ってみて多くのことがわかり、そして自分の想定の甘さと自分の経験のなさを痛感し、もっと開発だけではなく全てのことを改善していかなければならない、と強く感じました。 今回はそのことを書いていきたいと思います。

今回はほぼ技術的な話はありません。

TypoFixer

まずは作成したアプリを紹介します。TypoFixerです。

GitHub Appsとして作られているため、以下のような特徴を持ちます。

  • Repository単位で設定ができる。
  • 有償・無償の設定ができる。
  • GitHub Appsとしてインストールすればすぐに使うことができる。

現段階ではとくにオプションなどもなく、カスタマイズできないため、インストールすればそのまま使用できます。

TypoFixerは以下のような機能を持ちます。

  1. プルリクエストを作成したときに、タイポがあればTypoFixerがレビュコメントを追加する。
  2. そのコメントにはタイポの修正候補も表示されており、その中から適切なものを選択すれば、TypoFixerが自動でタイポを修正してコミットしてくれる。
  3. TypoFixerに指摘された単語がタイポではない場合、「Not Typo」を選択すれば、辞書に登録され、TypoFixerは以後、同じ指摘をしなくなる。

背景

このサービスと作る前に、同じようなサービスを探してみるとTypotが見つかりましたが、いくつかの点で自分の中で満足がいくとは言えませんでした。

  • (造語やサービスなどの固有名詞など)実はタイポではないがタイポとして指摘された単語は、その後も指摘され続けてしまう。
  • Typotで使用しているスペルチェックライブラリpyenchantがRepository上でメンテナンスしないことを公言している。
  • 上記の対処もなく、コミットログが止まっている点などからTypotというサービスがメンテされ続けているとは言いにくい状態である。

また、上記とは別にモチベーションの部分として、GitHub Appsを自分で作成した経験を得て、GitHubのプルリクエストやIssueなどに対して、何かしらの操作(ここではプルリクエストへのレビュ)ができるようになることは、今後、SETの活動を進めていく上で役に立つと思い、自分の勉強のために始めました。

そして、もしこのツールが普及したときに、運用・維持費(できれば儲けまで)を得ることができればよいという淡い期待もありました。

やってみた良かった点

以下にやってみてよかった点をあげますが、やっぱり自分の手で実装したことによって得られたものだと思いますので、手を動かして実装することはよいことだと思いました。

  • 自身で足りないと思っていた機能は実装でき、(当初)自分がほしかったサービスができた
  • Circle CI 2.0, Herokuなど今まで仕事などでは、あまり使ったことがないツールをしっかりドキュメント読みこんで、学びながら動かす機会ができた。

作ってみたあとに気づいた点

これらは実際に自分で作ってみて、試したあとに気づいた点です。

  • 実際に使ってみようとしたところ、自分が想定していたものとは以下の点で異なり、非常に使いにくかった
    • タイポの指摘の件数が多すぎる
    • タイポを自動修正したときに整合性が取れない(例えば、メソッド名を変更した場合にそれを呼び出している箇所までは修正しない)ことがある
    • 指摘する箇所に合わせて指摘内容を変更してほしい(例えば、ライブラリのimportする部分などは指摘しないでほしい)。

これらのことは作る前にプロトタイプを作ったり、サービスの動きを想定しなりなどをもっと事前に準備をしていれば、避けられた点かと思います。

また、現段階では、到底お金をもらうサービスには成り得てません。 このようにサービスを簡単に作ることは難しいことがわかりました…

今後の予定

反省点として、やはり自分がどういうものを作りたいか、そして実際にどのような動きになるかをプロトタイプなどを作って前段階で検証する、 つまり、要件定義の段階をもっと自分で行わなければならないと感じました。 そしてその力が自分には足りていないと実感することもできました。

あまりの使いにくさに一度このままサービスをクローズさせようかとも思いましたが、 とはいえ、このまま終わらせようとは思っていません。

せっかく作ろうと思ったサービスなので、もう少し形になるまで続けようとは思います。 そして、できればこのサービス開発で、自分なりの何かをつかめればよいと思っています。

これからやろうと思っている改善点

いまいまやろうと考えている改善点です。まだまだ構想段階です。

  • ファイル拡張子からタイポの指摘箇所を調整する。例えば、マークダウンなどのドキュメントであれば全てを対象として指摘する
  • 設定をカスタマイズ可能して、そのRepositoryにあった指摘を行うことができるようにする
  • 言語ごとの構文を把握して、構文にあった指摘に最適化する

Monitoring 〜雑まとめだけどドコかに残しておきたい〜

Monitoringについて詳しくないので、Web Developer Roadmap 2018が出たので2017年版と比較してみるMonitoring and Alertingにあるツールを全て調べてみた。

あとちょうど最近、流行っていると聞いたDataDog、Grafanaについても調べてみる。

Zabbix

インフラ寄りのイメージ。
ネットワーク、サーバ、クラウド、サービスと監視対象範囲は非常に広い気がする。

https://www.zabbix.com/jp/java_applications
JavaアプリケーションはJMXによる監視のみ。
ヒープ、メモリなどのJVM寄りの監視。

インフラ監視には◯、アプリケーション監視には✕。

Prometheus

https://prometheus.io/docs/instrumenting/clientlibs/
https://github.com/prometheus/client_java
完全にアプリケーション監視向け。

アプリケーション側にログを埋め込むため、アプリケーション側に改修が必要になるが、
その分、柔軟に必要な情報(例えばメソッド単位でのパフォーマンスなど)を取得することができる。

Graphite

Graphite自体は2つのことしかやらない。

  • Store numeric time-series data
  • Render graphs of this data on demand

なので、データ収集は他ツールとつなげる必要がある。

ツールはこちら。
http://graphite.readthedocs.io/en/latest/tools.html

オールインワンではないので、カスタマイズは柔軟だが構築に手こずりそう。

Munin

どうでもいいけどとにかくトップページがダサい。http通信だし…

Munin is a networked resource monitoring tool that can help analyze resource trends and "what just happened to kill our performance?" problems. It is designed to be very plug and play. A default installation provides a lot of graphs with almost no work.

プラグイン形式。
http://munin-monitoring.org/wiki/plugins

JavaJMXの監視。
https://github.com/munin-monitoring/contrib/tree/master/plugins/jmx

PagerDuty

http://blog.serverworks.co.jp/tech/2015/10/13/start-pagerduty/ (こっちのほうがわかりやすい)

つまり、通知サービス。
監視は別で、監視からの集約を担う。
集約した情報からSlack,SNS,メールへのエスカレーションを行う。

New Relic

いくつかのサービスに分かれている。
https://newrelic.com/products
アプリ、WEB、WEBアプリ、アプリケーション、インフラとダッシュボードのInsightsに分かれており、
広範囲のモニタリングがNew Relic製品の組み合わせで可能になる。

Javaについてもインストールは簡単で、エラー、トランザクション、レスポンスタイム、外部システムのレスポンスタイムなど必要な情報は見ることができる。
https://newrelic.com/java

Nagios

プラグイン形式。豊富なプラグイン
サーバ、ネットワーク、アプリケーションをカバー。

https://www.nagios.com/solutions/java-monitoring/
アプリケーション監視自体はJavaを見る限り、アプリケーション内部ではなく、メモリとかがメイン。

AppDynamics

アプリケーションのパフォーマンス、モバイル、ブラウザなどアプリケーション層を幅広くサポート。
またデータベースやサーバもサポートしている。

Java,Springにも対応している。
https://www.appdynamics.jp/java/
https://www.appdynamics.jp/java/spring/

DataDog

可視化がメインのよう。UIがすごいリッチ!

非常に豊富なインテグレーションをもつ。
https://www.datadoghq.com/product/integrations/#all

JMXによるJava監視が可視化。
https://docs.datadoghq.com/integrations/java/

Grafana

ビジュアライズツール。Kibanaみたいなツールと認識。

WEB API The Good Parts 6章の自分的まとめ

今回で本シリーズは最後となる。
以下の本の6章を自分的にまとめる。

Web API: The Good Parts

Web API: The Good Parts

もしAPI を開発している、もしくはAPI を日々触っているのであれば、 是非買って読んでみてほしい。

堅牢なWeb API を作る

今回は重要なポイントは多いが、疑問や問題点はなかったので、 各章を一言でまとめて終わろうと思う。

  • どんなセキュリティの問題があるか
    以下の3つのパターンに分けて考える。
    • サーバとクライアントの間での情報の不正入手
    • サーバの脆弱性による情報の不正入手や改ざん
    • ブラウザからのアクセスを想定しているAPI における問題

サーバとクライアントの間での情報の不正入手

  • HTTPS にする。ただし100%安全であるとは言えないが、有効な手段である。

サーバの脆弱性による情報の不正入手や改ざん

  • XSS対策
    JSON をブラウザが必ずJSON と認識するようにする

    • Content-Type: application/json を指定する
    • IE8以上対策 → X-Content-Type-Optioins: nosniff を指定する
    • IE7以下対策 → 追加のリクエストヘッダをつけてそれを検証する or JSON をエスケープする

  • XSRF対策

    • データの更新にはPOST,PUT,PATCH,DELETE を用いる
    • XSRF トークンを使う。(そのサイトが発行したワンタイムトークン、もしくはセッションごとにユニークなトークン)

  • JSON ハイジャック
    JSON ハイジャックを防止するには、現在のところ以下の対策が有効。

    • JSON をSCRIPT 要素では読み込めないようにする
      → 追加のリクエストヘッダをつけて検証する

    • JSON をブラウザが必ずJSON と認識するようにする
      XSS対策と一緒

    • JSONJavaScript として解釈不可能、あるいは実行時にデータを読み込めないようにする
      JSON の配列ではなくオブジェクトで返す。もしくはJSON ファイルの先頭に無限ループなどを仕込んで読み込み時に処理が進まないようにする。

セキュリティ関係のHTTPヘッダ

X-Content-Type-Options

X-Content-Type-Options: nosniff

上記のヘッダをつけるだけで、勝手にメディアタイプを解釈されることを防ぐ

X-XSS-Protection

ブラウザがXSS の検出、防御機能を備えている。 IE8 以上では以下のヘッダを送ることで、設定を有効化できる。 他ブラウザでは上書きできない(デフォルト有効)

X-XSS-Protection: 1; mode=block

X-Frame-Options

フレーム(FRAMEとIFRAME 要素)内で読み込まれることを防いでくれる。

X-Frame-Options: deny

Content-Security-Policy

読み込んだHTML内のIMG 要素、SCRIPT 要素、LINK 要素などの読み込み先としてどこを許可するのかを指定するためのヘッダ。
API の場合は要素を指定することがないため、直接は効果がないかも。

Contet-Security-Policy: default-src 'none'

Strict-Transport-Security

あらかじめこのサイトはHTTPS でのアクセスが必須、ということを記録させておくためのヘッダ。

Strict-Transport-Security: max-age=15678000

max-age で指定された期間中は、同じホストへのアクセスはHTTPが指定されていても、HTTPSを使うようにする。

Public-Key-Pins

SSL 証明書が偽造されたものでないかチェックする。
使い方は以下を参照。

Public Key Pinning - Web セキュリティ | MDN

Set-Cookieヘッダとセキュリティ

セッション管理にクッキーを使用する場合にセキュリティを向上させるために、 Secure、HttpOnly 属性を付与する。

Set-Cookie: session=XXXX; Path=/; Secure; HttpOnly
Secure HttpOnly
HTTPSの通信しかクッキーを送らない設定 クッキーがHTTP の通信のみで使われ、JavaScriptなどのスクリプトではアクセスできない設定

大量アクセスへの対策

そのサービス特性に基づき、ユーザ、IPごとなどの基準を設けて最大アクセス回数(レートリミット)を決める。

制限値を超えてしまった場合の対応

  • HTTPステータス 429 を返す。
  • エラーの詳細をレスポンスに含める。
  • Retry-After ヘッダを使って次のリクエストをするまでにどれくらい待てばよいかを指定する

レートリミットをユーザに伝える

以下のHTTP ヘッダを独自に作成して使う。

ヘッダ名 説明
X-RateLimit-Limit 単位時間あたりのアクセス上限
X-RateLimit-Remaining アクセスできる残り回数
X-RateLimit-Reset アクセス回数がリセットされるタイミング

リーダブルコード ~より良いコードを書くためのシンプルで実践的なテクニック~ エンジニアなら読むべき本の一つ!

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

昨日、今日でこの本を読んだ。
非常に読みやすく、遅読の自分でもすぐに読み終わることができた。

書いてある内容は保守性を気にしている人やリーダブルコードを意識している人にとっては、 新しいものではないが、非常に重要なことを整理してくれる。

とくに自分としては「短いコードを書く」という章を読んではっとした。
正しくてきれいなコードを書いたとしても、 その機能が過多で使われないならリーダブルコードにはならないのだ。
そう!必要な機能を見極めて、簡単に(=短いコードで)実装をするのが良いのだ。

自分のために各章のタイトルだけ書いておく。
自分としての目標はタイトルを見て、自分がなにをすべきか分かるようになるまで繰り返し読むことだ。

  1. 理解しやすいコード
  2. 名前に情報を詰め込む
  3. 誤解されない名前
  4. 美しさ
  5. コメントすべきことを知る
  6. コメントは正確で簡潔に
  7. 制御フローを読みやすくする
  8. 巨大な式を分割する
  9. 変数と読みやすさ
  10. 無関係の下位問題を抽出する
  11. 一度に1つのことを
  12. コードに思いを込める
  13. 短いコードを書く
  14. テストと読みやすさ 15.「分/時間カウンタ」を設計・実装する

WEB API The Good Parts 5章の自分的まとめ

続きで、今回は以下の本の5章を自分的にまとめたいと思う。

Web API: The Good Parts

Web API: The Good Parts

もしAPI を開発している、もしくはAPI を日々触っているのであれば、 是非買って読んでみてほしい。

設計変更をしやすいWeb API を作る

今回は重要なポイントは多いが、疑問や問題点はなかったので、 各章を一言でまとめて終わろうと思う。

  • 設計変更のしやすさの重要性

    • 外部に公開しているAPIの場合
      利用者が多く、利用者がきちんと対応できるようにするなどAPI の変更は超大変!
    • バイルアプリケーション向けAPI の場合
      外部公開よりは楽だが、それでもOSのバージョンがあげられないなどAPI の変更は結構大変!
    • ウェブサービス上で使っているAPI の場合
      ウェブも自分のものなので楽だが、それでもキャッシュされているなどちょっと大変!

      そうじて「一度公開をしたWeb APIの仕様を変更するのはいずれにせよ問題が発生する危険性がある」ということ。

  • API をバージョンで管理する
    以下のパターンがあるが、一般的に使われるのはURIにバージョンを埋め込む方法。

    • URI にバージョンを埋め込む
    • バージョンをクエリ文字列を入れる
    • メディアタイプでバージョンを指定する

  • バージョン番号をどうつけるか
    以下が一般的。

メジャーバージョン マイナーバージョン パッチバージョン
後方互換性のない変更が行われた場合 後方互換性のある機能変更、あるいは特定の機能が今後廃止されることが決まった場合 ソフトウェアのAPI に変更がないバグ修正などを行った場合


  • (メジャー)バージョンを変える際の指針
    後方互換性を保つことが可能な変更は可能な限り、同じバージョンでのマイナーバージョンアップで対応し、 バージョンをあげるのはどうしても後方互換性を保ったまま修正を行うことが難しい変更を加えなければならないときのみ、メジャーバージョンをあげる。

  • API の提供を終了する
    Twitter の例のように以下の対応が必要!

    • 新しいAPI の告知
    • API を廃止するという継続的な告知
    • Blackout Test と呼ばれる一時的に旧API を停止してアクセスできないようにするテスト

  • あらかじめ提供終了時の仕様を盛り込んでおく
    提供終了した場合に例えば410エラー(Gone)を返す、など提供終了時の仕様を決めておく。

  • 利用規約にサポート期限を明記する
    一定期間はAPI のバージョンをサポートする、と名言する。 サービスの目的により、保証とサービス提供の範囲の明確化が重要な場合は有効。

  • オーケストレーション
    汎用的なAPI はどうしても使い方が煩雑になる。 例えば1つのアクションを行うのに複数のAPI にアクセスしなければならなかったり、不要なデータを受け取らなければならず ペイロードが大きくなってしまう。

    サーバ側の汎用的なAPI とクライアントの間に"Client Adapter Code"を実行するオーケストレーション層をはさんで、 さまざまなデバイスに対応できるようにする。

    このオーケストレーション層を作成するのはクライアント側のエンジニアで、 クライアント側のデバイス機能やリリースサイクルに合わせて、エンドポイントを修正できる。

    Netflix のブログ記事 The Netflix Tech Blog: The Netflix Dynamic Scripting Platform によれば、
    しっかりとしたデバイス開発者用エンドポイント管理ツールが提供されており、 いわば社内専用PaaS として提供されているらしい。

さすがNetflix、はうらやましい限りですね!

WEB API The Good Parts 4章の自分的まとめ

前々回からの続きで、今回は以下の本の4章を自分的にまとめたいと思う。

Web API: The Good Parts

Web API: The Good Parts

もしAPI を開発している、もしくはAPI を日々触っているのであれば、 是非買って読んでみてほしい。

HTTP の仕様を最大限利用する

4.2 ステータスコードを正しく使う

この章についてはHTTP のステータスコードを解説しているが、 内容はwiki を見るだけで十分。

HTTPステータスコード - Wikipedia

4.4 メディアタイプの指定

メディアタイプとはデータ本体の形式を表すもので、 リクエスト時にはレスポンスがこの形式でほしいと指定する場合はAccecptヘッダー、 リクエスト、レスポンス時に送信するデータ本体はこの形式であると指定する場合はContent-Typeヘッダーで表す。

代表的なメディアタイプは以下の表を参照する。

メディアタイプ データ形式
text/plain プレーンテキスト
text/html HTML 文書
application/xml XML 文書
text/css CSS 文書
application/javascript JavaScript
application/json JSON 文書
application/rss+xml RSS フィード
application/atom+xml Atom フィード
application/octet-stream バイナリデータ
application/zip zip ファイル
image/jpeg JPEG 画像
image/png PNG 画像
image/svg+xml SVG 画像
multipart/form-data 複数のデータで構成されるウェブフォームデータ
video/mp4 MP4 動画ファイル
application/vnd.ms-exel Excel ファイル
application/x-msgpack MessagePage
application/x-yaml YAML
application/x-plist プロパティリスト
application/x-www-form-urlencoded HTMLのフォームデータ

トップレベルのapplication, text が混同しやすいが、 appllication のほうが主流。
plain, html ,css 以外はapplication となる。

サブタイプがx-で始まるものはIANA に登録されていない、 もしくはもしくは過去登録されていなかった歴史的経緯が残っているもの。

自分で作る場合は以下の表を参考にするとよい。
いまはあまりx-は推奨されないよう。。。だが一般的にはx-を付与している。

ツリー名 接頭辞
Standards tree(標準ツリー) なし
Vendor tree(ベンダツリー) vnd
Personal tree(パーソナルツリー) prs.
Unregistered tree(未登録ツリー) x.

vnd.ms-excelマイクロソフトというベンダーのexcel という意味。
ただし、通常は以下のように会社名を入れるらしい。

  application/vnd.companyname.awesomeformat

JSONXML を用いた新しいデータ形式を定義する場合は+ で記載する。
例えばXML をベースとしたRSSフィードの場合は以下のようになっている。

  application/rss+xml

4.3 キャッシュとHTTP の仕様

キャッシュタイプ タイプ説明 対応ヘッダ 説明
Expiration Model(期限切れモデル) あらかじめレスポンスデータに保存期間を決めておき、期限が切れたら再度アクセスをして取得する Expires: Fri, 01 Jan 2016 00:00:00 GMT 指定した時間以降はキャッシュ切れと判断する
Cache-Control: max-age=3600 指定した秒数が経過した以降はキャッシュ切れと判断する
Validation Model(検証モデル) いま保持しているキャッシュが最新であるかを問い合わせて、データが更新されていた場合に取得する response:Last-Modified: Tue, 01 Jul 2014 00:00:00 GMT → request:If-Modified-Since:Tue, 01 Jul 2014 00:00:00 GMT 最終更新時刻を記述する。時刻が一致すれば最新キャッシュと判断する。
response:ETag: "1234567890123456" → request:If-None-Match:"1234567890123456" 任意の文字列。通常はハッシュ値など。値が一致すれば最新キャッシュと判断する。
キャッシュさせない キャッシュを全くさせたくない場合に用いる Cache-Control: no-cache キャッシュしない

キャッシュを行う際に、URI 以外にどのリクエストヘッダ項目をデータを一意に特定するために利用するか特定するために、Varyヘッダを指定する。
例えば、Accept-Language: jaAccept-Language: en を指定して送信されたデータは異なるものになるため、 Vary: Accept-Language と指定する。

4.5 同一生成元ポリシーとクロスオリジンリソース共有

XHTTPRequest では異なるドメインに対してアクセスを行い、レスポンスデータを読み込むことができない。 これは同一生成元ポリシー(Same Origin Policy)というセキュリティ上のポリシーによるものである。

これを回避するために、クロスオリジンリソース共有(CORS:Cross-Origin Resource Sharing)がある。

実装方法はSpring-Bootならここを参考にしてほしい。

Getting Started · Enabling Cross Origin Requests for a RESTful Web Service

実装ができればこのような動きになる。

リクエストヘッダ サーバ処理 レスポン
Originヘッダに送信元のドメイン 許可する一覧かチェックする 許可されている場合:Access-Control-Allow-Originヘッダに許可されたドメイン
許可されてない場合:403 エラー

CORS にはプリフライトリクエストというものがあり、 これはリクエストを行う前にそのリクエストが受け入れられるかどうかを事前にチェックするもので、 OPTIONS メソッドを使ってリクエストすると、許可されているメソッドやヘッダーが表示される。

キャッシュヘッダの実装

今回、全然使ったことがないヘッダはキャッシュのところ。
なので、自分で実装を試してみた。

環境

  • Java 1.8
  • Spring Boot 1.3.6

そもそもヘッダー関連の実装って?

そう、レスポンスに毎回同じ設定をするのって大変だなぁ。。。
Spring-Boot でカバーしてないかなぁ?って思ったら、カバーしてました!

Spring Security
Spring Security Reference

さっそくpom.xml に追加してみる。

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

ここからはSpring Security 初心者の私につきあってもらいたい。。。

適当にAPI を作成してアクセスしてみると、Basic 認証が表示され、アクセスできない。。。

f:id:AHA_oretama:20160710164704j:plain

なんてこった。。。

Spring Security のページを見ると大半がAuthentication, Authorization とかについて書かれている。
デフォルトでBasic 認証が有効になってしまうのか。。。

一つ勉強になったところで、Basic 認証を無効にしてみる。

以下のクラスを追加することで、Basic 認証していたコードを上書きし、 全てのリクエストを許可するようになる。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();
    }

}

@EnableWebSecurityWebSecurityConfigurer を設定するときのアノテーションで、 WebSecurityConfigurerAdapterconfigure(HttpSecurity http) がデフォルトでBasic 認証をしているコードとなるので、 それをOverrideして上書きする。 ここではBasic 認証をしている親クラスのメソッドsuper.configure(http) を呼び出してはいけない。

Sping Security を導入したことで、レスポンスヘッダーが以下のようになって、 API のデフォルトのキャッシュは無効になっている。

Cache-Control:no-cache, no-store, max-age=0, must-revalidate
Content-Type:application/json;charset=UTF-8
Date:Sun, 10 Jul 2016 08:00:56 GMT
Expires:0
Pragma:no-cache
Server:Apache-Coyote/1.1
Transfer-Encoding:chunked
X-Content-Type-Options:nosniff
X-Frame-Options:DENY
X-XSS-Protection:1; mode=block

Pragma:no-cache は古いヘッダーのようで、Cache-Controlが効かないときの予備だそう。
それ以外にもXSS対策などもついている。

では、URI ごとのヘッダーの設定は?

まずは独自のヘッダーを記述するStaticHeadersWriter を継承したクラスを作成する。

public class CacheControlHeadersWriter extends StaticHeadersWriter {

    /**
     * Creates a new instance
     */
    public CacheControlHeadersWriter() {
        super(createHeaders());
    }

    private static List<Header> createHeaders() {
        List<Header> headers = new ArrayList<Header>();
        headers.add(new Header("Cache-Control", "Cache-Control: max-age=3600"));
        headers.add(new Header("Pragma", "cache"));
        return headers;
    }
}

ここではmax-ageを使用して1日、キャッシュを保持させるようにしている。

さきほどのSecurityConfigクラスに対象のURIパスを指定して、 一度キャッシュを無効にしてから上記のCacheControlHeadersWriter を追加する。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();

        CacheControlHeadersWriter headersWriter = new CacheControlHeadersWriter();
        http.antMatcher("/api/v1/prefectures/**").headers().cacheControl().disable().addHeaderWriter(headersWriter);
    }

}

これで特定のURI のときだけキャッシュが効くようになる。

ではではChrome で確かめてみよう!! あれ?効かない。。。

調べたらなんと!同じタブで直接アクセスするとキャッシュは効かないという!!

iis - Is Chrome ignoring Control-Cache: max-age? - Stack Overflow

別タブなら効くというなんということだ!
親切すぎて、逆にはまりやすいポイントになった!

たしかに別タブならキャッシュが効く。よかった。。。

RestTemplate によるキャッシュは?

RestTemplate はSpirng でAPI へリクエストを送信するためのクラスである。
このクラスでキャッシュを判別してキャッシュが有効であればそのキャッシュを利用する、ということをしたい。

しかし、どうやらまだ未対応なようだ。。。

[SPR-5821] HTTP cache and conditional requests support in RestTemplate - Spring JIRA

ただ他のGitHub に対応するためのMaven があるようだ。

GitHub - jirutka/spring-http-client-cache: A very simple HTTP cache for the Spring’s RestTemplate.

今回は上記は使用しないが、今後のSpring-Bootの開発に期待している。

【2016/08/02追記】Validation Model(検証モデル)

検証モデルの場合は簡単でSpring にその機能が提供されている

 @RequestMapping(value = "/get", method = RequestMethod.GET)
 public String myHandleMethod(WebRequest webRequest, Model model) {
    long lastModified = // application-specific calculation
    if (request.checkNotModified(lastModified)) {
      // shortcut exit - no further processing necessary
      return null;
    }
    // further request processing, actually building content
    model.addAttribute(...);
    return "myViewName";
}

上記のようにcheckNotModified を呼ぶだけで、
更新があった場合はLast-Modifiedヘッダーをつけてくれ、 更新がなかった場合はHTTPステータスを302 にして返却してくれる。

サンプル

いつものように作成したリポジトリを公開しているので、参考にしてほしい。

github.com

WEB API The Good Parts 3章の自分的まとめ

前回からの続きで、今回は以下の本の3章を自分的にまとめたいと思う。

Web API: The Good Parts

Web API: The Good Parts

3. レスポンスデータの設計

各サブの章を一言でまとめられた。
エラー部分については重要だなぁ、と感じたため、しっかり解説。

  • データフォーマット
    JSONをサポートすべき。それ以外(xml、MessagePackなど)はオプション。
    データフォーマットの指定方法は以下の3つ。記号はおすすめ度。

    • ○ クエリパラメータ
    • × 拡張子
    • ○ Acceptヘッダ

  • レスポンスの内容をユーザが選べるようにする
    そのままの意味。選べる値はフィールドごとか、グループ単位かのいずれかで、その情報をクエリパラメータに付与する。

http://api.example.com/v1/users/12345?fields=name,age
  • エンベロープは必要か
    メタデータを含んだ形ですべてのAPIが同じデータ構造を返すために実際のデータをくるむための構造をエンベロープと呼ぶ。
    HTTPヘッダがメタ情報を持っているため、エンベロープは不要。

  • データはフラットにすべきか
    不要な階層化はするべきではないが、階層化すべきものはする。
    たぶん開発時は普通にオブジェクトをつくれば、それで階層化すべきものはされ、不要に階層化していない形になると思う。

  • 配列とフォーマット
    トップレベルに配列を包んだオブジェクトを置くか、直接配列を置くか。
    筆者としてはオブジェクトで包んだほう推し。理由は以下のよう。

    • レスポンスデータが何を示しているものかがわかりやすくなる
    • レスポンスデータをオブジェクトに統一することができる
    • JSONインジェクションを防ぎ、セキュリティ上のリスクを避けることができる

  • 各データの名前
    前回と同じ考え方。

    • 多くのAPIで同じ意味に利用されている一般的な単語を持ちいる
    • なるべく少ない単語数で表現する
    • 複数の単語を連結する場合、その連結方法はAPI 全体を通して統一する
    • 変な省略形は極力利用しない
    • 単数形/複数形に気を付ける

  • 大きな整数とJSON
    32bit を超える数値は丸め込み誤差が発生するため、文字列を使うべき。

3.6 エラーの表現

3.6.1 ステータスコードでエラーを表現する

HTTP のステータスコードを正しく使って表現すること。

ステータスコード 意味
100番台 情報
200番台 成功
300番台 リダイレクト
400番台 クライアントサイドに起因するエラー
500番台 サーバサイドに起因するエラー

3.6.2 エラーの詳細をクライアントに返す

上記のステータスコードだけではわからないので、レスポンスボディにエラー専用のデータ構造を用意して、エラーの詳細を返す。

{
  "error" : {
    "code": 2013,
    "message": "Error Description",
    "info": "Detail Description or its url"
  }
}

3.6.4 エラーの際にHTMLが返ることを防ぐ

例えば存在しないURL を叩いた場合にWEBサーバ側の設定でHTML が返ってしまうことがないようにしっかり設定する。
つまり、API だけでなく、その周りのミドルウェアの設定も必要。

3.6.5 メンテナンスとステータスコード

メンテナンスでAPI を停止する場合は503 のHTTPステータスコードを返却し、Retry-After のヘッダで再開の時刻を知らせるべき。

じゃあどうやって実装するの?

レスポンスの内容をユーザが選べるようにする

今回の部分であれ?どうやって実装するんだろう?って思ったのは、
とくに「レスポンスの内容をユーザが選べるようにする」部分だ。

実装イメージがわかなかったので調べたみた。
実際に実装するのはJava、Spring Boot。

こういうのはやっぱりStack OverFlow に書いてあるね!
参考にしたのは以下。

json - Spring rest api filter fields in the response - Stack Overflow

Model クラスの作成

ここではlombok を使用している。

@JsonFilter(RestControllerAdvice.PROFILE_FILTER)
@Data
public class Profile {

    private String firstName;
    private String lastName;
    private LocalDate birthDay;
    private String gender;
}

ここで重要なのはJsonFilter アノテーションを付与していること!
これでfilter 対象であることを表している。

Controller クラスの作成

Profile クラスを作成して、値を全部つめて返却する簡単なController クラス。

@RestController
public class ProfileController {

    @RequestMapping(value = "/api/v1/users/me/profile", method = RequestMethod.GET)
    public Profile getV1Profile() {
        Profile profile = new Profile();
        profile.setFirstName("太郎");
        profile.setLastName("山田");
        profile.setBirthDay(LocalDate.of(1986, 1, 1));
        profile.setGender("MALE");

        return profile;
    }
}

Advice クラスの作成

リクエストに詰められたfields は一括してAdviceクラスで処理するようにする。

@ControllerAdvice(annotations = RestController.class)
public class RestControllerFilterAdvice extends AbstractMappingJacksonResponseBodyAdvice {

    public static final String PROFILE_FILTER = "PROFILE_FILTER";

    @Override
    protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
            MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {

        // fields パラメータの取得
        String fieldsParamter = ((ServletServerHttpRequest) request).getServletRequest().getParameter("fields");

        String[] fields = fieldsParamter == null ? new String[0] : fieldsParamter.split(",");
        // Filter の設定
        FilterProvider filters = new SimpleFilterProvider().addFilter(PROFILE_FILTER,
                fields.length == 0 ? SimpleBeanPropertyFilter.serializeAllExcept(fields)
                        : SimpleBeanPropertyFilter.filterOutAllExcept(fields));
        bodyContainer.setFilters(filters);
    }
}

まずAbstractMappingJacksonResponseBodyAdvice クラスをextends する。
このクラスを継承することでレスポンスボディの変更が楽に行える。
beforeBodyWriteInternal メソッドだけコーディングすればよくなるのだ!

レスポンスのフィールドを指定するのはFilterProvider クラスを作成して、 MappingJacksonValue のフィルターに指定すればよい。

リクエストにfieldsパラメータがあるかないかでfilter のメソッドを変更している。
これはパラメータがない場合には、全フィールドを返却するためである。

GitHub

今回作ったものはGitHub にあげておくので、参考にしてくださいねー。

github.com

所感

今回学んだ、こうあるべきって内容については普通に身についていた。

一つどうやって実装するんだろうってイメージがつかめない部分があったが、
実際にコーディングしてみて意外と簡単に実装できるものだなぁ、と感じた。

重要なのは概念を理解すること、そして実装イメージをもつこと!