Rails勉強会@東京 第10回に行ってきた。
今回もドリコムのオフィスを貸していただいての開催。
なんか、いっぱいセッション案が挙がってて、一部、すぐに終わっちゃいそうなネタもあったので統合してこんな感じのセッションを行なった。
私はRIA/Ajax/Cometのやつに出た。この辺の最近の動向についてのりおさんに色々お話を伺った。このセッションはRails Chatにて生中継を行った。
なんか期待されて話題になってるけど、たいしたものではない。SDKと銘打ってはいるもの、なんか自前のライブラリを提供している訳ではなく、ただの接続チュートリアル。
Railsをバックエンドにして、UIはFlex2で作成という、誰もが考えそうで実際にいくつかの案が出ているのをAdobeブランドを関してまとめただけ。とくに接続が便利になるとかそういう訳ではない。
ただ、Rails 1.2のActionController#respond_to{|format|}を使うと、HTML版とFlex版を柔軟に切替えられて良い。これからはそういうのが便利かもしれないとのこと。
ちなみに、Flex2の開発環境は日本語版も出たところ。EclipseベースのIDEがあって、体験版を無料で利用できる。買うと6万円くらい。
そもそもRIA自体がAjaxに食われた感があると、のりおさん。そういう感覚は私も持ってた。他の出席者にも大体共有されてた模様。リッチクライアント ソリューションカンファレンスとか、リッチクライアント製品への期待が最高に高まったところにAjaxが来ちゃったからね。
あと、競合製品としてはOpenLazloとか? 私はCurlが好きですが、何か? LLDNでCurl団扇を貰って喜ぶぐらいCurlに期待してますが、何か? でも、「Curlは解説書見た時点で、これはダメだと思った」とも言われてしまった。課金モデルが変わったり、サーバー環境が高いバージョンしかなかったり、敷居が高い面はあったよなー。最近ようやく個人向け無償版が出たけど、遅すぎたかも知れない。個人の利用を通じて開発者層を形成するのが大切なのにね。これをもっと早くやってたらね。AdobeにせよMacromediaにせよ競合がたくさんいるところに進出するには少々戦略がまずかった気もする。
あとはXUL? XULのいいところはpiroたんが萌えキャラなところだと思ってる。
のりおさんによるCometの解説。
RailsChatの場合、pushサーバーへの接続にはFlashのXMLSocketを使ってソケット通信してる。XMLSocketは実はFlash 5の頃からある枯れてる機能。
資料をいくつか。
ちなみに、AjaxもCometも洗剤の名前から来てるそうだ。SOAPのパロディで。
Ajaxライブラリの利用調査がある。一番使われてるのはやっぱりprototype.js
各種ライブラリを混用すると、大抵は問題なく動くもののとてつもなくメモリーを食う。……といっても、所詮は1ページあたり2MB程度なので、開きなおってMoo.fxもRicoも入れてなんとかならなくはない。ただし、開発環境ではさくさく動くのにお客さんの環境では重かったりして泣くことになるかもしれない。
色々なひとから出たコメント。
3つに分かれた。
私はAction Web Serviceのオーナーを務めさせていただいた。
勉強会の第5回でAction Web Serviceを触ってみるのは一応やってあるけれども、今回はじゃあ中身を1つ見てみようという催し。
私のマシンの画面をプロジェクタに写せなくて、参加者のみなさんには少し不便を強いてしまった。申し訳ない。お詫びに、もし希望する方がいたら次回もう少し丁寧にやり直します。shachiさんからは前回、「MacユーザーはVGA変換アダプタを買って持ってきなさい」という忠告をいただいていたのに土壇場まで忘れていて入手が間に合わなかったのが敗因。
ActionWebServiceのソースを見てみると、action_web_service/dispatcher/action_controller_dispatcher.rbというそのまんまの名前のファイルがある。 これを見てみると、ActionWebService::Dispatcher::ActionControllerというモジュールがある。このモジュールにはself.includedも定義されている。拡張機能をモジュールで定義してあとでincludeというRailsの慣習からすると、やっぱりこれがエントリーポイントらしい。これをあとでどこかでActionController::Baseからincludeするんだなというのはなんとなく想像がつく。
ActionWebService::Dispatcher::ActionController::included(base)を見ると、その中でやっぱり色々機能を足してるのが分かる。
class << base include ClassMethods alias_method_chain :inherited, :action_controller end
モジュールを元にクラスメソッドを足すのはRailsのコードではおなじみ。
base.class_eval do alias_method :web_service_direct_invoke_without_controller, :web_service_direct_invoke end
これは何やってるんだろうね。取り合えず、web_service_direct_invokeに何かフィーチャーを足してる可能性はあり。
base.add_web_service_api_callback do |klass, api|
if klass.web_service_dispatching_mode == :direct
klass.class_eval 'def api; dispatch_web_service_request; end'
end
end
エエェェ(´д`)ェェエエ。必要もないのに文字列evalは止めようよ。しかも、ですよ。ActionWebServiceを使ったときにAPIコールのためのエンドポイントになるのは/controller_name/apiというpathなのだけれど、そのpathに対してHTTP requestを掛けるとWebService呼び出しになるっていうのは、別にマジックでも何でもなくて、ただ単に、「そういう名前のアクションメソッドを勝手に定義してくれてる」だけってことね。
「それって何かでセキュリティ的にまずかったりするケースがないのか?」と危惧する人多数。
base.add_web_service_definition_callback do |klass, name, info|
if klass.web_service_dispatching_mode == :delegated
klass.class_eval "def #{name}; dispatch_web_service_request; end"
elsif klass.web_service_dispatching_mode == :layered
klass.class_eval 'def api; dispatch_web_service_request; end'
end
end
AWDwRや第5回のレポートでも触れてるdispatching modeによって切り分けしてる。でも、この程度の定義に文字列evalはやっぱり止めてほしい。
これを括ってるadd_web_service_api_callbackやadd_web_service_definition_callbackの中身はセッションでは追わなかったけれども、なんか面白そうだからあとで書く。
base.send(:include, ActionWebService::Dispatcher::ActionController::InstanceMethods)
で、シメはやっぱりモジュールのincludeによる拡張。
さて、上によって定義されたYourController#apiというアクションによって、dispatch_web_service_requestが呼ばれるということは分かった。じゃあ、そこから先は?
同じaction_web_service.dispatcher/action_controller_dispatcher.rbの下のほうに定義がある。
ActionWebService::Dispatcher::ActionController::InstanceMethods#dispatch_web_service_requestね。
exception = nil begin ws_request = discover_web_service_request(request) rescue Exception => e exception = e end
まずは、普通のControllerで使ってるリクエストオブジェクトを、ActionWebServiceの世界に固有のリクエストオブジェクトに変換してる。プロトコル(XML-RPC/SOAP)依存の処理が入るのはここのところがメイン。で、ここで例外を捕捉して保存してるのはなぜかというと、下のほうを見ると分かる。
if ws_request
# 正常系の処理
else
exception ||= DispatcherError.new("Malformed SOAP or XML-RPC protocol message")
log_error(exception) unless logger.nil?
send_web_service_error_response(ws_request, exception)
end
あー、そうね。エラーメッセージを返してるんね。で、省略してしまった正常系のところは、ログを取ってるのと、例外発生時にはやはり同様に捕捉してるのをのぞけば、
ws_response = invoke_web_service_request(ws_request)send_web_service_response(ws_response
これだけ。bmはinvoke_web_service_requestの処理時間を計ってるBenchmarkオブジェクトだけど。なんで計ってるんだろ。
さて、これ以上先に進む前に、さっき流してしまったdiscover_web_service_request(request)を見てみる。このメソッドはaction_web_service/protocol/discovery.rbにある。
(self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol|
protocol = protocol.create(self)
request = protocol.decode_action_pack_request(action_pack_request)
return request unless request.nil?
end
ここで、read_inheritable_attributeってなんぞ、と一瞬焦ったけれども、舞波さんが解説してくれた。RoR Wikiにも説明がある。本当に名前そのまんま、継承される属性なのね。
クラス変数はその変数を定義しているクラスだけじゃなく、サブクラスからもアクセス可能。だけれども、クラス階層を通じてオブジェクトを共有しているので、サブクラスでその値をいじってしまうと親クラスにまで影響してしまう。
じゃあサブクラスごとに値を持てばいいんでしょ、と思って「Classオブジェクトのインスタンス変数」を使う手も有る。でも、これの問題はそのままではサブクラスから親クラスの属性にアクセスできないこと。サブクラスのClassオブジェクトと親クラスのClassオブジェクトは当然オブジェクトとしては別だから。
まぁ、この問題に対処する方法はいくつかあるけど、その辺をよしなに処理しておいてくれるのが(read|write)_inheritable_attributeだそうな。あー、完全に忘れてたよ。ありがとう舞波たん。
さて、そういうわけで、"web_service_protocols"属性に対してeachを回す。中に入ってるのはActionWebService::Protocol::AbstractProtocolのサブクラスのClassオブジェクトね。デフォルトではSOAPとXMLRPC。
protocol.create(self) で、上手いことインスタンス化する。それで、protocol.decode_action_pack_request(action_pack_request)で、そのプロトコルのメッセージとしてparseしてみる。それで、うまいことparseできたらその結果を返す。
じゃあ、decode_action_pack_requestはどうなってるの? SOAPは関連ファイルがいっぱいあって、ややこしそう。なのでXMLRPCのほうを見てみる。
action_web_service/protocol/xmlrpc_protocol.rbを見る。ActionWebService::Protocol::XmlRpc::XmlRpcProtocolクラスが定義されてる。
まず、さっきprotocol.create(self)として呼んでたやつの実装がある。
def self.create(controller) XmlRpcProtocol.new end
本当にインスタンス化してるだけ。でも、本当はself.newのほうが望ましいけどナー。
で、decode_action_pack_requestのほうは、というと
def decode_action_pack_request(action_pack_request) service_name = action_pack_request.parameters['action'] decode_request(action_pack_request.raw_post, service_name) end
raw_postで、生のHTTP Request bodyにアクセスしてる。ふーん。次に行こう。
def decode_request(raw_request, service_name) method_name, params = XMLRPC::Marshal.load_call(raw_request) Request.new(self, method_name, params, service_name) end
あー、ここでファイルの最初を見ると
require 'xmlrpc/marshal'
XMLRPCメッセージのMarshalには標準添付ライブラリのやつを使ってるのね。
というわけで、requestをparseするところまでは読んだので、上に戻って、parseに成功した正常系の場合にどうやって実行されるのかを見てみる。今、parseして作成したリクエストオブジェクトを変数ws_requestに代入しておいて、
ws_response = invoke_web_service_request(ws_request) しているのであった。
invoke_web_requrestはaction_web_service/dispatcher/abstract.rbにある。
def invoke_web_service_request(protocol_request)
invocation = web_service_invocation(protocol_request)
if invocation.is_a?(Array) && protocol_request.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol)
xmlrpc_multicall_invoke(invocation)
else
web_service_invoke(invocation)
end
end
protocol_requestをここで更にinvocationに変換しなければならない理由がよくわからないんだけど、とにかくinvocationというのはこのファイルの下の方で定義されているActionWebService::Dispatcher::InstanceMethods::Invocationのインスタンスだ。
class Invocation # :nodoc: attr_accessor :protocol attr_accessor :protocol_options attr_accessor :service_name attr_accessor :api attr_accessor :api_method attr_accessor :method_ordered_params attr_accessor :method_named_params attr_accessor :service end
ようするに、呼び出しに必要な情報を束ねた構造体のようだ。
さて、そんな感じで、invoke_web_service_request(protocol_request)メソッドでは、invocationを構築した訳だ。で、次の行はktkr!
if invocation.is_a?(Array) && protocol_request.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol)
こういうコードがあと何ヶ所かあったんだよね。is_a?で分岐してる類のが。そこはAbstractProtocolのpolymorphismでなんとかしてほしい。そうでないとSOAP, XMLRPC以外のプロトコルを足すのが難しくなってくる。もうね、GoFを100回読めと。せめて、respond_to?で分岐して欲しい。
XMLRPCのmulticallだけ特別扱いしてる模様で、よくわからないけど、とりあえず「普通」のほうを見ることにする。「普通」の場合に呼ばれるのは
ActionWebService::Dispatcher::InstanceMethods#web_service_invoke(invocation)
で、ここでもまたdispatching modeによる場合分けをしていて、directならはweb_service_direct_invokeを、delegatedやlayeredならばweb_service_layered_invokeを呼んでる。
def web_service_invoke(invocation)
case web_service_dispatching_mode
when :direct
return_value = web_service_direct_invoke(invocation)
when :delegated, :layered
return_value = web_service_delegated_invoke(invocation)
end
web_service_create_response(invocation.protocol, invocation.protocol_options, invocation.api, invocation.api_method, return_value)
end
んー。なんかこれも、dispatching modeはinvocationを構築したときの場合分けで既知なわけだし、これこそ「呼び出しの内容」の実装の詳細だから、invocationのpolymorphismで対処してほしいなぁ。
まぁ、それで、web_service_direct_invokeの場合はちょっとした処理が入るけれども、どちらも最終的にはActionWebService::Dispatcher::InstanceMethods#web_service_filtered_invokeに帰着する。これも、例外処理やコールバックの呼び出しを除けば、invocation.serviceにsendしてるだけ。かくて、呼び出しができたのであった。
そして、最後はweb_service_create_responseを呼ぶ訳だ。これも、こまごまと色々してるけれども、容易に想像がつくように、やりたいのはprotocol実装に返り値をmarshalさせることだけ。このあたりは時間がなかったので少々駆け足でとばしてしまった。
そういえば、最初に見たようにActionWebServiceの機能はModule#includedを通じてコントローラーに提供されてる。じゃあ、そもそも関連モジュールがIncludeされるのはいつなんだろう。method_missingでも捕まえて、webサービス機能関連の宣言をするとincludeされる仕掛けでもあるんだろうか。
トップレベルのディレクトリにあるaction_web_service.rbを見ると書いてあった。
ActionController::Base.class_eval do include ActionWebService::Protocol::Discovery include ActionWebService::Protocol::Soap include ActionWebService::Protocol::XmlRpc include ActionWebService::Container::Direct include ActionWebService::Container::Delegated include ActionWebService::Container::ActionController include ActionWebService::Invocation include ActionWebService::Dispatcher include ActionWebService::Dispatcher::ActionController include ActionWebService::Scaffolding end
ちょww 無差別wwww
ここしばらく同じ店で懇親会やってたので、ちょっと飽きが来はじめるころ。そのへんを考えてか、今回はほかの店にしてくれた。