指定した期間内の特定の曜日をカウントする方法【JavaScript例】

標題の通り.考え方と実装例をメモしておきます. 「5月10日〜7月20日の間の水曜日の数」というような感じで値を得る方法です.

実装

擬似コード

切り捨て((期間の日数+(6+開始曜日-指定曜日)%7)/7);

例えば JavaScript では以下のような感じ.

const numOfDay = 10;     // 期間の日数=>10日
const beginDay = 3;     // 期間の開始日=>水曜日
const targetDay = 5;     // カウントする曜日=>金曜日

result = Math.floor((numOfDay + (6 + beginDay - targetDay) % 7) / 7);     // => 2

考え方

パッと最初に思いつきそうな方法として,期間(開始日から終了日まで)の日数を求めて,それを7で割るというものがあります.しかし,考えてみるとこの方法ではうまくいかないことがわかります.例えば,期間が8日だったとして,7で割ると1余ることになりますが,この余った1日分がカウントしたい特定の曜日なのかどうかを調べなければいけません.一方で,ある方法を採ると,単純に期間を7で割るだけで曜日をカウントできるようになります.その方法の考え方は,以下に書かれている「その1」と「その2」が参考になりました.

Excel一般機能:期間内の指定曜日の数

要は,期間の開始日を意図的にずらすことで,日数を7で割ったときの余りの扱いを簡単にするということです.上の記事のように開始日をずらせば,余った日が指定曜日になることがないため,余りは無視(切り捨て)すれば良くなります(6日前までなら開始日をずらしてもカウントに影響はない).

この方法で重要なのは「開始日の前の週の指定曜日の翌日」に開始日をずらす(戻す)ことです.こうすることで,7で割った商がカウント結果になります.となると,どうやってその開始日までもどすのか,という話になりますが,ここでは開始曜日(期間が始まる曜日)と指定曜日(カウントしたい曜日)が重要になります.

例えば,開始曜日が(火)で指定曜日も(火)だった場合,6日後ろにずらせば,「開始日の前の週の指定曜日の翌日」である前週の(水)に開始日をセットできます.次に指定曜日が開始曜日の1つ先である(水)だったとすると,1日分ずらす曜日が減って,5日後ろにずらすことになります.同じように指定曜日が(木)だと,2日分ずらす曜日が減って,4日ずらします.逆に開始曜日の1つ前である(月)が指定曜日だと,ずらす必要がなくなります.また,(日)が開始曜日だと,1日だけずらすことになります.

これを観察してみると,6日分戻すことをベースとして考えて,開始曜日と指定曜日の差でそこに調整を加えればよさそうです.差が開始曜日から後ろ方向に広がっていればずらす日数をその差分だけ減らし,前方向に広がっていればその差分だけプラスして,余剰をとります.これで,開始日から「何日前にずらせばいいか」を求められます.

で,日数を前にずらすというのは,期間をその分長くしているとも捉えられます.なのであとは,もともとの期間日数に上で求めた値を足してあげて,それを7で割った結果を切り捨てすれば完了です.

ずらす日数 = (6 + 開始曜日 - 指定曜日) % 7;
期間日数 = 期間日数 + ずらす日数;
指定曜日の数 = 切り捨て(期間日数/7);

つまり冒頭で記したように,

切り捨て((期間の日数+(6+開始曜日-指定曜日)%7)/7);

のようになります.

JavaScript で Date をディープコピーする

Date オブジェクトをコピーしたかったときのメモ.

どうやら標準ではオブジェクトのディープコピー用のメソッドは用意されていない模様.JavaScript でディープコピーする方法として JSON.stringify を使うものや jQuery.extend を使うものが紹介されているのを見かけましたが,これらは配列や文字列等を対象としていて,Date オブジェクトでは上手くできませんでした.

Date をコピーするために,以下のように元のオブジェクトのデータをコンストラクタに与えて新たにオブジェクトを生成するのが一番安心で簡単そうです.

let original = new Date();
let copy = new Date(original.getTime());

Date は割と簡単にコピーできましたが,生成が複雑なオブジェクトの場合には自前でコピーメソッドを用意する必要がありそうです.

Web認証におけるToken(JWT)とCookie(セッション)の違い

HTTP の世界はステートレスです.すなわち基礎的なプロトコル上では状態が管理されません.ユーザの認証状態などを管理するために,これまで主に Cookie & セッション ID による管理方式が使われてきました.一方で,最近ではトークンを使った認証がよく使われるようになってきています.この記事では,この辺の参考文献を見ながらそれぞれの特徴(と主にトークンの優位性)についてメモします.

それぞれの認証方式の概要を簡単に

まず Cookie でのセッション管理による認証では,ユーザが ID/PW をサーバに送り,認証結果としてセッション ID が Cookie として返されます.この際,サーバ側では,払い出したセッション ID に対応するセッション情報を裏方で管理しておきます.ユーザは,それ以降の HTTP リクエストで一緒に Cookie (セッションID) を送ることで,サーバ側に自分が誰であるのかを示します.サーバは送られてきた Cookie と管理しているセッション情報を照合してユーザの認証状態を判断します.Cookie & セッションによるこの仕組みは状態の管理を実現しており,ステートフルな環境と言えます.

