解讀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」中的文章。

相關文章
相關標籤/搜索