解读Rails - 处理异常

此文翻译自Reading Rails - Handling Exceptions,限于本人水平,翻译不当之处,敬请指教!html

咱们今天开始会读一些Rails的源码。咱们有双重的目的,先经过学习(Rails)如何处理异常,再扩展到整个Ruby中基础知识的学习。
git

Rails经过让你使用rescue_from方法,让你在你的controller里边为常见的异常定义处理方法。举例来讲吧,你能够在用户试图访问他们还没有付费的功能时将他们重定向到指定的付费页面。github

class ApplicationController
  # Redirect users if they try to use disabled features.
  rescue_from FeatureDisabledError, InsufficientAccessError do |ex|
    flash[:alert] = "Your account does not support #{ex.feature_name}"
    redirect_to "/pricing"
  end
  #...

咱们将会探索Rails是如何定义异常处理器,如何将它们与具体的异常进行匹配,以及如何使用它们去rescue失败的action。编程

若是须要跟着个人步骤走,请使用qwandry打开每个相关的代码库,或者直接从github查看源码便可。数组

定义处理器(Handlers)

ActiveSupport包含了一个用于定义异常如何被处理的模块Rescuable。第一个须要了解的方法就是rescue_from。这个方法经过方法名或者代码块为你想rescue的异常注册处理器(提示:查看代码,请在命令行中输入qw activesupport):ruby

def rescue_from(*klasses, &block)
  options = klasses.extract_options!

  unless options.has_key?(:with)
    if block_given?
      options[:with] = block
    else
      #...

首先,*klasses接收数量不定的异常类,因此你能够进行相似rescue_from(FeatureDisabledError, InsufficientAccessError)这样的调用。它们将会被存放在一个数组里。less

接下来,请留意extract_options!的使用。这是一个常见的用于从一个数组生成一个options哈希表的技巧。假如klasses里边的最后一个元素是一个哈希表,那么这个元素会被弹出数组。如今Rails将会使用:with项所指定的方法,或者是使用传递给rescue_from的代码块。Rails中的这种技巧创造了一个灵活的接口。ide

接着继续往下看这个方法,咱们看到每个异常类都被转换成一个String对象,咱们待会便会看到为何要这么作。学习

def rescue_from(*klasses, &block)
  #...
    key = if klass.is_a?(Class) && klass <= Exception
      klass.name
    elsif klass.is_a?(String)
      klass
    else
  #...

这里你应该注意的是,Rails是如何断定klass是否是继承自Exception的。一般状况下,你可能会经过使用obj.is_a?(Exception)来判断一个对象是否是某一个具体类型的实例,即便如此,klass并非Exception,而只是Class。那么咱们又怎么找出它使哪一类呢?Ruby在Module上定义了相似<=这样的用于比较的操做符。当操做符左边的对象是操做符右边对象的子类的时候,它会返回true。举个例子,ActiveRecord::RecordNotFound < Exception返回true,而ActiveRecord::RecordNotFound > Exception返回false。ui

在这个方法的末尾,咱们看到表示异常类的String对象稍后被储存在二元数组中:

def rescue_from(*klasses, &block)
  #...
  self.rescue_handlers += [[key, options[:with]]]
end

如今咱们已经知道了处理器是如何储存的,可是当Rails须要处理异常的时候,它又是如何查找这些处理器的呢?

查找处理器(Finding Handlers)

通过对rescue_handlers的快速搜索发现,这一切使用到了handler_for_rescue。咱们能够看到每个可能的处理器都被一一检查,直到咱们找到可以与exception匹配的处理器:

def handler_for_rescue(exception)
  # 咱们遵循从右到左的顺序,是由于每当发现一个rescue_from声明的时候,
  # 相应的klass_name, handler对就会被压入resuce_handlers里。
  _, rescuer = self.class.rescue_handlers.reverse.detect do |klass_name, handler|
    #...
    klass = self.class.const_get(klass_name) rescue nil
    klass ||= klass_name.constantize rescue nil
    exception.is_a?(klass) if klass
  end
  #...