一方で,トークンでの認証は,ユーザは認証結果として Cookie の代わりにトークンを受け取ります.ここで,サーバ側は何もユーザの状態に関する情報を管理しません.ユーザは HTTP リスエストと一緒にトークンを送ることで,サーバ側に自分が誰であるのかを示します.サーバは送られてきたトークンを検証し,そのユーザを認証します.トークンによるこの仕組みではサーバは状態管理から開放されており,ステートレスと言えます(クライアントに状態を保存しているとの見方もできなくはなさそうですが).ちなみにここでいうトークンは事実上のスタンダードである JWT(JSON Web Token) を想定しています.

トークン(JWT)の特徴

ステートレス

セッションを使った認証管理では,サーバ側でユーザの状態情報を管理する必要があります.で,ロードバランサを使うような分散環境の場合には,複数のサーバ間でセッション情報を同期したりとか,ユーザと接続先サーバの組み合わせを固定するようにリクエストを振り分けるとかしなければならないわけです.この辺の話は過去に以下の記事を書いたのでそちらを参照.

norikone.hatenablog.com

で,トークンを使った認証ではサーバ側で状態を管理しません.サーバ側でやることは,送られてきたトークンの検証だけです.このため,上記のような,サーバ側で認証状態をどうやって扱うかという話を考えなくていいのです.状態とサーバの結合していないため並列分散が容易で,スケーラブルであると言えます.まあ最近では KVS やフレームワークを使い比較的簡単にこのようなことが実現できるようになってきてはいますが.ただ,そういった状態管理用のサーバがそもそも不要という点で,コストの低減はできるかもしれません.

クロスドメイン

Cookie は有効範囲がドメイン単位で設定されます.あるドメイン(サイト)でもらった Cookie の情報は,他のドメインでは使えないということです.滅多矢鱈に Cookie の情報を誰にでも渡してしまう状態もよろしくなさそうなので,こうなっているのは妥当と考えられます.

しかし,Web API による第三者への機能提供が盛んになっている現在,このような制約は円滑なサービス提供の足枷となり得ます.1つの認証結果を使って,複数の第三者サービスの機能を利用したいことが結構出てくるのです.

CORS を有効にしたトークン方式では Cookie 方式のような有効範囲の制限を解除できます.トークンを受け入れているサービスであればどこでもトークン1つで機能を利用できる世界です.SSO 的な発想とも言えそうです.トークン方式ではアプリケーション側で状態を持たなくて済むということも,このようなサービス間連携における利点となります.

データストア

Cookie & セッションによる方式は,Cookie にセッション ID という単一の文字列を管理するに留まります.トークン(JWT)は,トークン自体に様々なメタデータJSON として格納することが可能であり,表現力に優れています.

例えば,トークンを使って何かを実行できるユーザの権限の設定なんかをここでできたりするわけですね.使い方はそれぞれのアプリケーション次第といったところでしょうか.

パフォーマンス

リクエストのたびにユーザから送られてきたセッション ID で認証するというのは,毎回それをキーとしてデータベースを検索する処理が走るということを意味します.リクエストと裏で管理している状態のマッピングをとらなければいけません.

トークンを使う場合にサーバ側でやるのは,そのトークンの検証だけです.データベースを検索する処理は不要です.このため,トークン方式のほうがパフォーマンスが高いみたいです.実際に評価したわけではありませんが,確かに検証動作の方がすぐに終わりそうな印象があります.

また,トークンには任意のデータを格納できます.例えば,ユーザのロール情報(管理者 or 一般)などを入れておけば,サーバ側でデータベースを検索してユーザのロールを判定するような処理をしなくてもよくなります.このような使い方ができるという点で,トークンがよりパフォーマンスで優位であると言えそうです.

ただ,トークンに格納するデータが過剰に大きくなった場合,情報の転送がボトルネックになって逆にパフォーマンスが落ちるということも考えられなくはなさそうです.

モバイル対応

Web API を使うのはブラウザだけではありません.ネイティブアプリや IoT デバイスなんかもガンガン API を使う時代が来ています.で,こういったネイティブ環境で Cookie を扱うのは割と面倒臭く,考慮事項が多いです.IoT デバイスなんかは Cookie を管理する機能が備わってなかったりしそうです.

そこでトークンを使えば,このような Cookie を使うのが難しいような場合でも,比較的容易に実装できます.ブラウザ以外のクライアントでは基本的にトークンを使うことになりそうです.

ビッグサイズ

トークンで使われる JWT はサイズが大きいという欠点があります.が,そこまで気にしなくてもいいのかなという印象です.トークンを管理する場所は LocalStorage か Cookie かを選択できるのですが,Cookie では 4kb がデータサイズの上限なので,トークンに沢山の情報を埋め込もうとすると問題になる可能性があります.

XSRFXSS 耐性

