ActiveSupport::Rescuable
以下例子摘自Agile Web Development with Rails 5的Iteration E2
class CartsController < ApplicationController
binding.trace_tree(html: true, tmp: ['rails', 'rescue_from.html']) do
rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart
end
#...
end
完整调用栈如下
可见rescue_from是来自activesupport-5.0.2/lib/active_support/rescuable.rb
此方法极其简单,就是将Error类型作为key,with作为value,暂存到rescue_handlers,以备匹配Error然后调用,其中with可以是方法名或block
module ActiveSupport
# Rescuable module adds support for easier exception handling.
module Rescuable
extend Concern
included do
class_attribute :rescue_handlers
self.rescue_handlers = []
end
module ClassMethods
def rescue_from(*klasses, with: nil, &block)
unless with
if block_given?
with = block
else
raise ArgumentError, 'Need a handler. Pass the with: keyword argument or provide a block.'
end
end
klasses.each do |klass|
key = if klass.is_a?(Module) && klass.respond_to?(:===)
klass.name
elsif klass.is_a?(String)
klass
else
raise ArgumentError, "#{klass.inspect} must be an Exception class or a String referencing an Exception class"
end
# Put the new handler at the end because the list is read in reverse.
self.rescue_handlers += [[key, with]]
end
end
end
end
end
不过,这里并不会重定义方法以作拦截。rescue_handlers的使用是由其他地方发起的
于是,检查下invalid_cart的caller
From: /home/z/test_rails/depot/app/controllers/carts_controller.rb @ line 81 CartsController#invalid_cart:
79: def invalid_cart
80: binding.pry
=> 81: logger.error "Attempt to access invalid cart #{params[:id]}"
82: redirect_to store_index_url, notice: 'Invalid cart'
83: end
[1] pry(#)> _bs_
=> [#<binding:70245290801400 cartscontroller#invalid_cart="" home="" z="" test_rails="" depot="" app="" controllers="" carts_controller.rb:80="">,
#<binding:70245290186860 cartscontroller.block="" in="" handler_for_rescue="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activesupport-5.0.2="" lib="" active_support="" rescuable.rb:101="">,
#<binding:70245289547340 cartscontroller.rescue_with_handler="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activesupport-5.0.2="" lib="" active_support="" rescuable.rb:89="">,
#<binding:70245289177500 cartscontroller#rescue_with_handler="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activesupport-5.0.2="" lib="" active_support="" rescuable.rb:158="">,
#<binding:70245286947220 cartscontroller#rescue="" in="" process_action="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionpack-5.0.2="" lib="" action_controller="" metal="" rescue.rb:23="">,
#<binding:70245286435320 cartscontroller#process_action="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionpack-5.0.2="" lib="" action_controller="" metal="" rescue.rb:20="">,
#<binding:70244861574420 cartscontroller#block="" in="" process_action="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionpack-5.0.2="" lib="" action_controller="" metal="" instrumentation.rb:32="">,
#<binding:70244861528940 activesupport::notifications.block="" in="" instrument="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activesupport-5.0.2="" lib="" active_support="" notifications.rb:164="">,
#<binding:70244947270040 activesupport::notifications::instrumenter#instrument="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activesupport-5.0.2="" lib="" active_support="" notifications="" instrumenter.rb:21="">,
#<binding:70244947247960 activesupport::notifications.instrument="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activesupport-5.0.2="" lib="" active_support="" notifications.rb:164="">,
#<binding:70244947227060 cartscontroller#process_action="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionpack-5.0.2="" lib="" action_controller="" metal="" instrumentation.rb:30="">,
#<binding:70244947205700 cartscontroller#process_action="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionpack-5.0.2="" lib="" action_controller="" metal="" params_wrapper.rb:248="">,
#<binding:70244947184720 cartscontroller#process_action="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" railties="" controller_runtime.rb:18="">,
#<binding:70244947161340 cartscontroller#process="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionpack-5.0.2="" lib="" abstract_controller="" base.rb:126="">,
#<binding:70244861475780 cartscontroller#process="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionview-5.0.2="" lib="" action_view="" rendering.rb:30="">,
#<binding:70244861446440 cartscontroller#dispatch="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionpack-5.0.2="" lib="" action_controller="" metal.rb:190="">,
#<binding:70244861408100 cartscontroller.dispatch="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionpack-5.0.2="" lib="" action_controller="" metal.rb:262="">,
#...</binding:70244861408100></binding:70244861446440></binding:70244861475780></binding:70244947161340></binding:70244947184720></binding:70244947205700></binding:70244947227060></binding:70244947247960></binding:70244947270040></binding:70244861528940></binding:70244861574420></binding:70245286435320></binding:70245286947220></binding:70245289177500></binding:70245289547340></binding:70245290186860></binding:70245290801400>
首先,rescue_with_handler是include Rescuable而得的,没什么好看,但值得注意的是,Rescuable中handler的查找是reverse_each,即是后定义的优先
def find_rescue_handler(exception)
if exception
# Handlers are in order of declaration but the most recently declared
# is the highest priority match, so we search for matching handlers
# in reverse.
_, handler = rescue_handlers.reverse_each.detect do |class_or_name, _|
if klass = constantize_rescue_handler_class(class_or_name)
klass === exception
end
end
handler
end
end
再往上看,rescue关键字出现在ActionController::Rescue
module ActionController #:nodoc:
# This module is responsible for providing `rescue_from` helpers
# to controllers and configuring when detailed exceptions must be
# shown.
module Rescue
extend ActiveSupport::Concern
include ActiveSupport::Rescuable
private
def process_action(*args)
super
rescue Exception => exception
request.env['action_dispatch.show_detailed_exceptions'] ||= show_detailed_exceptions?
rescue_with_handler(exception) || raise
end
end
end
猜想是ApplicationController会include各种各样的module,然后层层super,以达到一种数据流的效果
检查controller的继承链,会发现就是这样
[7] pry(#)> puts self.class.ancestors
CartsController
#
ApplicationController
#
#
#
ActionController::Base
#...
ActionController::Rescue
#...
ActiveSupport::Rescuable
#...
AbstractController::Base
#...
Kernel
BasicObject
=> nil
在actionpack-5.0.2/lib/action_controller/base.rb中:
MODULES = [
#...
Cookies,
Flash,
#...
# Append rescue at the bottom to wrap as much as possible.
Rescue,
]
MODULES.each do |mod|
include mod
end
总结起来,rescue_from这种做法的目的是,使编写controller时无需为每个action重复rescue,因为action是用process_action来动态调用各种action的,这样就可以统一地rescue