當開發 Rails 3 應用程式時,了解新的 call stack 設計對除錯和開發新功能的幫助會很大:

首先 Action::Dispatch 成為整個 call stack 的主角,Jeremy McAnally 歸納出幾個特點:
如果熟悉 Rails 3 的初始化程序,應該可以了解 rack server 在接收到一個 HTTP request 後,要怎麼誘發 Rails::Application 的 .call 實體方法:
Rails::Server.new.start
Rack::Handler::Mongrel.run(wrapped_app)
Mongrel::HttpServer.new.run
Mongrel::HttpServer.new.process_client
Rack::Handler::Mongrel.new.process
Rack::Chunked.new.call(env)
Rack::ContentLength.new.call(env)
Rails::Rack::LogTailer.new.call(env)
YourApp::Application.call(env)
Rails::Application.call(env)
Rails::Application.app.call(env)
Rails::Application.middleware.build(routes).call(env) # @app.call(env)
Rails 3 為了符合 Rack 的設計理念(能回應 call 方法的物件),於是將 routes 和 middlewares 設計成串接式(cascade)型態的物件,並在執行 initializers 時實體化成 rails app stack 物件。其中有三個 initializer 跟 Action::Dispatch 最相關:
## actionpack/lib/action_dispatch/railtie.rb
initializer "action_dispatch.prepare_dispatcher" do |app|
require 'rails/dispatcher'
ActionDispatch::Callbacks.to_prepare { app.routes_reloader.reload_if_changed }
end
## railties/lib/rails/engine.rb
initializer :add_routing_paths do |app|
paths.config.routes.to_a.each do |route|
app.routes_reloader.paths.unshift(route) if File.exists?(route)
end
end
## railties/lib/rails/application/finisher.rb
initializer :build_middleware_stack do
app
end
其中 action_dispatch.prepare_dispatcher 會設定一個 ActionDispatch 的 callback 內容。而 add_routing_paths 會用 Rails::Application.routes_reloader 方法實體化一個 Rails::Application::RoutesReloader 物件,並將所有 Rails::Engine 的 config/routes.rb 加入到 @path 陣列。當執行 build_middleware_stack 時,會實體化 ActionDispatch::Routing::RouteSet 物件,連同各式剛實體化好的 middleware 物件一起放入 ActionDispatch::MiddlewareStack 陣列物件中,最後轉換成串接式型態的物件:
## railties/lib/rails/application.rb
class Rails::Application
def app
@app ||= middleware.build(routes)
end
def routes
@routes ||= ActionDispatch::Routing::RouteSet.new
end
end
## railties/lib/rails/engine.rb
class Rails::Engine
delegate :middleware, :paths, :root, :to => :config
end
## railties/lib/rails/application/configuration.rb
class Rails::Application::Configuration
def middleware
@middleware ||= default_middleware_stack
end
def default_middleware_stack
ActionDispatch::MiddlewareStack.new.tap do |middleware|
......
middleware.use('::ActionDispatch::Callbacks', lambda { !cache_classes })
......
end
end
end
## actionpack/lib/action_dispatch/middleware/stack.rb
class ActionDispatch::MiddlewareStack
def build(app = nil, &blk)
app ||= blk
raise "MiddlewareStack#build requires an app" unless app
active.reverse.inject(app) { |a, e| e.build(a) }
end
def use(*args, &block)
middleware = Middleware.new(*args, &block)
push(middleware)
end
end
仔細地看 Rails::Application.app 物件真正的樣貌,就是把一個擁有 @app 類別變數的 middleware 物件當成另一個擁有 @app 類別變數的 middleware 物件的初始化參數,一個個串接下去形成一個大型的 rack app stack 物件。這 rack app stack 物件也會被當成參數成為 rack server stack 的串接式物件,以便呼應 rack server 傳來的 call 方法:
#> s1 = ::ActiveRecord::QueryCache.new(@routes)
#> s9 = ::ActionDispatch::Callbacks.new(s8, !cache_classes)
#> ……
#> @app = ::ActionDispatch::Static.new(s13, Rails.public_path)
#> Rails::Application.app.call(env) = @app.call(env)
@app = #<ActionDispatch::Static:0x2ff9fa4
@app=#<Rack::Lock:0x2ffa1ac
@app=#<Rack::Runtime:0x2ffa2b0
@app=#<Rails::Rack::Logger:0x2ffaa1c
@app=#<ActionDispatch::ShowExceptions:0x2ffaabc
@app=#<ActionDispatch::Callbacks:0x2ffac60
@app=#<ActionDispatch::Cookies:0x2ffae40
@app=#<ActiveRecord::SessionStore:0x2ffaf1c
@app=#<ActionDispatch::Flash:0x2ffb0ac
@app=#<ActionDispatch::ParamsParser:0x2ffb50c
@app=#<Rack::MethodOverride:0x2ffb64c
@app=#<ActionDispatch::Head:0x2ffb944
@app=#<ActiveRecord::ConnectionAdapters::ConnectionManagement:0x300e2d8
@app=#<ActiveRecord::QueryCache:0x300e33c
@app=#<ActionDispatch::Routing::RouteSet:0x151c588»»»»»»»>
這裡要特別注意 ActionDispatch::Callbacks 這個 middleware 物件,當它初始化時會執行名為 :prepare 的 callback(先前的 initializer 已設定好內容)。這個 callback 執行時會用 Rails::Application.routes_reloader 方法來實體化一個 Rails::Application::RoutesReloader 物件,並動用 reload! 方法。這時會得到 ActionDispatch::Routing::RouteSet 實體物件,接著利用 clear! 方法取得一個新的 Rack::Mount::RouteSet 實體物件 @set,依序載入所有 Rails::Engine 的 config/routes.rb,最後執行 finalizer! 方法,將 @set 凍結住避免變動:
## railties/lib/rails/application.rb
class Rails::Application
def routes_reloader
@routes_reloader ||= RoutesReloader.new
end
end
## actionpack/lib/action_dispatch/middleware/callback.rb
class ActionDispatch::Callbacks
define_callbacks :prepare, :scope => :name
def self.to_prepare(*args, &block)
if args.first.is_a?(Symbol) && block_given?
define_method :"__#{args.first}", &block
set_callback(:prepare, :"__#{args.first}")
else
set_callback(:prepare, *args, &block)
end
end
def initialize(app, prepare_each_request = false)
@app, @prepare_each_request = app, prepare_each_request
run_callbacks(:prepare)
end
end
## railties/lib/rails/application/routes_reloader.rb
class Rails::Application::RoutesReloader
def reload!
routes = Rails::Application.routes
routes.disable_clear_and_finalize = true
routes.clear!
paths.each { |path| load(path) }
ActiveSupport.on_load(:action_controller) { routes.finalize! }
nil
ensure
routes.disable_clear_and_finalize = false
end
end
## actionpack/lib/action_dispatch/routing/route_set.rb
class ActionDispatch::Routing::RouteSet
def clear!
# Clear the controller cache so we may discover new ones
@controller_constraints = nil
@finalized = false
routes.clear
named_routes.clear
@set = ::Rack::Mount::RouteSet.new(:parameters_key => PARAMETERS_KEY)
end
def finalize!
return if @finalized
@finalized = true
@set.add_route(NotFound)
@set.freeze
end
end
當 routes_reloader 載入 config/routes.rb 時,會誘發 Rails::Application.routes 執行 draw 實體方法。這個 draw 方法會把 routes 自己(ActionDispatch::Routing::RouteSet 實體物件)當成參數實體化一個 ActionDispatch::Routing::Mapper 物件 mapper,並成為 mapper 的 @set 變數,而 mapper 會負責解譯 block 中定義的 Rails routes mapping DSL 內容。
例如當定義一個 match(‘/’, :to => “your_home#index”) 時,首先會把傳入 match 方法的參數轉換成 ActionDispatch::Routing::Mapper::Mapping 實體物件,再轉換成參數陣列 mapping。接著 mapper 的 @set 變數(routes)會以 add_route 方法把 mapping 參數實體化成 ActionDispatch::Routing::Route 物件 route。由於 routes 本身也有一個 @set 變數是個 Rack::Mount::RouteSet 實體物件,同樣會以 add_route 方法將 route 加入這個 @set 變數,以便串接 rack mount 的 router 辨識功能:
## actionpack/lib/action_dispatch/routing/route_set.rb
class ActionDispatch::Routing::RouteSet
def draw(&block)
clear! unless @disable_clear_and_finalize
mapper = Mapper.new(self)
if block.arity == 1
mapper.instance_exec(DeprecatedMapper.new(self), &block)
else
mapper.instance_exec(&block)
end
finalize! unless @disable_clear_and_finalize
nil
end
# @set = ::Rack::Mount::RouteSet.new
def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
route = Route.new(app, conditions, requirements, defaults, name, anchor)
@set.add_route(*route)
named_routes[name] = route if name
routes << route
route
end
end
class ActionDispatch::Routing::Dispatcher
def initialize(options={})
@defaults = options[:defaults]
@glob_param = options.delete(:glob)
end
end
## actionpack/lib/action_dispatch/routing/mapper.rb
class ActionDispatch::Routing::Mapper
def initialize(set)
@set = set
end
# @set = ::ActionDispatch::Routing::RouteSet.new
def match(*args)
mapping = Mapping.new(@set, @scope, args).to_route
@set.add_route(*mapping)
self
end
end
class ActionDispatch::Routing::Mapper::Mapping
def to_route
[ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
end
def app
Constraints.new(
to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
blocks
)
end
end
產生 mapping 陣列時會實體化一個 Routing::RouteSet::Dispatcher 物件 app,負責拆解 params,執行特定 controller 的 action 內容,屬於仲介 route 到 controller 的主要 app stack 物件。這時 initializer 才算完成整個 rails app stack 的準備工作。
當一個 HTTP request 從 rack server stack 傳遞到 rails app stack 後,會先經過 middlewares 一層層地處理,直到 ActionDispatch::Routing::RouteSet 實體物件傳遞給 Rack::Mount::RouteSet 實體物件,才會辨識 http request 該由哪個 ActionDispatch::Routing::Route 實體物件來執行,並經由 ActionDispatch::Routing::RouteSet::Dispatcher 實體物件誘發特定 controller 的 action 內容:
Rails::Application.middleware.build(routes).call(env) # @app.call(env)
ActionDispatch::Static.new.call(env) # @app.call(env)
…… # cascade @app.call(env) of middlewares
ActionDispatch::Routing::RouteSet.new.call(env) # @app.call(env)
Rack::Mount::RouteSet.new.call(env) # @set.call(env)
ActionDispatch::Routing::Route.new.call(env) # route.app.call(env)
ActionDispatch::Routing::RouteSet::Dispatcher.new.call(env) # @app.call
YourHomeController.action(params[:action]).call(env)
YourHomeController.middleware_stack.build(default_stack_proc).call(env)
#> default_stack_proc = Proc.new { new.dispatch(name, klass.new(env)) }
YourHomeController.new.dispatch(params[:action], request)
#> request = ActionDispatch::Request.new(env)
YourHomeController.new.send(params[:action])
YourHomeController.new.to_a
從程式碼中可以發覺 ActionDispatch::Routing::RouteSet::Dispatcher 實體物件利用 controller.action(params[:action]) 方法來取得任一個 controller 的 action dispatch stack 串接式物件(ActionDispatch::MiddlewareStack 物件),這個 stack 預設是一個 Proc 物件,能夠執行 controller 的某個 action:
## railties/lib/rails/application.rb
class Rails::Application
def call(env)
env["action_dispatch.parameter_filter"] = config.filter_parameters
app.call(env)
end
end
## actionpack/lib/action_dispatch/routing/route_set.rb
class ActionDispatch::Routing::RouteSet
def call(env)
finalize!
@set.call(env)
end
end
## rack-mount/lib/rack/mount/recognition/route_set.rb
class Rack::Mount::RouteSet
def call(env)
raise 'route set not finalized' unless @recognition_graph
env[PATH_INFO] = Utils.normalize_path(env[PATH_INFO])
request = nil
req = @request_class.new(env)
recognize(req) do |route, matches, params|
# TODO: We only want to unescape params from uri related methods
params.each { |k, v| params[k] = Utils.unescape_uri(v) if v.is_a?(String) }
if route.prefix?
env[Prefix::KEY] = matches[:path_info].to_s
end
env[@parameters_key] = params
result = route.app.call(env)
return result unless result[1][X_CASCADE] == PASS
end
request || [404, {'Content-Type' => 'text/html', 'X-Cascade' => 'pass'}, ['Not Found']]
end
end
## actionpack/lib/action_dispatch/routing/route_set.rb
class ActionDispatch::Routing::RouteSet::Dispatcher
def call(env)
params = env[PARAMETERS_KEY]
prepare_params!(params)
# Just raise undefined constant errors if a controller was specified as default.
unless controller = controller(params, @defaults.key?(:controller))
return [404, {'X-Cascade' => 'pass'}, []]
end
controller.action(params[:action]).call(env)
end
end
## actionpack/action_controller/metal.rb
class ActionController::Metal
class_attribute :middleware_stack
self.middleware_stack = ActionDispatch::MiddlewareStack.new
def self.inherited(base)
self.middleware_stack = base.middleware_stack.dup
super
end
def self.action(name, klass = ActionDispatch::Request)
middleware_stack.build do |env|
new.dispatch(name, klass.new(env))
end
end
def dispatch(name, request)
@_request = request
@_env = request.env
@_env['action_controller.instance'] = self
process(name)
to_a
end
def to_a
response ? response.to_a : [status, headers, response_body]
end
end
## actionpack/abstract_controller/base.rb
class AbstractController::Base
def process(action, *args)
@_action_name = action_name = action.to_s
unless action_name = method_for_action(action_name)
raise ActionNotFound, "The action '#{action}' could not be found"
end
@_response_body = nil
process_action(action_name, *args)
end
def process_action(method_name, *args)
send_action(method_name, *args)
end
alias send_action send
end
由於 controller 的 action 不會再傳遞 HTTP request 到其他物件,而是直接回傳標準的 rack response 陣列 [status, headers, response_body],成了所謂的 rack endpoint。最後再由 rack server 把 HTTP response 送回到使用者個瀏覽器中解讀。
以上就是 Action::Dispatch 整個 call stack 大略的樣貌。如果想要在這中間做些處理,則是安插個 middleware 進去,如 José Valim 就寫了個 devise 提供 rack 風格的 authentication 功能。