XSRFXSS の説明は割愛しますが,XSRF の大きな原因の一つとして,Web アクセス時に自動的にそのサイトに紐付いた Cookie を送ってしまうというものがあります.攻撃者はこれを利用し,認証済みユーザを使って間接的に Web アクセスを発生させ,アクセス先で認証を得ます.

トークンは基本的に LocalStorage か Cookie で保管されますが,LocalStorage で保存することで,勝手にアクセス先に認証情報を送ることはなくなるので(Cookie ではないので),XSRF を受けなくなります.もちろん,トークンを Cookie に保存した場合には,同様の問題が発生してしまいます.

しかし,LocalStorage にトークンを保存すると,今度は XSS を受けやすくなります.LocalStorage は JavaScript から容易にアクセスできるためです.一方,トークンを Cookie で扱えば httpOnly 属性により XSS 対策ができます.しかし,こうすると逆に Cookie の特性から,XSRF を受けやすくなります.

したがって,トークンを管理する方法に応じて意識すべきセキュリティ対策が変わってきそうです.

OAuth2やOpenID Connectとは何か、なんとなくわかった気になるための概要

今更ですが備忘のため,OAuth とか OpenID とかその辺の概要についてまとめるメモ.具体的な仕様については書かないけど RFC や公式サイトに載っている.便宜上,厳密性を疎かにしている点あり.

OAuthについて

背景

現在,Twitter や Yahoo など様々なサービスが,Web API として機能を提供しています.API エコノミーなんていう言葉がありますが,Web API としてサービスを提供していくこの動きは今後も続きそうです.

これに伴って,API をラップしてサービスを提供するサードパーティ製のアプリケーションが続々と出てきています.それをここでは「クライアント」と呼びますが,サービスを使いたいユーザはこのクライアントを通して API を利用し,何かしらの価値を享受するわけです.この方式が現在の Web 世界ではよく普及しています.

例えばユーザが Twitter クライアントを使うと,クライアントは TwitterAPI を叩き,タイムラインを取得して結果をユーザに表示したりするわけですね.基本的にはこのように「ユーザ ↔ クライアント ↔ API」という関係になります.クライアントは API サービスのフロントエンドとも言えるかもしれません.

この際,クライアントからリクエストを受けたAPI 提供側は,どのユーザからのリクエストなのかを判別する必要があります.API 提供側が,自分を使おうとしているクライアントの向こうにいるユーザが誰なのかを検証できなければ,ユーザが他人になりすまして他人の情報を知ったり,他人のアカウントで活動することが可能になってしまうからです.

Twitter で言えば,他人になりすましてダイレクトメッセージを取得する API を叩ければ,盗聴できてしまいます.これは,他人のアカウントでWeb サービスにログイン出来てしまうようなものです.

つまり,普通(?)の Web サービスを利用する時と同じように,API の利用/提供においても,ログインのような認証/認可の仕組みが必要になるわけです.が,この認証/認可について,一般的な Web サービスと同様に考えることはできません.これは,「クライアント(第三者)の存在の有無」という違いがあるためです.

このため,API の世界では,クライアントから API 提供側に対して「ユーザからは許可をもらっているから,ユーザ情報を扱う API を使わせてくれお」というような要求が発生し,そのセキュリティチェックが発生することになります.要は,第三者が存在するため,それを含めた間接的なセキュリティチェックが必要になるのです.では,API 提供側は,あるクライアントがあるユーザの許可をもらったことをどうやって検証すれば良いのでしょうか.

ここで,ユーザがクライアントに ID/PW(API提供側サービスのモノ) を渡しておく方法が容易に思いつきます.クライアントはその情報を使って,ユーザに成り代わるような形で API を叩くということです.API 提供側は送られてきた ID/PW を見て,クライアントの向こう側にいるユーザを特定し,本人にしか渡さない情報を返したりします.

Twitter の例で言うと,ユーザが Twitter の ID/PW を Twitter クライアントにあらかじめ渡しておけば,クライアントはその情報を使って Twitter にアクセス(ログイン)し,当該ユーザ情報の取得や操作ができるようになるよね,ということです.

しかし,この方式にはいくつか問題があります.例えば,ID/PW をクライアントに渡してしまうと,基本的にはクライアントがそのアカウントの全ての権限を持つことになります.クライアントに「タイムラインの閲覧だけを許可する」といった制限をかけるのが難しくなるということです.悪意のあるクライアントを想定した場合にはより深刻な問題になりますし,クライアントから情報が漏れたときのインパクトが大きなものになります.すなわち,できればクライアントという謂わば第三者にユーザの認証機密情報を渡すことは避けたいわけです.

また,ユーザがあるクライアントに 自分の ID/PW を使って API を叩くことを取りやめさせる場合には,ID/PW を変更せざるを得なくなります.この時,同じ ID/PW 情報を使っている他のクライアントにもこの変更の影響が出るため,アクセス許可の取り消しのような変更を個々のクライアントに適用することが難しくなります.

で,このように第三者が存在する API の世界で,イイ感じにセキュリティチェックをやろうというのが OAuth です.

概要

上に書いたように,ID/PW を単純にクライアントに譲渡する方法では,様々な問題が起こり得ます.そこで OAuth では,ID/PW をクライアントに教えずに済む方法が採用されています.すなわち,ID/PW 以外の何かで API アクセスの認可をするということです.

