#contents *はじめに [#p9a961bc] Ruby on Railsでは通常以下のようにアプリケーションを作成・実行します。なお、以下の例は私が実験として作った家計簿アプリケーションを用います。 +$ rails accountbook +$ ruby script/generate model Outgo +$ ruby script/generate controller accountbook +app/controllers/accountbook_controller.rbに「scaffold :outgo」追加 +$ ruby script/server これで、http://localhost:3000/accountbook/にアクセスするとoutgosテーブルが表示されます。それではサーバの起動処理を見ていくことにしましょう。 なお、今回対象としたバージョンは1.2.3です。 *<アプリケーションディレクトリ>/config/boot.rb [#if85eb6b] ではサーバを起動するための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) [#ld97e27a] 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 [#ya70cdb4] README通り、Mongrelやlighttpdが使えるか調べています。ちらちらとactivesupportが使われています。末端まで追おうとすると大変です。RubyGemsも対応してるソースコードブラウザってありましたっけ? Mongrelもlighttpdも入れていないのでcommands/servers/webrick.rbに移りましょう。 *$GEM_HOME/gems/rails/lib/commands/servers/webrick.rb [#dfcde8be] まずオプション解析が行われてますがそこは無視します。 アプリケーションディレクトリのconfig/environment.rbを読み込むことにより再びRails::Initializer.runが実行されています。今度は引数なし、ブロックありです。 Rails::Initializer#processメソッドによる初期化の中で興味深い&重要なものとして、initialize_databaseメソッドとinitialize_routingメソッドがあります。それぞれ見ていきましょう。 **Rails::Initializer#initialize_database [#bb339a7d] 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) [#ued49413] ActiveRecord::Base.establish_connectionメソッドです。引数によって以下のように何回も再帰的に呼び出されます。なお、既存の接続はないはずなので無視しています。 +nilでRAILS_ENVが指定されているので、RAILS_ENV(Stringオブジェクト)を引数にestablish_connectionを再帰呼び出し +configurations[spec.to_s]の戻り値としてデータベース接続設定が格納されたHashオブジェクトが返るのでそれを引数にしてestablish_connectionメソッドを再帰呼び出し +adapter_methodで指定されるメソッドが定義されているので、ConnectionSpecficationオブジェクトを作成してestablish_connectionメソッドを再帰呼び出し +設定をクラス変数に格納 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 [#j6fca8f1] 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) [#ee6a78f4] 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 [#y0f718a2] 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 [#qa2ac68a] 以上でルーティング情報が構築されたので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) [#e4e697d8] $GEM_HOME/rails/lib/commands/servers/webrick.rbはDispatchServlet.dispatchメソッドを呼び出して終わりです。 dispatchメソッドでは、WEBrick::HTTPServerオブジェクトを作成し、 server.mount('/', DispatchServlet, options) とすることで全てのリクエストを自分で処理することになっています。下のinitializeメソッドを見てみるとWEBrick::HTTPServlet::FileHandlerオブジェクトを作成しているので通常のファイル取得リクエストはそちらに流していると想像できます。 *おわりに [#yf98cadf] 今回はRuby on Rallsのうち、サーバ起動時にどのような処理が行われるかを見ていきました。感想としては、 -RubyGemsが使われているとクラスやメソッドが定義されているファイルを探すのが大変 -activesupportが使われているともっと大変 といったところです:-) それではみなさんもよいコードリーディングを。