WebAuthenticationBroker を使用して OAuth による認可処理を Windows ストアアプリ内に組み込む
Windows ストアアプリ開発の話です。 Windows 8.1 および Windows Phone 8.1 を対象とした内容です。 (8.0 以前あるいは 10 以降については触れません。)
「Authentication and User Identity (HTML)」 に書かれているように、Windows ストアアプリ内にユーザー認証の機能を組み込むための選択肢はいろいろあります。 このエントリでは、その中の一つである Web 認証ブローカー (Web authentication broker) について紹介します。
Web 認証ブローカー (Web authentication broker) の概要
Web 認証ブローカーは、OpenID や OAuth などのインターネット経由の認証プロトコル *1 を使うオンライン ID プロバイダーに接続してユーザー認証する (あるいは認可を得る) 際に利用できる機能です。
OAuth などのプロトコルでは、クライアントに許可を与えるためにユーザー (OAuth ではリソース保持者) がサーバー (ID プロバイダー) の web ページ上で操作する必要があります。 ユーザーにサーバーの web ページを表示するためにアプリ内に web ビューを表示して操作させる場合もありますが、自前で web ビューを用意する代わりに Web 認証ブローカーを使用することができます。
Web 認証ブローカーの仕組みなどは、次のページを見ると良いでしょう。
Web 認証ブローカーの使い方
OAuth 1.0 Protocol を用いて認可を得る場合を例に、どのように web 認証ブローカーを使用するかを見ていきます。
前準備
Web 認証ブローカーを使用する前に、予め Temporary Credentials を取得しておきます。 このとき、oauth_callback
パラメータによりコールバック URI を指定できますが、任意の URI (「http://localhost/」 など) を指定して良いです。
サーバー側が対応しているのであれば WebAuthenticationBroker.getCurrentApplicationCallbackUri
メソッド の返り値 (「ms-app://s-1-15-2-9999-999999999-999999-99999/」 みたいな URI) を使うのが良いです。 ただ、サーバー側が 「http(s)」 プロトコルしか許可していない場合もありますので、その場合は適当な http プロトコルの URI を指定しましょう。
Web 認証ブローカーによる Resource Owner Authorization
Temporary Credentials を取得した後、リソース保持者による認可処理があります。 すなわち、ユーザーをサーバーに送り、リクエストを認可してもらう必要があります。 ここで web 認証ブローカーを使用できます。
- Windows 8.1 プラットフォーム用:
WebAuthenticationBroker.authenticateAsync
メソッド - Windows Phone 8.1 プラットフォーム用:
WebAuthenticationBroker.authenticateAndContinue
メソッド
Web 認証ブローカーには、最初にユーザーに表示する web ページの URI (OAuth では Resource Owner Authorization endpoint URI) と、認可された後のリダイレクト先 URI (Temporary Credentials 取得時に指定したコールバック URI) を指定します。
var Uri = Windows.Foundation.Uri; var WebAuthenticationBroker = Windows.Security.Authentication.Web.WebAuthenticationBroker; var WebAuthenticationOptions = Windows.Security.Authentication.Web.WebAuthenticationOptions; var WebAuthenticationStatus = Windows.Security.Authentication.Web.WebAuthenticationStatus; // はてなの OAuth の場合。 `tempCred.identifier` は先に取得しておいた temporary credentials の identifier。 var startUri = new Uri("https://www.hatena.ne.jp/oauth/authorize?oauth_token=" + encodeURIComponent(tempCred.identifier)); // Temporary credentials 取得時に `oauth_callback` で http://localhost/ を指定していた場合。 var endUri = new Uri("http://localhost/"); // WebAuthenticationBroker が authenticateAndContinue メソッドを持っているかどうかで処理を変える。 var authenticateAndContinue = WebAuthenticationBroker.authenticateAndContinue; if (authenticateAndContinue) { // Windows Phone 8.1 用 authenticateAndContinue(startUri, endUri, null, WebAuthenticationOptions.none); // 結果の受け取り方は下の記述を見ること。 } else { // Windows 8.1 用 WebAuthenticationBroker.authenticateAsync(WebAuthenticationOptions.none, startUri, endUri).done(function (result) { if (result.responseStatus === WebAuthenticationStatus.success) { // `result.responseData` にはリダイレクト先 URI 文字列が入っている。 // 例 : "http://localhost/?oauth_token=xxxxxxoauth_verifier=xxxxxx" } else { // 失敗 (ユーザー操作で戻って来た場合など。) } }, function (err) { // 失敗 (HTTP レベルでのエラーが発生した場合など。) }); }
Windows Phone 8.1 でのレスポンスの受け取り方
Windows 8.1 の場合は、WebAuthenticationBroker.authenticateAsync
メソッドを呼ぶとアプリ内にサーバーのページが表示され、ユーザーによる認可が完了すると Promise
から結果を受け取ることができます。
一方、Windows Phone 8.1 では、WebAuthenticationBroker.authenticateAndContinue
メソッドを呼ぶと一旦アプリが停止してアプリの外部でサーバーのページが表示され、ユーザーによる認可が完了するとアプリがアクティベートされます。 メモリ使用量を抑えるために Windows Phone プラットフォームではこのような挙動になっているようです。
authenticateAndContinue
メソッドのように xxxAndContinue
という名前のメソッドがいくつかありますが、それらのメソッドを呼びだした後の結果はアクティベーションイベントとして受け取ることができます。 authenticateAndContinue
メソッドではなくファイルピッカーの例ですが、次のページが参考になります。
XAML 版なので直接は利用できませんが、次のページの情報も参考になります。
ページナビゲーションをサポートするアプリの場合、次のような感じで WinJS.Application
の activated
イベントのリスナーを登録し、アクティベーションの情報を WinJS.Navigation.navigate
メソッドに渡すようにします。
var SESSION_STAT_NAV_HISTORY = "nav_history"; var activation = Windows.ApplicationModel.Activation; var app = WinJS.Application; var nav = WinJS.Navigation; var sched = WinJS.Utilities.Scheduler; var ui = WinJS.UI; var activationKinds = Windows.ApplicationModel.Activation.ActivationKind; function activateOnLaunchOrContinuationProcs(args) { if (args.previousExecutionState !== activation.ApplicationExecutionState.terminated) { // TODO: This application has been newly launched. Initialize your application here. } else { // TODO: This application has been reactivated from suspension. Restore application state here. } // Optimize the load of the application and while the splash screen is shown, execute high priority scheduled work. ui.disableAnimations(); var p = ui.processAll().then(function () { var url = Application.navigator.home; var activationKind = args.kind; var activatedEventArgs = args; // アクティベーションの情報。 `xxxAndContinue` メソッドの結果もここに含まれる。 var initialState = { activationKind: null, activatedEventArgs: null }; var navHistory = app.sessionState[SESSION_STAT_NAV_HISTORY]; if (navHistory) { url = navHistory.current.location; initialState = navHistory.current.state || initialState; } initialState.activationKind = activationKind; initialState.activatedEventArgs = activatedEventArgs; nav.history = navHistory || {}; nav.history.current.initialPlaceholder = true; return nav.navigate(url, initialState); }).then(function () { return sched.requestDrain(sched.Priority.aboveNormal + 1); }).then(function () { ui.enableAnimations(); }); return p; } app.addEventListener("activated", (args) => { // Windows.ApplicationModel.Activation.IActivatedEventArgs インターフェイスを実装したオブジェクト。 var activatedEventArgs = args.detail; switch (activatedEventArgs.kind) { case activationKinds.launch: case activationKinds.pickFileContinuation: case activationKinds.pickSaveFileContinuation: case activationKinds.pickFolderContinuation: case activationKinds.webAuthenticationBrokerContinuation: args.setPromise(activateOnLaunchOrContinuationProcs(activatedEventArgs)); break; default: break; } });
上のようにしてアクティベーションの情報を WinJS.Navigation.navigate
メソッドに渡すようにすると、その情報がページコントロールにわたってくるので、ページコントロールの ready
で次のように情報を受け取ることができます。
var ActivationKind = Windows.ApplicationModel.Activation.ActivationKind; var WebAuthenticationStatus = Windows.Security.Authentication.Web.WebAuthenticationStatus; WinJS.UI.Pages.define("/pages/auth/auth.html", { ready: function (element, options) { // Continuation handlers are specific to Windows Phone. if (options && options.activationKind === ActivationKind.webAuthenticationBrokerContinuation) { // Windows.ApplicationModel.Activation.IWebAuthenticationBrokerContinuationEventArgs インターフェイスを実装したオブジェクト。 var eventArgs = options.activatedEventArgs; // eventArgs から WebAuthenticationBroker からの結果を取りだして処理する。 var result = eventArgs.webAuthenticationResult; if (result.responseStatus === WebAuthenticationStatus.success) { // `result.responseData` にはリダイレクト先 URI 文字列が入っている。 // 例 : "http://localhost/?oauth_token=xxxxxxoauth_verifier=xxxxxx" } else { // 失敗 (HTTP レベルでのエラーが発生した場合やユーザーがキャンセルした場合。) // ユーザーによるキャンセルと HTTP レベルでのエラーが発生した場合の区別ができなさそう? // (手元で試したところ HTTP レベルでのエラーでも `result.responseStatus` の値が 1 になっていた。) } } } });
ちょっと面倒ですがしょうがないですね。
WebAuthenticationBroker から結果を受け取った後
ユーザーが認可を行った場合は WebAuthenticationBroker
からの結果に oauth_verification
が含まれているはずですので、それを使って Token Credentials の取得処理を行えば良いです。
サンプルプロジェクト
WebAuthenticationBroker
を使用するサンプルプロジェクトがあります。 JS のものも他の言語のものもあるので参考になるでしょう。 ただ、OAuth 周りの処理は完全ではなさそう (OAuth のパーセントエンコードの代わりに encodeURIComponent
が使われていたりする) なので、あくまで参考程度に。
セキュリティ的な話
アプリ内に web ビューを表示する方法だと、アプリが信頼できないものの場合にパスワードが盗まれる可能性があるという問題があるのでセキュリティ的には外部ブラウザを使う方法を提供するべき (下記ページ参照) なわけですが、web 認証ブローカーについても同様の問題がありそうな気がします。
Web 認証ブローカーは Windows 側が提供している API なので、「接続先 URL が表示される」 + 「Web 認証ブローカーを用いた場合と同様の表示をアプリ側が独自に実装できないようになっている」 という条件が満たされれば外部ブラウザを使用するのと同等になるのかなーと思いますが。 *2
そもそも Android アプリと違って審査があって、要件に 『4.3 アプリはユーザーのセキュリティ、Windows デバイス、システム、関連するシステムのセキュリティまたは機能を危険にさらしたり侵害したりしてはならない。また、Windows ユーザーまたは他の個人に被害を与える可能性があってはならない』 ってのがあるから、パスワードを抜こうとするようなアプリは審査で落とされるから大丈夫なのかもですけど。
Windows ストアアプリの JS で文字列をエンコードするために Windows Runtime コンポーネントを作成する
前置き: JS での Windows ストアアプリ開発時に C# の機能を使いたい
Windows ストアアプリを JS + HTML で開発する場合、たまに JS の非力さに困ることがあります。 例えば、文字列を UTF-8 でエンコードしてバイト列を得たいという場合。 JS だけでエンコードするのは難しいですよね。 JS での文字列のエンコードの現状としては、WHATWG の Encoding Standard で TextEncoder
インターフェイスが定義されていて、Firefox や Chrome、Opera では使えるようになっているようです。 しかし、残念ながら Windows ストアアプリでは使えません (Windows 8.1 時点)。
Windows ストアアプリで使える一つの方法として、エンコード処理を全て JS で記述するという方法があります。 JS の文字列を UTF-8 でエンコードして得られたバイト列を Uint8Array
オブジェクトとして得る関数 (strToUTF8Arr
関数) のサンプルコードが MDN にあります。
このような関数を用意してもよいのですが、Windows ストアアプリ開発をする場合は C# の力を借りるということが簡単にできるので C# の力を借りるのが良いでしょう。 (C# だけでなく Visual Basic と C++ も使用できますが、他の言語を選択する理由が特にないのであれば C# を使うのが楽だと思います。)
Windows Runtime コンポーネント (WinRT コンポーネント)
Windows ストアアプリの JS からは Windows Runtime API を触ることはできますが、ネイティブの C++ のライブラリを直接使用したり、.NET Framework のクラスライブラリを直接使用したりすることはできません。 JS から C++ や .NET Framework の機能を使用するためには Windows Runtime を使ってコンポーネントを作成する必要があります。 Windows Runtime コンポーネントは、C#、C++、Visual Basic のいずれかの言語で作成できます。 作成された Windows Runtime コンポーネントは、JS からも使用できますし、当然他の言語 (C#、C++、Visual Basic) からも使用できます。
Windows Runtime コンポーネントの概要は次の記事が参考になります。
公式のドキュメントとしては次を見ると良いと思います。
MS によるブログエントリも参考になります。
実際の作り方
C# で Windows Runtime コンポーネントを記述するには、次のチュートリアルを見るのが良いでしょう。
Visual Studio Professional (または Community) 2013 を使っていれば、上のチュートリアル通りに進めることで Windows Runtime コンポーネントを作成し、JS から使用するということができるはずです。
配列の扱いなどの注意すべき点は次の文書を読むと良いでしょう。 (日本語だとタイトルが 「C++ および〜」 となっていますが、正しくは 「C# および〜」 です。)
サンプル
例として、文字列を UTF-8 エンコーディングでエンコードしたり、逆にデコードしたりするための Windows Runtime コンポーネントを作成して、JS から使用してみます。
まず、新たに C# 用の Windows Runtime Componenet プロジェクトテンプレートを使って、新しいプロジェクトを作成します。 今回は 「WindowsRuntimeComponentSample」 というプロジェクト名にしました。 そして、次のようなクラス (WindowsRuntimeComponentSample.Utf8Encoding
クラス) を作成しました。
using System; using System.Runtime.InteropServices.WindowsRuntime; using System.Text; namespace WindowsRuntimeComponentSample { public sealed class Utf8Encoding { public static byte[] encode(String str) { return Encoding.UTF8.GetBytes(str); } public static String decode([ReadOnlyArrayAttribute] byte[] bytes) { return decode(bytes, 0, bytes.Length); } public static String decode([ReadOnlyArrayAttribute] byte[] bytes, int index, int count) { return Encoding.UTF8.GetString(bytes, index, count); } } }
そしてこのプロジェクトを JS のプロジェクト (プロジェクト名 「App1」) から参照するように設定します。 ソリューションの構成は次のようになりました。
これで JS から WindowsRuntimeComponentSample.Utf8Encoding
クラスが使用できます。
// 文字列を UTF-8 でエンコードする。 (返り値は Uint8Array オブジェクト。) WindowsRuntimeComponentSample.Utf8Encoding.encode("あ"); // バイト列 (Uint8Array オブジェクト) を UTF-8 でデコードする。 WindowsRuntimeComponentSample.Utf8Encoding.decode(new Uint8Array([0xE3, 0x81, 0x82]));
JS での string
型は C# では System.String
型になります。 また、C# での byte[]
型は JS では Uint8Array
オブジェクトとして扱われます。
TypeScript で扱う際に型情報を自前で用意する必要があるのがちょっと面倒だなーと思いました。 *1
おわりに
UTF-8 での文字列エンコーディングを例にしましたが、文字列のエンコード・デコード処理以外でも JS では書きにくい処理を C# などで実装して JS から呼び出すということがさほど難しくなく実現できます。
Windows アプリ開発時にどの言語 (JS、C#、C++、Visual Basic) を選択するか考える際に 「基本的に JS で大丈夫だけど、JS で書きにくい処理を書く必要が出てきたらどうしよう」 と悩むこともあるかもしれません。 このエントリで見たように、そのような処理は C# などで Windows Runtime コンポーネントとして書いて、JS から呼び出す、ということが可能です。 言語選択の参考にしてください。
*1:もしかしたら自動で生成される機能があったりするかもしれないけど知らないです。
読んだ: Java エンジニア養成読本
2014 年 12 月に発売された 『Java エンジニア養成読本 [現場で役立つ最新知識、満載!]』 を読みました。 これだけを読んで Java がわかるわけではないけど、歴史から Java EE、周辺技術に至るまで説明されていて、Java 初心者が全体を俯瞰するのに良さそうだと思いました!
内容紹介
本書は、複数の著者による共著になっています。
まず、巻頭記事がきしださんによる 「誰も教えてくれない Java の世界」。 Java の歴史や、Java のエディション (Java SE、EE、ME について)、JDK や JRE、JCP、JSR の紹介や各種 IDE や有名なフレームワークの紹介など、Java を使い始めてみたけど世界観がよくわからない、という人に役立つ情報が凝縮されています。 「お前に Sun が救えるか」 といったネタも紹介されています。
続く特集 1 が irof さんによる 「Java 入門」。 もちろん Java の構文の細かいところまでは説明されていませんが、クラス定義についてや例外処理、標準ライブラリについてのプラクティスなどが紹介されています。
特集 2 は bitter_fox さんによる 「Java SE 8 時代のデータ処理入門」。 Java SE 8 で導入されたラムダ式や Stream API について説明されます。 私もここら辺のことはまだ詳しくは知らなかったのでためになりました。
特集 3 はキクタローさんによる 「現場で役立つ Java EE」。 Java 初心者にとっては Java EE とは何者なのか良くわからない存在だと思いますが、この特集を読むことでなんとなく何者であるかはわかると思います。 Java EE と一口に言ってもその技術要素は多岐に渡りますが、この特集では Servlet や JSF、JAX-RS、JPA、CDI、EJB といったわりと良く使われる技術要素について主に説明されます。 私も JSF や EJB については詳しくなかったので、本書を読んで勉強になりました。
特集 4 は渡辺さんによる 「現場で役立つチーム開発入門」。 Git や Maven、JUnit によるテストや Jenkins といった、開発現場で良く使われている周辺技術が説明されます。
最後に 「イマドキの Java 受託開発の現場で求められる知識」 というタイトルで、提案や設計について、「web 開発の現場では Java の知識だけでなく、HTML や CSS、JS の知識なども必要である」 みたいな話や、テストやデプロイについて説明されます。
感想
Java 初心者が Java を使った web アプリケーション開発について学ぶのにちょうど良い書籍になっていると思います。 もちろん、前半は web アプリケーション開発とは関係なく Java について学べますし、Java を使うなら (web アプリケーション開発をするつもりがなくても) 読むと良いでしょう。
文体がいいのか構成がいいのかムックという本の形態が良いのかわかりませんが、技術書としては読みやすかったです。
既に Java について深い造詣があるならこの本を読む必要はないと思います *1 が、これから Java を使っていきたい人や最近 Java を学び始めた人は、この本を早い段階で呼んでおくと後々の学習が捗ると思います。 Java の良書はいろいろありますが、重量級なものも多いですので、まずは軽く本書から読んでみる、というのをオススメします。
あなたと Java エンジニア養成読本、今すぐ購入!
Kindle 版もあるようですよ!
Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] (Software Design plus)
- 作者: きしだなおき,のざきひろふみ,吉田真也,菊田洋一,渡辺修司,伊賀敏樹
- 出版社/メーカー: 技術評論社
- 発売日: 2014/11/11
- メディア: 大型本
- この商品を含むブログ (6件) を見る
- 作者: きしだなおき,のざきひろふみ,吉田真也,菊田洋一,渡辺修司,伊賀敏樹
- 出版社/メーカー: 技術評論社
- 発売日: 2014/12/11
- メディア: Kindle版
- この商品を含むブログを見る
関連エントリ
著者
Espresso 2.0 が Android support library の一部としてリリースされた
上記エントリにあるように、Espresso 2.0 がリリースされた。 Espresso は Android アプリの自動テストのためのライブラリである。 ほぼ上のエントリに書かれている内容であるが、Espresso 2.0 のリリースについて紹介する。
変更内容など
一番大きな変更は、Android support library の一部になったことだと思われる。 (上のエントリにもそう書かれている。) *1 そのおかげで、Android SDK で 「Android Support Repository」 をインストールしておけば (JAR をダウンロードしたりせずに) 簡単に使用できるようになった。
API 的には、パッケージ名の変更以外は大きな変更点はなさそうである。 とはいえ一部非互換な変更が加わっているので、そこら辺は気を付ける必要がある。 変更内容はリリースノートを見ると良い。
また、Instrumentation テストランナーとして GoogleInstrumentationTestRunner
にいくつかの機能を追加した AndroidJUnitRunner
が含まれている。 JUnit 4 サポートも含まれていて、これを使うことで JUnit 4 を使ったテストを書けるようになる。
ドキュメント
2015 年には Android Developers の方にドキュメントが移される予定のようだが、今のところはまだ android-test-kit でホストされている。 2016 年現在、ドキュメントは既に移されている。
- Android Developers 内のドキュメント : Testing Support Library | Android Developers
- Testing support library の GitHub Page : Android Testing Support Library
Testing Support Library の Javadoc は以下。
サンプルコード
サンプルプロジェクトが GitHub にホストされている。 JUnit 4 を使ったテストの例などもあり、参考になる。
また、私が公開している Espresso を使用したテストの例も Espresso 2.0 に対応させた。
「Espresso を使って PreferenceActivity の自動 UI テストを行う」 のエントリを書いたときに作ったサンプルプロジェクトである。
関連エントリ
JUnit 4 によるテストの説明や紹介をしているエントリがあります。
変更履歴
- 2016-10-02 : リンク先が Not Found になっているものがあったので、リンク先を変更しました。
*1:早く Volley も support library の一部にならないかな。
【Retrofit を読む】 利用者が定義したインターフェイスに実装を提供する Java ライブラリの作り方 【リフクレション】
この記事は、はてなエンジニアアドベントカレンダー 2014 の 15 日目のエントリです。 昨日は id:chris4403 による 「開発合宿で何を考えてどう作ったか」 でした。
このエントリでは、Android アプリおよび Java アプリケーション用の REST クライアントライブラリである Retrofit のコードを参照しながら、利用者が定義したインターフェイスの実装を提供するようなライブラリの実装方法について説明します。 主に Java のリフレクションの話になります。
注意点など
- 本記事中に掲載されている Retrofit のコードは、Apache License, Version 2.0 のもとで公開されているものです。
- 記事執筆時点の master ブランチの最新のコミットを参照しています。
- Android アプリ開発者で Retrofit のコードを読みたい人は Android Studio で Retrofit のプロジェクトを開くのが楽で良いと思います。
Retrofit の紹介
Retrofit について簡単に紹介します。 詳しい話は公式サイトの方を見てください。
- 公式サイト : Retrofit
Retrofit は Square によって開発されている REST クライアントライブラリです。 HTTP リクエストを実行するためのメソッドを持ったインターフェイスを定義すると、Retrofit がそのインターフェイスの実装を提供してくれます。 アノテーションにより、HTTP メソッドの指定やどの引数がなんのパラメータであるか、あるいはリクエストボディであるかといったことを指定できます。
下記は Groovy スクリプトではてなハイクの公開タイムラインを取得する REST クライアントのサンプルコード *1 です。 HTTP メソッドの種類とリクエスト先のパスを GET
アノテーション で指定しています。 そして、リクエストごとにクエリパラメータを変更できるように、クエリパラメータの値を引数として受け取るようになっています。
@Grab('com.squareup.retrofit:retrofit:1.8.0') import retrofit.RestAdapter import retrofit.http.GET import retrofit.http.Query // HTTP リクエストを投げるメソッドを持つインターフェイスを定義する。 interface HaikuService { /** はてなハイクの公開タイムラインを取得する */ @GET("/api/statuses/public_timeline.json") List<?> getPublicTimeline(@Query("page") int page, @Query("count") int count) } // 定義したインターフェイスの実装を取得。 RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint("http://h.hatena.ne.jp") .build() HaikuService haiku = restAdapter.create(HaikuService.class) // 取得した実装を使用してハイクの公開タイムラインの内容を表示。 def statuses = haiku.getPublicTimeline(1, 10) for (def status : statuses) { println '---' println status.user.name println status.text }
利用者側がインターフェイスを定義して、そのインターフェイスの実装をライブラリが提供する、というのが特徴的ですね。
なお、HTTP 通信の実装や、リクエストボディおよびレスポンスボディと Java オブジェクトの変換機能の実装については Retrofit 自体が持っているわけではありません。 (デフォルトで HTTP 通信の実装はプラットフォームに応じたものが使われ、変換機能には Gson が使われますが、好みに応じてそれらは変更できます。)
利用者が定義したインターフェイスに対して実装を提供するために必要な技術
さて、Retrofit のように利用者が定義したインターフェイスに対して実装を提供するようなライブラリを書くことを考えましょう。 Java では、リフレクションを使用することで実現可能です。
任意のインターフェイスに対して実装を提供するために、Proxy
クラス を使用することができます。 そして、実行されたメソッドの情報を取得するために、リフレクションのための各種メソッドを使用できます。 この 2 つについて順番に説明します。
Proxy
クラス
Proxy
クラスは、動的プロキシクラス (実行時に指定されたインターフェイスを実装するクラス) を生成するためのクラスであり、また、動的クラスのスーパークラスにもなるものです。 実際のメソッド呼び出しの処理は InvocationHandler
インターフェイス を実装したクラスのインスタンスが担います。 Proxy
クラスを使うことで、実行時に任意のインターフェイスに実装を提供することができます。
Retrofit では、retrofit.RestAdapter#create(Class
メソッド の中で動的プロキシクラスのインスタンス生成が行われます。
/** Create an implementation of the API defined by the specified {@code service} interface. */ @SuppressWarnings("unchecked") public <T> T create(Class<T> service) { Utils.validateServiceClass(service); return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service }, new RestHandler(getMethodInfoCache(service))); }
retrofit.RestAdapter#create(Class<T>)
メソッド
上記コードを見ればわかるように、指定されたインターフェイスの実際のメソッド呼び出しを処理するのは retrofit.RestAdapter.RestHandler
クラスです。
リフレクションによるメソッド情報の取得
メソッドが呼ばれた際に実行される HTTP リクエストの内容は、各メソッドに付けられたアノテーションや返り値の型、仮引数の情報によって決まります。 すなわち、メソッドの情報を実行時に見る必要があります。 メソッドの情報を実行時に得るには、Method
クラス に定義されている各種メソッドが使用できます。
Retrofit ではどのような処理になっているのか見ていきましょう。 retrofit.RestAdapter.RestHandler#invoke(Object, Method, Object[])
メソッドの実装は次のようになっています。
@SuppressWarnings("unchecked") // @Override public Object invoke(Object proxy, Method method, final Object[] args) throws Throwable { /* (略) */ // Load or create the details cache for the current method. final RestMethodInfo methodInfo = getMethodInfo(methodDetailsCache, method); if (methodInfo.isSynchronous) { try { return invokeRequest(requestInterceptor, methodInfo, args); } catch (RetrofitError error) { /* (略) */
retrofit.RestAdapter.RestHandler#invoke(Object, Method, Object[])
メソッド
まず、getMethodInfo(Method)
メソッド呼び出しにより指定のメソッドの情報を RestMethodInfo
オブジェクトとして受け取り、その後 invokeRequest
メソッドを呼び出して実際の HTTP リクエスト処理に入ります。 実際には、同期実行か非同期実行か、あるいは RxJava を使っているかで処理が分かれたりしますが、ここではそこら辺の詳細には立ち入りません。
例えば、HTTP リクエストの結果の受け取り方は RestMethodInfo#parseResponseType()
メソッドで解析されます。
private ResponseType parseResponseType() { // Synchronous methods have a non-void return type. // Observable methods have a return type of Observable. Type returnType = method.getGenericReturnType(); // Asynchronous methods should have a Callback type as the last argument. Type lastArgType = null; Class<?> lastArgClass = null; Type[] parameterTypes = method.getGenericParameterTypes(); if (parameterTypes.length > 0) { Type typeToCheck = parameterTypes[parameterTypes.length - 1]; lastArgType = typeToCheck; if (typeToCheck instanceof ParameterizedType) { typeToCheck = ((ParameterizedType) typeToCheck).getRawType(); } if (typeToCheck instanceof Class) { lastArgClass = (Class<?>) typeToCheck; } } /* (略) */
retrofit.RestMethodInfo#parseResponseType()
メソッド
同期実行の場合はメソッドの返り値として HTTP リクエストの結果が返され、非同期実行の場合はコールバックオブジェクトが引数に渡されるようになっているので、下記のメソッドを呼ぶことでメソッドの返り値と仮引数との両方を確認しています。
- 返り値の情報取得 :
Method#getGeneticReturnType()
メソッド - 仮引数の情報取得 :
Method#getGeneticParameterTypes()
メソッド
他にも、メソッドに付けられたアノテーション一覧を取得するために getAnnotations()
メソッド が使われたり、仮引数につけられたアノテーションを取得するために getParameterAnnotations()
メソッド が使われたりしています。
おわりに
この記事では、Retrofit の実装を参照しながら Java のリフレクション機能について紹介しました。 バグの原因になりがちなのでアプリケーションコード中で直接リフレクションを使用するのはできるだけ避けた方がいいと思いますが、リフレクションにより Retrofit のような便利なライブラリを実現することも可能ですので、いい感じに使っていきたいですね。
*1:サンプルコードなのでエラー処理などちゃんと行っていません。