はじめに

Ruby on Railsでは通常以下のようにアプリケーションを作成・実行します。なお、以下の例は私が実験として作った家計簿アプリケーションを用います。

  1. $ rails accountbook
  2. $ ruby script/generate model Outgo
  3. $ ruby script/generate controller accountbook
  4. app/controllers/accountbook_controller.rbに「scaffold :outgo」追加
  5. $ ruby script/server

これで、http://localhost:3000/accountbook/にアクセスするとoutgosテーブルが表示されます。それではサーバの起動処理を見ていくことにしましょう。

なお、今回対象としたバージョンは1.2.3です。

<アプリケーションディレクトリ>/config/boot.rb

ではサーバを起動するためのscript/serverを見てみましょう。以下の3行だけです。

#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/server'

というわけでconfig/boot.rbに移ります。

まずRAILS_ROOTとしてアプリケーションのルートディレクトリを設定しています。

次に、デフォルトではvendor/railsはないのでRubyGems経由でrailsをロードすることになります。また、config/environment.rbにRAILS_GEM_VERSIONが書かれているのでバージョン限定でrailsをロードすることになります。

最後に以下の一行が実行されています。

Rails::Initializer.run(:set_load_path)

Rails::Initializer($GEM_HOME/gems/rails/lib/initializer.rb)

Ruby::Initializer.runメソッドを見てみましょう。

def self.run(command = :process, configuration = Configuration.new)
  yield configuration if block_given?
  initializer = new configuration
  initializer.send(command)
  initializer
end

ブロックは付けていないので、ただset_load_pathメソッドが呼ばれるだけということになります。

def set_load_path
  load_paths = configuration.load_paths + configuration.framework_paths
  load_paths.reverse_each { |dir| $LOAD_PATH.unshift(dir) if File.directory?(dir) }
  $LOAD_PATH.uniq!
end

やっていることは直感的だと思うので、実際にどのように動くかを見てみましょう。

まず、configuration.load_pathsはRails::Configuration#default_pathsで設定される各ディレクトリです。ここにapp/controllers等が含まれています。次のframework_pathsですが、RAILS_FRAMEWORK_ROOTは定義されていないはずなので#{RAILS_ROOT}/vendor/railsの各ディレクトリ(activerecordなど)になります。これらは存在しないので$LOAD_PATHに追加されないはず。

$GEM_HOME/gems/rails/lib/commands/server.rb

README通り、Mongrelやlighttpdが使えるか調べています。ちらちらとactivesupportが使われています。末端まで追おうとすると大変です。RubyGemsも対応してるソースコードブラウザってありましたっけ?

Mongrelもlighttpdも入れていないのでcommands/servers/webrick.rbに移りましょう。

$GEM_HOME/gems/rails/lib/commands/servers/webrick.rb

まずオプション解析が行われてますがそこは無視します。

アプリケーションディレクトリのconfig/environment.rbを読み込むことにより再びRails::Initializer.runが実行されています。今度は引数なし、ブロックありです。

Rails::Initializer#processメソッドによる初期化の中で興味深い&重要なものとして、initialize_databaseメソッドとinitialize_routingメソッドがあります。それぞれ見ていきましょう。

Rails::Initializer#initialize_database

initialize_databaseメソッドです。

def initialize_database
  return unless configuration.frameworks.include?(:active_record)
  ActiveRecord::Base.configurations = configuration.database_configuration
  ActiveRecord::Base.establish_connection
end

Rails::Configuration#database_configurationメソッドです。database_configuration_fileは通常config/database.ymlです。

def database_configuration
  YAML::load(ERB.new(IO.read(database_configuration_file)).result)
end

ActiveRecord::Base.establish_connection ($GEM_HOME/gems/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb)

ActiveRecord::Base.establish_connectionメソッドです。引数によって以下のように何回も再帰的に呼び出されます。なお、既存の接続はないはずなので無視しています。

  1. nilでRAILS_ENVが指定されているので、RAILS_ENV(Stringオブジェクト)を引数にestablish_connectionを再帰呼び出し
  2. configurations[spec.to_s]の戻り値としてデータベース接続設定が格納されたHashオブジェクトが返るのでそれを引数にしてestablish_connectionメソッドを再帰呼び出し
  3. adapter_methodで指定されるメソッドが定義されているので、ConnectionSpecficationオブジェクトを作成してestablish_connectionメソッドを再帰呼び出し
  4. 設定をクラス変数に格納

adapter_methodで指定されるメソッドは$GEM_HOME/gems/activerecord/lib/active_record.rbがrequireされたとき(Rails::Initializer#require_frameworksメソッド)に定義されています。

unless defined?(RAILS_CONNECTION_ADAPTERS)
  RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase frontbase )
end

RAILS_CONNECTION_ADAPTERS.each do |adapter|
  require "active_record/connection_adapters/" + adapter + "_adapter"
end

名前はestablish_coonnectionですが、実際の接続はまだ行っていないようです。

Rails::Initializer#initialize_routing

initialize_routingです。

def initialize_routing
  return unless configuration.frameworks.include?(:action_controller)
  ActionController::Routing.controller_paths = configuration.controller_paths
  ActionController::Routing::Routes.reload
