在 Clojure 中处理异常

Update: As of Clojure 1.3, Clojure standardized the exception handling mechanism. Refer to [url=https://github.com/mmcgrana/clj-stacktrace]clj-stacktrace[/url]for 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,请参阅:[url=http://groups.google.com/group/clojure/browse_thread/thread/2d1d91693887a45b]ANN: clojure.contrib.error-kit[/url]。

但是如果你不需要 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 库的加强。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值