本章は、Rails初期化プロセスの内部について解説します。上級Rails開発者向けに推奨される、きわめて高度な内容を扱っています。
このガイドの内容:
bin/rails server
の利用法Rails::Server
インターフェイスの定義方法と利用法本ガイドでは、デフォルトのRailsアプリケーションでRuby on Railsスタックの起動時に必要なすべてのメソッド呼び出しについて詳しく解説します。具体的には、bin/rails server
を実行してアプリケーションを起動するとどのようなことが行われているかに注目して解説します。
特に記載のない限り、文中に記載されるRuby on Railsアプリケーションへのパスは相対パスです。
Railsのソースコードを参照しながら読み進めるのであれば、GitHubページのt
キーバインドでfile finderを起動するとその場でファイルを素早く検索できます。
それではアプリケーションを起動して初期化を開始しましょう。Railsアプリケーションの起動は、bin/rails console
またはbin/rails server
を実行して行うのが普通です。
bin/rails
このファイルの内容は次のとおりです。
#!/usr/bin/env ruby APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands"
APP_PATH
定数は、後でrails/commands
で使われます。この行で参照されているconfig/boot
ファイルは、Railsアプリケーションのconfig/boot.rb
ファイルであり、Bundlerの読み込みと設定を担当します。
config/boot.rb
config/boot.rb
には以下の行が含まれています。
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. require "bootsnap/setup" # Speed up boot time by caching expensive operations.
標準的なRailsアプリケーションにはGemfileというファイルがあり、アプリケーション内のすべての依存関係がそのファイル内で宣言されています。config/boot.rb
はGemfileの位置をENV['BUNDLE_GEMFILE']
に設定します。Gemfileが存在する場合、bundler/setup
をrequire
します。このrequire
は、Gemfileの依存ファイルが置かれている読み込みパスをBundlerで設定するときに使われます。
rails/commands.rb
config/boot.rb
の実行が完了すると、次にコマンドのエイリアスを拡張するrails/commands
をrequire
します。この状況ではARGV
配列にserver
だけが含まれており、以下のように渡されます。
require "rails/command" aliases = { "g" => "generate", "d" => "destroy", "c" => "console", "s" => "server", "db" => "dbconsole", "r" => "runner", "t" => "test" } command = ARGV.shift command = aliases[command] || command Rails::Command.invoke command, ARGV
server
の代わりにs
が渡されると、ここで定義されているaliases
の中からマッチするコマンドを探します。
rails/command.rb
Railsコマンドを入力すると、invoke
が指定の名前空間内でコマンドを探索し、見つかった場合はそのコマンドを実行します。
コマンドがRailsによって認識されない場合は、Rakeが引き継いで同じ名前でタスクを実行します。
以下のソースコードでわかるように、namespace
が空の場合、Rails::Command
は自動的にヘルプを出力します。
module Rails module Command class << self def invoke(full_namespace, args = [], **config) namespace = full_namespace = full_namespace.to_s if char = namespace =~ /:(\w+)$/ command_name, namespace = $1, namespace.slice(0, char) else command_name = namespace end command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name) command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name) command = find_by_namespace(namespace, command_name) if command && command.all_commands[command_name] command.perform(command_name, args, config) else find_by_namespace("rake").perform(full_namespace, args, config) end end end end end
server
コマンドが指定されると、Railsはさらに以下のコードを実行します。
module Rails module Command class ServerCommand < Base # :nodoc: def perform extract_environment_option_from_argument set_application_directory! prepare_restart Rails::Server.new(server_options).tap do |server| # Require application after server sets environment to propagate # the --environment option. require APP_PATH Dir.chdir(Rails.application.root) if server.serveable? print_boot_information(server.server, server.served_url) after_stop_callback = -> { say "Exiting" unless options[:daemon] } server.start(after_stop_callback) else say rack_server_suggestion(using) end end end end end end
上のファイルは、config.ru
ファイルが見つからない場合に限り、Railsのルートディレクトリ(config/application.rb
を指すAPP_PATH
から2階層上のディレクトリ)に移動します。これによって、次はRails::Server
クラスが起動されます。
actionpack/lib/action_dispatch.rb
Action Dispatchは、Railsフレームワークのルーティングコンポーネントです。ルーティング、セッション、共通のミドルウェアなどの機能を提供します。
rails/commands/server_command.rb
Rails::Server
クラスは、Rack::Server
を継承することでこのファイル内で定義されます。Rails::Server.new
を呼び出すと、rails/commands/server/server_command.rb
のinitialize
メソッドが呼び出されます。
module Rails class Server < ::Rack::Server def initialize(options = nil) @default_options = options || {} super(@default_options) set_environment end end end
最初にsuper
が呼び出され、そこからRack::Server
のinitialize
メソッドを呼び出します。
lib/rack/server.rb
Rack::Server
は、あらゆるRackベースのアプリケーション向けに共通のサーバーインターフェイスを提供する役割を担います(RailsもRackアプリケーションの一種です)。
Rack::Server
のinitialize
は、いくつかの変数を設定するだけの簡単なメソッドです。
module Rack class Server def initialize(options = nil) @ignore_options = [] if options @use_default_options = false @options = options @app = options[:app] if options[:app] else argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV @use_default_options = true @options = parse_options(argv) end end end end
ここでは、Rails::Command::ServerCommand#server_options
が返す値がoptions
に代入されます。
if
文の内側の行が評価されると、いくつかのインスタンス変数が設定されます。
Rails::Command::ServerCommand
のserver_options
メソッド定義は以下のとおりです。
module Rails module Command class ServerCommand no_commands do def server_options { user_supplied_options: user_supplied_options, server: using, log_stdout: log_to_stdout?, Port: port, Host: host, DoNotReverseLookup: true, config: options[:config], environment: environment, daemonize: options[:daemon], pid: pid, caching: options[:dev_caching], restart_cmd: restart_command, early_hints: early_hints } end end end end end
この値が@options
インスタンス変数に代入されます。
super
がRack::Server
の中で完了すると、rails/commands/server_command.rb
に制御が戻ります。この時点で、set_environment
がRails::Server
オブジェクトのコンテキスト内で呼び出されます。
module Rails module Server def set_environment ENV["RAILS_ENV"] ||= options[:environment] end end end
initialize
が完了すると、サーバーコマンドに制御が戻り、そこでAPP_PATH
(先ほど設定済み)がrequire
されます。
config/application
require APP_PATH
が実行されると、続いてconfig/application.rb
が読み込まれます(APP_PATH
はbin/rails
で定義されていることを思い出しましょう)。この設定ファイルはRailsアプリケーションの中にあり、必要に応じて自由に変更できます。
Rails::Server#start
config/application
の読み込みが完了すると、server.start
が呼び出されます。このメソッド定義は以下のようになっています。
module Rails class Server < ::Rack::Server def start(after_stop_callback = nil) trap(:INT) { exit } create_tmp_directories setup_dev_caching log_to_stdout if options[:log_stdout] super() # ... end private def setup_dev_caching if options[:environment] == "development" Rails::DevCaching.enable_by_argument(options[:caching]) end end def create_tmp_directories %w(cache pids sockets).each do |dir_to_make| FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make)) end end def log_to_stdout wrapped_app # touch the app so the logger is set up console = ActiveSupport::Logger.new(STDOUT) console.formatter = Rails.logger.formatter console.level = Rails.logger.level unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) end end end end
このメソッドはINT
シグナルのトラップを作成するので、CTRL-C
キーを押したときにサーバープロセスが終了するようになります。
コードに示されているように、ここではtmp/cache
、tmp/pids
、tmp/sockets
ディレクトリが作成されます。bin/rails server
に--dev-caching
オプションを指定して呼び出した場合は、development環境でのキャッシュをオンにします。最後にwrapped_app
が呼び出されます。このメソッドは、ActiveSupport::Logger
のインスタンスの作成と代入の前に、Rackアプリケーションを作成する役割を担います。
super
メソッドはRack::Server.start
を呼び出します。このメソッド定義の冒頭は以下のようになっています。
module Rack class Server def start(&blk) 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 p options[:server] pp wrapped_app pp app end check_pid! if options[:pid] # Touch the wrapped app, so that the config.ru is loaded before # daemonization (i.e. before chdir, etc). handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do wrapped_app end daemonize_app if options[:daemonize] write_pid if options[:pid] trap(:INT) do if server.respond_to?(:shutdown) server.shutdown else exit end end server.run wrapped_app, options, &blk end end end
Railsアプリケーションとして興味深い部分は、最終行のserver.run
でしょう。ここでもwrapped_app
メソッドが再び使われています。今度はこのメソッドをもう少し詳しく調べてみましょう(既に一度実行されてメモ化済みですが)。
module Rack class Server def wrapped_app @wrapped_app ||= build_app app end end end
このapp
メソッドの定義は以下のようになっています。
module Rack class Server def app @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config end # ... private def build_app_and_options_from_config if !::File.exist? options[:config] abort "configuration #{options[:config]} not found" end app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) @options.merge!(options) { |key, old, new| old } app end def build_app_from_string Rack::Builder.new_from_string(self.options[:builder]) end end end
options[:config]
の値はデフォルトではconfig.ru
です。config.ru
の内容は以下のようになっています。
# This file is used by Rack-based servers to start the application. require_relative "config/environment" run Rails.application
上のコードのRack::Builder.parse_file
メソッドは、このconfig.ru
ファイルの内容を受け取って、以下のコードで解析(parse)します。
module Rack class Builder def self.load_file(path, opts = Server::Options.new) # ... app = new_from_string cfgfile, config # ... end # ... def self.new_from_string(builder_script, file = "(rackup)") eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", TOPLEVEL_BINDING, file, 0 end end end
Rack::Builder
のinitialize
メソッドはこのブロックを受け取り、Rack::Builder
のインスタンスの中で実行します。Railsの初期化プロセスの大半がこの場所で実行されます。
最初に実行されるのは、config.ru
のconfig/environment.rb
のrequire
行です。
require_relative "config/environment"
config/environment.rb
このファイルは、config.ru
(rails server
)とPassengerの両方でrequire
されるコマンドファイルです。サーバーを実行するためのこれら2種類の方法はここで合流します。ここより前の部分はすべてRackとRailsの設定です。
このファイルの冒頭部分ではconfig/application.rb
をrequire
します。
require_relative "application"
config/application.rb
このファイルはconfig/boot.rb
をrequire
します。
require_relative "boot"
ただし、それまでrequire
されていなかった場合に限り、bin/rails server
の場合にboot.rbがrequire
されます。ただしPassengerを使う場合はboot.rbをrequire
しません。
ここからいよいよ面白くなってきます。
config/application.rb
の次の行は以下のようになっています。
require "rails/all"
railties/lib/rails/all.rb
このファイルはRailsのすべてのフレームワークをrequire
する役目を担当します。
require "rails" %w( active_record/railtie active_storage/engine action_controller/railtie action_view/railtie action_mailer/railtie active_job/railtie action_cable/engine action_mailbox/engine action_text/engine rails/test_unit/railtie ).each do |railtie| begin require railtie rescue LoadError end end
ここでRailsのすべてのフレームワークが読み込まれ、アプリケーションで利用できるようになります。本ガイドではこれらのフレームワークの詳細については触れませんが、ぜひこれらのフレームワークを自分で調べてみることをおすすめします。
現時点では、Railsエンジン、I18n、Rails設定などの共通機能がここで定義されていることを押さえておいてください。
config/environment.rb
に戻るconfig/application.rb
の残りの行ではRails::Application
を設定します。この設定が使われるのは、アプリケーションの初期化が完全に終わった後です。
config/application.rb
がRailsの読み込みを完了し、アプリケーションの名前空間が定義されると、config/environment.rb
に制御が戻ります。
ここではRails.application.initialize!
でアプリケーションが初期化されます。これはrails/application.rb
で定義されています。
railties/lib/rails/application.rb
initialize!
メソッドは以下のようなコードです。
def initialize!(group = :default) # :nodoc: raise "Application has been already initialized." if @initialized run_initializers(group, self) @initialized = true self end
アプリケーションは一度だけ初期化できます。railties/lib/rails/initializable.rb
で定義されているrun_initializers
メソッドによって、Railtieのさまざまなイニシャライザが実行されます。
def run_initializers(group = :default, *args) return if instance_variable_defined?(:@ran) initializers.tsort_each do |initializer| initializer.run(*args) if initializer.belongs_to?(group) end @ran = true end
run_initializers
のコード自身はトリッキーです。Railsはここで、あらゆる先祖クラスの中からinitializers
メソッドに応答するものを探索します。次にそれらを名前順でソートして実行します。たとえば、Engine
クラスはinitializers
メソッドを提供しているので、あらゆるエンジンを利用できるようになります。
Rails::Application
クラスはrailties/lib/rails/application.rb
ファイルで定義されており、その中でbootstrap
、railtie
、finisher
イニシャライザをそれぞれ定義しています。
bootstrap
イニシャライザは、ロガーの初期化などアプリケーションの準備を行います
最後に実行されるfinisher
イニシャライザは、ミドルウェアスタックのビルドなどを行います。
railtie
イニシャライザはRails::Application
自身で定義されており、bootstrap
とfinishers
の間に実行されます。
Railtieイニシャライザ全体と、load_config_initializersイニシャライザのインスタンスやそれに関連するconfig/initializers
以下のイニシャライザ設定ファイルを混同しないようにしましょう。
以上の処理が完了すると、制御はRack::Server
に移ります。
これまで進んだのは、以下のapp
メソッドが定義されている部分まででした。
module Rack class Server def app @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config end # ... private def build_app_and_options_from_config if !::File.exist? options[:config] abort "configuration #{options[:config]} not found" end app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) @options.merge!(options) { |key, old, new| old } app end def build_app_from_string Rack::Builder.new_from_string(self.options[:builder]) end end end
このコードのapp
とは、Railsアプリケーション自身(ミドルウェアの一種)であり、ここから先は、提供されているすべてのミドルウェアをRackが呼び出します。
module Rack class Server private def build_app(app) middleware[options[:environment]].reverse_each do |middleware| middleware = middleware.call(self) if middleware.respond_to?(:call) next unless middleware klass, *args = middleware app = klass.new(app, *args) end app end end end
Server#start
の最終行で、build_app
が(wrapped_app
によって)呼び出されていたことを思い出しましょう。最後に見たときのコードは以下のようになっていました。
server.run wrapped_app, options, &blk
このserver.run
の実装は、アプリケーションで使うWebサーバーによって異なります。たとえばPumaを使う場合のrun
メソッドは以下のようになります。
module Rack module Handler module Puma # ... def self.run(app, options = {}) conf = self.config(app, options) events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio launcher = ::Puma::Launcher.new(conf, events: events) yield launcher if block_given? begin launcher.run rescue Interrupt puts "* Gracefully stopping, waiting for requests to finish" launcher.stop puts "* Goodbye!" end end # ... end end end
本ガイドではサーバーの設定自体については詳しく解説しませんが、Railsの初期化プロセスという長い旅はここで終点になります。
本ガイドで解説した高度な概要は、自分が開発したコードがいつどのように実行されるかを理解するためにも、そしてより優れたRails開発者になるためにも役に立つことでしょう。もっと詳しく知りたいのであれば、次のステップとしてRailsのソースコードそのものを読むのがおそらくベストです。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。