ちなみに「認可」というのはアクセス等の許可をすることで,「認証」とは別です.「認証」は簡単に言えば本人を確認することで,例えば,ID/PW での認証や指紋認証があります.この辺は割愛.考え方は次の記事になんとなく書いてあります.認証と認可の違い - ITmedia エンタープライズ

で,OAuth ではトークンというものを使って API アクセスの認可をします.トークンは,API を使うための鍵のようなものです(実際はタダの文字列ですが).ユーザがクライアントに自分の情報へのアクセスを許可した証として発行されるイメージです,

具体的には,API を使う側(クライアント)はこのトークンを API リクエストと一緒に送ることで,その API の使用を認可されます.API 提供側は送られてきたトークンを検証することで API へのアクセスを許可します.すなわち,クライアントは「ユーザに許可を得た証としてトークンを持っているから,ユーザ情報を扱う API を使わせてくれお」という感じで API 提供側にリクエストを送ることになります.

OAuth ではこんな感じでトークンを使って認可をします.となると,じゃあクライアントは一体どうやってトークンを手に入れるんだ,という話になります.トークンは,例えばある特定のアカウントへのアクセス許可証なわけですから,誰にでも渡していいわけではありません.ユーザに紐付いたトークンを渡していいのは,そのアカウントを持ったユーザ本人だけです.ここで「認証」が必要になります.

ユーザがクライアントを使おうとすると,API 提供側からユーザに対して,認証とトークンの発行許可が求められます.クライアント視点だと,「ユーザさん,私はトークンが必要です.API提供側に本人証明をして,私にトークンを発行するよう連絡してくだし」という感じです.

API 提供側がユーザからその連絡を受けて認証が完了すると,クライアントにトークンが発行されます(実際にはもう少し複雑な流れがあったりしますが).この「ユーザ ↔ API 提供側」間の認証は基本的にユーザの ID/PW で行われます.もちろん,ここで認証をするのは他人になりすましてトークンを発行されることを防ぐためです.この点から,トークンを持っている=ユーザ本人から認証およびトークンの使用許可を得た,と判断するような形で,API の使用を認可するのです.

ここで,OAuth ではトークンで認可が行われるため,クライアントに ID/PW を教える必要がないということが重要です.クライアントはアクセス許可証としてのトークンを持つだけです.これで上に挙げたいくつかの問題を解決できます.

例えば,このアクセス許可証にはアクセス有効範囲が設定されます.つまり,ID/PW を渡してしまう場合の課題の1つだった,無駄に権限を与えてしまう問題をシンプルに解決できます.Twitter の例で言えば,ある Twitter クライアントがもつトークンでは,タイムラインの取得はできるけど,ツイートはできない,という状態を作れたりします.

ここではざっくりと概要を書きましたが,トークンの具体的な発行方式にも色々あって,ケースに応じて使い分けられています.以下,それについて説明されている記事です.

OAuth 2.0 全フローの図解と動画

まとめると

クライアント(第三者)が存在する Web API の世界では,それを含めた間接的なセキュリティチェックの仕組みが必要になります.ユーザが ID/PW をクライアントにわたす形でこれを実現しようとすると,色々な問題が発生してしまいます.そこで,「認証」と「認可」を切り離し,トークンを使ってクライアントを「認可」する仕組みを導入します.ユーザと API 提供側との間には「認証」が発生し,その結果としてトークンがクライアントに渡されます.クライアントと API 提供側との間にはトークンによる「認可」が発生します.この際,クライアントはユーザの認証情報に触れずに,認可を得ていることがポイントです.

OpenID Connect について

概要

Web サービスなどを使っていると,基本的にはサービスごとに ID/PW の組み合わせ=認証情報を用意することになります.が,これは結構問題です.

沢山のサイトを使うとすると,ユーザ側での認証情報の管理が非常に煩雑になり,不便です.また,そもそも ID/PW での認証は,PW がバレてしまえばいくらでもなりすましできてしまう点で,脆弱でもあります.

このようなことから,ID/PW によらない方式で,1つの認証情報で複数のサービスを使えるようにしたいというモチベーションが生まれます.OpenID Connect は,このような課題にアプローチするために発展してきた技術と言えます.SSO(シングルサインオン)的なイメージでしょうか.これができれば,ユーザはサービスごとに個別の認証情報を用意して管理する手間から開放されます.また,サービス提供側としては,ユーザの認証情報を管理するリスクをヘッジできます.

「あるサービスで認証された結果」を複数のサービス間で使い回すことができれば,それを実現できます.例えば,Yahoo! で認証されたことを示す証明書のようなものが存在し,他のサービスがその証明書を使ってログイン(認証)することを許してくれればいいのです.「Yahoo!で認証されていることを確認できたからそのIDでウチにもログインしていいよ」というような世界です.

