首先调用的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模式下才启动,它在启动时require
了ruby-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::Engine
的call
方法,定义在railties-3.2.13/lib/rails/engine.rb
中,这个方法第一次将之前所有在它上面注册过的Middleware build成链表,设置endpoint,并调用middleware处理请求:
def call(env)
app.call(env.merge!(env_config))
end
这里的env_config
在Rails::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_stack
和config.middleware
merge在一起。注意,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.rb
的ActionDispatch::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增加Location
和Content-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
对象,prepare
和cleanup
,分别在处理请求前和处理请求后执行。这个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功能,在请求处理后恢复原设置。