在 Emacs 中开发 Java 一直以来的首选都是 lsp-java,但我不喜欢 company 的庞大,不喜欢 lsp-java 依赖众多。因此选择了 eglot 作为 lsp 客户端,加上 elgot-java 来简化配置,再加上 dape 来进行 debug。
这一套在 SpringBoot 下使用没什么问题,SpringBoot 内置了一些 servlet 容器,默认是 Tomcat。启动 dape 选择 jdtls 调用 dape 中默认的 dape-configs 直接启动内置的 Tomcat 是没有什么问题的。
但很多公司的项目还停留在 JDK 7 和 8上,使用的还是 Spring 框架 + 外置的 servlet 容器。这个时候本地环境的 JDK 版本众多,也不能直接拉起 Tomcat,但其实在 IDEA 当中是可以做到一键启动的,因此模仿其流程写了一些函数来照顾到低版本 JDK + 外置 servlet 容器的情况(仅包含 Tomcat)。
如何在 Emacs 中利用 dape 进行 Junit debug
先讲这个是因为后续外置 tomcat 的原理和这个类似。eglot-java 中提供了 eglot-java-run-test 运行 Junit 测试,查看函数定义可知,传入参数 t 即可开启 debug 模式,默认的端口是 8000。
那么我们只需要配置 dape-configs attach 到 Junit debug 的进程就可以了。参考 https://github.com/svaante/dape/issues/108。
(add-to-list 'dape-configs
`(jdtls-extra
modes (java-mode java-ts-mode)
fn (lambda (config)
(with-current-buffer
(find-file-noselect (expand-file-name (plist-get config :program)
(project-root (project-current))))
(thread-first
config
(plist-put 'hostname "localhost")
(plist-put 'port (eglot-execute-command (eglot-current-server)
"vscode.java.startDebugSession" nil))
(plist-put :projectName (project-name (project-current))))))
:program dape-buffer-default
:request "attach"
:hostname "localhost"
:port 8000))
外置 tomcat 启动及 debug
外置 tomcat 的原理其实和上面 Junit 的思路一直,也就是 dape attach 到 tomcat 进程,和上面的使用 dape-configs 配置一致。
但 Tomcat 需要 JPDA 模式启动,才能进行 debug 调试,不建议直接修改 Tomcat 本身自带的脚本,我们可以通过新增 setenv.sh 脚本放在 $TOMCAT_HOME/bin 下。(这里的 8000 端口是因为 Junit 默认的端口是 8000,就顺手也用了这个端口。)
#!/bin/sh
JPDA_TRANSPORT="dt_socket"
JPDA_ADDRESS="8000" # Custom port, for example, 8000
JPDA_SUSPEND="n"
# Combine debug options
JPDA_OPTS="-agentlib:jdwp=transport=$JPDA_TRANSPORT,address=$JPDA_ADDRESS,server=y,suspend=$JPDA_SUSPEND"
# Append to Tomcat startup options
CATALINA_OPTS="$CATALINA_OPTS $JPDA_OPTS"
这样我们就可以在调用 $TOMCAT_HOME/bin/startup.sh 时加上 jpda start 启动,因此下面的函数中我也增加了 debug 参数,传入 t 的时候自动加上 jpda start 拉起 Tomcat。
确认 Tomcat 是否真的在 debug 模式运行,可以用 ps 或者 jps -v 查看进程参数:
jsp -v | grep tomcat
你应该可以从输出中看到类似下面的内容。
-agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n
下面的一些函数一方面是为了简化tomcat启动关闭,可以直接在Emacs 中操作,兼容了 arch Linux 和 macOS 的路径获取;另外一方面是 jenv 在 Emacs 中无法生效,因此写了个选择 JAVA_HOME 的函数。
;; Multiple JDK versions are installed locally,
;; especially when older code cannot be compiled with newer versions,
;; it is necessary to select an older JDK version.
(defun select-java-home ()
"List all available JDK home paths and let the user choose one.
The selected path will be exported to JAVA_HOME, and PATH will be
updated so that the chosen JDK's `bin/` directory comes first."
(interactive)
(let* ((candidates
(cond
;; macOS: use /usr/libexec/java_home
((eq system-type 'darwin)
(split-string
(shell-command-to-string
"/usr/libexec/java_home -V 2>&1 | grep '/Library' | awk '{print $NF}'")
"\n" t))
;; Linux (Arch and others): list /usr/lib/jvm/
((eq system-type 'gnu/linux)
(split-string
(shell-command-to-string "ls -d /usr/lib/jvm/*/ 2>/dev/null")
"\n" t))
(t
(user-error "Unsupported system: %s" system-type))))
;; Let user pick one JDK path
(choice (completing-read "Select JAVA_HOME: " candidates nil t)))
;; Set JAVA_HOME environment variable
(setenv "JAVA_HOME" choice)
;; Prepend its bin/ to PATH
(setenv "PATH" (concat (expand-file-name "bin/" choice) ":" (getenv "PATH")))
(message "JAVA_HOME set to %s" choice)))
(defun detect-tomcat-home ()
"Return TOMCAT_HOME path for macOS (Homebrew) or Arch Linux."
(string-trim
(shell-command-to-string
(concat
"( if command -v brew >/dev/null 2>&1; then\n"
" prefix=$(brew --prefix tomcat@9 2>/dev/null || brew --prefix tomcat 2>/dev/null);\n"
" [ -n \"$prefix\" ] && echo \"$prefix/libexec\";\n"
"elif [ -d /usr/share/tomcat10 ]; then\n"
" echo /usr/share/tomcat10;\n"
"elif [ -d /usr/share/tomcat9 ]; then\n"
" echo /usr/share/tomcat9;\n"
"fi )"))))
(defun detect-project-home-and-name ()
"Detect the project home directory and the project name based on the current project."
(let ((project (project-current)))
(if project
(list :name (project-name project) :home (cdr project))
(error "Could not determine the project root"))))
(defun compile-and-start-tomcat (debug)
"Compile the project, copy the WAR file to Tomcat's webapps directory, and start Tomcat.
If DEBUG is non-nil, start Tomcat with JPDA debugging enabled."
(interactive "P")
(let* ((tomcat-home (detect-tomcat-home))
;; Use detect-project-home-and-name to get project details
(project-details (detect-project-home-and-name))
(project-name (plist-get project-details :name))
(project-home (plist-get project-details :home))
(webapps-path (concat tomcat-home "/webapps/"))
(war-file (concat project-home "/target/" project-name ".war"))
(shutdown-script (concat tomcat-home "/bin/shutdown.sh"))
(startup-script (concat tomcat-home "/bin/startup.sh"))
(startup-command (if debug
(concat startup-script " jpda start")
startup-script)))
;; Remove existing WAR and exploded directory
(delete-file (concat webapps-path project-name ".war"))
(delete-directory (concat webapps-path project-name) t)
;; Copy the new WAR file
(copy-file war-file webapps-path)
;; Shutdown Tomcat
(shell-command (concat shutdown-script " || true"))
(sleep-for 3)
;; Startup Tomcat with or without JPDA
(shell-command startup-command)
(sleep-for 5)
;; Check if Tomcat is running
(if (shell-command (format "nc -z localhost %d" tomcat-port))
(message "Deployment successful and Tomcat is running.")
(progn
(message "Tomcat failed to start, retrying...")
(shell-command startup-command)
(sleep-for 5)
(if (shell-command (format "nc -z localhost %d" tomcat-port))
(message "Deployment successful and Tomcat is running.")
(message "Tomcat failed to start after retrying. Please check the logs for more details."))))))
(defun stop-tomcat ()
"Stop Tomcat server."
(interactive)
(let* ((tomcat-home (detect-tomcat-home))
(shutdown-script (concat tomcat-home "/bin/shutdown.sh")))
;; Execute the shutdown script
(shell-command shutdown-script)
(message "Tomcat server stopped.")))
具体的可以参考我的配置 .emacs.d/lisp/init-prog.el at e446a19c9f3329d162d34925752e3f7d6c0979a2 · LuciusChen/.emacs.d · GitHub