実は OpenID Connect でも,OAuth と同様にトークンを使ってこれを行います.上に書いた証明書の役割を果たすのがトークンです.OpenID に対応しているサービスは,トークンを得ることで他のサービスの ID と連携できます.ユーザから見れば,あるサービスの ID を使って別のサービスにログインできるということです.トークンを発行するのは,プロバイダと呼ばれる機関です.Yahoo!はてななどがこれに当たります.

ユーザがとあるプロバイダの OpenID(OpenID Connect でログインするためのID) で第三者サービスにログインしようとすると,そのプロバイダからユーザに対して,認証とトークンの発行許可が求められます.第三者サービスの視点だと,「ユーザさん,あたなを認証するためにトークンが必要です.プロバイダに本人証明をして,私にトークンを発行するよう連絡してくだし」という感じです.

トークンを得られた第三者サービスは,ログインしようとしているユーザが本当にプロバイダから認証されたのかを確認します.OK なら,プロバイダが提供している ID と自社サービス内の ID を紐づけ,ID 連携が完了します.

ちなみに,ユーザが第三者サービスに新規登録する際などはプロフィール情報を入力する必要があります.OpenID Connect では,このような入力を支援するための仕様も定義されています.第三者サービスはプロバイダ側の ID を参照して基本的なユーザ情報を取得し,予めプロフィール情報などをセットできるようになります.折角 ID を連携するのだから,ユーザ情報の初期化等に連携元の情報を少し使わせてくれ,という感じです.

さて,このトークンの発行から利用の大きな流れについて,OAuth と全く同じであることに気が付かれたかもしれません.というのも,OpenID Connect は OAuth の仕様をベースにしているためです.では,これらの違いは何なのかについて,なんとなくの位置づけを次に書きます.

OAuth との比較

OAuth と OpenID Connect の大きな違いとして,「OAuthは認可のために使うが,OpenID は認証のために使う」ということがよく言われています.OAuth におけるトークンは,API へのアクセス権です.すなわち,トークンは API 利用の認可に使われます.一方で,OpenID はサービスにアクセスしたきた人を認証するためにトークンを使います.

ここでまず,OAuth のトークンを OpenID のような認証に使うことを考えてみます.両者が似たような仕様なのであれば,OAuth でも認証ができそうです.OAuth でも,トークンを持っている=ユーザ本人から認証およびトークンの発行許可を得た,と判断することができるため,トークンを持っていたら認証,とすればいいのではないかという話です.ちなみにここでいう OAuth での認証というのは,OAuth のトークンをログインのための認証情報として使うという意味です.

さて,OAuth の文脈でのトークンというのは,API の利用権です.これはしばしば鍵に例えられます.鍵を持っているからアクセスロックを解錠して API を使えるという感じです.OAuth で認証をするというのは,鍵を持っていればその鍵の所有者として認証する,というやり方です.これはセキュリティ的にまずいです.

なぜなら,OAuth では,鍵とその所有者のマッピング,すなわちトークンとユーザのマッピングについては考慮されていないのです.言い換えれば,OAuth でのクライアントは,その鍵を使おうとしている人が誰なのかは考えないわけです.なので,安易に OAuth で認証しようとすれば,鍵を持っていれば本人でなくても認証してしまいますし,自分の鍵だと言い張って他人の鍵を渡したのにも関わらず認証してしまうことすら考えられます.

しかし,これは OAuth の欠陥というわけではありません.OAuth は認可のための仕組みなので,このような認証については対象外で OK なわけです.そこで,OAuth とは別に,OAuth の仕組みをベースに認証を可能にした仕様が OpenID Connect なのです.

OpenID Connect ではトークンが大きく拡張されており,トークン自体に認証のための情報が付加されています.これにより,上に書いた OAuth での認証で発生していたような問題に対処しています.すなわち,OpenID Connect の文脈でのトークンというのは,非 ID/PW 形式の認証情報なのです.これは,OAuth でのトークンと役割が異なります.

OAuth でのトークンはタダの単一のキー文字列ですが,OpenID Connect のトークンは JWT と呼ばれる形式の構造化データです.トークンのデータ構造を拡張することで,認証の仕組みを実現しているわけですね.OAuth と OpenID Connect ではトークンの発行や利用の大きな流れは一緒でしたが,主にはこのようにトークンの利用目的や実態が異なっているのです.

まとめると

利便性向上を主な目的として,1つの ID を複数サービスで使い回せる仕組みとして発展してきたのが OpenID Connect です(SSO を実現する他の手段として SAML 等もありますがここでは割愛).OpenID Connect は OAuth をベースにした仕組みであるため,トークンの発行や利用の大きな流れは非常に似ています.しかし,「認証」か「認可」という枠組みで,実現したい目的や思想が異なります.OpenID Connect では,主にトークンのデータ構造を拡張することでトークンによる「認証」を実現しています.


仕様書とか参考になりそうなもの

一番分かりやすい OAuth の説明

一番分かりやすい OpenID Connect の説明

RFC 6749 - The OAuth 2.0 Authorization Framework

単なる OAuth 2.0 を認証に使うと、車が通れるほどのどでかいセキュリティー・ホールができる | @_Nat Zone

Final: OpenID Connect Core 1.0 incorporating errata set 1

