Skip to content

Latest commit

 

History

History
1749 lines (1486 loc) · 57.5 KB

02.middlewares-1.md

File metadata and controls

1749 lines (1486 loc) · 57.5 KB

An Http Request Through Rails

02. Middlewares - 1

首先调用的Middleware并不是真正的Rails服务器的Middleware,而是Rails声明的,仅在rails s命令下才加入的Middleware,它们在Middleware链的最开始被调用。 声明这些Middleware的代码在railties-3.2.13/lib/rails/commands/server.rb中的middleware方法:

def middleware
  middlewares = []
  middlewares << [Rails::Rack::LogTailer, log_path] unless options[:daemonize]
  middlewares << [Rails::Rack::Debugger]  if options[:debugger]
  middlewares << [::Rack::ContentLength]
  Hash.new(middlewares)
end

第一个Middleware仅在server不是在daemon模式下启动时才加入的Middleware,代码写在railties-3.2.13/lib/rails/rack/log_tailer.rb中:

module Rails
  module Rack
    class LogTailer
      def initialize(app, log = nil)
        @app = app
        path = Pathname.new(log || "#{::File.expand_path(Rails.root)}/log/#{Rails.env}.log").cleanpath
        @cursor = @file = nil
        if ::File.exists?(path)
          @cursor = ::File.size(path)
          @file = ::File.open(path, 'r')
        end
      end

      def call(env)
        response = @app.call(env)
        tail!
        response
      end

      def tail!
        return unless @cursor
        @file.seek @cursor
        unless @file.eof?
          contents = @file.read
          @cursor = @file.tell
          $stdout.print contents
        end
      end
    end
  end
end

这个Middleware在每次请求结束后从Rails的Log中找到因为本次请求而增加出来的Log,将它们通过STDOUT打印在屏幕上。

然后是Debugger,代码在railties-3.2.13/lib/rails/rack/debugger.rb中:

module Rails
  module Rack
    class Debugger
      def initialize(app)
        @app = app
        ARGV.clear # clear ARGV so that rails server options aren't passed to IRB
        require 'ruby-debug'

        ::Debugger.start
        ::Debugger.settings[:autoeval] = true if ::Debugger.respond_to?(:settings)
        puts "=> Debugger enabled"
      rescue LoadError
        puts "You need to install ruby-debug to run the server in debugging mode. With gems, use 'gem install ruby-debug'"
        exit
      end

      def call(env)
        @app.call(env)
      end
    end
  end
end

这个Middleware仅仅在声明了Debug模式下才启动,它在启动时requireruby-debug库,再输出一些基本信息。接着就是在执行到debugger语句的时候进入断点状态了。

下一个Middleware是ContentLength,定义在rack-1.4.5/lib/rack/content_length.rb中:

require 'rack/utils'
module Rack
  # Sets the Content-Length header on responses with fixed-length bodies.
  class ContentLength
    include Rack::Utils

    def initialize(app)
      @app = app
    end

    def call(env)
      status, headers, body = @app.call(env)
      headers = HeaderHash.new(headers)
      if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
         !headers['Content-Length'] &&
         !headers['Transfer-Encoding'] &&
         body.respond_to?(:to_ary)
        obody = body
        body, length = [], 0
        obody.each { |part| body << part; length += bytesize(part) }
        obody.close if obody.respond_to?(:close)
        headers['Content-Length'] = length.to_s
      end
      [status, headers, body]
    end
  end
end

ContentLength解决了当Request结束后返回的header里没有ContentLength并且body实现了to_ary方法的时候的计算ContentLength的问题。 接下来进入Rails::Application类的call方法,这个实质上并非Middleware,但是也只是将获取到的Rails env中的PATH_INFO,QUERY_STRING,SCRIPT_NAME合并成ORIGINAL_FULLPATH(即用户最开始请求的完整的地址)的过程,代码在railties-3.2.13/lib/rails/application.rb中定义:

def call(env)
  env["ORIGINAL_FULLPATH"] = build_original_fullpath(env)
  super(env)
end
def build_original_fullpath(env)
  path_info    = env["PATH_INFO"]
  query_string = env["QUERY_STRING"]
  script_name  = env["SCRIPT_NAME"]
  if query_string.present?
    "#{script_name}#{path_info}?#{query_string}"
  else
    "#{script_name}#{path_info}"
  end
end

可以看到这里仅仅是简单的字符串合并。

然后调用父类,也就是Rails::Enginecall方法,定义在railties-3.2.13/lib/rails/engine.rb中,这个方法第一次将之前所有在它上面注册过的Middleware build成链表,设置endpoint,并调用middleware处理请求:

def call(env)
  app.call(env.merge!(env_config))
end

这里的env_configRails::Application中定义了action_dispatch相关的对象:

# Rails.application.env_config stores some of the Rails initial environment parameters.
# Currently stores:
#
#   * "action_dispatch.parameter_filter"         => config.filter_parameters,
#   * "action_dispatch.secret_token"             => config.secret_token,
#   * "action_dispatch.show_exceptions"          => config.action_dispatch.show_exceptions,
#   * "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
#   * "action_dispatch.logger"                   => Rails.logger,
#   * "action_dispatch.backtrace_cleaner"        => Rails.backtrace_cleaner
#
# These parameters will be used by middlewares and engines to configure themselves.
#

def env_config
  @env_config ||= super.merge({
    "action_dispatch.parameter_filter" => config.filter_parameters,
    "action_dispatch.secret_token" => config.secret_token,
    "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
    "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
    "action_dispatch.logger" => Rails.logger,
    "action_dispatch.backtrace_cleaner" => Rails.backtrace_cleaner
  })
end

在engine中定义了routes对象:

def env_config
  @env_config ||= {
    'action_dispatch.routes' => routes
  }
end

而routes对象实质是ActionDispatch::Routing::RouteSet类的实例,这个类未来将会记录当前engine下所有routes

def routes
  @routes ||= ActionDispatch::Routing::RouteSet.new
  @routes.append(&Proc.new) if block_given?
  @routes
end

app负责对middleware的build工作:

def app
  @app ||= begin
    config.middleware = config.middleware.merge_into(default_middleware_stack)
    config.middleware.build(endpoint)
  end
end

这里的default_middleware_stack是engine定义的默认middleware stack对象,engine实质上没有规定任何的默认middleware,但是如果是对Rails Application的请求,那么就被声明了许多处理这个请求的middleware,定义在Rails::Application中:

