rails3 初始化和启动 Initialization Process

原创文章,转载请注明来源并保留原文链接

一直想搞清楚rails的启动和整个生命进程,好在有官方的guide用来参考,The Rails Initialization Process
1)先来看看rails的组织结构

%w(
  actionmailer
  actionpack
  activemodel
  activerecord
  activeresource
  activesupport
  railties
)

2)rails是如何启动的?
按下‘rails s’ 命令发生了什么?
rails3中rails已经变成了一个全局的命令,s是它的参数,s是server的缩写。
rails命令在gem包的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_path加到当前环境变量中。railties作用是将每个rails模块串联起来,它负责rails的启动顺序,管理rails的命令行接口,并且提供Rails的generators。

” rails/cli ” 是负责什么的?

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(1) }

if ARGV.first == 'plugin'
  ARGV.shift
  require 'rails/commands/plugin_new'
else
  require 'rails/commands/application'
end

rbconfig是对ruby标准库的配置和补充,在ruby编译的时候起效,和rails关系不大,不谈。
走进script_rails_loader.rb文件,里面定义了两个常量

RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
SCRIPT_RAILS = File.join('script', 'rails')

RUBY是ruby的bin文件位置,不管是Mac OS还是Windows还是其他,都会指向可执行文件。
SCRIPT_RAILS就是指向rails生成项目script目录下的rails脚本。大体内容如下:

#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.

APP_PATH = File.expand_path('../../config/application',  __FILE__)
require File.expand_path('../../config/boot',  __FILE__)
require 'rails/commands'

回到cli文件中,下一句可以看到执行了一个方法

Rails::ScriptRailsLoader.exec_script_rails!

exec_script_rails!方法的内容:

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

exec RUBY, SCRIPT_RAILS, *ARGV 这边大致上可以看出端倪了,这行代码的实质是:

ruby script/rails [arguments]
# 这里的arguments 可以是server, generate, console等

OK,重点来看看script/rails

首先有一个APP_PATH的常量,可以看到指向的是config目录下的application.rb文件。接着require了boot文件和commands文件。这里可以看到boot文件是第一个被载入的。

boot文件的任务其实很简单,就是准备好Gemfile中的gems。

# rubygems第一个被加载
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/commands.rb中了,源码地址commands.rb

ARGV < < '--help' if ARGV.empty?

aliases = {
  "g" => "generate",
  "c" => "console",
  "s" => "server",
  "db" => "dbconsole"
}

command = ARGV.shift
command = aliases[command] || command

可以看到这里的ARGV就是上面提到的exec RUBY, SCRIPT_RAILS, *ARGV后面的参数,也就是我们传进去的”server,generate,console”等等。这里为空就是”–help”,并且还有4个aliases方便简写。

主要还是看server是如何启动的。

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
  }

上面代码有三点,第一是实例化了Rails::server,第二是require APP_PATH(也就是application.rb),第三调用了server#start方法

实例化了一个Rails::Server,这是个什么东东?参考源码commands/server.rb

require 'fileutils'
require 'optparse'
require 'action_dispatch'

module Rails
  class Server < ::Rack::Server
    ......
  end
end

原来Rails::Server继承了Rack::Server(终于和Rack挂钩了),Rack::Server用来给所有基于rack的应用提供一般的server接口。而且,这里第一次引入了action_dispatch。Rails::Server的initialize方法如下:

def initialize(*)
      super
      set_environment
end

superRack::Server源码那么Rack::Server的initialize方法如下:

def initialize(options = nil)
      @options = options
      @app = options[:app] if options && options[:app]
end

由于options为nil所以Rack::Server中的initialize无作为,回到Rails::Server中,super的下一句是set_environment,代码如下:

def set_environment
      ENV["RAILS_ENV"] ||= options[:environment]
end

一目了然,设定环境咯,production,development,还是test。这里options是父类Rack::Server中的:

def options
      @options ||= parse_options(ARGV)
end

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在Rails::Server中被覆写了:

def default_options
      super.merge({
        :Port => 3000,
        :environment => (ENV['RAILS_ENV'] || "development").dup,
        :daemonize => false,
        :debugger => false,
        :pid => File.expand_path("tmp/pids/server.pid"),
        :config => File.expand_path("config.ru") # config.ru被指定,这也是commands.rb中server要chdir项目根目录的原因。
      })
end

第二是require APP_PATH(也就是application.rb),其实就是将项目的application给配置好,里面有这么一句:

require 'rails/all'

其实就是将rails所有的模块都引入进来,all.rb如下:

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

第三是调用了server#start方法,在Rails::Server中:

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

多熟悉啊,先一步步的屏显。然后是trap(:INT) { exit },是响应Ctrl-C中断的。再创建了tmp文件夹和四个子文件夹,最后回到Rack::Server的start方法中。

Rack::Server的start方法有这么一句:

wrapped_app

实际调用的是:

def wrapped_app
    @wrapped_app ||= build_app app
end

这里有两个方法的调用,一个是#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

了然了,原来通过Rack::Builder和config.ru构建了一个Rack application啊.Rack::Builder源码

再来看#build_app方法:

def build_app(app)
  middleware[options[:environment]].reverse_each do |middleware|
    middleware = middleware.call(self) if middleware.respond_to?(:call)
    next unless middleware
    klass = middleware.shift
    app = klass.new(app, *middleware)
  end
  app
end

通过middleware来新建一个app,像剥洋葱一样,一个一个middleware的往外套。

ok,由上面我们知道,通过config.ru,我们创建了一个Rack Application,下面来看看config.ru

# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment',  __FILE__)
run example::Application

可以看到引入了environment.rb文件。如下:

# Load the rails application
require File.expand_path('../application', __FILE__)

# Initialize the rails application
example::Application.initialize!

初始化了example::Application,example::Application是继承的Rails::Application,那么Rails::Application是什么呢?

#Rails::Application is responsible for executing all railties, engines and plugin
# initializers. Besides, it also executed some bootstrap initializers (check
# Rails::Application::Bootstrap) and finishing initializers, after all the others
# are executed (check Rails::Application::Finisher).

通过代码,Rails::Application继承了Engine。

看看initialize!方法:

def initialize!
  raise "Application has been already initialized." if @initialized
  run_initializers(self)
  @initialized = true
  self
end

至此,Rails Application的初始化和Rails Server基本上都已经启动完成。

3)rails是如何响应一个请求的?
占座,等下一篇文章完成后链接到这里。

4 thoughts on “rails3 初始化和启动 Initialization Process

  1. 刚好最近也在看rails3的启动过程,写了一篇和你几乎一样的文章:
    http://blog.csdn.net/tomwang1013/article/details/8657191
    请教个简单的问题:
    environment.rb中的MyApp::Application.initialize!
    这个初始动作说明initialize!是一个class method,但在Rails::Application中这是一个instance method,但是还是执行这个方法了,不知道为什么,兄弟可否释疑?

    • 其实Rails这里用了单例模式,在整个运行中,只有一个MyApp::Application的实例。https://github.com/rails/rails/blob/master/railties/lib/rails/railtie/configurable.rb 可以看到method_missing中找不到的方法都是通过instance来调用的

  2. Pingback: rails3 初始化和启动 Initialization Process | Frank Sun

  3. Pingback: rails3 初始化和启动 Initialization Process | Frank Sun

Leave a Reply

Your email address will not be published. Required fields are marked *