美文网首页每天写1000字每周500字程序员
Rails 中间件的初始化是怎么完成的

Rails 中间件的初始化是怎么完成的

作者: 刘思宁 | 来源:发表于2018-09-07 20:03 被阅读143次
    from @unsplash

    据说 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):

    1. 构造函数的第一个参数接受另外一个中间件对象,并把这个对象封装进一个实例变量(通常叫 app)
    2. 都包含一个叫 call 的实例方法,call 方法接受一个 env 参数(包含各种 http 请求参数),并且一定返回 http_status, http_headers, response_body 三个对象
    3. 在 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 的过程中,Railsapp_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 的地方。

    当然,这个调查过程中展现的其他信息或许还有别的用,希望大家以后能够遇到。


    微信公众号:刘思宁

    相关文章

      网友评论

        本文标题:Rails 中间件的初始化是怎么完成的

        本文链接:https://www.haomeiwen.com/subject/gychgftx.html