def default_middleware_stack
  require 'action_controller/railtie'

  ActionDispatch::MiddlewareStack.new.tap do |middleware|
    if rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache
      require "action_dispatch/http/rack_cache"
      middleware.use ::Rack::Cache, rack_cache
    end

    if config.force_ssl
      require "rack/ssl"
      middleware.use ::Rack::SSL, config.ssl_options
    end

    if config.serve_static_assets
      middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control
    end

    middleware.use ::Rack::Lock unless config.allow_concurrency
    middleware.use ::Rack::Runtime
    middleware.use ::Rack::MethodOverride
    middleware.use ::ActionDispatch::RequestId
    middleware.use ::Rails::Rack::Logger, config.log_tags # must come after Rack::MethodOverride to properly log overridden methods
    middleware.use ::ActionDispatch::ShowExceptions, config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path)
    middleware.use ::ActionDispatch::DebugExceptions
    middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies

    if config.action_dispatch.x_sendfile_header.present?
      middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header
    end

    unless config.cache_classes
      app = self
      middleware.use ::ActionDispatch::Reloader, lambda { app.reload_dependencies? }
    end

    middleware.use ::ActionDispatch::Callbacks
    middleware.use ::ActionDispatch::Cookies

    if config.session_store
      if config.force_ssl && !config.session_options.key?(:secure)
        config.session_options[:secure] = true
      end

      middleware.use config.session_store, config.session_options
      middleware.use ::ActionDispatch::Flash
    end

    middleware.use ::ActionDispatch::ParamsParser
    middleware.use ::ActionDispatch::Head
    middleware.use ::Rack::ConditionalGet
    middleware.use ::Rack::ETag, "no-cache"

    if config.action_dispatch.best_standards_support
      middleware.use ::ActionDispatch::BestStandardsSupport, config.action_dispatch.best_standards_support
    end
  end
end

接下来所有即将执行的Middleware几乎都已经在这个列表中列出。

随后将default_middleware_stackconfig.middlewaremerge在一起。注意,config.middleware在railtie初始化的时候activerecord会为它增加一些middleware,在接下来也会被执行到。

接着是build的过程,build就是将所有middleware串成链表,尾节点即是endpoint,在这里,endpoint是:

def endpoint
  self.class.endpoint || routes
end

默认情况下是routes,也就是ActionDispatch::Routing::RouteSet类的实例,也就是说,当程序执行到路由层时,这一部分的middleware的执行就结束了。

build的代码在actionpack-3.2.13/lib/action_dispatch/middleware/stack.rbActionDispatch::MiddlewareStack类中定义:

def build(app = nil, &block)
  app ||= block
  raise "MiddlewareStack#build requires an app" unless app
  middlewares.reverse.inject(app) { |a, e| e.build(a) }
end

这里面还有一个build方法,实际上是通过ActionDispatch::MiddlewareStack::Middleware创建Middleware类实例的方法:

def build(app)
  klass.new(app, *args, &block)
end

build完成后,回到app方法,这里将链表的首节点赋值给@app实例变量,然后执行它的call方法,这样就开始了链表的执行。 第一个middleware是Rack::SSL,这个middleware只有当启动了SSL后才会启用,定义在rack-ssl-1.3.3/lib/rack/ssl.rb中:

require 'rack'
require 'rack/request'

module Rack
  class SSL
    YEAR = 31536000

    def self.default_hsts_options
      { :expires => YEAR, :subdomains => false }
    end

    def initialize(app, options = {})
      @app = app

      @hsts = options[:hsts]
      @hsts = {} if @hsts.nil? || @hsts == true
      @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts

      @exclude = options[:exclude]
      @host    = options[:host]
    end

    def call(env)
      if @exclude && @exclude.call(env)
        @app.call(env)
      elsif scheme(env) == 'https'
        status, headers, body = @app.call(env)
        headers = hsts_headers.merge(headers)
        flag_cookies_as_secure!(headers)
        [status, headers, body]
      else
        redirect_to_https(env)
      end
    end

    private
      # Fixed in rack >= 1.3
      def scheme(env)
        if env['HTTPS'] == 'on'
          'https'
        elsif env['HTTP_X_FORWARDED_PROTO']
          env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
        else
          env['rack.url_scheme']
        end
      end

      def redirect_to_https(env)
        req        = Request.new(env)
        url        = URI(req.url)
        url.scheme = "https"
        url.host   = @host if @host
        status     = %w[GET HEAD].include?(req.request_method) ? 301 : 307
        headers    = hsts_headers.merge('Content-Type' => 'text/html',
                                        'Location'     => url.to_s)

        [status, headers, []]
      end

      # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
      def hsts_headers
        if @hsts
          value = "max-age=#{@hsts[:expires]}"
          value += "; includeSubDomains" if @hsts[:subdomains]
          { 'Strict-Transport-Security' => value }
        else
          {}
        end
      end

      def flag_cookies_as_secure!(headers)
        if cookies = headers['Set-Cookie']
          # Rack 1.1's set_cookie_header! will sometimes wrap
          # Set-Cookie in an array
          unless cookies.respond_to?(:to_ary)
            cookies = cookies.split("\n")
          end

          headers['Set-Cookie'] = cookies.map { |cookie|
            if cookie !~ /; secure(;|$)/
              "#{cookie}; secure"
            else
              cookie
            end
          }.join("\n")
        end
      end
  end
end

这个Middleware的主要功能是,除非exclude选项指定,否则检查是否是https协议,如果不是的话,则将请求的HTTP Header增加LocationContent-Type选项,使浏览器用https重新发出请求。如果是的话,增加与https相关的安全方面的http header,再把所有的Cookie设置成secure。需要注意的是,这里的Cookie由于已经超出了Cookies Middleware的范围,所以直接读取了headers中的Set-Cookie项,而不能通过传统的方法进行设置。

第二个进入的middleware是ActionDispatch::Static,写在actionpack-3.2.13/lib/action_dispatch/middleware/static.rb中,整个文件的内容是:

require 'rack/utils'