如同注释所言,rescue_handlers被反序读取。假若有两个处理器可以处理同一个异常,那么最后定义的处理器会被优先选中。假如你先定义了一个针对ActiveRecord::NotFoundError异常的处理器,接着又定义了针对Exception异常的处理器,那么前者将永远都不会被调用,由于针对Exception的处理器老是会优先匹配。

如今,在代码块里边,又发生了什么呢?

首先,字符串对象klass_name被当作当前类内部的常量进行查找,在找不到的状况下会继续判断它是否是定义在程序内部其余地方的常量,以此将klass_name转换为实际的类。每一步都经过返回nil进行rescue。这么作的一个缘由就是当前处理器多是针对某个还没有加载的异常的类型。举例来讲,一个插件里可能为ActiveRecord::NotFoundError定义了错误处理,可是你可能并无使用ActiveRecord。在这样的状况下,引用这个异常将会致使异常。每一行最后的rescue nil可以在没法找到类时无声无息地组织异常的抛出。

最后咱们检查这个异常(等待匹配的异常)是不是这个处理器所对应异常类的实例。若是是,数组[klass_name, handler]将会被返回。返回到上边看看_, rescuer = ...这一行代码,这一一个数组拆分的例子。由于咱们实际上只想要返回数组的第二个元素,也就是处理器,因此_在这里只是一个占位符。

处理异常(Rescuing Exceptions)

如今咱们知道了程序是如何查找异常处理器的,可是它又是如何被调用的呢?为了回答这最后一个问题,咱们能够返回到源代码文件的顶部而后探索一下rescue_with_handler方法。当给它传递一个异常的时候,它将会尝试经过调用合适的处理器来处理这个异常。

def rescue_with_handler(exception)
  if handler = handler_for_rescue(exception)
    handler.arity != 0 ? handler.call(exception) : handler.call
  end
end

为了了解这个方法是如何在你的controller里边生效的,咱们须要查看ActionPack包里边的代码。(提示:能够在命令行中键入qw actionpack打开ActionPace的代码)Rails定义了一个叫作ActionController::Rescue的中间件,它被混入到了Rescuable模块里边,而且经过precess_action调用。

def process_action(*args)
  super
rescue Exception => exception
  rescue_with_handler(exception) || raise(exception)
end

Rails在收到每个请求时都会调用process_action,假如请求致使一个异常即将被抛出,rescue_with_handler都会试图去处理这个异常。

在Rails以外使用Rescuable(Using Rescuable Outside of Rails)

Rescuable可以被混入到其它代码之中。假如你想集中化你的异常处理部分的逻辑,那么你能够考虑一下使用Rescuable。举个例子,假如你有不少发向远程服务的请求,而且你不想在每个方法里边重复异常处理的逻辑:

class RemoteService
  include Rescuable

  rescue_from Net::HTTPNotFound, Net::HTTPNotAcceptable do |ex|
    disable_service!
    log_http_failure(@endpoint, ex)
  end

  rescue_from Net::HTTPNetworkAuthenticationRequired do |ex|
    authorize!
  end

  def get_status
    #...
  rescue Exception => exception
    rescue_with_handler(exception) || raise(exception)
  end

  def update_status
    #...
  rescue Exception => exception
    rescue_with_handler(exception) || raise(exception)
  end

end

使用一点元编程的技巧,你甚至能够经过相似的模式对已有的方法进行封装以免rescue代码块。

总结(Recap)

ActiveSupport的Rescuable模块容许咱们定义异常处理方法。ActionController的Rescue中间件捕捉异常,并试图处理这些异常。
咱们也同时了解到:

  • 一个签名相似rescue_from(*klasses)的方法能够接收数量不定的参数。
  • Array#extract_options!方法是一个用于从arguments数组获得options的技巧。
  • 你能够经过相似klass <= Exception这样的代码判读一个类是否某个类的子类。
  • rescue nil将会静默地消除异常。

就算是再小的代码片断都包含了很是多有用的信息,请让我知道你下一步想要了解什么东西,咱们还会看到可以从Rails里边挖掘到的新奇玩意。

喜欢这篇文章?

阅读另外8篇“解读Rails”中的文章。

相关文章
相关标签/搜索