【クラスローダ】JVMが読み込むクラスを見つける仕組み【パッケージ】

NoClassDefFoundError や ClassNotFoundException に遭遇した時など,Java で書くならクラスローディングについてある程度知っておいたほうがよいケースというのはあるかなあと思い調べたときのメモ.なんとなくの概要.

クラスローダ

基本的な仕組み

Java プログラムは基本的に,複数のファイルに分かれた沢山のクラスによって成り立っています.プログラムを動かすためには,これらのクラスを JVM が読み込まなければなりません.Java では JVM のクラスローダと呼ばれる機構がこの処理を担います.クラスローダは,読み込むべきクラスを判断し,ストレージから当該クラスファイルを探し出し,ファイルを開いてクラスをロードします.

クラスロードの戦略としては,オンデマンドな感じです.プログラムの開始時に全てのクラスを読み込むのではなく,実行中に必要になったタイミングで必要になったクラスの検索→ロードをします.

クラスローダは役割ごとに複数存在していて,それらが階層構造(親子関係)をとっています.役割といってもやることは同じなのですが,クラスをロードする場所がそれぞれ異なる形です.あるクラスローダはどこどこのパスからクラスをロードするのを担当して,あるクラスローダは別のパスからロードするのを担当するという感じで.

で,基本的には,親の担当範囲で見つけられるクラスなら親にロードしてもらう,親がロードできなければ子がロードするという風に,上位のクラスローダから順に特定のクラスのロードを試みていきます.ロードの起点となるのは,実行されているクラスをロードしたクラスローダです.起点となったクラスローダは親がいれば親にロードを任せ,その親が更にその親に任せていきます.この仕組みは委譲(delegate)と呼ばれます.

また,クラスローダは基本的に子に委譲しません.これは結構重要なポイントです.例えば,クラスローダAによってロードされたクラスAを実行する際,クラスAが必要とする他のクラスのロードの起点となるのはクラスローダAです.すなわち,クラスA実行時には,その子クラスローダであるクラスローダBがロードを担当する範囲にあるクラスBをロード出来ないということを意味します.起点となったクラスローダより階層的に下位に存在するクラスローダは使われず,その下位クラスローダにしかロードできないクラスはロードできないのです(が,このスタイルが問題になるケースも存在するということで,委譲の順番をあえて変更しているサーバ等も一応存在).

このように,ロードに優先順位というか,方向性をもたせることで,複数のクラスローダが同じクラスを重複してロードしたりして競合が発生したりすることを防げます.このような仕組みになっているので,とあるクラスが,起点となるクラスローダの下位クラスローダ配下にしか存在しないクラスに依存する,という設計はよろしくないでしょう.

また,クラスローダは階層構造の横方向にも委譲しません.すなわち,兄弟関係にあるクラスローダ同士はお互いに影響し合わない仕組みになっています.このようになっていると,単一のサーバにデプロイされる複数のアプリケーションごとに兄弟関係になるクラスローダを作ることで,それぞれのアプリケーションでバージョンが違うクラスを読み込んだりできます.

基本となるクラスローダ

で,基本的なクラスローダは,ブートストラップクラスローダ,拡張クラスローダ,システムクラスローダの3つです.これらは挙げた順に親子関係になっています.

ブートストラップクラスローダは JVM そのものの実行に必要になるクラスをロードする基礎的なクラスローダです.JDK のコア API に関するクラスなどがこのクラスローダにロードされます.例えば,java.lang.* などがそれに当たります.<JAVA_HOME>/lib にあるような jar とかですね.また,このクラスローダは基本的には JVM 内にネイティブ実装されています.そのおかげで,そもそも最上位のクラスローダは誰がロードするのか問題(パラドックス)を解決できています.

拡張クラスローダは,拡張用ディレクトリ(一般的には <JAVA_HOME>/lib/ext)にあるクラスをロードします.クラスパスを変更することなく基礎的な機能を Java に追加したい場合には,このディレクトリに追加してあげれば簡単に実現できます.

システムクラスローダは,クラスパス(環境変数)で指定している場所のクラスのロードを担当します.普段プログラムを動かすためにクラスパスを追加すると思いますが,その場所のクラスはこいつが読み込んでくれています.また,このシステムクラスローダを親として独自のクラスローダを作成することも可能です(ユーザ定義クラスローダ).ユーザ定義クラスローダを作ることで,アプリケーション単位でクラスローダを割り当て,ライブラリのロード方法を変えたりできます.

ブートストラップクラスローダはネイティブ実装されているのですが,その他のクラスローダは Java オブジェクトとして存在するので,プログラム中でそのインスタンスを取得したりなんてことも簡単にできます.

クラスローディングの流れの大枠

JVM がプログラムを実行中にクラスのロードが必要だと判断すると,クラスローダに処理を依頼します.クラスローダはロードすべきクラスの完全修飾名を基に処理を始めます.完全修飾名とは,com.example.Hoge のようにクラス名とパッケージ名を繋げたものです.クラスローダは自分がどの場所からクラスを探せばよいかは知っていますが,その場所のどのクラスをロードすればよいかは知らないので,この完全修飾名を与えてロードすべきクラスを決定します.