module ActionDispatch
  class FileHandler
    def initialize(root, cache_control)
      @root          = root.chomp('/')
      @compiled_root = /^#{Regexp.escape(root)}/
      headers = cache_control && { 'Cache-Control' => cache_control }
      @file_server   = ::Rack::File.new(@root, headers)
    end

    def match?(path)
      path = path.dup
      full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(unescape_path(path)))
      paths = "#{full_path}#{ext}"
      matches = Dir[paths]
      match = matches.detect { |m| File.file?(m) }
      if match
        match.sub!(@compiled_root, '')
        ::Rack::Utils.escape(match)
      end
    end

    def call(env)
      @file_server.call(env)
    end

    def ext
      @ext ||= begin
        ext = ::ActionController::Base.page_cache_extension
        "{,#{ext},/index#{ext}}"
      end
    end

    def unescape_path(path)
      URI.parser.unescape(path)
    end

    def escape_glob_chars(path)
      path.force_encoding('binary') if path.respond_to? :force_encoding
      path.gsub(/[*?{}\[\]]/, "\\\\\\&")
    end
  end

  class Static

    def initialize(app, path, cache_control=nil)
      @app = app
      @file_handler = FileHandler.new(path, cache_control)
    end

    def call(env)
      case env['REQUEST_METHOD']
      when 'GET', 'HEAD'
        path = env['PATH_INFO'].chomp('/')
        if match = @file_handler.match?(path)
          env["PATH_INFO"] = match
          return @file_handler.call(env)
        end
      end

      @app.call(env)
    end
  end
end

这个middleware是把请求的地址在文件系统上搜索,目录是Rails根目录下的public/目录,如果文件系统里确实有这个文件,或者是增加了.html扩展名后有这个文件,或者是存在这个路径的目录且目录下有index.html文件。则match?将返回这个文件路径,然后调用@file_handler.call将文件的内容予以返回。这里的@file_handler是一个Rack::File对象,代码定义在rack-1.4.5/lib/rack/file.rb中:

require 'time'
require 'rack/utils'
require 'rack/mime'

module Rack
  # Rack::File serves files below the +root+ directory given, according to the
  # path info of the Rack request.
  # e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
  # as http://localhost:9292/passwd
  #
  # Handlers can detect if bodies are a Rack::File, and use mechanisms
  # like sendfile on the +path+.

  class File
    SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
    ALLOWED_VERBS = %w[GET HEAD]

    attr_accessor :root
    attr_accessor :path
    attr_accessor :cache_control

    alias :to_path :path

    def initialize(root, headers={})
      @root = root
      # Allow a cache_control string for backwards compatibility
      if headers.instance_of? String
        warn \
          "Rack::File headers parameter replaces cache_control after Rack 1.5."
        @headers = { 'Cache-Control' => headers }
      else
        @headers = headers
      end
    end

    def call(env)
      dup._call(env)
    end

    F = ::File

    def _call(env)
      unless ALLOWED_VERBS.include? env["REQUEST_METHOD"]
        return fail(405, "Method Not Allowed")
      end

      @path_info = Utils.unescape(env["PATH_INFO"])
      parts = @path_info.split SEPS

      clean = []

      parts.each do |part|
        next if part.empty? || part == '.'
        part == '..' ? clean.pop : clean << part
      end

      @path = F.join(@root, *clean)

      available = begin
        F.file?(@path) && F.readable?(@path)
      rescue SystemCallError
        false
      end

      if available
        serving(env)
      else
        fail(404, "File not found: #{@path_info}")
      end
    end

    def serving(env)
      last_modified = F.mtime(@path).httpdate
      return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
      response = [
        200,
        {
          "Last-Modified"  => last_modified,
          "Content-Type"   => Mime.mime_type(F.extname(@path), 'text/plain')
        },
        env["REQUEST_METHOD"] == "HEAD" ? [] : self
      ]

      # Set custom headers
      @headers.each { |field, content| response[1][field] = content } if @headers

      # NOTE:
      #   We check via File::size? whether this file provides size info
      #   via stat (e.g. /proc files often don't), otherwise we have to
      #   figure it out by reading the whole file into memory.
      size = F.size?(@path) || Utils.bytesize(F.read(@path))

      ranges = Rack::Utils.byte_ranges(env, size)
      if ranges.nil? || ranges.length > 1
        # No ranges, or multiple ranges (which we don't support):
        # TODO: Support multiple byte-ranges
        response[0] = 200
        @range = 0..size-1
      elsif ranges.empty?
        # Unsatisfiable. Return error, and file size:
        response = fail(416, "Byte range unsatisfiable")
        response[1]["Content-Range"] = "bytes */#{size}"
        return response
      else
        # Partial content:
        @range = ranges[0]
        response[0] = 206
        response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
        size = @range.end - @range.begin + 1
      end

      response[1]["Content-Length"] = size.to_s
      response
    end

    def each
      F.open(@path, "rb") do |file|
        file.seek(@range.begin)
        remaining_len = @range.end-@range.begin+1
        while remaining_len > 0
          part = file.read([8192, remaining_len].min)
          break unless part
          remaining_len -= part.length

          yield part
        end
      end
    end

    private

    def fail(status, body)
      body += "\n"
      [
        status,
        {
          "Content-Type" => "text/plain",
          "Content-Length" => body.size.to_s,
          "X-Cascade" => "pass"
        },
        [body]
      ]
    end

  end
end

下面一个middleware是Rack::Lock,定义在rack-1.4.5/lib/rack/lock.rb中,代码是:

require 'thread'
require 'rack/body_proxy'

module Rack
  class Lock
    FLAG = 'rack.multithread'.freeze

    def initialize(app, mutex = Mutex.new)
      @app, @mutex = app, mutex
    end

    def call(env)
      old, env[FLAG] = env[FLAG], false
      @mutex.lock
      response = @app.call(env)
      body = BodyProxy.new(response[2]) { @mutex.unlock }
      response[2] = body
      response
    ensure
      @mutex.unlock unless body
      env[FLAG] = old
    end
  end
end

当且仅当config.allow_concurrency为false时该middleware有效,可以看到这里提供了一个锁,在进入请求前先加锁,在从middleware退出后,将body改造成BodyProxy对象的实例,并且将解锁的代码作为block传入。BodyProxy定义在rack-1.4.5/lib/rack/body_proxy.rb中:

module Rack
  class BodyProxy
    def initialize(body, &block)
      @body, @block, @closed = body, block, false
    end

    def respond_to?(*args)
      return false if args.first.to_s =~ /^to_ary$/
      super or @body.respond_to?(*args)
    end

    def close
      return if @closed
      @closed = true
      begin
        @body.close if @body.respond_to? :close
      ensure
        @block.call
      end
    end

    def closed?
      @closed
    end

    # N.B. This method is a special case to address the bug described by #434.
    # We are applying this special case for #each only. Future bugs of this
    # class will be handled by requesting users to patch their ruby
    # implementation, to save adding too many methods in this class.
    def each(*args, &block)
      @body.each(*args, &block)
    end

    def method_missing(*args, &block)
      super if args.first.to_s =~ /^to_ary$/
      @body.__send__(*args, &block)
    end
  end
