据说 Rails 是由中间件(middleware)组合而成的,但他们是怎么组合的呢?
我们先来看一个中间件的代码:
# actionpack-5.1.6/lib/action_dispatch/middleware/request_id.rb
module ActionDispatch
class RequestId
X_REQUEST_ID = "X-Request-Id".freeze #:nodoc:
def initialize(app)
@app = app
end
def call(env)
req = ActionDispatch::Request.new env
req.request_id = make_request_id(req.x_request_id)
@app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
end
# ...
end
这个中间件是比较简单的,在 Rails 中的作用是为每个请求创建一个唯一的 request_id。他有一些特点,是所有中间件都会有的特点(TODO:核对别人的博客和 Rails Guide):
- 构造函数的第一个参数接受另外一个中间件对象,并把这个对象封装进一个实例变量(通常叫 app)
- 都包含一个叫 call 的实例方法,call 方法接受一个 env 参数(包含各种 http 请求参数),并且一定返回 http_status, http_headers, response_body 三个对象
- 在 call 的实例方法中,一定会用本中间件对象中的那个别的中间件对象(通常叫app)调用他自己 call 方法。
因此我们可以把一个中间件对象这样表示:
image我们从这个结构中可以看出,这些中间件明显就是要和其他中间件组合起来,形成一种链式的或者栈式的调用。那么,多个中间件联合起来就是这样的:
image但这个链条不会无限的进行,他会有两个端点。这个我们后面再说,现在我们先来看看这些中间件对象是一种怎样的存在。
首先说,这些中间件是有顺序存在的,可能是因为某些层的中间件需要互相依赖,也可能只是因为乱序的调用实现上比较麻烦,或者是其他的一些原因。但总之,如果我们在一个 Rails 项目的目录下执行 rails middleware
就能看到顺序显示的中间件们的类。比如这样:
$ rails middleware
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
run YourProject::Application.routes
这些只是中间件的类,但具体要调用上面提到的 call 方法的是中间件的实例对象。这些对象是如何产生的呢?是每次请求的时候,这些类调用 new 方法构造一个新的对象,还是这些中间件的对象一直存在一个地方,每次请求的时候,Rails 都通过一些方法取出一个中间件的对象呢(比如,从一个固定的位置取出)?
这个答案隐藏在 Rails
这个对象中。
Rails
本来是个 module (模块),里面有很多关于 Rails 的代码(见 railties-5.1.6/lib/rails.rb),但就像大家都知道的那样, module 在 ruby 中也是一个对象。我们能够隐约感觉到这里面不只有代码,而是也存了一些项目信息的,是通过这些常用方法:
Rails.root # => #<Pathname:/path/to/your/project>
# or
Rails.env # => "development"
我们当然可以认为当我们调用这些方法的时候,代码去访问了环境变量,然后把这些信息返回回来,但也可能是存在 Rails
这个 module 对象中。我们去看看源代码就好了:
# railties-5.1.6/lib/rails.rb#Line24
module Rails
# ...
class << self
@application = @app_class = nil
attr_writer :application
attr_accessor :app_class, :cache, :logger
def application
@application ||= (app_class.instance if app_class)
end
# ...
def root
application && application.config.root
end
def env
@_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development")
end
end
# ...
end
我们可以看到,在 Rails 这个对象中,至少存了 @_env
这个实例变量供我们查询 env。当然,这里更有 @application
这个我们要用到的实例变量。
后面我们会看到,这个 @application
是一个对象,他里面有一个叫做 @app
的实例变量,指向第一层的 middleware。不过,我们先来看看这个 @application
是怎么生成的。
我们都知道我们的项目在 rails new
(即创建项目)之后,会有一个 config/application.rb,在这里,rails 会用我们的项目名称,自动的创建一个 module,比如叫 YourProject
:
# config/application.rb
module YourProject
class Application < Rails::Application
# ...
end
end
这里的重点是,因为我们的 YourProject::Application
组成的这个类,继承了Rails::Application
,就会有一些事情发生。请看 Rails::Application
中的代码:
# railties-5.1.6/lib/rails/application.rb#Line86
module Rails
class Application < Engine
# ...
class << self
def inherited(base)
super
Rails.app_class = base
add_lib_to_load_path!(find_root(base.called_from))
ActiveSupport.run_load_hooks(:before_configuration, base)
end
def instance
super.run_load_hooks!
end
# ...
end
end
end
这里重要的是 inherited
方法(这是一个类似于继承后回调的方法),其中的 Rails.app_class = base
,会把刚刚说到的 Rails
这个对象中的 app_class
属性赋值为YourProject::Application
,也就是跟你的项目挂钩。
我们回到上面 Rails
module 中的一段代码:
# railties-5.1.6/lib/rails.rb#Line37
def application
@application ||= (app_class.instance if app_class)
end
这里 app_class 又会去调用 instance 方法,这个方法我们就不暴露了,但总之是创建了一个 YourProject::Application
对象。
好了,我们再来看看上面提到的 inherited
方法是什么时候调用的,这可以让我们知道 @applicatoin
中的对象是什么时候生成的。
首先说,当代码加载 config/application.rb 的时候,就会执行 class YourProject::Application < Rails::Application
这行代码,进而调用 inherited
方法。而 config/application.rb 是什么时候加载的呢?
大家知道 Rails 项目启动的时候会由 puma 之类的服务器调用 config.ru 文件:
# config.ru
require_relative 'config/environment'
run Rails.application
我们顺着从这里开始的,一层层 require 的文件树,就能在看到如下一些代码:
# config/environment.rb
require_relative 'application'
Rails.application.initialize!
其中的 require_relative 'application'
就是在 require 我们的 config/application.rb 了,也就是说在启动 rails 的过程中,Rails
的 app_class
就已经和我们项目名称绑定,将一个 @application
对象放在 Rails
中。
又因为,Rails
这个常量是唯一的(在一个 ruby 进程中),他的 application
属性也是唯一的,或者说 @application
对象是唯一的。那里面存储着用我们的项目命名的一个类的对象。
接下来,rails 项目会执行很多叫做 initializer 的代码块,这些 initializer 中,有相当一部分的动作都可以简单理解为:往 Rails.application
这个对象中添加一些实例变量,存储一些数据。跟中间件相关的这个 initializer 也不例外:
# railties-5.1.6/lib/rails/application/finisher.rb#Line44
initializer :build_middleware_stack do
build_middleware_stack
end
(顺带说一句,initializer 特别多,而有些需要先执行,有些需要后执行,这种位置关系让他们形成了一个图的结构,rails 在执行这个图中的 intializer 之前会遍历这个图中的节点,形成一个没有回路的路径?学名单源最短路径?)
当执行到这里的时候,Rails.application
开始调用一个叫做 build_middleware_stack
的方法。看看这个名字就知道和我们要探索的 middleware 有关了。
这个方法是另外一个方法(app方法)的别名:
# railties-5.1.6/lib/rails/engine.rb#Line503
def app
@app || @app_build_lock.synchronize {
@app ||= begin
stack = default_middleware_stack
config.middleware = build_middleware.merge_into(stack)
config.middleware.build(endpoint)
end
}
end
一旦这里执行完成,我们的 Rails.application
中就会多一个叫做 @app 的实例变量。这个变量叫做 @app,和中间件对象中的 @app 实例变量不是巧合的对应。
他都做了什么呢?
首先说,这里的 config.middleware
中包含的是我们中间件的各个类(class,不是对象,这些类怎么加载进来的还没有调查,但其中一些是通过 config.middleware.use ActionDispatch::Cookies
这样的语句,那些类常量就被存在了另外一个我们这里不会讲的实例变量中,然后这里通过 config.middleware 把他们取出来)。
更重要的是 config.middleware 去调用 build 方法,同时传入了一个 endpoint。endpoint 顾名思义,是middleware链条的终点,这个方法返回的默认是 YourProject::Application.routes
,也就是 rails middleware
这个指令输出的最后一个所谓的中间件,而其实 YourProject::Application.routes
是方法,这个方法返回的是一个 ActionDispatch::Routing::RouteSet
对象。当然,如果有其他配置或许就不一样了。但我们这里还是以默认的情况为例子。
顺带说一句,其中的 @app_build_lock
是个线程锁,显然这里 Rails 考虑到 rails 项目启动时如果 puma 这样的服务器已经启动了多线程,就要避免竞争关系。
接下来,带着这个 RouteSet 的对象作为终点,我们看看 config.middleware.build 方法都做了什么:
# actionpack-5.1.6/lib/action_dispatch/middleware/stack.rb#Line98
def build(app = Proc.new)
middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
end
传入的是 endpoint,从它开始,递归地构建前一层的中间件对象(看 inject 方法递归)。而这些 middlewares 的类,本来是在一个数组中,按照从起点到终点的方式排序的,但是因为我们是从终点开始递归创建的,所以这里要 reverse 一下。大家可以想想看 reverse 后第一个被创建的中间件是哪个:就是第二靠近内层的中间件,对吧。
inject 的 block 里面的 build 其实就是用 e 这个类去调用构造函数,这个就不再赘述:
# actionpack-5.1.6/lib/action_dispatch/middleware/stack.rb#Line34
def build(app)
klass.new(app, *args, &block)
end
总之,就在这样一层一层从最内层向外创建中间件对象,同时把内层的中间件封装在外层的中间件中,这个递归的过程会遍历完 middlewares 这个数组,然后开始创建最外层的中间件的对象,然后返回给 Rails.application 这个对象,使其中拥有一个叫做 @app 的实例变量。
到这里我们可以看出,一个 rails 的进程中,有唯一的 Rails 这个对象,其中有唯一的 application 属性了,其中又有一个启动后初始化完成的 @app
对象。这个 @app
对象就是最外层的中间件。所以,刚刚我们说找到了中间件链条的终点(routes),现在我们也找到了起点(Rails.application
)。
画成图,就是这样的:
image如果想把 Rails.application
的结构打印出来,可以用 pp Rails.application
, 很长:
#<YourProject::Application:0x007ff5f59989f8
@app=
#<Rack::Sendfile:0x007ff5f5e13460
@app=
#<ActionDispatch::Static:0x007ff5f5e13690
@app=
#<ActionDispatch::Executor:0x007ff5f5e136e0
@app=
#<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ff5f5e0ae78
@app=
#<Rack::Runtime:0x007ff5f5e13708
@app=
#<ActionDispatch::RequestId:0x007ff5f5e13780
@app=
#<ActionDispatch::RemoteIp:0x007ff5f5e137a8
@app=
#<Rails::Rack::Logger:0x007ff5f5e13820
@app=
#<ActionDispatch::ShowExceptions:0x007ff5f5e13848
@app=
#<ActionDispatch::DebugExceptions:0x007ff5f5e13870
@app=
#<ActionDispatch::Reloader:0x007ff5f5e138e8
@app=
#<ActionDispatch::Callbacks:0x007ff5f5e13910
@app=
#<ActiveRecord::Migration::CheckPending:0x007ff5f5e13938
@app=
#<Rack::Head:0x007ff5f5e13960
@app=
#<Rack::ConditionalGet:0x007ff5f5e13988
@app=
#<Rack::ETag:0x007ff5f5e139b0
@app=
#<ActionDispatch::Cookies:0x007ff5f5e139d8
@app=
#<ActionDispatch::Session::CookieStore:0x007ff5f5e13ac8
@app=
#<RequestLog:0x007ff5f5e13e88
@app=
#<ActionDispatch::Routing::RouteSet:0x007ff5f5f499d8
@append=
[#<Proc:0x007ff5f5b6a4c0@/Users/SiningLiu/.rvm/gems/ruby-2.3.4/gems/railties-5.1.6/lib/rails/application/finisher.rb:30>],
@config=
#<struct ActionDispatch::Routing::RouteSet::Config
relative_url_root=nil,
这个调查过程能回答一个问题,就是 rails 在接受每个 request 的时候,并不会重新构建中间件的对象,而是他们早在项目初始化的时候就已经存在于一个地方,一个叫做 Rails.application
的地方。
当然,这个调查过程中展现的其他信息或许还有别的用,希望大家以后能够遇到。
微信公众号:刘思宁
网友评论