例えば,/usr/local/lib を探すクラスローダがあったとして,JVM がそれに com.example.Hoge クラスをロードしてもらう指示を出した場合,完全修飾名がパスに変換され /usr/local/lib/com/example/Hoge.class をロードしようとします.この際,クラスが見つからない場合には ClassNotFound の例外が投げられることになります.

言い換えると,読み込まれるクラスは,パッケージ名とクラス名,そしてクラスローダの3つによって決定されるということです.完全修飾名が全く同じクラスが別の場所に複数存在していた場合,処理をするクラスローダによってどのクラスが読み込まれるかが変わってきます.また,同名の完全修飾名をもったクラスが複数のクラスローダからロードされ得る場合には,一番最初に発見されたクラスがロードされる事になっています.

クラスが無事発見されると,そのクラスが正しい構造になっているかどうかを検証したり,static フィールドを初期化したり,static イニシャライザを実行したりして,クラスを使える状態にします.

おわり

このように Java ではクラスローダという機構がクラスロードを担当しています.他の言語でのクラスロードの仕組みがどうなっているのか少し気になったので,また暇な時に調べて記事にしようかなーと思います.おわり.

参考

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.3
https://www.ibm.com/developerworks/jp/websphere/library/java/j2ee_classloader/1.html

Javaでtail -fならcommons-ioが楽

Java で tail -f のようなファイルの監視・追跡をしたい時に0から実装するのもなかなか面倒くさいです.Apache commons-io というライブラリにそれを簡単に実現できる機能があったのでメモしておきます.

Commons IO

IO 系をよしなに扱うためのオープンソースライブラリです.今回はその中のファイル監視機能を使って tail -f を簡単に実現します.
ライブラリは以下からダウンロード可能です.

https://commons.apache.org/proper/commons-io/download_io.cgi

勿論 Maven Repo にもあります.

https://mvnrepository.com/artifact/commons-io/commons-io

Tailerを使う

tail -f を実現するために,commons-io の中の Tailer というクラスを使います.
以下が基本的な Tailer の使い方です.

public class Test {
    public static void main(String args[]){
        File targetFile = new File("log.txt");  // 監視対象のファイル
        TailerListener listener = new MyListener();  // 対象ファイルに追記があったときの動作を指定
        int checkDelay = 100;  // 追記の確認間隔
        boolean tailFromEOF = true;  // trueにするとファイルの最終行から読み取る. falseだと最初の行から.

        Tailer tailer = Tailer.create(targetFile, listener, checkDelay, tailFromEOF);

        // その他の処理
    }
}

public class MyListener extends TailerListenerAdapter {
    @Override
    public void handle(String line) {
        System.out.println(line);
    }
}

ファイルに追記があった場合の動作を規定したリスナークラスを自分で作成して,それを元に Tailer インスタンスを作ってあげる形です.追記が発生すると,追記された文字列を引数にリスナークラスの handle() が呼び出されるので,ここでは追記された行を単に標準出力することになります.

また,上では create() というファクトリメソッドで tailer インスタンスを作成しています.で,最終的にたどり着く create() の中を見ると以下のようになっています.

public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end, boolean reOpen, int bufSize) {
    Tailer tailer = new Tailer(file, listener, delayMillis, end, reOpen, bufSize);
    Thread thread = new Thread(tailer);
    thread.setDaemon(true);
    thread.start();
    return tailer;
}

注意したいのは,create() インスタンスの中でスレッドをスタートさせているところです.したがって,create() を実行するとバックグラウンドで tail -f が走る形になります.なので,メインロジックが終了すると tail 処理も止まります.つまり,先のプログラムで「その他の処理」と書いた部分に何も書かなければ,tail 機能は動き始めた瞬間に止まることになります.

もしバックグラウンドではなくメイン処理としてファイルの監視ループをしたいのであれば,以下のように Tailer を使います.

public class Test {
    public static void main(String args[]){
        ...省略...
        Tailer tailer = new Tailer(targetFile, listener, checkDelay, tailFromEOF);
        tailer.run();  // ここでループが走りmain処理が止まる
    }
}

こちらでは,Tailer のコンストラクタを使ってインスタンスを生成し,その後でインスタンスの run() を呼び出しています.

run() ではスレッドを起動せず,run という private メンバを終了条件として while によるフラグループをします.run は private なので,外から状態を変えるには stop() を使います.stop() は run を false にするだけのメソッドです.なので,ループを止めたい状況では他のスレッドなどから tailer.stop() を呼んであげれば大丈夫です.

Tailerの動作の大枠

以下 run() の中身です.簡単のため,メイン処理部分だけ抽出して後は省いています.

public void run() {
    // ファイルオープンと初期化
    RandomAccessFile reader = null;
    try {
        long position = 0;

        while (run && reader == null) {
            reader = new RandomAccessFile(file, RAF_MODE);
            
            // ポイント位置の初期化
            position = end ? file.length() : 0;
            reader.seek(position);
        }
    }

    // 監視ループ
    while (run) {
        long length = file.length();

        if (length > position) {
            // 追記あり position の更新とリスナの呼び出し
            position = readLines(reader);
        }
        
        try {
            Thread.sleep(delayMillis);  // 意図的なディレイによる待機
        }
    }
}