end

BodyProxy的功能是实现了close方法,当执行到close方法的时候会调用传入的block,这样就完成了解锁的过程。

下面一个Middleware是ActiveSupport::Cache::Strategy::LocalCache下的Middleware类对象,定义在activesupport-3.2.13/lib/active_support/cache/strategy/local_cache.rb中:

require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/string/inflections'

module ActiveSupport
  module Cache
    module Strategy
      # Caches that implement LocalCache will be backed by an in-memory cache for the
      # duration of a block. Repeated calls to the cache for the same key will hit the
      # in-memory cache for faster access.
      module LocalCache
        # Simple memory backed cache. This cache is not thread safe and is intended only
        # for serving as a temporary memory cache for a single thread.
        class LocalStore < Store
          def initialize
            super
            @data = {}
          end

          # Don't allow synchronizing since it isn't thread safe,
          def synchronize # :nodoc:
            yield
          end

          def clear(options = nil)
            @data.clear
          end

          def read_entry(key, options)
            @data[key]
          end

          def write_entry(key, value, options)
            @data[key] = value
            true
          end

          def delete_entry(key, options)
            !!@data.delete(key)
          end
        end

        # Use a local cache for the duration of block.
        def with_local_cache
          save_val = Thread.current[thread_local_key]
          begin
            Thread.current[thread_local_key] = LocalStore.new
            yield
          ensure
            Thread.current[thread_local_key] = save_val
          end
        end

        #--
        # This class wraps up local storage for middlewares. Only the middleware method should
        # construct them.
        class Middleware # :nodoc:
          attr_reader :name, :thread_local_key

          def initialize(name, thread_local_key)
            @name             = name
            @thread_local_key = thread_local_key
            @app              = nil
          end

          def new(app)
            @app = app
            self
          end

          def call(env)
            Thread.current[thread_local_key] = LocalStore.new
            @app.call(env)
          ensure
            Thread.current[thread_local_key] = nil
          end
        end

        # Middleware class can be inserted as a Rack handler to be local cache for the
        # duration of request.
        def middleware
          @middleware ||= Middleware.new(
            "ActiveSupport::Cache::Strategy::LocalCache",
            thread_local_key)
        end

        def clear(options = nil) # :nodoc:
          local_cache.clear(options) if local_cache
          super
        end

        def cleanup(options = nil) # :nodoc:
          local_cache.clear(options) if local_cache
          super
        end

        def increment(name, amount = 1, options = nil) # :nodoc:
          value = bypass_local_cache{super}
          if local_cache
            local_cache.mute do
              if value
                local_cache.write(name, value, options)
              else
                local_cache.delete(name, options)
              end
            end
          end
          value
        end

        def decrement(name, amount = 1, options = nil) # :nodoc:
          value = bypass_local_cache{super}
          if local_cache
            local_cache.mute do
              if value
                local_cache.write(name, value, options)
              else
                local_cache.delete(name, options)
              end
            end
          end
          value
        end

        protected
          def read_entry(key, options) # :nodoc:
            if local_cache
              entry = local_cache.read_entry(key, options)
              unless entry
                entry = super
                local_cache.write_entry(key, entry, options)
              end
              entry
            else
              super
            end
          end

          def write_entry(key, entry, options) # :nodoc:
            local_cache.write_entry(key, entry, options) if local_cache
            super
          end

          def delete_entry(key, options) # :nodoc:
            local_cache.delete_entry(key, options) if local_cache
            super
          end

        private
          def thread_local_key
            @thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
          end

          def local_cache
            Thread.current[thread_local_key]
          end

          def bypass_local_cache
            save_cache = Thread.current[thread_local_key]
            begin
              Thread.current[thread_local_key] = nil
              yield
            ensure
              Thread.current[thread_local_key] = save_cache
            end
          end
      end
    end
  end
end

这个Middleware实质就是在当前Thread中增加一个local store对象,所谓local store,即是指用内存来维护一个键值对,和Hash非常接近(其实也是内部实现机制),但是实现了Store这个父类的接口。

再下面一个Middleware是Rack::Runtime对象,定义在rack-1.4.5/lib/rack/runtime.rb中:

module Rack
  # Sets an "X-Runtime" response header, indicating the response
  # time of the request, in seconds
  #
  # You can put it right before the application to see the processing
  # time, or before all the other middlewares to include time for them,
  # too.
  class Runtime
    def initialize(app, name = nil)
      @app = app
      @header_name = "X-Runtime"
      @header_name << "-#{name}" if name
    end

    def call(env)
      start_time = Time.now
      status, headers, body = @app.call(env)
      request_time = Time.now - start_time

      if !headers.has_key?(@header_name)
        headers[@header_name] = "%0.6f" % request_time
      end

      [status, headers, body]
    end
  end
end

每次处理请求前取当前时间,结束后再取当前时间,二者相减即是处理请求的时间,将结果处理后写入返回的HTTP Header中的X-Runtime项中。

然后进入的是Rack::MethodOverride类,定义在rack-1.4.5/lib/rack/methodoverride.rb中:

module Rack
  class MethodOverride
    HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS PATCH)

    METHOD_OVERRIDE_PARAM_KEY = "_method".freeze
    HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze

    def initialize(app)
      @app = app
    end

    def call(env)
      if env["REQUEST_METHOD"] == "POST"
        method = method_override(env)
        if HTTP_METHODS.include?(method)
          env["rack.methodoverride.original_method"] = env["REQUEST_METHOD"]
          env["REQUEST_METHOD"] = method
        end
      end

      @app.call(env)
    end

    def method_override(env)
      req = Request.new(env)
      method = req.POST[METHOD_OVERRIDE_PARAM_KEY] ||
        env[HTTP_METHOD_OVERRIDE_HEADER]
      method.to_s.upcase
    rescue EOFError
      ""
    end
  end
end

对于一些无法直接发送除了GET和POST以外请求的客户端,就可以先用POST,然后把实际请求方法写在参数里传出。在进入该middleware后,将从参数中取出实际请求方法并将其写入Rails env环境中。

下面一个进入的是ActionDispatch::RequestId,定义在actionpack-3.2.13/lib/action_dispatch/middleware/request_id.rb

require 'securerandom'
require 'active_support/core_ext/string/access'
require 'active_support/core_ext/object/blank'

