May 15, 2022
By: Kevin

luminus模版生成的项目中断点无效

  1. luminus模版生成的项目中断点无效
    1. 描述
      1. 重现
    2. 问题分析
      1. 不要慌
      2. 从原理出发
      3. 发现
      4. 有这么一个中间件
      5. 最终的答案
    3. 更深入的了解
      1. Clojure的var
      2. Clojure的reader
      3. Clojure web开发
      4. 编辑器cider/calva都做了什么

luminus模版生成的项目中断点无效

描述

有些同学提到Clojure项目中, 设置断点后无效, 所以尝试复现这个问题.

重现

  • 创建一个全新的后台服务开始 lein new luminus debug-test +service

  • 默认生成的模版项目中, 在/plus这个路由下挂载的函数, 增加debug

    #dbg
    (defn plus [x y]
      (debug "plus working....")
      (+ x y))
    

    手动eval这个函数后通过swagger测试plus接口, 确实没有断住.

问题分析

不要慌

事必有因

从原理出发

函数是一个java对象, 此对象实现了IFn接口, 以inc函数为例

(ancestors (class inc))

=> #{java.lang.Runnable
     clojure.lang.IObj
     java.io.Serializable
     java.util.concurrent.Callable
     clojure.lang.IFn
     clojure.lang.AFn
     java.util.Comparator
     java.lang.Object
     clojure.lang.AFunction
     clojure.lang.Fn
     clojure.lang.IMeta}

在repl里我们能看到这个对象, 对象的toString能看到该对象的hash值:

(defn plus [x y]
  (debug "plus working....")
  (+ x y))

(.toString plus)
=> "debug_test.routes.services$plus@7af7a9d"

可以debug的func是一个经过特殊eval的func, 替代到默认的func版本

#dbg
(defn plus [x y]
  (debug "plus working....")
  (+ x y))

(.toString plus)
=> "debug_test.routes.services$eval24320$plus__24321@7231d787"

发现

在接口请求以后, 我们的plus函数对应的对象又变了!!

(.toString plus)
=> "debug_test.routes.services$plus@2fba93e5"

有这么一个中间件

在文件 dev_middleware.clj 中, 有这么一段:

(defn wrap-dev [handler]
  (-> handler
      wrap-reload   ;; wrap-reload 的作用是在src目录下的文件更新后, 自动relaod这个ns
      wrap-error-page
      (wrap-exceptions {:app-namespaces ['debug-test]})))

每一个http请求, 会触发wrap-reload 的源代码目录中修改文件的重新编译. 带来的好处是, 只要代码改了, 你一定能看到修改的效果. 坏处显而易见, debug 没有起到作用

(defn wrap-reload
  "Reload namespaces of modified files before the request is passed to the
  supplied handler.

  Accepts the following options:

  :dirs                   - A list of directories that contain the source files.
                            Defaults to [\"src\"].
  :reload-compile-errors? - If true, keep attempting to reload namespaces
                            that have compile errors.  Defaults to true."
  ([handler]
   (wrap-reload handler {}))
  ([handler options]
   (let [reload! (reloader (:dirs options ["src"])
                           (:reload-compile-errors? options true))]
     (fn
       ([request]
        (reload!)
        (handler request))
       ([request respond raise]
        (reload!)
        (handler request respond raise))))))

最终的答案

所以, 解决这个问题, 只要不触发中间件的编译就好了

  1. 不用中间件, 把中间件这一行删掉
  2. 以不触发中间件的方式(不修改文件)生成调试调试编译(debug compiling)

更深入的了解

下面的每一个小节都值得一片长文, 放在这里仅仅是个路标.

Clojure的var

参见blog: 理解clojure var

(def a 1)

(type a)
;; => java.lang.Long

(type #'a)
;; => clojure.lang.Var

(symbol? 'a)
;; => false

Clojure的reader

同其他lisp一样, reader是clojure显著的一个特性, 甚至说repl就是reader的副产品

  1. reader tag

    #dbg 和 #break是cider中引入的新reader tag 就像clojure语言已经内置的几个reader tag一样:比如

    1. #inst : 时间的字符串表示
    2. #uuid : uuid字符串表示
    3. #? : 条件编译
    @(def a
       #?(:clj "clj")
       #?(:cljs "clojurescript"))
    ;; => "clj"
    
    [1 2 #?@(:clj [3 4] :cljs [5 6])]
    ;; => [1 2 3 4]
    
    {:date #inst "2022-05-14T11:53:07.699-00:00"
     :uuid #uuid "2700cb34-04bc-448a-8a48-bd4f42fd2cbf" }
    
  2. 用reader实现一个debugger

    这是来自 The Joy of Clojure 的一个例子, 读懂这个例子要理解clojure的macro.

    (defn contextual-eval [ctx expr]
      (eval
       `(let [~@(mapcat (fn [[k v]] [k `'~v]) ctx)]
          ~expr)))
    
    
    (defmacro local-context []
      (let [symbols (keys &env)]
        (zipmap (map (fn [sym] `(quote ~sym))
                     symbols)
                symbols)))
    
    (defn readr [prompt exit-code]
      (let [input (clojure.main/repl-read prompt exit-code)]
        (if (= input ::tl)
          exit-code
          input)))
    
    (defmacro break []
      `(clojure.main/repl
        :prompt #(print "debug=> ")
        :read readr
        :eval (partial contextual-eval (local-context))))
    

    接下来我们使用这个自定义的调试器

    (defn div [n d] (break) (int (/ n d)))
    (div 10 0)
    
    debug=> n
    ;;=> 10
    debug=> d
    ;;=> 0
    debug=> (local-context)
    ;;=> {n 10, d 0}
    

Clojure web开发

  1. luminus 模版

    非常方便的创建各色clojure/clojureScript项目 链接

  2. ring的中间件

    每一个http请求, 都会触发一些列的中间件操作

编辑器cider/calva都做了什么

以emacs为例, 快捷键 M-x cider-debug-defun-at-point 的工作 读一下cider的源代码, 就是在debug的函数外面套了个 #dbg, 没啥花里胡哨的.

(defun cider-eval-defun-at-point (&optional debug-it)
  "Evaluate the current toplevel form, and print result in the minibuffer.
With DEBUG-IT prefix argument, also debug the entire form as with the
command `cider-debug-defun-at-point'."
  (interactive "P")
  (let ((inline-debug (eq 16 (car-safe debug-it))))
    (when debug-it
      (when (derived-mode-p 'clojurescript-mode)
        (when (y-or-n-p (concat "The debugger doesn't support ClojureScript yet, and we need help with that."
                                "  \nWould you like to read the Feature Request?"))
          (browse-url "https://github.com/clojure-emacs/cider/issues/1416"))
        (user-error "The debugger does not support ClojureScript"))
      (when inline-debug
        (cider--prompt-and-insert-inline-dbg)))
    (cider-interactive-eval (when (and debug-it (not inline-debug))
                              (concat "#dbg\n" (cider-defun-at-point)))
                            nil
                            (cider-defun-at-point 'bounds)
                            (cider--nrepl-pr-request-map))))
Tags: clojure luminus