在 Clojure 中处理异常

Update: As of Clojure 1.3, Clojure standardized the exception handling mechanism. Refer to clj-stacktracefor the details.


Clojure 中虽然使用了 Java 的异常处理机制。但是,Clojure 很难自然地自定义自己的异常。我在与 Java 类库进行交互就时恰恰遇到了这种需求。下面的代码是与 svn-kit 进行交互的代码,它们提供了 svn-kit 的一个 wrapper。
(defmacro- try-catch-svn-ex [& exprs]
  `(try ~@exprs
    (catch org.tmatesoft.svn.core.SVNAuthenticationException e#
      :auth-ex)
    (catch org.tmatesoft.svn.core.SVNException e#
      (if (re-matches #".*404 Not Found.*" (.getMessage e#))
        nil
        (throw e#)))))

(defn svn-get-file! [svn-repo file-path local-file]
  (with-open [os (output-stream (file local-file))]
    (try-catch-svn-ex
      (.getFile svn-repo file-path -1 (SVNProperties.) os)
      local-file)))

调用 svn-get-file! 时可能会出现用户名密码无效的问题,这时候我希望能给用户重新输入的机会。但是又不想被其它的异常干扰。这时候我可以选择将 SVNAuthenticationException 暴露出去,但是明显捕获这样一个异常是很让外层函数头疼的事。同时,自定义 Clojure 异常在外部捕获更让人头疼。所以,我在捕获了 SVNAuthenticationException 后返回一个 :auth-ex。

这种异常处理机制的最大的问题就是回到 C 语言时代检查函数返回值的方式上。这种方式写出来的程序会比较繁琐。最好的办法是用 Stuart Chouser 写的 clojure.contrib.error-kit 库。它提供了类似 Common Lisp 的异常处理体系。比传统的 try...catch 要强大很多。现在,我用 error-kit 库重写上面的函数:
(require '[clojure.contrib.error-kit :as ek])

(ek/deferror *svn-auth-error* [] [msg]
  (:msg msg)
  (:unhandled (ek/throw-msg Exception)))
    
(defmacro- try-catch-svn-ex [& exprs]
  `(try ~@exprs
    (catch org.tmatesoft.svn.core.SVNAuthenticationException e#
      (ek/raise *svn-auth-error* (.getMessage e#)))
    (catch org.tmatesoft.svn.core.SVNException e#
      (if (re-matches #".*404 Not Found.*" (.getMessage e#))
        nil
        (throw e#)))))

(defn svn-get-file! [svn-repo file-path local-file]
  (with-open [os (output-stream (file local-file))]
    (try-catch-svn-ex
      (.getFile svn-repo file-path -1 (SVNProperties.) os)
      local-file)))

注意我用 raise 调用代替了 :auth-ex 返回值。如果捕获到了权限异常,那么我们就 raise 一个 error。这个 error 必须用 deferror 函数定义。这个 *svn-auth-error* 在没有处理函数来处理它时会通过 throw-msg 调用抛出 Exception 异常,异常的消息内容就是 :msg 所指定的消息。

注意 *svn-auth-error* 后面的第一个括号表示“父”error 是谁。这个父子关系内部通过标准库的 derive 方法定义。这里它没有父 error,所以留空。这时调用 svn-get-file! 的函数就可以拿到这个 error,可以选择让栈爆掉,也可以选择在异常抛出点继续执行。这里我们选择简单地处理后重新执行函数:
(defn svn-get-file-ex! [svn-repo file-path local-file]
  (let [ret (ek/with-handler
              (svn-get-file! svn-repo file-path local-file)
              (ek/handle *svn-auth-error* [msg]
                (println (str "Error getting " file-path ", authentication failed"))
                (rm-scm-repo-username!)
                (rm-scm-repo-password!)
                (get-scm-repo-username!)
                (get-scm-repo-password!)
                (svn-get-file-ex! (get-scm-repo) file-path local-file)))]
    (if
      (nil? ret)
        (ek/raise *get-scm-file-error* (str "404 not found: " file-path))
      ret)))

注意此时对 svn-get-file-ex! 的递归调用不能用 recur。很遗憾,可能是因为 with-handler 或 handle 宏展开后定义了新的函数或者 loop。同时也请注意 deferror 时的 :unhandled 后面的 throw-msg 不要用 (throw (Exception. msg)) 来代替。如果这样做,你会发现异常是抛出去了,但是却捕获不到。原因是 :unhandled 后面期望跟的是一个函数定义。具体可以参看 throw-msg 的实现。

更多关于 error-kit 的信息,比如 continue,请参阅: ANN: clojure.contrib.error-kit。

但是如果你不需要 error-kit 里的 continue 相关的功能的话,也可以使用 clojure.contrib.condition。这个库比较容易使用。而且还带了一个 print-stack-trace 方法,可以打印出比较干净的栈。示例可以参看 contrib 库源代码里面的 example 目录中的 condition/example.clj。

这两种库实现上都利用 Java 的异常来跳出栈。所以,如果你想捕获所有的异常,包括这两种库抛出来的,可以用 catch Throwable。值得一提的是,condition 库的 print-stack-trace 是通用的。不仅可以打印 condition 库抛出来的异常,也可以打印其它的异常。

contrib 库中还有一个 except,也是用来处理异常的。作者跟 condition 库是一个人。根据作者的原话,condition 库是 except 库的加强。

你可能感兴趣的:(在 Clojure 中处理异常)