分享 [Rails] rescue_from :symbol or proc

hiveer · 2015年10月16日 · 2823 次阅读

最近在做重构的时候干了这样的一件事情,把

def resource_not_found
  ...
end

改成了

def resource_not_found message: nil, id: nil
  ...
end

然后像下面的这样的代码就会出现异常了

rescue_from Exception, with: :rescue_not_found

从图片可以看出,报错的代码是在 Rails 的源码中,也就是说我的代码违背了 Rails 的 convention。 直接原因是handler.call(exception)误传了参数,但是根据handler.arity != 0说明是需要参数的。奇怪? 其实对比一下我重构之后的方法也就比较明显了,重构之后resource_not_found需要接受 optional 的 keyword 参数。 而源码中明显是在按照 positional params 的方式在调用。 所以,出现了ArgumentError

这里引伸出一个问题,像上面重构的方法,不仅仅是用在rescue_from,也会在别的场景下单独调用。也就是说我们暂时认为这个重构是 ok 的,那么我们应该怎么处理rescue_from带来的问题呢?

根据源码来看

# Exceptions raised inside exception handlers are not propagated up.
    def rescue_from(*klasses, &block)
      options = klasses.extract_options!

      unless options.has_key?(:with)
        if block_given?
          options[:with] = block
        else
          raise ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument."
        end
      end

      klasses.each do |klass|
        key = if klass.is_a?(Class) && klass <= Exception
          klass.name
        elsif klass.is_a?(String)
          klass
        else
          raise ArgumentError, "#{klass} is neither an Exception nor a String"
        end

        # put the new handler at the end because the list is read in reverse
        self.rescue_handlers += [[key, options[:with]]]
      end
    end
# Tries to rescue the exception by looking up and calling a registered handler.
    def rescue_with_handler(exception)
      if handler = handler_for_rescue(exception)
        handler.arity != 0 ? handler.call(exception) : handler.call
        true # don't rely on the return value of the handler
      end
    end

    def handler_for_rescue(exception)
      # We go from right to left because pairs are pushed onto rescue_handlers
      # as rescue_from declarations are found.
      _, rescuer = self.class.rescue_handlers.reverse.detect do |klass_name, handler|
        # The purpose of allowing strings in rescue_from is to support the
        # declaration of handler associations for exception classes whose
        # definition is yet unknown.
        #
        # Since this loop needs the constants it would be inconsistent to
        # assume they should exist at this point. An early raised exception
        # could trigger some other handler and the array could include
        # precisely a string whose corresponding constant has not yet been
        # seen. This is why we are tolerant to unknown constants.
        #
        # Note that this tolerance only matters if the exception was given as
        # a string, otherwise a NameError will be raised by the interpreter
        # itself when rescue_from CONSTANT is executed.
        klass = self.class.const_get(klass_name) rescue nil
        klass ||= klass_name.constantize rescue nil
        exception.is_a?(klass) if klass
      end

      case rescuer
      when Symbol
        method(rescuer)
      when Proc
        if rescuer.arity == 0
          Proc.new { instance_exec(&rescuer) }
        else
          Proc.new { |_exception| instance_exec(_exception, &rescuer) }
        end
      end
    end
  end

根据rescue_from的源码来看,我们会得到一个数组,[[key, options[:with]]。这里的key是 Exception 的名字,如 “ArgumentError”;options[:with]就是我们在调用rescue_from的时候传给:with的值,如果是 block 的话那么就是 block 本身。

所以在handler_for_rescue中,根据options[:with]是 Symbol 还是 Block 的不同,会有不同的处理方式来得到 handler。如果是 Symbol,那么直接返回了method(rescuer),如果是 Block 那么回重新用 Proc 来包装。

rescue_with_handler中会根据handler.arity != 0来决定 handler 的调用方式。根据上面的分析,当我们以 Symbol 的方式来调用,我们会得到这样的 handler,method(:resource_not_found), 而method(:resource_not_found).arity = -1, 所以会采用`handler.call(exception) 的调用方式,那么错误也就必然了。 但是如果我们用 Block 的方式呢,像

rescue_from Exception, with: -> { resource_not_found }

#这里的rescuer就是:with后面的 block 那么我们得到的 handler 会是, Proc.new { instance_exec(&rescuer) },并且(Proc.new { instance_exec(&rescuer) }).arity == 0,所以会采用handler.call的方式来调用,那么就可以避免这个错误了。

FYI

上面的这些分析我是基于说我的这个重构是可以接受的,那么可以用 Block 的方式来避免报错。但是这个重构真的是否能接受,还希望大家给点意见!

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号