読者です 読者をやめる 読者になる 読者になる

Railsのコードを読む(1)

ちょっと出来心でRailsのコードを読んでみる。

まずはクライアントからリクエストが飛んでくると、Railsアプリのドキュメントルートである/public以下の.htaccessで、

RewriteEngine On
...
RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
...

と記述されてるので、とりあえず処理がdispatch.cgiに移る。

dispatch.cgiは

#!/usr/local/bin/ruby18
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
require "dispatcher"
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
Dispatcher.dispatch

という感じになっており、cgiを経由してrubyスクリプトが起動する。で、まずはconfig/environment.rbがrequireされる。ここで、File.dirname(__FILE__)でdispatch.cgi自体の相対パスを取得している。
environment.rbはrailsの各種設定をおこなうスクリプトである。文字コードの設定(KCODE)やログの設定をどうするか、などのいろんなアプリの設定が可能。アプリの設定自体もrubyのスクリプトで行っているあたりがJava屋さんにとってはちょっと驚くところ。web.xmlJavaで書く気分だろうか。で、ここではまず真っ先にconfing/boot.rbが呼び出されている。

boot.rbまでくると、いよいよrubyコードっぽくなってきた。

RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
unless defined?(Rails::Initializer)
  if File.directory?("#{RAILS_ROOT}/vendor/rails")
    require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
  else
    require 'rubygems'
    rails_gem_version =
      if defined? RAILS_GEM_VERSION
        RAILS_GEM_VERSION
      else
        File.read("#{File.dirname(__FILE__)}/environment.rb") =~ /^[^#]*RAILS_GEM_VERSION\s+=\s+'([\d.]+)'/
        $1
      end

    if rails_gem_version
      rails_gem = Gem.cache.search('rails', "=#{rails_gem_version}.0").sort_by { |g| g.version.version }.last

      if rails_gem
        gem "rails", "=#{rails_gem.version.version}"
        require rails_gem.full_gem_path + '/lib/initializer'
      else
...
  Rails::Initializer.run(:set_load_path)
end

ここでは、まずRAILS_ROOTという変数にアプリのルートフォルダのパスを設定している。これはアプリ内でもよく使う定数。
次に"rubygems"というライブラリをrequireしている。rubyでは$LOAD_PATHという配列にライブラリを探す場所のパスが格納されており、requireが呼ばれたときはこのパスを見て、requireを要求されたライブラリがないかどうかを探す。Javaのクラスパスと同じようなもんですね。Rubyでは$LOAD_PATH配列を見ればロードパスがわかるし、インタプリタ実行時に明示的に指定してもよい。で私の環境では /usr/lib/ruby/site_ruby/1.8/rubygems.rbを発見。これが読み込まれる(内容は後述)。
次はrails_gem_versionの取得。railsフレームワーク自体のバージョン番号を取得しているわけやね。これも先ほどと同様environment.rbから取得。これはロードするんじゃなくて文字列として読み込んで正規表現でバージョンを引っ掛けている。RAILS_GEM_VERSION = ....の"="の前後にはスペース必須なのか。ふむふむ。
Railsのバージョン番号がわかったので、次はいよいよRailsのロード。Gem.cache.search(..)で、多分キャッシュされたRailsのライブラリコードを探してる。sort_byしてlastしてるのは、つまり1.8.0.0と1.8.0.1が見つかったときに、新しいほうをロードするためかな?この辺は想像。
で、次のgem関数は、なんなんだろう。rails関連のクラスを全部ロードしている、って感じなのかな。これは要調査。その後、/lib/initializerというファイルをロード。
最後に、ロードしたInitializarをrunして終了。Initializarが何をやっているのかは後日。

これでenvironment.rbに戻ると、

Rails::Initializer.run do |config|
...
end

が実行され、ロード済みのInitializerによって各種設定が読み込まれる、と。

これでrails自体の準備は完了。dispatch.rbに戻って、dispatherをロード。
dispatherが起動する前に、ADDITIONAL_LOAD_PATHSがどーたらこーたらやっているが、不明。Apache関連のなんかの設定?
そんなわけで最後にdispatherによってリクエストのディスパッチが行われ、いよいよRailsの世界へ。。。。。

いまはRailsをCGIで動かしてるので、アクセスがあるたびにgemやrailsのロードを繰り返しているのだな。gemのcacheはこのへんを考慮していたのかもしれんが、ちょっと重そうだな。

以上、出来心でRailsコードリーディングでした。たぶん続く。