やっていることは,ループして現在のファイルのポイント位置とファイルの長さを比較して,追記行の存在判定をしているだけです.ここでは,RandomAccessFile というクラスを使ってファイルのポインタ位置の細かい制御とシーク処理を実現しています.自分で tail を実装しようとする場合にも,お世話になりそうなクラスです.以下は概要記事.

http://www.javaroad.jp/java_io9.htm

で,追記行があれば readLines() を呼び出して追記行を読み込み,リスナに処理させています.以下 readLines() の中身です.

private long readLines(RandomAccessFile reader) throws IOException {
    StringBuilder sb = new StringBuilder();

    long pos = reader.getFilePointer();
    long rePos = pos; // position to re-read
    int num;

    // 追記行を読んでリスナーのhandleを呼び出す
    while (run && ((num = reader.read(inbuf)) != -1)) { 
        for (int i = 0; i < num; i++) {
            byte ch = inbuf[i];
            switch (ch) {
            case '\n':
                listener.handle(sb.toString());
                sb.setLength(0);
                rePos = pos + i + 1;
                break;
            default:
                sb.append((char) ch);
            }
        }

        pos = reader.getFilePointer();
    }

    reader.seek(rePos);
    return rePos;
}

やっていることは単純に行を読み込んでリスナーの handle() に渡しているだけです.が,改行文字の扱いなどがあるため,少しだけ複雑になります.で,最後 rePos を return する前に RandomAccessFile の reader をシークして,ファイルのポイント位置を更新しています.あとは run() に戻り,これの繰り返しです.

JavaでShell的に使えるコマンドラインツールを作る

Javaコマンドラインツールを作ったときのメモ.あえて Java でやる必要性は無いのですが,なんとなく Java で書きたいこともあるでしょう.

JCommanderを使ってコマンドライン引数の管理する

基本的にコマンドはオプションありきなので,引数をパースしてオプションを解釈しなければなりません.JCommander というオープンソースライブラリを使うと与えられた引数文字列→オブジェクトのマッピングをしてくれるので,かなり楽になります.例えば,「-f filename.txt」という文字列から File 型のオブジェクトに変換して保管してくれたりします.

このようなライブラリはコマンドラインパーサは JCommander 以外にもいくつかあります.が,個人的になんとなく良さそうだと感じたのが JCommander だったのでこれにしました.引数を特定の型のオブジェクトに変換するとか,文字列のバリデーションとかの機能の拡張も簡単です.まあこの辺は好みで選べばいいかと思います.

Javaコマンドラインパーサをまとめてくれている人がいました↓.

CLI Comparison · remkop/picocli Wiki · GitHub

ダウンロード

ソースは以下

GitHub - cbeust/jcommander: Command line parsing framework for Java

Maven や Gradle を使っている場合は以下から.ちなみに現在,公式Docには Maven Repository から ver1.71 をダウンロードするような記述がありますが,1.71 は今管理されていないので 1.72 を指定します.プルリク送ろうと思ったらもうだいぶ前に出している人がいました(ザワ).

Maven Repository: com.beust » jcommander

基本機能

public class Args {
  @Parameter(names = { "-log", "-verbose" }, description = "Level of verbosity")
  private Integer verbose = 1;

  @Parameter(names = "-debug", description = "Debug mode")
  private boolean debug = false;

  @Parameter(names = "-out",
      description = "File name",
      converter = FileConverter.class)
  private File file;

  // あとgetterとか
}

こんな感じで引数クラスを定義して,オプション用のフィールドはアノテーションで指定します.文字列として渡される引数を File 型のインスタンスに変換したい場合などは,Converter クラスを作成してアノテーションの converter オプションに指定してあげます.

public static void main(String args[]) {
    Args argsInstance = new Args();
    JCommander.newBuilder()
            .addObject(argsInstance)
            .build()
            .parse(args);
    argsInstance.getFile();
}

あとは上のように Args 用のインスタンスを用意してパースメソッドに渡してあげるだけです.

詳細は以下の公式Docから

JCommander

成果物のjarファイルをコマンド的に使えるようにする

Java だとプログラムを書き終わったら jar ファイルとして出力するのが一般的だと思います.ただ,このプログラムを使うのにイチイチ以下のように書くのは面倒です.

java -jar hogehoge.jar -debug -out filename.txt

ので,この jar をコマンド化します.

コマンドライン

コマンドラインからだと以下でOKです.

(echo '#!/usr/bin/env java -jar'; cat hogehoge.jar) > hogehoge
chmod +x hogehoge

1行目に java -jar を指定した shebang,その後に jar を続けたファイルを用意し,実行権を付与しています. で,hogehoge を /usr/local/bin とかパスの通った場所に置くと,以下のように実行できます.

hogehoge -debug -out filename.txt

Gradleタスクで

この動作を Gradle のタスクとして書いている人がいましたので以下に貼っておきます.

gist.github.com