end

ActionController::Routing::Routes.reload ($GEM_HOME/gems/actionpack/lib/action_controller/routing.rb)

ActionController::Routing::RoutesはActionController::Routing::RouteSetオブジェクトです。reloadメソッドはload!メソッドのエイリアスなのでload!メソッドを見てみましょう。

def load!
  Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
  clear!
  load_routes!
  named_routes.install
end

初めの2行は前の設定を破棄しているようなので無視しましょう。というわけでload_routes!メソッドです。

def load_routes!
  if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes
    load File.join("#{RAILS_ROOT}/config/routes.rb")
  else
    add_route ":controller/:action/:id"
  end
end

ifの条件式が成り立つのでアプリケーションディレクトリにあるconfig/routes.rbが読み込まれます。config/routes.rbではActionController::Routing::Routes.drawメソッドがブロック付きで呼ばれています。特定のURLがアクセスされたときにどんな振る舞いを行うかはconfig/routes.rbで定義するようです。

drawメソッドです。こちらでもclear!やnamed_routes.installが呼び出されてますね。

def draw
  clear!
  yield Mapper.new(self)
  named_routes.install
end

ActionController::Routing::RouteBuilder

yieldによってconfig/routes.rbに書かれているブロックに戻ってくると、Mapper#connectメソッドが呼ばれています。見覚えのあるURLに反応しそうな以下の呼び出しを追ってみることにしましょう。

map.connect ':controller/:action/:id'

Mapper#connectメソッドです。

def connect(path, options = {})
  @set.add_route(path, options)
end

@setは先ほどから見ているActionController::Routing::Routesオブジェクトです。RouteSet#add_routeメソッドに進みましょう。

def add_route(path, options = {})
  route = builder.build(path, options)
  routes << route
  route
end

builderメソッドはRouteBuilderオブジェクトを返します。RouteBuilder#buildメソッドに進みます。初めと最後を除くとこうなっています。

segments = segments_for_route_path(path)
defaults, requirements, conditions = divide_route_options(segments, options)
requirements = assign_route_options(segments, defaults, requirements)

route = Route.new
route.segments = segments
route.requirements = requirements
route.conditions = conditions

if !route.significant_keys.include?(:action) && !route.requirements[:action]
  route.requirements[:action] = "index"
  route.significant_keys << :action
end

segments_for_route_pathメソッドは渡された文字列を複数のセグメントに分割しています。"/:controller/:action/:id/"(先頭と末尾の'/'はbuildメソッドの初めで追加されます)の場合、以下のようになります。

  • DividerSegment('/'), is_optional == true
  • ControllerSegment(:controller), is_optional == false
  • DividerSegment('/'), is_optional == true
  • DynamicSegment(:action), is_optional == false
  • DividerSegment('/'), is_optional == true
  • DynamicSegment(:id), is_optional == false
  • DividerSegment('/'), is_optional == true

buildメソッドに戻って、divide_route_optionsメソッドはoptionsが空ハッシュなのでdefaults, requirements, conditionsのそれぞれは空ハッシュになるはずです。

次にassign_route_optionsメソッドが呼ばれていますがdefaultsとrequirementsは空ハッシュなので前半は無視です。とすると、以下の2メソッドが呼ばれます。

assign_default_route_options(segments)
ensure_required_segments(segments)

assign_default_route_optionsメソッドでは、:actionおよび:idのSegmentオブジェクトをいじっています。これで、

http://localhost:3000/accountbook/

とだけ指定した場合に一覧画面が表示されるということになるようです。

buildメソッドに戻るとRouteオブジェクトが構築され、route中に:actionが含まれているのでifの中身は実行されないはずです。

ActionController::Routing::RouteSet::NamedRouteCollection

以上でルーティング情報が構築されたのでActionController::Routing::Routesオブジェクトのnamed_routes.installメソッドです。named_routesの実体はNamedRouteCollectionオブジェクトです。

def install(destinations = [ActionController::Base, ActionView::Base])
  Array(destinations).each { |dest| dest.send :include, @module }
end

ActionController::BaseとActionView::Baseにメソッドを追加しているようですが、今までに見たところで@moduleを操作しているところはなかったはず。

DispatchServlet($GEM_HOME/gems/rails/lib/webrick_server.rb)

$GEM_HOME/rails/lib/commands/servers/webrick.rbはDispatchServlet.dispatchメソッドを呼び出して終わりです。

dispatchメソッドでは、WEBrick::HTTPServerオブジェクトを作成し、

server.mount('/', DispatchServlet, options)

とすることで全てのリクエストを自分で処理することになっています。下のinitializeメソッドを見てみるとWEBrick::HTTPServlet::FileHandlerオブジェクトを作成しているので通常のファイル取得リクエストはそちらに流していると想像できます。

おわりに

今回はRuby on Rallsのうち、サーバ起動時にどのような処理が行われるかを見ていきました。感想としては、

  • RubyGemsが使われているとクラスやメソッドが定義されているファイルを探すのが大変
  • activesupportが使われているともっと大変

といったところです:-)

それではみなさんもよいコードリーディングを。


トップ   編集 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2007-08-16 (木) 20:16:59 (6098d)