解讀Rails – 處理異常

Martin91發表於2014-11-05

此文翻譯自Reading Rails – Handling Exceptions,限於本人水平,翻譯不當之處,敬請指教!

我們今天開始會讀一些Rails的原始碼。我們有雙重的目的,先通過學習(Rails)如何處理異常,再擴充套件到整個Ruby中基礎知識的學習。

Rails通過讓你使用rescue_from方法,讓你在你的controller裡邊為常見的異常定義處理方法。舉例來說吧,你可以在使用者試圖訪問他們尚未付費的功能時將他們重定向到指定的付費頁面。

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):

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物件,我們待會便會看到為什麼要這麼做。

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物件稍後被儲存在二元陣列中:

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”中的文章。

相關文章