徹底了解 Rails 3 的 boot/initialization 流程對於控制 Rails 應用程式是很重要的一個課題,當我們下了 rails server 命令,究竟發生了什麼事,Bundler 和 Rack 又扮演了什麼角色,讓我們來瞧個仔細。
說明:環境變數 GEM_PATH 是 rubygems 的安裝路徑,ROOT_PATH 是應用程式根目錄,而本篇文章使用 RAILTIES_PATH 代表 railties 這個 gem 的根目錄,依此類推。引述段落則是依序指出重點檔案或指令。
在 Rails 3 應用程式根目錄下,執行 rails server 指令,第一步便是透過 boot.rb 誘發 bundler 將所有相關 rubygems 的 lib 都加到 $LOAD_PATH:
GEM_PATH/bin/rails server
RAILTIES_PATH/bin/rails
ROOT_PATH/script/rails
#> also define ENV_PATH, BOOT_PATH, APP_PATH
ROOT_PATH/config/boot.rb
#> add required gems’s lib into $LOAD_PATH
bundler 預設用 Definition.from_gemfile 解讀 ROOT_PATH/Gemfile,再以 Bundler.setup 方法呼叫 Bundle::Runtime 設定 rubygems;或是先執行 bundle lock 產生 ROOT_PATH/.bundle/environment 檔案(由 Definition.from_lock 解讀 ROOT_PATH/Gemfile.lock,再由 Bundle::Runtime 生成),當被 require 時會執行新的 Bundler.setup 方法設定 rubygems。
而 ROOT_PATH/script/rails 最後所 require 的 commands.rb 便會依序載入 rails.rb,設定基礎 Rails 環境,加載 rails/application.rb 及各式 Railtie,掛載透過 bundler 設定的 rubygems,啟動 Rails::Server 通知 Rack::Server 跑 Rails::Application:
RAILTIES_PATH/lib/rails/commands.rb
RAILTIES_PATH/lib/rails/commands/server.rb
rails_server = Rails::Server.new #=> also set ENV[“RAILS_ENV”]
ROOT_PATH/config/application.rb
RAILTIES_PATH/lib/rails/all.rb
RAILTIES_PATH/lib/rails.rb
ACTIVESUPPORT_PATH/lib/active_support.rb
……
RAILTIES_PATH/lib/rails/application.rb
……
Bundler.require :default, Rails.env
……
rails_server.start
這時 Rack::Server.start 會呼叫 .app 實體方法,先讓 Rack::Builder 以 #parse_file 類別方法解讀 config.ru,產生 Rakc::Builder 實體物件。此 Rack::Builder 物件會載入 environment.rb 來初始化 YourApp::Application,用 .run 實體方法把 YourApp::Application 這個龐大的 Rack application(可回應 call 方法傳入 env 參數並回傳 [status, header, body] 的 Ruby 實體物件/Proc 物件)也加入 Rack::Builder@ins 陣列,並回傳成 Rack::Server@app 陣列。最後 Rack::Server 透過 .wrapped_app 實體方法把 Rails::Server 的 middlewares 包裹成 @app 陣列,並實體化為 rack application,一起丟給 Rack::Handler 處理:
rails_server.start
……
wrapped_app = rails_server.build_app(@app)
#> @app = Rack::Builder.parse_file(‘config.ru’) = YourApp::Application
##> ROOT_PATH/config.ru
##> ROOT_PATH/config/environment.rb
##> YourApp::Application.initialize!
##> run YourApp::Application
#> rails_server.middleware[‘development’]
rack_app = wrapped_app = #<Rails::Rack::LogTailer:0x2f92228 @app=YourApp::Application, @cursor=612423, @file=#<File:log/development.log>, @last_checked=1270047942.86726>
……
rails_server.server.run(wrapped_app)
#> Rack::Server.new.server.run(wrapped_app)
#> Rack::Handler.default(options).run(wrapped_app)
rails_server = #<Rails::Server:0x2dafa14 @app=YourApp::Application, @_server=Rack::Handler::Mongrel, @options={:server=>nil, :AccessLog=>[], :pid=>”tmp/pids/server.pid”, :Host=>”0.0.0.0”, :debugger=>false, :daemonize=>false, :environment=>”development”, :config=>”config.ru”, :Port=>3000}>
……
Rack::Handler::Mongrel.run(rack_app)
#> http_server = Mongrel::HttpServer.new
#> handler = Rack::Handler::Mongrel.new(rack_app)
#> handler = Rack::Chunked.new(Rack::ContentLength.new(rack_app))
#> http_server.register(‘/’, handler)
……
Rack::Handler::Mongrel.process
如果跑 mongrel server 則 Rack::Handler.default 會以 Rack::Handler::Mongrel.run 方法跑一個 Mongrel::HttpServer 實體物件,並把每個 rack application(獨立的 Rack::Handler::Mongrel 實體物件)都註冊起來。
當 mongrel server 收到 http request 後,會送給 Rack::Handler::Mongrel 的 .process 實體方法,呼叫 rack application 的 .call 實體方法送出 http response。因為 Rails 3 每個 controller action、metal 都被設計成 rack application,所以創造出無限可能。
另外要提的是 Phusion Passenger 2.2.9 之後也採用類似的方法跑 Rails 3 application(主要是 require 的順序不一樣):
PhusionPassenger::Railz::ApplicationSpawner#preload_application
ROOT_PATH/config/environment.rb
ROOT_PATH/config/application.rb
ROOT_PATH/config/boot.rb
….
YourApp::Application.initialize!
run YourApp::Application
以上是 rails boot process 的概要,接著來看 rails initialization process 的部份。當 Rack Server 在解讀 config.ru 時,會產生一個這樣的 rack application 物件:
rack_app = Rack::Builder.new {
require ::File.expand_path('../config/environment', __FILE__)
run YourApp::Application
}.to_app
而 Rails::Application 初始化等於是展開 config.ru 這個 block 內容,最終會產生一個描述完整的 Rack::Builder 實體物件:
rack_app = Rack::Builder.new {
use ActionDispatch::Static
use Rack::Lock
use Rack::Runtime
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::RemoteIp
use Rack::Sendfile
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::MethodOverride
use ActionDispatch::Head
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
run YourApp::Application.routes
}.to_app
一切都從類別宣告開始。從 ROOT_PATH/config/environment.rb 把 ROOT_PATH/config/application.rb 給 require 後的宣告開始:
YourApp::Application < Rails::Application < Rails::Engine < Rails::Railtie
這 YourApp::Application 的繼承宣告主要做了幾件事,定義 called_from 變數,引入 Rails::Application::Configurable 模組,定義 #config 類別方法用來建立 Rails::Application::Configuration 實體物件,排 subclasses 順序,把自己定義成 Singleton,最後把自己實體化:
## snippet of Rails::Application
def inherited(base)
raise "You cannot have more than one Rails::Application" if Rails.application
super
Rails.application = base.instance
end
## snippet of Rails::Engine
def inherited(base)
unless base.abstract_railtie?
base.called_from = begin
# Remove the line number from backtraces making sure we don't leave anything behind
call_stack = caller.map { |p| p.split(':')[0..-2].join(':') }
File.dirname(call_stack.detect { |p| p !~ %r[railties[\w\-\.]*/lib/rails|rack[\w\-\.]*/lib/rack] })
end
end
super
end
## snippet of Rails::Railtie
def inherited(base)
unless base.abstract_railtie?
base.send(:include, self::Configurable)
subclasses << base
end
end
每種 Railtie(Application, Engine, Plugin 都是一種 Railtie 型態)的 #config 類別方法都是在管理 Rails::Railtie::Configuration(子)物件。如呼叫 Rails::Application.config 時,會以 called_from 路徑中第一個有 config.ru 的路徑做為 root 參數建立 Rails::Application::Configuration 實體物件。
## snippet of Rails::Application::Configurable
def config
@config ||= Application::Configuration.new(self.class.find_root_with_flag("config.ru", Dir.pwd))
end
## snippet of Rails::Application::Configuration
def initialize(*)
super
@allow_concurrency = false
@filter_parameters = []
@dependency_loading = true
@serve_static_assets = true
@time_zone = "UTC"
@consider_all_requests_local = true
@session_store = :cookie_store
@session_options = {}
end
## snippet of Rails::Engine::Configuration
def initialize(root=nil)
super
@root = root
end
## snippet of Rails::Railtie::Configuration
def initialize
@@options ||= {}
end
基本上各 Railtie 利用 #initializer 類別方法定義了許多初始化時要執行的 initializer block,當 YourApp::Application.initialize! 執行時,會透過 #method_missing 類別方法得到 YourApp::Application 實體物件,透過 railties.all 實體方法取得所有 Railtie 實體物件,接著取得各 initializers 實體物件,再依序執行這些 initializer block,完成整個應用程式初始化動作:
## snippet of Rails::Application
def self.method_missing(*args, &block)
instance.send(*args, &block)
end
def initialize!
run_initializers(self)
self
end
def initializers
initializers = Bootstrap.initializers_for(self)
railties.all { |r| initializers += r.initializers }
initializers += super
initializers += Finisher.initializers_for(self)
initializers
end
## Rails::Initializable
def run_initializers(*args)
return if instance_variable_defined?(:@ran)
initializers.each do |initializer|
initializer.run(*args)
end
@ran = true
end
def self.initializer(name, opts = {}, &blk)
raise ArgumentError, "A block must be passed when defining an initializer" unless blk
opts[:after] ||= initializers.last.name unless initializers.empty? || initializers.find { |i| i.name == opts[:before] }
initializers << Initializer.new(name, nil, opts, &blk)
end
而 initializers 實體物件群組執行順序是 Rails::Application::Bootstrap -> Rails::Railtie.subclasses -> Rails::Engine.subclasses -> Rails::Plugin.all -> Rails::Engine -> Rails::Application::Finisher。雖然 initializer 執行順序預設是依照宣告順序,也可用 :before, :after 參數排序,但 Railtie、Engine、Plugin 各群組內依名稱字母排序的特性,實際撰寫 Engine 或 Plugin 時還是要多留心一下:
Rails::Application::Bootstrap
ActiveSupport::Railtie
I18n::Railtie
ActionDispatch::Railtie
ActionView::Railtie
ActionController::Railtie
ActiveRecord::Railtie
ActionMailer::Railtie
ActiveResource::Railtie
Rails::TestUnitRailtie
AllKindsOfEnginesOrderedByAlphabet (Rails::Engine)
AllKindsOfPluginsOrderedByAlphabet (Rails::Plugin)
YourApp::Application (Rails::Engine)
Rails::Application::Finisher
其中 Rails::Engine 有個名為 :add_routing_paths 的 initializer 會處理各 engine 的 routes.rb 載入;另 Rails::Application::Finisher 中有個名為 :build_middleware_stack 的 initializer 會呼叫 Rails::Application 的 .app 實體方法將所有 default_middleware、Railtie 的 middlewares 或自定的 middlewares 都丟到一個 ActionDispatch::MiddlewareStack 陣列中,並建立這些 middlewares 實體物件,這都是值得留意的基本 initializer。當所有 initializers 都執行完畢,一個 Rails 3 的 rack application(@app)便建立完成,達到初始化的最終目的:
## Rails::Application def app @app ||= middleware.build(routes) end def call(env) env["action_dispatch.parameter_filter"] = config.filter_parameters app.call(env) end
接著就是搬椅子等待 Rack server 呼叫 Rails::Application 的 .call 實體方法囉。
更新:內文已更新為 rails-3.0.0.beta2 的版本為主。