解读Rails - 处理异常

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

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

Rails通过让你使用rescue_from方法,让你在你的controller里边为常见的异常定义处理方法。举例来说吧,你可以在用户试图访问他们尚未付费的功能时将他们重定向到指定的付费页面。

1
2
3
4
5
6
7
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):

1
2
3
4
5
6
7
8
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)这样的调用。它们将会被存放在一个数组里。

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

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

1
2
3
4
5
6
7
8
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。

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

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

现在我们已经知道了处理器是如何储存的,但是当Rails需要处理异常的时候,它又是如何查找这些处理器的呢?

查找处理器(Finding Handlers)

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

1
2
3
4
5
6
7
8
9
10
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方法。当给它传递一个异常的时候,它将会尝试通过调用合适的处理器来处理这个异常。

1
2
3
4
5
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调用。

1
2
3
4
5
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。举个例子,假如你有很多发向远程服务的请求,并且你不想在每一个方法里边重复异常处理的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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里边挖掘到的新奇玩意。

喜欢这篇文章?

阅读更多“解读Rails”中的文章。“解读Rails”中的文章。

Comments