以下例子摘自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.html

可见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