INFO: Gradle support is experimental. Please let us know about any issues or suggestions to help us improve it!
In the Java ecosystem, a Java Agent is a native JVM plugin that uses bytecode
instrumentation to modify compiled code on the fly. To use one, you must
explicitly pass its JAR file path via the -javaagent:/path/to/agent.jar
argument when launching the JVM.
JAL is an Emacs package that automates this manual configuration for your
development environment. It bridges the gap between your project’s build
configuration and your LSP client (lsp-java or eglot-java) by managing that
command-line injection behind the scenes.
Historical Note: In the Java ecosystem, “Java Agents” have existed as a core JVM bytecode technology since 2004 (Java 5)–long before the rise of modern AI. Despite the name, this package is strictly a utility for JVM bytecode instrumentation management and has absolutely no relation to AI assistants, LLM workflows, or autonomous software agents.
- Automatic Detection: When you open a Java project, JAL scans your Maven
(
pom.xml) or Gradle (build.gradle) configuration to see which agents (like Lombok or JaCoCo) your project requires. - Resolution: It resolves the exact absolute path to that agent’s JAR file
inside your local build cache (e.g.,
$HOME/.m2/repositoryor$HOME/.gradle/caches). - Injection: It intercepts the startup hook of the JDT Language Server (JDTLS)
and appends the fully constructed
-javaagent:/path/to/resolved.jarstring directly to the LSP server launch command. - Global Caching: To prevent Emacs from freezing while waiting for slow Maven/Gradle CLI lookups on every single file visit, JAL caches these resolved paths globally after the first run for instant subsequent startups.
- LSP Integration: Works with both
lsp-javaandeglot-java. - Zero Mutated State (Safe): Uses dynamic let-bindings during the injection
process–it never permanently alters or pollutes your global
lsp-java-vmargsoreglot-java-eclipse-jdt-args. - Flexible Configuration: Moves beyond default lookups to support custom agent JAR names, absolute fallback paths, and specialized agent parameters.
- Build-System Aware: Quietly stays out of your way; it only triggers agent resolution when you are actively inside a valid Maven or Gradle project.
- Instant Startups: Completely bypasses blocking build-tool CLI lookups on daily file visits by leveraging project-specific path caching.
JAL integrates with either lsp-java or eglot-java (note: plain eglot is
not supported: eglot-java is required). At least one of these must be
installed:
- lsp-java - LSP client for Java using JDTLS
- eglot-java - eglot-java extension for Emacs
Enable the matching integration mode once in your configuration:
jal-lsp-java-mode for lsp-java, or jal-eglot-java-mode for eglot-java.
NOTE: Both modes are global minor modes. Enable them once at the top level of your configuration (for example in a
use-package :configblock) with(jal-lsp-java-mode 1)or(jal-eglot-java-mode 1). Do not add them to a buffer-local hook such asjava-mode-hook. Use-1to disable.
This is the recommended method for installing this package.
First, ensure you have the MELPA repository configured in your Emacs. If you haven’t already, add this to your init file:
(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(package-initialize)Then install the package:
M-x package-install RET jal RETJAL must be enabled to work. Here is a configuration example using `use-package`:
(use-package jal
:ensure t
:custom
(jal-auto-setup t)
:config
(jal-lsp-java-mode 1)) ; or (jal-eglot-java-mode 1)Emacs 29 and later can install packages directly from Git repositories.
(use-package jal
:ensure t
:vc (:url "https://github.com/saulotoledo/java-agent-loader" :rev :newest)
:custom
(jal-auto-setup t)
:config
(jal-lsp-java-mode 1)) ; or (jal-eglot-java-mode 1)You can later update all VC-installed packages with:
(package-vc-update-all)Clone the repository to a local directory and point use-package at it:
(use-package jal
:ensure t
:load-path "/path/to/java-agent-loader"
:custom
(jal-auto-setup t)
:config
(jal-lsp-java-mode 1)) ; or (jal-eglot-java-mode 1)(straight-use-package
'(jal :type git :host github :repo "saulotoledo/java-agent-loader"))
(setq jal-auto-setup t)
(jal-lsp-java-mode 1) ; or (jal-eglot-java-mode 1)JAL requires no mandatory configuration. By default it looks for all agents
listed in jal-known-agents (Lombok, OpenTelemetry Java Agent, JaCoCo).
By default, JAL asks once per project: “Do you want to setup java agents for
this project?” To run detection automatically without prompting, set
jal-auto-setup to t.
To add agents or override known-agent defaults, set jal-additional-agents
before you enable jal-lsp-java-mode or jal-eglot-java-mode.
(use-package jal
:ensure t
:custom
(jal-auto-setup t)
(jal-additional-agents
'(
;; Override a known agent with custom parameters
("org.jacoco.agent" :params "destfile=target/jacoco.exec")
;; Add an agent not in the known list
("my-custom-agent" :jar-path "/opt/agents/my-agent.jar")
))
:config
(jal-lsp-java-mode 1)) ; or (jal-eglot-java-mode 1)JAL hooks into lsp-java and eglot-java once you enable jal-lsp-java-mode
or jal-eglot-java-mode. These are global modes – enable one once at the top
level; no further setup is needed in the lsp-java or eglot-java blocks.
Each entry is either ("ARTIFACT-ID") (use defaults) or ("ARTIFACT-ID" :key
value ...) (override properties).
:params(String): Arguments appended after the=in-javaagent:...=<params>.:jar-path(String): A format string or absolute path for the agent JAR.- **Relative Pattern**: If it does not start with
/, it is treated as a pattern relative to your local Maven/Gradle repository.%a: artifactId%v: version%g: groupId
- **Absolute Path**: If it starts with
/, it is used as-is. Placeholders (%a,%v) are still expanded.
- **Relative Pattern**: If it does not start with
Enable jal-lsp-java-mode (a global mode) once in your configuration.
(use-package jal
:ensure t
:custom
(jal-additional-agents '(("my-custom-agent"))) ; Optional
:config
(jal-lsp-java-mode 1))
(use-package lsp-java
:ensure t
:hook ((java-mode . lsp)
(java-ts-mode . lsp)))Internally JAL installs an :around advice on lsp-java--ls-command that
dynamically extends lsp-java-vmargs with the resolved -javaagent arguments
for each server startup.
eglot-java is required (not plain eglot). Enable jal-eglot-java-mode (a
global mode) once in your configuration.
(use-package jal
:ensure t
:custom
(jal-additional-agents '(("my-custom-agent"))) ; Optional
:config
(jal-eglot-java-mode 1))
(use-package eglot-java
:ensure t
:hook ((java-mode . eglot-java-mode)
(java-ts-mode . eglot-java-mode)))Internally JAL installs an :around advice on eglot-java--eclipse-jdt-contact
that dynamically extends eglot-java-eclipse-jdt-args with the resolved
-javaagent arguments for each server startup.
- When the LSP/Eglot server starts, the advised function checks for a
project-local cache file (
.jal-config.el). - If the cache is missing or empty, JAL prompts once: “Do you want to setup
java agents for this project?” (Only prompts when inside a Maven or Gradle
project.) Set
jal-auto-setuptotto skip this prompt and always run detection automatically. - On confirmation, JAL runs
mvn dependency:listor a Gradle init script to resolve the agent JAR paths and versions. - Results are written to
.jal-config.elin the project root and reused on future startups.
You can also run detection manually: M-x jal-detect-java-agents.
If your pom.xml declares Lombok in its dependencies:
(use-package jal
:ensure t
:custom
(jal-auto-setup t)
:config
(jal-lsp-java-mode 1))JAL will find it automatically – Lombok is in jal-known-agents, so no custom
configuration is needed.
(use-package jal
:ensure t
:custom
(jal-additional-agents
'(("org.jacoco.agent" :params "destfile=build/reports/jacoco.exec")))
:config
(jal-lsp-java-mode 1)) ; or (jal-eglot-java-mode 1)(use-package jal
:custom
(jal-additional-agents '(("my-agent" :jar-path "/opt/agents/my-agent.jar" :params "config=prod")))
:config
(jal-lsp-java-mode 1)) ; or (jal-eglot-java-mode 1)Manually detect a single agent in the current project: M-x
jal-detect-agent-interactively -> enter the artifact ID (e.g., lombok).
The cache file .jal-config.el in your project root can be edited by hand or
committed to your repository (it contains no secrets by default). But we
recommend adding it to .gitignore to avoid merge conflicts, as it may contain
paths specific to your local environment, and due to the possibility of agents
with parameters that differ between developers or that contains secrets.
Example .jal-config.el:
(("/usr/lib/jvm/java-21/bin/java"
(("lombok"
"/home/user/.m2/repository/org/projectlombok/lombok/1.18.30/lombok-1.18.30.jar"
""
"1.18.30")
("org.jacoco.agent"
"/home/user/.m2/repository/org/jacoco/org.jacoco.agent/0.8.11/org.jacoco.agent-0.8.11-runtime.jar"
"destfile=target/jacoco.exec"
"0.8.11"))))Each agent entry inside the scope is: (ARTIFACT-ID PATH PARAMS VERSION)
make testThis byte-compiles all .el files and runs the ERT test suite in a clean batch
Emacs session.
- Secrets Management: There is no approach yet to allow
.jal-config.elto avoid exposing agent options that contain sensitive values. Therefore, we recommend adding.jal-config.elto.gitignoreto avoid accidentally committing secrets or environment-specific paths. If this is a limitation for your use case, please open an issue to discuss possible solutions.