Railsをブラックボックスのまま使うのは嫌だなあと思っていたところ、Railsが起動する順序を知るために最適な記事を発見。
元記事: Ruby on Rails Guides: The Rails Initialization Process
config/initializersとかcoufig.ruとかapplication.rbとかの関係/起動順序について大変勉強になりました。Railsの使い方一通り分かったけど、なんとなく動くぜヒャッハー状態がモヤモヤするRails学習者各位は一度目を通しとくとえんちゃうかと思われ。なので翻訳してみる。
手元にRailsのソースコードを置いてお読みください。僕が参考にしたバージョンは以下の通り。
- rails-3.1.0
- rack-1.3.3
- actionpack-3.1.0
- activemodel-3.1.0
- activerecord-3.1.0
- activeresource-3.1.0
- activesupport-3.1.0
バージョンの違いによるものか、元記事と異なる部分がある時は適宜訳注を入れた。あと必要ないところでも記憶に定着させるため訳注として突っ込み/感想を入れている。なお元記事のライセンスがクリエイティブ・コモンズBY-SA 3.0であるため、二次著作物の作成は自由であるが同様のライセンスで公開する必要がある。したがって当翻訳記事もクリエイティブ・コモンズBY-SA 3.0に基づくものとします。
以下、超訳。超訳ってみんな翻訳に自信ないときに防御として使ってるよねどうでもいいけど。まあ仕事じゃないしいちいち正確性求められるのもめんどくさいですしね。閑話休題、"rails server" コマンドを実行したところからスタートです。
1. Launch!
Rails3では、関連コマンドをまとめるためにscript/serverがrails serverに変わっている。
1.1 bin/rails
実際のrailsコマンドはbin/railsにある。内容はこんな感じ。
#!/usr/bin/env ruby
begin
require "rails/cli"
rescue LoadError
railties_path = File.expand_path('../../railties/lib', __FILE__)
$:.unshift(railties_path)
require "rails/cli"
end
このファイルはrails/cliをロードしようとしている。もし失敗すればrailties/libをロードパス($:)に加えて再度実行する。
(訳注: 僕の手元ではbegin/rescueの代わりにif File.exists?(File.join(File.expand_path('../..', __FILE__), '.git'))という文が挟まれていた)
1.2 railties/lib/rails/cli.rb
内容は以下。
(訳注: Bundlerで入れていれば#{Rails.root}/vendor/bundle/ruby/1.9.1/gems/railties-3.1.0/lib/rails/cli.rbにあるはず)
require 'rbconfig'
require 'rails/script_rails_loader'
# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::ScriptRailsLoader.exec_script_rails!
require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit }
if ARGV.first == 'plugin'
ARGV.shift
require 'rails/commands/plugin_new'
else
require 'rails/commands/application'
end
ここのrbconfigファイルは標準Rubyライブラリには含まれていないもので、RbConfigクラスを提供する。RbConfigクラスにはコンパイル時に展開されたRubyの情報が詰まっている。
このRbConfigクラスは(次の行でrequireされている)railties/lib/rails/script_rails_loaderの中で、以下のように利用されている。
require 'pathname'
module Rails
module ScriptRailsLoader
RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
SCRIPT_RAILS = File.join('script', 'rails')
...
end
end
rails/script_rails_loaderファイルの中では、bin_dirとruby_install_nameを統合するためにRbConfig::Configが使われている。結果として得られるのは、例えばMac OS Xの場合/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby などである。WindowsならC:/Ruby192/bin/ruby のようになる。要するに実行パスは人によって違うという話だが、ポイントは実行可能なrubyの場所を見つけ出す、というコトだ。RbConfig::CONFIG["EXEEXT"] はWindowsの拡張子".exe"を示す。この定数は後ほど exec_script_rails! で使われる。また定数SCRIPT_RAILSには、in_rails_applicatoin?メソッドを説明するときに戻ってくることになる。
rails/cliに戻ると、次の行は
Rails::ScriptRailsLoader.exec_script_rails!
となっており、このメソッドはrails/script_rails_loaderで以下のように定義されている。
def self.exec_script_rails!
cwd = Dir.pwd
return unless in_rails_application? || in_rails_application_subdirectory?
exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
Dir.chdir("..") do
# Recurse in a chdir block: if the search fails we want to be sure
# the application is generated in the original working directory.
exec_script_rails! unless cwd == Dir.pwd
end
rescue SystemCallError
# could not chdir, no problem just return
end
まず、カレントディレクトリがRailsアプリのディレクトリ、もしくはサブディレクトリに相当することを確認している。どのように判別するのだろうか?in_rails_application?メソッドを見てみよう。
def self.in_rails_application?
File.exists?(SCRIPT_RAILS)
end
さっき定義された定数SCRIPT_RAILSがここで使われている。File.exists?を使って(SCRIPT_RAILSが)カレントディレクトリに存在することをチェックする。もしこのメソッドがfalseを返せば、in_rails_application_subdirectory?メソッドが呼ばれる。
def self.in_rails_application_subdirectory?(path = Pathname.new(Dir.pwd))
File.exists?(File.join(path, SCRIPT_RAILS)) || !path.root? && in_rails_application_subdirectory?(path.parent)
end
このメソッドはscript/railsが存在するディレクトリがみつかるまでディレクトリを登って行く。ディレクトリが見つかれば次の行を実行する。
(訳注: ちょっと見失った。どこだ? => まだrails/script_rails_loader内の話だった)
exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
以上の処理は、ruby script/rails [arguments](この例では[arguments] は"server"になる)を実行しているのと同じである。
1.3 script/rails
内容は以下のようになっている。
APP_PATH = File.expand_path('../../config/application', __FILE__)
require File.expand_path('../../config/boot', __FILE__)
require 'rails/commands'
定数APP_PATHは後ほどrails/commandsで使われる。ここでscript/railsが参照しているconfig/bootとは、Railsアプリに置かれているconfig/boot.rbのことだ。このファイルはRailsアプリケーションにおいてBundlerをロード・準備する役割を持っている。
1.4 config/boot.rb
config/boot.rbの内容は次の通り。
require 'rubygems'
# Set up gems listed in the Gemfile.
gemfile = File.expand_path('../../Gemfile', __FILE__)
begin
ENV['BUNDLE_GEMFILE'] = gemfile
require 'bundler'
Bundler.setup
rescue Bundler::GemNotFound => e
STDERR.puts e.message
STDERR.puts "Try running `bundle install`."
exit!
end if File.exist?(gemfile)
標準的なRailsアプリでは、利用するライブラリを記述するためにGemfileを使う。config/boot.rbはENV["BUNDLE_GEMFILE"]の値としてこのGemfileの場所をセットして、Bundlerをrequireした後Bundler.setupを呼び出す。このBundler.setupはアプリケーションの依存ライブラリ(Railsそのものを含む)をロードパスに加え、アプリケーションから利用可能にする。
Rails3.1アプリケーションが依存するライブラリは以下の通り。
- abstract (1.0.0)
- actionmailer (3.1.0.beta)
- actionpack (3.1.0.beta)
- activemodel (3.1.0.beta)
- activerecord (3.1.0.beta)
- activeresource (3.1.0.beta)
- activesupport (3.1.0.beta)
- arel (2.0.7)
- builder (3.0.0)
- bundler (1.0.6)
- erubis (2.6.6)
- i18n (0.5.0)
- mail (2.2.12)
- mime-types (1.16)
- polyglot (0.3.1)
- rack (1.2.1)
- rack-cache (0.5.3)
- rack-mount (0.6.13)
- rack-test (0.5.6)
- rails (3.1.0.beta)
- railties (3.1.0.beta)
- rake (0.8.7)
- sqlite3-ruby (1.3.2)
- thor (0.14.6)
- treetop (1.4.9)
- tzinfo (0.3.23)
1.5 rails/commands.rb
(訳注: 実際のファイルは#{Rails.root}/vendor/bundle/ruby/1.9.1/gems/railties-3.1.0/lib/rails/commands.rbにある。commands/以下にはrails generate, console, runnnerなどの定義が書かれていておもしろい。benchmarkとかあるんや。)
config/boot.rbが終われば次はrails/commandsに移る。このファイルは("rails"に渡した)引数に応じてコマンドを実行させている。今回は単純にARGVが"server"のみを要素とする配列なので、次のような処理によって変数commandに"server"が格納される。
aliases = {
"g" => "generate",
"c" => "console",
"s" => "server",
"db" => "dbconsole",
"r" => "runner"
}
command = ARGV.shift
command = aliases[command] || command
もしserverの代わりにsを使っていれば、Railsはこのファイル内で定義されたaliasesを元に対応コマンドを探す。serverコマンドの場合Railsは次のような処理を行う。
when 'server'
# Change to the application's path if there is no config.ru file in current dir.
# This allows us to run script/rails server from other directories, but still get
# the main config.ru and properly set the tmp directory.
Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru"))
require 'rails/commands/server'
Rails::Server.new.tap { |server|
# We need to require application after the server sets environment,
# otherwise the --environment option given to the server won't propagate.
require APP_PATH
Dir.chdir(Rails.application.root)
server.start
}
もしconfig.ruが存在しなければ、rootディレクトリをAPP_PATH(ここではconfig/application.rbがセットされている)からふたつ上のディレクトリに変更する。その後rails/commands/serverをrequireするが、この中ではaction_dispatchをrequireしてRails::Serverクラスをセットアップしている。
railties-3.1.0/lib/rails/commands/server.rb(訳注: #{Rails.root}/vendor/bundle...gems/はそろそろ自明として省略)の中を覗くと、アタマの方で
require 'action_dispatch'
module Rails
class Server < ::Rack::Server
という処理をしている。このことを指しているのだろう。
1.6 actionpack/lib/action_dispatch.rb
Action DispatchはRailsのルーティングコンポーネントだ。前提としてActive Supportとactionpack/lib/action_pack.rbとRackが利用可能でなければならない。まずはactive_supportをrequireする。
1.7 activesupport/lib/active_support.rb
active_supportはrequireされてスタートする。active_support/lib/active_support/dependencies/autoload.rbは"Eagerオートローディング"の機能を持たせるため、Rubyのautoloadメソッドを再定義している。Eagerオートローディングはrequireされているすべてのクラスをロードする処理で、config.cache_classesがtrueになっているときに実行されるものだ。
requireされたactive_support/dependencies/autoloadは、さらにacctive_support/lazy_load_hooksをrequireする。
1.8 activesupport/lib/activesupport/lazyload_hooks.rb
このファイルはActiveSupport.on_load フックを定義する。このフックは、とあるコードがロードされたとき、そのコードを実行するために使われる(訳注: コメントを見るとlazy_loadによって起動が速くなるそうだ)。具体的な例はもうちょっと後で見ることになる。
lazy_load_hooks.rbはactive_support/inflector/methodsをrequireするところから始まる(訳注: 手元のファイルを見ると、lazy_load_hooks.rbの46行のコード中にrequireは1個もなく、代わりにactive_support/dependencies/autoload.rbからactive_support/inflector/methodsがrequireされているようである。バージョンの違いか。)。
1.9 activesupport/lib/active_support/inflector/methods.rb
methods.rbの中ではcamelizeやunderscore、dasherizeなどが定義されている。ActiveSupport::Inflector documentationに丁寧な説明があるので参照されたし。
このファイル(訳注: activesupport/lib/active_support.rbのことっぽい?)にはActiveSupportモジュール内に次のような行がたくさんある。
autoload :Inflector
先に見たようにautoloadメソッドを拡張しているので、Inflectorクラスが最初に参照されていれば、Rubyはactivesupport/lib/active_support/inflector.rbの中からどのようにこのファイルを参照するかを知っている。
ここでrequireされているactive_support/lib/active_support/version.rbは単に定数ActiveSupport::VERSIONを定義しており、さらにActiveSupport::VERSIONの下にはたくさんの定数が定義されている。メインはActiveSupport::VERSION::STRINGであり、こいつはActiveSupportのバージョンを文字列で返す。
active_support/lib/active_support.rbは単純にActiveSupportモジュールを定義し、いくつかのモジュールをautoloadしているファイルである。
1.10 actionpack/lib/action_dispatch.rb cont'd.
さて1.6のaction_pack/lib/action_dispatch.rbまで戻って来た。次にrequireされるのはaction_packだが、これはただActionPack::VERSIONといくつかの定数を定義するaction_pack/version.rbを呼んでいるだけで、ActiveSupportがやってたこととだいたい同じだ。
その次の行ではRailsのActiveModelパートを構成するactive_modelがrequireされている。ここでは後ほど使われるActiveModelモジュールを定義する。
最後のrequireはrack。これも先のactive_modelやactive_supportと同じく、Rackモジュールを定義していくつかの定数をautoloadする。
続けてaction_dispatch.rbでは、ActionDispatchモジュールと、 ActionDispatch内の autoloadが宣言される。
1.11 rails/commands/server.rb
(訳者補足: railties-3.1.0/lib/rails/commands/server.rbからのrequire action_dispatchがようやくおわったので)
Rack::Serverを継承してRails::Serverクラスが定義されている(メモ: Rack::Serverではなく::Rack::Serverとなっているのはなんだろう)。Rails::Server.newが呼ばれるとrails/commands/server.rbのinitializeメソッドを呼び出す。
def initialize(*)
super
set_environment
end
んでもって、superでRack::Serverのinitializeメソッドが呼ばれる。
1.12 Rack: lib/rack/server.rb
Rack::ServerはRackベースのサーバインターフェイスを提供する。いまのRailsもRackベースのアプリケーションのひとつだ。
Rack::Serverのinitializeメソッドは単純に定数をセットしているだけ。
def initialize(options = nil)
@options = options
@app = options[:app] if options && options[:app]
end
今回はoptionsがnilなので何も起こらない。
Rack::Serverのsuperが終わればrails/commands/server.rbに戻る。この時点で、set_environmentがRails::Serverオブジェクトのコンテキストで(?)呼ばれる。このメソッドは一見大したことをしていないように見える。
(訳注: ここは手元で見ながらやるといいと思うのでvendor/bundle/ruby/1.9.1/gems/rack-1.3.5/lib/rack/server.rbをご用意ください)
def set_environment
ENV["RAILS_ENV"] ||= options[:environment]
end
しかし実のところ、optionsメソッドがいろいろやってる。定義はRack::Serverの中にある。
def options
@options ||= parse_options(ARGV)
end
さらにparse_optionsメソッド。
def parse_options(args)
options = default_options
# Don't evaluate CGI ISINDEX parameters.
# http://hoohoo.ncsa.uiuc.edu/cgi/cl.html
args.clear if ENV.include?("REQUEST_METHOD")
options.merge! opt_parser.parse! args
options[:config] = ::File.expand_path(options[:config])
ENV["RACK_ENV"] = options[:environment]
options
end
そしてdefault_options。
def default_options
{
:environment => ENV['RACK_ENV'] || "development",
:pid => nil,
:Port => 9292,
:Host => "0.0.0.0",
:AccessLog => [],
:config => "config.ru"
}
end
(メモ: ここでparse_optionsメソッドに視点が戻る)REQUEST_METHODキーがENVに含まれていないのでargs.clear行はスキップする。次の行ではRack::Serverで定義されるopt_parserでパースした項目をoptionsにマージしている。
def opt_parser
Options.new
end
ここで出てくるOptionsクラスはRack::Serverの中で定義されているのだが、違う引数を使うためにRails::Serverでオーバーライドされる。(視点はRails::Serverに戻って、railties-3.1.0/lib/rails/commands/server.rb中の)parse!メソッド冒頭は次のようになっている。
def parse!(args)
args, options = args.dup, {}
opt_parser = OptionParser.new do |opts|
opts.banner = "Usage: rails server [mongrel, thin, etc] [options]"
opts.on("-p", "--port=port", Integer,
"Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v }
...
このメソッドは、Railsがサーバ起動方法を定義するいくつかのkeyをセットしているのがわかる(メモ: rackの9292ポートから、Railsおなじみ3000番ポートになってますね)。
initializeが終わると、startメソッドがサーバを起動する。
1.13 Rails::Server#start
Rails::Server#startメソッドは次のように定義されている。
def start
puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
puts "=> Rails #{Rails.version} application starting in #{Rails.env} on http://#{options[:Host]}:#{options[:Port]}"
puts "=> Call with -d to detach" unless options[:daemonize]
trap(:INT) { exit }
puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
#Create required tmp directories if not found
%w(cache pids sessions sockets).each do |dir_to_make|
FileUtils.mkdir_p(Rails.root.join('tmp', dir_to_make))
end
super
ensure
# The '-h' option calls exit before @options is set.
# If we call 'options' with it unset, we get double help banners.
puts 'Exiting' unless @options && options[:daemonize]
end
(訳注: 手元のソースではdef start行直後にSSL判定してurlをhttpsにするかhttpにするかを決める処理が入っていた。)
ここにきてようやくRails初期化プロセス最初の出力がでてくる。このメソッドはINTシグナルをトラップしているので、CTRL-Cを押すとプロセスをexitする。コードからわかるが、superする前にtmp/cache, tmp/pids, tmp/sessions, tmp/socketsディレクトリを(なければ)作成している。
superメソッドはRack::Server.startを呼び出す。
def start
if options[:warn]
$-w = true
end
if includes = options[:include]
$LOAD_PATH.unshift(*includes)
end
if library = options[:require]
require library
end
if options[:debug]
$DEBUG = true
require 'pp'
p options[:server]
pp wrapped_app
pp app
end
end
Railsアプリではこれらのoptionsはひとつも使われない。実行される最初のコードは
wrapped_app
である。
(訳注: あれ?def startの定義を見るとoptionsが空なら何も呼ばれずstartが終わるんじゃないか? => ソースを見ると、上のコードはendを一つ多く書いているようだ。実際はstartメソッドはまだ続き、if options[:debug] ... endのあと、def start直下にてwrapped_appが呼ばれる)
このメソッドは別のメソッドを呼び出す。
@wrapped_app ||= build_app app
appメソッドの定義は次の通り。
def app
@app ||= begin
if !::File.exist? options[:config]
abort "configuration #{options[:config]} not found"
end
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
self.options.merge! options
app
end
end
ここでoptions[:config]はconfig.ruを指す。config.ruの内容は次のようになっている。
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
run YourApp::Application
Rack::Builder.parse_fileメソッドはこのconfig.ruファイルの内容を取得し、
app = eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app",
TOPLEVEL_BINDING, config
というコードで内容をパースする。initializeメソッドはブロックを受け取り、Rack::Builderのインスタンス内で(within an instance of Rack::Builder)それを実行する。(訳注: instance_eval(&block) if block_given?という処理を行っていた)
そして、この部分でRails初期化プロセスの大部分が行われている。次のシンプルな1行から始まるイベントの連鎖が本記事の焦点となるのである。まず最初に実行されるのは、config.ru内でconfig/environment.rbをrequireしている部分である。
require ::File.expand_path('../config/environment', __FILE__)
1.14 config/environment.rb
このファイルはconfig.ru(rails server)とPassengerから共通して呼び出される。つまり、ここで2つのサーバ起動方法が合流することになる。ここまでの処理はすべてRackとRailsのセットアップである。
(訳注: 実はPassengerで動かすならRackサーバ起動ファイルconfig.ruは不要なのだろうか)
最初の1行はconfig/application.rbのrequireである。
1.15 config/application.rb
まずconfig/boot.rbをrequireしているが、それはconfig/boot.rbがまだrequireされていない場合に限られる(訳注: boot.rb内のrequire 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])のことを言っているのだろうか?)。具体的に言うと、rails serverの場合requireされるが、Passengerの場合はされない。
さあ盛り上がってまいりました。
2. Loading Rails
config/application.rbの次の行。
require 'rails/all'
2.1 railties/lib/rails/all.rb
all.rbは、Railsを構成する各種ライブラリをrequireする役割を持っている。(訳注: 手元の3.1.0ではsprocketsも含まれてた。)
require "rails"
%w(
active_record
action_controller
action_mailer
active_resource
rails/test_unit
).each do |framework|
begin
require "#{framework}/railtie"
rescue LoadError
end
end
1行目はrails自身のrequireだ。
2.2 railties/lib/rails.rb
rails.rbが担当するのはRailsモジュールの初期値定義と、ActiveSupportやActionDispatchなどの自動ロード設定であり、実質的にrailsの機能を定義している。Rails3のrootやenv、applicationといったメソッドは非常に便利だ。
いずれにせよ、これらすべてに先立ってrails/ruby_version_checkが一番最初にrequireされるので、こいつを見ていこう。
2.3 railties/lib/rails/rubyversioncheck.rb
これは単純にRubyのバージョンが1.8.7未満、もしくは1.9.1に当たるかどうかを調べ、該当する場合はエラーを出す。単純に、Rails3は1.8.7未満もしくは1.9.1では動かないためだ。
※つねにRuby, Railsは最新のバージョンを使うように努めるべきだ。セキュリティ向上やスピードアップなどのメリットは数知れない。気をつけるべきなのは最新バージョンにアップグレードするとコードが動かなくなることがある点だが、それを怖れてアップグレードしないよりも最新バージョンで動くように修正する方がベターである。
2.4 activesupport/coreext/kernel/reporting.rb
これ以降のRails処理でいくつも使われることになるActive Support "core extensions"の最初のファイルがこれである。Kernelモジュールにメソッドを定義する。KernelモジュールはObjectクラスにmixinされ、次のようにmain部分で使えるようになる。
silence_warnings do
# some code
end
これらのメソッドはSTDERRを出力しないように制御しており、silence_streamによって他のストリームでも同様に出力を握りつぶすことが出来る。加えてこのmixinによって例外を抑制してその出力を捕捉することもできるようになる。
詳細はActive Support Core Extensions GuideのSilencing Warnings, Streams, and Exceptionsを参照のこと。
2.5 activesupport/coreext/logger.rb
引き続き、railties/lib/rails.rbのrequire行を追って行く。次はLoggerクラス。around_[level]ヘルパーや、loggerオブジェクトと紐付いたformatterのdatetime_formatゲッター/セッターを定義。
詳細は同じくActive Support Core Extensions GuideのExtensions to Loggerを参照のこと。
2.6 railties/lib/rails/application.rb ★
railties/lib/rails.rbが次にrequireするのはapplication.rb。標準的なRailsアプリではconfig/application.rbでアプリケーションクラスが定義されるが、そのクラスが依存するRails::Applicationがここで定義されている。が、実際はRails::Applicationクラスが定義される前にいくつかのファイルがrequireされている。
最初にrequireされるのはactive_support/core_ext/hash/reverse_mergeである。詳細はread about in the Active Support Core Extensions guideの"Merging"セクションを参照のこと。
2.7 activesupport/fileupdate_checker
ActiveSupport::FileUpdateChecherは前回チェック時からのファイル更新の有無をチェックするクラス。development環境でroutesファイルの変更を検知する用途で使われる。
2.8 railties/lib/rails/plugin.rb
Rails::Engineを継承したRails::Pluginを定義する。Rails::Pluginは、Rails::EngineやRails::Railtieとは異なり、こいつを継承して使うわけではない(訳注: 継承して使われることを防止するため、self.inheritedというメソッドの中で例外をraiseしている。へぇ。)。
2.9 railties/lib/rails/engine.rb ★
Rails::Railtiesを継承したRails::Engineクラスを定義する。routesやconfigといった標準的なアプリケーションの機能が定義されているのは、このRails::Engineである。
説明はRails::EngineのAPIドキュメントに詳しい。
このファイルはまず最初にrails/railties.rbをrequireする。
2.10 railties/lib/rails/railtie.rb ★
rails/railtie.rbファイルは現行バージョンのRailsにおいて、あらゆる関連の基礎となるRails::Railtieを定義する。Gemの中でも独自のinitializerやrakeタスク、フックなどを持つものは、Rails::Railtieを継承したGemName::Railtieを定義しておかなければならない。
詳しい説明はRails::Engineと同じようにRails::RailtieのAPIドキュメントに記載されている。
このファイルで最初にrequireするのは、rails/initializable.rbだ。
2.11 railties/lib/rails/initializable.rb
さて、ようやく深いrequire階層の最下層、rails/initializable.rbまで降りてきた。Ruby標準ライブラリのtsortを除けばひとつもRailsファイルをrequireしていない。
ここでは、Initializerクラス(これはすべてのRails初期化プロセスの基礎となる)を含むRails::Initializableモジュールが定義されている。また同モジュールはClassMethodsクラスも含んでおり、ClassMethodsはすべてのrequire終了時にRails::Railtieクラスにincludeされる。
2.12 railties/lib/rails/configuration.rb
Rails::Configurationモジュールの定義。GeneratorsクラスとMiddlewareStackProxyクラスも定義されている。MiddlewareStackProxyクラスの用途はアプリケーションで利用されるミドルウェアの管理(少し後に例がある)で、Generatorsクラスが提供するのは、config.generatorsオプションによって利用するgeneratorを設定する機能である。
最初にrequireされるのはactive_support/deprecationだ。
2.13 activesupport/lib/active_support/deprecation.rb
このdeprecation.rbとdeprecation.rbがrequireするファイル群は、基本的なdeprecation(廃止予定,非推奨)の警告を定義する。deprecation.rbはActiveSupport::Deprecationモジュール内におけるdeprecation_horizon, silenced, debugのデフォルト値を定義している。requireされているファイル群は次の通り。
- active_support/deprecation/behaviors
- active_support/deprecation/reporting
- activesupport/deprecation/methodwrappers
- activesupport/deprecation/proxywrappers
2.14 activesupport/lib/active_support/deprecation/behaviors.rb
DEFAULT_BEHAVIORSというハッシュ定数を通じてActiveSupport::Deprecationモジュールのふるまい(behavior)を定義するファイル。deprecation警告を出力する際における:stderr, :log, :notifyの3種のデフォルト値を含む。activesupport/notificationsとactivesupport/core_ext/array/wrapをrequireするところから始まる。
2.15 activesupport/lib/active_support/notifications.rb
ActiveSupport::Notificationsモジュールを定義。NotificationsはRubyの計測APIを提供するキューの実装で、イベントをログサブスクライバに通知する(?)。
ActiveSupport::NotificationsのAPIドキュメントにモジュールの使い方が載っている。
また、active_support/notifications.rbでrequireされているactive_support/core_ext/module/delegationについてはActive Support Core Extensions Guide.に詳しい。
2.16 activesupport/core_ext/array/wrap
これもcore extensionのひとつで、Active Support Core Extensions guideを見れば載っている。
2.17 activesupport/lib/active_support/deprecation/reporting.rb
ActiveSupport::Deprecationのwarn, silenceメソッドなどを定義する。
2.18 activesupport/lib/activesupport/deprecation/methodwrappers.rb
deprecate_methodsメソッドを定義する。これはmodule/deprecation(ファイル1行目でrequireされている)というcore extensionで最初に使われる。
また、requireされている他のcore extensionsはmodule/aliasingとarray/extract_optionsである。
2.19 activesupport/lib/activesupport/deprecation/proxywrappers.rb
proxy_wrappers.rbはメソッド,インスタンス変数や定数に対するdeprecation(非推奨)ラッパーを定義する。3.0以前はRAILS_ENVやRAILS_ROOTといった定数が使われていたが現在では削除されている。以下のようになdeprecation(非推奨)メッセージが出力される。
BadConstant is deprecated! Use GoodConstant instead.
2.20 activesupport/orderedoptions
次に rails/configuration.rbがrequireするのはActiveSupport::OrderedOptions、これはconfig.active_supportなどのオプションに利用されるクラスだ。
続いてrequireされるactive_support/core_ext/hash/deep_dupについてはActive Support Core Extensions guideで詳しく扱っている。
そして、その次にrequireされるのはrails/pathsファイルである。
2.21 railties/lib/rails/paths.rb ★
Rails::Pathsモジュールを定義する。このモジュールによってRailsアプリケーション、Railsエンジンのパスを設定することが可能になる。
(訳注: 使用例 => Rails3.1でroutes.rbファイルを分割する #Rails #Ruby - Qiita)
当記事の後半で、Rails初期化プロセスの課程でrails/paths.rbによってデフォルトパスがセットされ、そのうちのいくつかはeager loadで再設定される様子を見ていく。
2.22 railties/lib/rails/rack.rb
railties/lib/rails/configuration.rbでrequireされる最後のファイルはrails/rackである。ここではいくつかのシンプルなautoloadsが定義されている。
module Rails
module Rack
autoload :Debugger, "rails/rack/debugger"
autoload :Logger, "rails/rack/logger"
autoload :LogTailer, "rails/rack/log_tailer"
autoload :Static, "rails/rack/static"
end
end
ここまでロードが終わればRails::Configurationクラスが初期化される。以上でrailties/lib/rails/configuration.rbのロードが終了し、railties/lib/rails/railtie.rbに戻る。そこでは、次にactive_support/inflectorがロードされている。
2.23 activesupport/lib/active_support/inflector.rb
activesupport/lib/active_support/inflector.rbは、単語を複数形/単数形に相互変換するために使われる一連のファイルをrequireする。
require 'active_support/inflector/inflections'
require 'active_support/inflector/transliterate'
require 'active_support/inflector/methods'
require 'active_support/inflections'
require 'active_support/core_ext/string/inflections'
active_support/inflector/methods は既にactive_support/autoloadでrequireされているのでここで再ロードされることはない。activesupport/lib/active_support/inflector/inflections.rbはそのactive_support/inflector/methods内でrequire済である。
2.24 active_support/inflections
このファイルは、現時点ではロードされていない定数ActiveSupport::Inflectorを参照しているが、activesupport/lib/active_support.rb内にあったいくつかのautoloadがActiveSupport::Inflectorをロードするので問題ない。
で、肝心の中身は、Railsで使われる複数形/単数形ルールを定義している。Railsが"tomato"の複数形が"tomatoes"だと知っているのはこいつのおかげだ。
2.25 activesupport/lib/active_support/inflector/transliterate.rb
このファイル内ではtransliterateおよびparameterizeメソッドが定義されている。これらメソッドのドキュメントは読む価値がある。
2.26 railties/lib/rails/railtie.rbに戻る
inflectorファイルをロードし終われば、Rails::Railtieクラスが定義される。このクラスはinitializable(実際はRails::Initializable)と呼ばれるmoduleをincludeしている。Initializableモジュールはinitializerというメソッドを含んでおり、後にinitializersをセットアップする際に使われる。
2.27 railties/lib/rails/initializable.rb ★
このファイル内のRails::Initializableモジュールがincludeされると、includeしたクラスを拡張する。具体的にはClassMethodsモジュールを(include元クラスに)extendさせる。ClassMethodsモジュールはinitializerメソッドを定義し、このメソッドはrailties全体を通じてinitializersを定義するために使われる。
このファイルを以てrailties/lib/rails/railtie.rbのロードは完了する。戻る先はrails/engine.rbである。
2.28 railties/lib/rails/engine.rb
rails/engine.rb内で次にrequireされるのはactive_support/core_ext/module/delegationである(ドキュメントはActive Support Core Extensions Guideにある)。
続く2ファイルはRuby標準ライブラリであるpathnameとrbconfigで、rails/engine/railtiesがそれに続く。
2.29 railties/lib/rails/engine/railties.rb
Rails::Engine::Railtiesクラスを定義する。中ではenginesおよびrailtiesというメソッドが定義されており、rakeタスクや、その他のengines/railties機能を定義する際に使われる。
2.30 railties/lib/rails/engine.rbに戻る
rails/engine/railties.rbのロードが終了すると(たとえばクラスが継承されたときに呼ばれるinheritedメソッドのような)Rails::Engineクラス基本機能の定義は完了する。
railties/lib/rails/plugin.rbファイルに戻ろう。
2.31 railties/lib/rails/plugin.rbに戻る
次にrequireされるのはarray/conversionsと呼ばれるActive Supportのcore extensionで、詳しくはActive Support Core Extensions Guideのこのセクションで扱っている。
ロードが終わればRails::Pluginクラスの定義も完了だ。
2.32 railties/lib/rails/application.rbに戻るってばよ
rails/application.rbに戻って来た。ここでRails::Applicationクラスが定義され、Railsアプリはこのクラスを継承することになる。Rails::Applicationクラス(と、そのスーパークラス)はアプリケーションの設定を行うときに使うconfigメソッドなど、Railsの基本的な動きを定義している。
ここが終わればrailties/lib/rails.rbに戻る。次の行はrails/versionだ。
2.33 railties/lib/rails/version.rb
active_support/versionのように、文字列の定数を持つVERSIONを定義し、現在のRailsのバージョンを返させる。
ここが終わればrailties/lib/rails.rbに戻り、続いてrequireされるactive_support/railtie.rbを見ていこう。
2.34 activesupport/lib/active_support/railtie.rb
active_supportとrailsをrequireしているが、既にrequire済なので意味はない。3行目にはactive_support/i18n_railtie.rbのrequireが来る。
2.35 activesupport/lib/active_support/i18n_railtie.rb
ここに来て初めて設定らしい設定を行う。
class Railtie < Rails::Railtie
config.i18n = ActiveSupport::OrderedOptions.new
config.i18n.railties_load_path = []
config.i18n.load_path = []
config.i18n.fallbacks = ActiveSupport::OrderedOptions.new
Rails::Railtieクラスを継承することで、Rails::Railtie#inheritedメソッドが呼ばれる(感想: Rubyすげー)。
def inherited(base)
unless base.abstract_railtie?
base.send(:include, Railtie::Configurable)
subclasses << base
end
end
まず1行目では継承先のRailtieがRailsそのものの構成要素であるかどうかをチェックしている。
ABSTRACT_RAILTIES = %w(Rails::Railtie Rails::Plugin Rails::Engine Rails::Application)
...
def abstract_railtie?
ABSTRACT_RAILTIES.include?(name)
end
I18n::Railtieはリストにないのでabstract_railtie?はfalseを返す。Railtie::Configurableモジュールは晴れてincludeされ、subclassesメソッドが呼ばれて空の配列にI18n::Railtieが加えられる。
def subclasses
@subclasses ||= []
end
I18n::Railtieの冒頭で利用されているconfigメソッドはRails::Railtieで以下のように定義されている。
def config
@config ||= Railtie::Configuration.new
end
この時点で自動的にRailtie::Configurationがロードされ、rails/railties/configurationファイルが読み込まれる。railties/lib/rails/railtie.rbに記述された次の行がそのカラクリである。
autoload :Configuration, "rails/railtie/configuration"
2.36 railties/lib/rails/railtie/configuration.rb
rails/configurationのrequireから始まるが,既にロードされているため無視される。
Rails::Railtie::Configurationはrailtieの設定を容易にするためのクラスだ。上のconfigメソッド内でRailtie::Configuration.newが実行された時に、このクラスのinitializeメソッドが実行される。configオブジェクトをレシーバとしてメソッドが実行された時、メソッドが存在しなければ、configuration.rbで定義されたmethod_missingがそいつをrescueする。
def method_missing(name, *args, &blk)
if name.to_s =~ /=$/
@@options[$`.to_sym] = args.first
elsif @@options.key?(name)
@@options[name]
else
super
end
end
したがってオプションがセッターの文脈で使われれば値を保存し、ゲッターの文脈であれば値を返す。それだけだ。奇跡も、魔法も、ないんだよ。
2.37 activesupport/lib/active_support/i18n_railtie.rbに戻る
configurationメソッドの次はreloaderメソッドの定義があり、そして最初のRailties initializerであるi18n.callbacksの定義がこれに続く。
initializer "i18n.callbacks" do
ActionDispatch::Reloader.to_prepare do
I18n::Railtie.reloader.execute_if_updated
end
end
Rails::Initializableモジュール由来であるこのinitializerメソッドは、ここではblockを実行せず、後で実行するために格納する。
def 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
initializerは、他のinitializerの前(もしくは後)に実行するように設定することができる。実例は初期化プロセスでたくさん出てくる。
ちなみにRails::Railtieを継承したクラスもinitializerメソッドを使えるのだが、これについては Configurationguideを参考にして欲しい。
ここでInitializerクラスはRails::Initializableモジュール内で定義されており、inigializeメソッドは単にいくつかの変数を準備するだけの役割を果たす。
def initialize(name, context, options, &block)
@name, @context, @options, @block = name, context, options, block
end
initializeメソッドが終了すれば、initializersメソッドが実行される。
def initializers
@initializers ||= self.class.initializers_for(self)
end
@initializersが未定義であれば(現時点では定義されようがないが)initializers_forメソッドがselfのクラスに対して呼ばれる。
def initializers_for(binding)
Collection.new(initializers_chain.map { |i| i.bind(binding) })
end
railties/lib/rails/initializable.rb内のCollectionクラスはArrayを継承し、TSortモジュール(initializersを置かれた順にソートするために使う)をincludeしている。
initializers_forで呼ばれるinitializers_chainメソッドは次のように定義されている。
def initializers_chain
initializers = Collection.new
ancestors.reverse_each do | klass |
next unless klass.respond_to?(:initializers)
initializers = initializers + klass.initializers
end
initializers
end
このメソッドは先祖を辿ってinitializersを収集し、新しいCollectionオブジェクトに格納する。ここで使われる"+"メソッドはCollectionクラスで次のように再定義されている。
def +(other)
Collection.new(to_a + other.to_a)
end
「既存collectionとナニカをArrayにしてArray#+メソッドで結合した配列」を引数に取り、新しいcollectionオブジェクトを生成して返している。現時点ではi18n.callbacksが唯一のinitializerなので、Collectionオブジェクトにはこいつしか含まれない。
次に実行されるメソッドはafter_initializeで、定義は次の通り。
def after_initialize(&block)
ActiveSupport.on_load(:after_initialize, :yield => true, &block)
end
中にあるon_loadメソッドはactive_support/lazy_load_hooksから来ている。
def self.on_load(name, options = {}, &block)
if base = @loaded[name]
execute_hook(base, options, block)
else
@load_hooks[name] << [block, options]
end
end
@loaded変数は読み込み済RailsコンポーネントからなるHashで、いまのところカラである。そのためelse以下が実行されるわけだが、@load_hooksはactive_support/lazy_load_hooksの定義によれば
@load_hooks = Hash.new {|h,k| h[k] = [] }
この通り、keyに対するデフォルト値がカラ配列であるHashを新規作成している。こうして定義しておけば、Railsは次のように@load_hooksを利用することが出来る。
@load_hooks[name] = []
@load_hooks[name] << [block, options]
@load_hooks[name]の配列に追加されたブロックとオプションのペアはafter_initializeに渡される。@load_hooksは初期化プロセスの後ろの方で使われるので後で見ることになる。
さて、あとi18n_railtie.rbの中には、protectedなクラスメソッドであるinclude_fallback_modules、init_fallbacks、そしてvalidate_fallbacksの定義が残っている。
2.38 activesupport/lib/active_support/railtie.rbに戻る
I18n::RailtieのようにActiveSupport::Railtieを定義するファイルはRails::Railtieを継承していて、inheritedメソッドが呼ばれてRails::Configurableがincludeされる。このクラスによって再びRails::Railtieのconfigメソッドが使われ、Active Support向けの設定項目が準備される。
Railtieはさらに以下3つのinitializerの準備も行う。
active_support.initialize_whiny_nilsactive_support.deprecation_behavioractive_support.initialize_time_zone
それぞれのinitializerが何をしているのか、あとで覗いてみるとしよう。
active_support/railtieのロードが終われば、railties/lib/rails.rbが次にrequireするのはaction_dispatch/railtieだ。
2.39 activesupport/lib/action_dispatch/railtie.rb
ここでActionDispatch::Railtieクラスを定義しているのだが、まずはaction_dispatchのrequireが先だ。
2.40 activesupport/lib/action_dispatch.rb
まず、ディレクトリをさかのぼってactive_supportとactive_modelを探し、それらのlibディレクトリをロードパスに加える(すでに含まれていればスキップ)。
activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__)
$:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include?(activemodel_path)
やってることは単にactivesupport_pathとactivemodel_pathを定義しているだけ。
次の2行は既にrequire済みなので実行されない。
require 'active_support'
require 'active_support/dependencies/autoload'
続いてrequireされているのはaction_pack(activesupport/lib/action_pack.rb)で、こいつは22行のコピーライト表示の後にぽつんと1行action_pack/versionをrequireしている。他のversion.rbと同様にActionPack::VERSION定数を定義するためのものだ。
module ActionPack
module VERSION #:nodoc:
MAJOR = 3
MINOR = 1
TINY = 0
PRE = "beta"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end
end
action_packが終わったので、次はactive_modelのrequireに移ろう。
2.41 activemodel/lib/active_model.rb
ここも同じ。active_model/versionをrequireして、次のようにActive Modelのバージョンを定義している。
module ActiveModel
module VERSION #:nodoc:
MAJOR = 3
MINOR = 1
TINY = 0
PRE = "beta"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end
end
version.rbのロードが完了すればActiveModelモジュールが定義され、その中でautoload群と、ActiveModel::Serializersと呼ばれるサブモジュール(内部に自分自身のautoloadを持つ)が読み込まれる。ActiveModelモジュールが終了した後、active_support/i18nファイルがrequireされる。
2.42 activesupport/lib/active_support/i18n.rb
ここにきて初めてi18nのgemがrequireされ、設定が行われる。
begin
require 'i18n'
require 'active_support/lazy_load_hooks'
rescue LoadError => e
$stderr.puts "You don't have i18n installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml"
最初i18n_railtieによって定義されたI18nモジュールはi18nのgemで拡張される。逆ではないことに注意。この方法に特に悪影響はないので心配は要らない。
ここでもactive_support/lazy_load_hooksがrequireされているが、もうrequire済なので無視。
i18nがうまくロードされなかった場合、ユーザに対してその旨のエラーと、明示的にGemfileに加えることを推奨するメッセージが表示される。まあ標準的なRailsアプリではこのgemはちゃんとロードされるから心配要らない。
ロードが終わればI18n.load_pathにactivesupport/lib/active_support/locale/en.ymlが追加される。このen.ymlは初期化プロセスでtranslationsがロードされたときに利用されることになるファイルの一つ。
これでactive_modelのロードは終わりなので、action_dispatchに戻る。
2.43 activesupport/lib/action_dispatch.rbに戻る
残るrequire対象ファイルはrack。以上。で、その下にはRack、ActionDispatch、ActionDispatch::Http、ActionDispatch::Sessionに対するautoload定義がずらずらと並ぶ。ここで使われているautoload_unerという新顔メソッドは、単に各モジュールの呼び出し元ファイルに指定したパスを追加するものだ。こんな感じで使う。
autoload_under 'testing' do
autoload :Assertions
...
こうすれば、ここでautoloadするAssertionsモジュールはaction_dispatch直下ではなくaction_dispatch/testingフォルダ以下にあるものを指すようになる。
最後にトップレベルのスコープでMimeをautoloadしておしまい。
2.44 activesupport/lib/action_dispatch/railtie.rbに戻る
action_dispatchをrequireした後はActionDispatch::Railtieクラスが定義されている。これもRails::Railtieを継承した仲間のひとつだ。config.action_dispatchに対していくつか初期値を設定し、action_dispatch.configureというinitializerを作成している。
action_dispatch/railtieが終わったのでrailties/lib/rails.rbに戻ろう。
2.45 railties/lib/rails.rbに戻る
以上でActive SupportとAction Dispatchのロードが終了。デフォルトエンコーディングをUTF-8に指定したのち、最後にRailsモジュールの初期化に入る。このモジュール内ではRails.loggerやRails.application、Rails.env、Rails.rootといった便利なメソッドが定義される。
2.46 railties/lib/rails/all.rbに戻る
rails.rbのrequireも終わったので、残るrailtiesが次にロードされる。まずはactive_record/railtieからだ。
2.47 activerecord/lib/active_record/railtie.rb
ActiveRecord::Railtieクラスの定義に先立っていくつかのファイル、最初はactive_recordがrequireされる。
2.48 activerecord/lib/active_record.rb
active_supportとactive_modelがロードバスに含まれない場合はlibディレクトリを見つけて追加するところからスタートする。が、action_dispatch.rbを思い出してほしい。もうロードパスに入っているはずだ。
requireされる先頭3ファイルは既に読み込み済。4番目の"arel"はArelというgemに含まれるファイルで、Arelモジュールを定義しているものだ。
require 'active_support'
require 'active_support/i18n'
require 'active_model'
require 'arel'
5個目にrequireされるactive_record/versionはActiveRecord::VERSIONを定義する。
module ActiveRecord
module VERSION #:nodoc:
MAJOR = 3
MINOR = 1
TINY = 0
PRE = "beta"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end
end
以上のrequireが終了すれば、autoload機能によってActiveRecord基板部分もロードされる。
ファイル終了近くにあるこの行は、
ActiveSupport.on_load(:active_record) do
Arel::Table.engine = self
end
Arel::TableのエンジンをActiveRecord::Baseに置き換える処理だ。
そして以下の行でファイルは終了する。
I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml'
翻訳データをactiverecord/lib/active_record/locale/en.ymlからI18nのロードパスに読み込んでいる。読み込みが終了すれば、ファイルのパース処理が実行される。
2.49 activerecord/lib/active_record/railtie.rbに戻る
続いてrequireされる2ファイルはrequire済なのでスキップ。railsはrails/allでrequireされているし、active_model/railtieはaction_dispatchからrequireされていた。
require "rails"
require "active_model/railtie"
そんなわけで、次にrequireされるのはaction_controller/railtieとなる。
2.50 actionpack/lib/action_controller/railtie.rb
このファイルもrequire群からスタートするが、以下ファイルはもうrequireされている。
require "rails"
require "action_controller"
require "action_dispatch/railtie"
ただし次のaction_view/railtieは初見だ。このファイルを見てみると、action_viewのrequireから始まる。
2.51 actionpack/lib/action_view.rb
action_view.rb
↑以上。(訳注: なんか唐突に終了した)
感想・おまけなど
合わせて読みたいRailsソースコード
-
activesupport-3.1.0/lib/active_support/deprecation/method_wrappers.rb- こういうメタ機能でRailsにはどんどんメソッドが自動定義されるのだな。
-
actionpack-3.1.0/lib/action_dispatch/routing/*- もやもやするルーティングもソースを読めばいいじゃない
いろいろ
ActiveSupport::Inflector#parameterize が素敵
-
FileUpdateCheckerでupdate_atを取る実装
paths.map { |path| File.stat(path).mtime }.max
-
rails本体はものすごく小さいスクリプトで、railtiesと
active*シリーズが従来の"Rails"フレームワークを構成しているのか。- Rails2から大きく進化した部分はRails自体のModule化だと聞いていたが、これは確かにカッコイイ設計に思える。
1.11で見るように
Rails::ServerはRackに環境という色づけをした拡張。部分的にRailsGuidesを和訳しているwikiがあったけど、去年の今頃から更新されてないのかな。
-
railties-3.1.0/lib/rails.rbを見るとRails.envってRAILS_ENV || RACK_ENV || "development"みたいな感じで最悪developmentが返るようにできている。- 使うべきはRails.envか
ENV['RAILS_ENV']か、という議論が社内で起こったことがあるが、Rails.envの方がよさそう。
- 使うべきはRails.envか
図示したい。WEB+DB PRESSのRails3特集で簡単な関係は図示されていたけどもうちょっと細かいヤツ。
次はrails newを使わず手作業で最小限のRailsアプリを組み立てることで理解を深めるのもいいかもしれない