module ActionDispatch
  # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
  # ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header.
  #
  # The unique request id is either based off the X-Request-Id header in the request, which would typically be generated
  # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
  # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
  #
  # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
  # from multiple pieces of the stack.
  class RequestId
    def initialize(app)
      @app = app
    end

    def call(env)
      env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id
      status, headers, body = @app.call(env)

      headers["X-Request-Id"] = env["action_dispatch.request_id"]
      [ status, headers, body ]
    end

    private
      def external_request_id(env)
        if request_id = env["HTTP_X_REQUEST_ID"].presence
          request_id.gsub(/[^\w\-]/, "").first(255)
        end
      end

      def internal_request_id
        SecureRandom.hex(16)
      end
  end
end

这个Middleware为每一个新的HTTP Request分配一个唯一的Request Id,并且放在HTTP Header中。浏览器可以在接收到Response后,取出Request id,并且在下一次发送请求的时候将其设为X-REQUEST-ID,这样就可以起到标示作用并且在再下次请求返回的时候得到相同的Request Id。

再下一个Middleware是Rails::Rack::Logger,定义在railties-3.2.13/lib/rails/rack/logger.rb中:

require 'active_support/core_ext/time/conversions'
require 'active_support/core_ext/object/blank'

module Rails
  module Rack
    # Sets log tags, logs the request, calls the app, and flushes the logs.
    class Logger < ActiveSupport::LogSubscriber
      def initialize(app, taggers = nil)
        @app, @taggers = app, taggers || []
      end

      def call(env)
        request = ActionDispatch::Request.new(env)

        if Rails.logger.respond_to?(:tagged)
          Rails.logger.tagged(compute_tags(request)) { call_app(request, env) }
        else
          call_app(request, env)
        end
      end

    protected

      def call_app(request, env)
        # Put some space between requests in development logs.
        if Rails.env.development?
          Rails.logger.info ''
          Rails.logger.info ''
        end

        Rails.logger.info started_request_message(request)
        @app.call(env)
      ensure
        ActiveSupport::LogSubscriber.flush_all!
      end

      # Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700
      def started_request_message(request)
        'Started %s "%s" for %s at %s' % [
          request.request_method,
          request.filtered_path,
          request.ip,
          Time.now.to_default_s ]
      end

      def compute_tags(request)
        @taggers.collect do |tag|
          case tag
          when Proc
            tag.call(request)
          when Symbol
            request.send(tag)
          else
            tag
          end
        end
      end
    end
  end
end

这个Middleware主要负责设置Logger的tag,然后增加一个输出Request method,输出请求地址,输出IP,输出当前时间的日志。在每次请求结束后还会刷新缓存区。

然后进入ActionDispatch::ShowExceptions,这个Middleware定义在actionpack-3.2.13/lib/action_dispatch/middleware/show_exceptions.rb中:

require 'action_dispatch/http/request'
require 'action_dispatch/middleware/exception_wrapper'
require 'active_support/deprecation'

module ActionDispatch
  # This middleware rescues any exception returned by the application
  # and calls an exceptions app that will wrap it in a format for the end user.
  #
  # The exceptions app should be passed as parameter on initialization
  # of ShowExceptions. Everytime there is an exception, ShowExceptions will
  # store the exception in env["action_dispatch.exception"], rewrite the
  # PATH_INFO to the exception status code and call the rack app.
  # 
  # If the application returns a "X-Cascade" pass response, this middleware
  # will send an empty response as result with the correct status code.
  # If any exception happens inside the exceptions app, this middleware
  # catches the exceptions and returns a FAILSAFE_RESPONSE.
  class ShowExceptions
    FAILSAFE_RESPONSE = [500, {'Content-Type' => 'text/html'},
      ["<html><body><h1>500 Internal Server Error</h1>" <<
       "If you are the administrator of this website, then please read this web " <<
       "application's log file and/or the web server's log file to find out what " <<
       "went wrong.</body></html>"]]

    class << self
      def rescue_responses
        ActiveSupport::Deprecation.warn "ActionDispatch::ShowExceptions.rescue_responses is deprecated. " \
          "Please configure your exceptions using a railtie or in your application config instead."
        ExceptionWrapper.rescue_responses
      end

      def rescue_templates
        ActiveSupport::Deprecation.warn "ActionDispatch::ShowExceptions.rescue_templates is deprecated. " \
          "Please configure your exceptions using a railtie or in your application config instead."
        ExceptionWrapper.rescue_templates
      end
    end

    def initialize(app, exceptions_app = nil)
      if [true, false].include?(exceptions_app)
        ActiveSupport::Deprecation.warn "Passing consider_all_requests_local option to ActionDispatch::ShowExceptions middleware no longer works"
        exceptions_app = nil
      end

      if exceptions_app.nil?
        raise ArgumentError, "You need to pass an exceptions_app when initializing ActionDispatch::ShowExceptions. " \
          "In case you want to render pages from a public path, you can use ActionDispatch::PublicExceptions.new('path/to/public')"
      end

      @app = app
      @exceptions_app = exceptions_app
    end

    def call(env)
      begin
        response  = @app.call(env)
      rescue Exception => exception
        raise exception if env['action_dispatch.show_exceptions'] == false
      end

      response || render_exception(env, exception)
    end

    private

    # Define this method because some plugins were monkey patching it.
    # Remove this after 3.2 is out with the other deprecations in this class.
    def status_code(*)
    end

    def render_exception(env, exception)
      wrapper = ExceptionWrapper.new(env, exception)
      status  = wrapper.status_code
      env["action_dispatch.exception"] = wrapper.exception
      env["PATH_INFO"] = "/#{status}"
      response = @exceptions_app.call(env)
      response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
    rescue Exception => failsafe_error
      $stderr.puts "Error during failsafe response: #{failsafe_error}\n  #{failsafe_error.backtrace * "\n  "}"
      FAILSAFE_RESPONSE
    end

    def pass_response(status)
      [status, {"Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0"}, []]
    end
  end
end

ShowExceptions在处理请求发生错误时,如果action_dispatch.show_exceptions设置为true,则启用@exceptions_app去处理这个异常,@exceptions_app默认为ActionDispatch::PublicExceptions对象,定义在actionpack-3.2.13/lib/action_dispatch/middleware/public_exceptions.rb中:

module ActionDispatch
  # A simple Rack application that renders exceptions in the given public path.
  class PublicExceptions
    attr_accessor :public_path

    def initialize(public_path)
      @public_path = public_path
    end

    def call(env)
      status      = env["PATH_INFO"][1..-1]
      locale_path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
      path        = "#{public_path}/#{status}.html"

      if locale_path && File.exist?(locale_path)
        render(status, File.read(locale_path))
      elsif File.exist?(path)
        render(status, File.read(path))
      else
        [404, { "X-Cascade" => "pass" }, []]
      end
    end

    private

    def render(status, body)
      [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
    end
  end
end

这个类从/public目录下寻找一个以错误号为文件名的html文件,如果能找到,返回该文件的内容,否则,返回一个404错误回去。

如果找不到错误号对应的文件,ShowExceptions就会回复原错误号和空信息回去。如果在PublicExceptions执行期间发生错误,就返回一段默认的500错误信息回去。

接下来一个Middleware依然和错误有关,DebugExceptions,定义在actionpack-3.2.13/lib/action_dispatch/middleware/debug_exceptions.rb中:

require 'action_dispatch/http/request'
require 'action_dispatch/middleware/exception_wrapper'

module ActionDispatch
  # This middleware is responsible for logging exceptions and
  # showing a debugging page in case the request is local.
  class DebugExceptions
    RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates')

    def initialize(app)
      @app = app
    end

    def call(env)
      begin
        response = @app.call(env)

        if response[1]['X-Cascade'] == 'pass'
          body = response[2]
          body.close if body.respond_to?(:close)
          raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
        end
      rescue Exception => exception
        raise exception if env['action_dispatch.show_exceptions'] == false
      end

      exception ? render_exception(env, exception) : response
    end

    private

    def render_exception(env, exception)
      wrapper = ExceptionWrapper.new(env, exception)
      log_error(env, wrapper)

      if env['action_dispatch.show_detailed_exceptions']
        template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
          :request => Request.new(env),
          :exception => wrapper.exception,
          :application_trace => wrapper.application_trace,
          :framework_trace => wrapper.framework_trace,
          :full_trace => wrapper.full_trace
        )

        file = "rescues/#{wrapper.rescue_template}"
        body = template.render(:template => file, :layout => 'rescues/layout')
        render(wrapper.status_code, body)
      else
        raise exception
      end
    end

    def render(status, body)
      [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
    end

    def log_error(env, wrapper)
      logger = logger(env)
      return unless logger

      exception = wrapper.exception

      trace = wrapper.application_trace
      trace = wrapper.framework_trace if trace.empty?

      ActiveSupport::Deprecation.silence do
        message = "\n#{exception.class} (#{exception.message}):\n"
        message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
        message << "  " << trace.join("\n  ")
        logger.fatal("#{message}\n\n")
      end
    end

    def logger(env)
      env['action_dispatch.logger'] || stderr_logger
    end

    def stderr_logger
      @stderr_logger ||= Logger.new($stderr)
    end
  end
end

这个Middleware提供一个相对比较好的用户体验,它和前一个Middleware ShowExceptions的相同之处在于,action_dispatch.show_exceptions的值为false。主要区别是,这个Middleware会输出调试信息,有可能泄露敏感信息,造成安全隐患,因此通常在服务器环境上不会启用,而ShowExceptions总是会启用的。不过就算ShowExceptions也没有启用,最后将会由Rack来负责对于错误信息的处理。

DebugExceptions由于显示动态页面,因此这里先创建了ActionView::Base对象,然后找到了template文件和layout文件,这些文件放在actionpack-3.2.13/lib/action_dispatch/middleware/templates/下,不同的错误信息会使用不一样的模版文件。然后调用render方法对也没进行渲染,最后返回。

下一个Middleware是ActionDispatch::RemoteIp,定义在actionpack-3.2.13/lib/action_dispatch/middleware/remote_ip.rb中:

module ActionDispatch
  class RemoteIp
    class IpSpoofAttackError < StandardError ; end

    # IP addresses that are "trusted proxies" that can be stripped from
    # the comma-delimited list in the X-Forwarded-For header. See also:
    # http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
    TRUSTED_PROXIES = %r{
      ^127\.0\.0\.1$                | # localhost
      ^(10                          | # private IP 10.x.x.x
        172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255
        192\.168                      # private IP 192.168.x.x
       )\.
    }x

    attr_reader :check_ip, :proxies

    def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
      @app = app
      @check_ip = check_ip_spoofing
      if custom_proxies
        custom_regexp = Regexp.new(custom_proxies)
        @proxies = Regexp.union(TRUSTED_PROXIES, custom_regexp)
      else
        @proxies = TRUSTED_PROXIES
      end
    end

    def call(env)
      env["action_dispatch.remote_ip"] = GetIp.new(env, self)
      @app.call(env)
    end

    class GetIp
      def initialize(env, middleware)
        @env          = env
        @middleware   = middleware
        @calculated_ip = false
      end

      # Determines originating IP address. REMOTE_ADDR is the standard
      # but will be wrong if the user is behind a proxy. Proxies will set
      # HTTP_CLIENT_IP and/or HTTP_X_FORWARDED_FOR, so we prioritize those.
      # HTTP_X_FORWARDED_FOR may be a comma-delimited list in the case of
      # multiple chained proxies. The last address which is not a known proxy
      # will be the originating IP.
      def calculate_ip
        client_ip     = @env['HTTP_CLIENT_IP']
        forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR')
        remote_addrs  = ips_from('REMOTE_ADDR')

        check_ip = client_ip && @middleware.check_ip
        if check_ip && !forwarded_ips.include?(client_ip)
          # We don't know which came from the proxy, and which from the user
          raise IpSpoofAttackError, "IP spoofing attack?!" \
            "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}" \
            "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}"
        end

        not_proxy = client_ip || forwarded_ips.last || remote_addrs.first

        # Return first REMOTE_ADDR if there are no other options
        not_proxy || ips_from('REMOTE_ADDR', :allow_proxies).first
      end

      def to_s
        return @ip if @calculated_ip
        @calculated_ip = true
        @ip = calculate_ip
      end

    protected

      def ips_from(header, allow_proxies = false)
        ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : []
        allow_proxies ? ips : ips.reject{|ip| ip =~ @middleware.proxies }
      end
    end

  end
end

RemoteIp负责设置Rails env中的action_dispatch.remote_ip项为一个GetIp类的对象,当这个对象的to_s方法被调用时,将会触发IpSpoof攻击检查。攻击检测方法是,检查Http Header中的CLIENT_IP项是否出现在X_FORWARDED_FOR项中,如果没有出现,判断为IpSpoof攻击,将抛出异常,否则将返回经过计算的Ip地址。

随即下一个Middleware通常只用于生产环境,那就是::Rack::Sendfile,它的使用依赖于Rails必须设置SendFile的Http Header,之所以这样做是因为这个Http Header并不是标准,需要前端的Web Server做特别支持,其中Lighttpd和Apache用的是X-Sendfile而Nginx用的是X-Accel-Redirect,需要用户根据生产环境实际情况设置。::Rack::Sendfile实现在rack-1.4.5/lib/rack/sendfile.rb

require 'rack/file'

module Rack

  # = Sendfile
  #
  # The Sendfile middleware intercepts responses whose body is being
  # served from a file and replaces it with a server specific X-Sendfile
  # header. The web server is then responsible for writing the file contents
  # to the client. This can dramatically reduce the amount of work required
  # by the Ruby backend and takes advantage of the web server's optimized file
  # delivery code.
  #
  # In order to take advantage of this middleware, the response body must
  # respond to +to_path+ and the request must include an X-Sendfile-Type
  # header. Rack::File and other components implement +to_path+ so there's
  # rarely anything you need to do in your application. The X-Sendfile-Type
  # header is typically set in your web servers configuration. The following
  # sections attempt to document
  #
  # === Nginx
  #
  # Nginx supports the X-Accel-Redirect header. This is similar to X-Sendfile
  # but requires parts of the filesystem to be mapped into a private URL
  # hierarachy.
  #
  # The following example shows the Nginx configuration required to create
  # a private "/files/" area, enable X-Accel-Redirect, and pass the special
  # X-Sendfile-Type and X-Accel-Mapping headers to the backend:
  #
  #   location ~ /files/(.*) {
  #     internal;
  #     alias /var/www/$1;
  #   }
  #
  #   location / {
  #     proxy_redirect     off;
  #
  #     proxy_set_header   Host                $host;
  #     proxy_set_header   X-Real-IP           $remote_addr;
  #     proxy_set_header   X-Forwarded-For     $proxy_add_x_forwarded_for;
  #
  #     proxy_set_header   X-Sendfile-Type     X-Accel-Redirect;
  #     proxy_set_header   X-Accel-Mapping     /var/www/=/files/;
  #
  #     proxy_pass         http://127.0.0.1:8080/;
  #   }
  #
  # Note that the X-Sendfile-Type header must be set exactly as shown above.
  # The X-Accel-Mapping header should specify the location on the file system,
  # followed by an equals sign (=), followed name of the private URL pattern
  # that it maps to. The middleware performs a simple substitution on the
  # resulting path.
  #
  # See Also: http://wiki.codemongers.com/NginxXSendfile
  #
  # === lighttpd
  #
  # Lighttpd has supported some variation of the X-Sendfile header for some
  # time, although only recent version support X-Sendfile in a reverse proxy
  # configuration.
  #
  #   $HTTP["host"] == "example.com" {
  #      proxy-core.protocol = "http"
  #      proxy-core.balancer = "round-robin"
  #      proxy-core.backends = (
  #        "127.0.0.1:8000",
  #        "127.0.0.1:8001",
  #        ...
  #      )
  #
  #      proxy-core.allow-x-sendfile = "enable"
  #      proxy-core.rewrite-request = (
  #        "X-Sendfile-Type" => (".*" => "X-Sendfile")
  #      )
  #    }
  #
  # See Also: http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModProxyCore
  #
  # === Apache
  #
  # X-Sendfile is supported under Apache 2.x using a separate module:
  #
  # https://tn123.org/mod_xsendfile/
  #
  # Once the module is compiled and installed, you can enable it using
  # XSendFile config directive:
  #
  #   RequestHeader Set X-Sendfile-Type X-Sendfile
  #   ProxyPassReverse / http://localhost:8001/
  #   XSendFile on

  class Sendfile
    F = ::File

    def initialize(app, variation=nil)
      @app = app
      @variation = variation
    end

    def call(env)
      status, headers, body = @app.call(env)
      if body.respond_to?(:to_path)
        case type = variation(env)
        when 'X-Accel-Redirect'
          path = F.expand_path(body.to_path)
          if url = map_accel_path(env, path)
            headers['Content-Length'] = '0'
            headers[type] = url
            body = []
          else
            env['rack.errors'].puts "X-Accel-Mapping header missing"
          end
        when 'X-Sendfile', 'X-Lighttpd-Send-File'
          path = F.expand_path(body.to_path)
          headers['Content-Length'] = '0'
          headers[type] = path
          body = []
        when '', nil
        else
          env['rack.errors'].puts "Unknown x-sendfile variation: '#{variation}'.\n"
        end
      end
      [status, headers, body]
    end

    private
    def variation(env)
      @variation ||
        env['sendfile.type'] ||
        env['HTTP_X_SENDFILE_TYPE']
    end

    def map_accel_path(env, file)
      if mapping = env['HTTP_X_ACCEL_MAPPING']
        internal, external = mapping.split('=', 2).map{ |p| p.strip }
        file.sub(/^#{internal}/i, external)
      end
    end
  end
end

从代码中可见,如果Response的Http Header中存在X-Sendfile-Type,且值为'X-Accel-Redirect',将认为服务器是Nginx,从Http Header中再取出'X-Accel-Mapping',对文件地址做Mapping,将internal的地址转换成external的地址。然后设置Header,将'X-Accel-Redirect'的值设置成实际文件再文件系统所在的路径,取出body的内容后返回,这样前端的Nginx服务器将会自动读取所在文件并且返回其内容。如果是'X-Sendfile'或是'X-Lighttpd-Send-File',则同样设置Header和返回空body的内容,但没有Mapping的过程,完成后将由前端的Apache或是Lighttpd完成剩余的文件传输工作。

下一个Middleware是ActionDispatch::Reloader,这个Middleware定义在actionpack-3.2.13/lib/action_dispatch/middleware/reloader.rb中:

require 'action_dispatch/middleware/body_proxy'

module ActionDispatch
  # ActionDispatch::Reloader provides prepare and cleanup callbacks,
  # intended to assist with code reloading during development.
  #
  # Prepare callbacks are run before each request, and cleanup callbacks
  # after each request. In this respect they are analogs of ActionDispatch::Callback's
  # before and after callbacks. However, cleanup callbacks are not called until the
  # request is fully complete -- that is, after #close has been called on
  # the response body. This is important for streaming responses such as the
  # following:
  #
  #     self.response_body = lambda { |response, output|
  #       # code here which refers to application models
  #     }
  #
  # Cleanup callbacks will not be called until after the response_body lambda
  # is evaluated, ensuring that it can refer to application models and other
  # classes before they are unloaded.
  #
  # By default, ActionDispatch::Reloader is included in the middleware stack
  # only in the development environment; specifically, when config.cache_classes
  # is false. Callbacks may be registered even when it is not included in the
  # middleware stack, but are executed only when +ActionDispatch::Reloader.prepare!+
  # or +ActionDispatch::Reloader.cleanup!+ are called manually.
  #
  class Reloader
    include ActiveSupport::Callbacks

    define_callbacks :prepare, :scope => :name
    define_callbacks :cleanup, :scope => :name

    # Add a prepare callback. Prepare callbacks are run before each request, prior
    # to ActionDispatch::Callback's before callbacks.
    def self.to_prepare(*args, &block)
      set_callback(:prepare, *args, &block)
    end

    # Add a cleanup callback. Cleanup callbacks are run after each request is
    # complete (after #close is called on the response body).
    def self.to_cleanup(*args, &block)
      set_callback(:cleanup, *args, &block)
    end

    # Execute all prepare callbacks.
    def self.prepare!
      new(nil).prepare!
    end

    # Execute all cleanup callbacks.
    def self.cleanup!
      new(nil).cleanup!
    end

    def initialize(app, condition=nil)
      @app = app
      @condition = condition || lambda { true }
      @validated = true
    end

    def call(env)
      @validated = @condition.call
      prepare!
      response = @app.call(env)
      response[2] = ActionDispatch::BodyProxy.new(response[2]) { cleanup! }
      response
    rescue Exception
      cleanup!
      raise
    end

    def prepare!
      run_callbacks :prepare if validated?
    end

    def cleanup!
      run_callbacks :cleanup if validated?
    ensure
      @validated = true
    end

    private

    def validated?
      @validated
    end
  end
end

这个Middleware定义了两个ActiveSupport::Callbacks对象,preparecleanup,分别在处理请求前和处理请求后执行。这个Middleware使用的条件是config.cache_classes为false,同时它在执行请求前会检查条件是否匹配,在Rails中,为了实现在启动时对connection和cache的清理,以及在开发环境下检测文件是否有更新,它的条件是config.reload_classes_only_on_change不等于true。

接着一个Middleware同样和Callback有关,它是ActionDispatch::Callbacks,定义在actionpack-3.2.13/lib/action_dispatch/middleware/callbacks.rb中:

require 'active_support/core_ext/module/delegation'

module ActionDispatch
  # Provide callbacks to be executed before and after the request dispatch.
  class Callbacks
    include ActiveSupport::Callbacks

    define_callbacks :call, :rescuable => true

    class << self
      delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader"
    end

    def self.before(*args, &block)
      set_callback(:call, :before, *args, &block)
    end

    def self.after(*args, &block)
      set_callback(:call, :after, *args, &block)
    end

    def initialize(app)
      @app = app
    end

    def call(env)
      run_callbacks :call do
        @app.call(env)
      end
    end
  end
end

它和前面的ActionDispatch::Reloader的主要区别是,后者尽量等到Middleware返回的body的close方法被执行时才调用cleanup操作,并且依赖一些Rails设置的条件。而前者在进入和退出当前Middleware是执行两个callback,执行没有条件约束。

下一个Middleware是ActiveRecord组件增加的功能,叫ActiveRecord::ConnectionAdapters::ConnectionManagement,定义在activerecord-3.2.13/lib/active_record/connection_adapters/abstract/connection_pool.rb中:

class ConnectionManagement
  class Proxy # :nodoc:
    attr_reader :body, :testing

    def initialize(body, testing = false)
      @body    = body
      @testing = testing
    end

    def method_missing(method_sym, *arguments, &block)
      @body.send(method_sym, *arguments, &block)
    end

    def respond_to?(method_sym, include_private = false)
      super || @body.respond_to?(method_sym)
    end

    def each(&block)
      body.each(&block)
    end

    def close
      body.close if body.respond_to?(:close)

      # Don't return connection (and perform implicit rollback) if
      # this request is a part of integration test
      ActiveRecord::Base.clear_active_connections! unless testing
    end
  end

  def initialize(app)
    @app = app
  end

  def call(env)
    testing = env.key?('rack.test')

    status, headers, body = @app.call(env)

    [status, headers, Proxy.new(body, testing)]
  rescue
    ActiveRecord::Base.clear_active_connections! unless testing
    raise
  end
end

这里实现了一个和BodyProxy相类似的Proxy类,同样在调用Middleware返回的body的close方法时才回调,目的是调用ActiveRecord::Base.clear_active_connections!以清理connections。

下一个Middleware是ActiveRecord::QueryCache,定义在activerecord-3.2.13/lib/active_record/query_cache.rb:

require 'active_support/core_ext/object/blank'

module ActiveRecord
  # = Active Record Query Cache
  class QueryCache
    module ClassMethods
      # Enable the query cache within the block if Active Record is configured.
      def cache(&block)
        if ActiveRecord::Base.connected?
          connection.cache(&block)
        else
          yield
        end
      end

      # Disable the query cache within the block if Active Record is configured.
      def uncached(&block)
        if ActiveRecord::Base.connected?
          connection.uncached(&block)
        else
          yield
        end
      end
    end

    def initialize(app)
      @app = app
    end

    class BodyProxy # :nodoc:
      def initialize(original_cache_value, target, connection_id)
        @original_cache_value = original_cache_value
        @target               = target
        @connection_id        = connection_id
      end

      def method_missing(method_sym, *arguments, &block)
        @target.send(method_sym, *arguments, &block)
      end

      def respond_to?(method_sym, include_private = false)
        super || @target.respond_to?(method_sym)
      end

      def each(&block)
        @target.each(&block)
      end

      def close
        @target.close if @target.respond_to?(:close)
      ensure
        ActiveRecord::Base.connection_id = @connection_id
        ActiveRecord::Base.connection.clear_query_cache
        unless @original_cache_value
          ActiveRecord::Base.connection.disable_query_cache!
        end
      end
    end

    def call(env)
      old = ActiveRecord::Base.connection.query_cache_enabled
      ActiveRecord::Base.connection.enable_query_cache!

      status, headers, body = @app.call(env)
      [status, headers, BodyProxy.new(old, body, ActiveRecord::Base.connection_id)]
    rescue Exception => e
      ActiveRecord::Base.connection.clear_query_cache
      unless old
        ActiveRecord::Base.connection.disable_query_cache!
      end
      raise e
    end
  end
end

实现的功能是在每次处理请求前开启query cache功能,在请求处理后恢复原设置。