Skip to content

Repository files navigation

https://melpa.org/packages/jal-badge.svg https://stable.melpa.org/packages/jal-badge.svg

Java Agent Loader (JAL)

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.

./preview.gif

Quick Reference

All JAL configuration variables:

VariableTypeDefaultDescription
jal-auto-setupbooleannilSkip prompt; auto-detect agents on every startup
jal-known-agentslist(built-in)Read-only. Known agents (Lombok, OpenTelemetry, JaCoCo)
jal-additional-agentslistnilUse this to add agents or override jal-known-agents entries
jal-maven-lifecycle-phasestring"compile"Maven phase prepended to dependency:list. nil skips compilation (faster, but may fail on unbuilt projects)

NOTE: Do not set jal-known-agents directly. It is the internal registry of pre-configured agents and is not designed for user customization. To add a new agent or override parameters of a known one, use jal-additional-agents instead.

Enable exactly one global integration mode (never both, never in a hook):

ModeRequires packageActivate with
jal-lsp-java-modelsp-java(jal-lsp-java-mode 1)
jal-eglot-java-modeeglot-java(jal-eglot-java-mode 1)
;; ✓ CORRECT: top-level :config block
(use-package jal
  :custom
  (jal-auto-setup t)
  :config
  ;; Optional: override known agents or add new ones
  (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")
          ))
  (jal-lsp-java-mode 1))

;; ✗ WRONG: never add to a buffer-local hook
(add-hook 'java-mode-hook (lambda () (jal-lsp-java-mode 1)))

Why the hook approach is wrong: JAL needs to intercept the language server startup before it launches, so it can inject the agent arguments. This interception is set up once when the mode is enabled, and torn down when it is disabled. Adding the mode to java-mode-hook means it is enabled and disabled on every Java buffer visit and kill – the interception may not be in place by the time the language server starts, or the configuration state may be reset between visits.

Enable the mode exactly once at the top level to keep the interception stable for the lifetime of the Emacs session.

How It Works

  • 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/repository or $HOME/.gradle/caches).
  • Injection: It intercepts the startup hook of the JDT Language Server (JDTLS) and appends the fully constructed -javaagent:/path/to/resolved.jar string 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.

Features

  • LSP Integration: Works with both lsp-java and eglot-java.
  • Zero Mutated State (Safe): Uses dynamic let-bindings during the injection process–it never permanently alters or pollutes your global lsp-java-vmargs or eglot-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.

Installation

Prerequisites

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:

Choose an Installation Method

Pick one of the following methods. Do not mix keys from different methods (e.g., do not combine :vc with :load-path).

MethodRequirementRecommended when
MELPAMELPA in archivesMost users (recommended)
package-vc-installEmacs 29+Direct Git install, no MELPA needed
Local cloneGit clone on diskDevelopment / unpublished changes
straight.elstraight.el set upLockfile-based reproducible configs

Via MELPA (recommended)

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 RET

JAL 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)

Via package-vc-install (Emacs 29+)

Emacs 29 and later can install packages directly from Git repositories. Uses the :vc keyword.

Do not combine :vc with :load-path.

(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)

Via local clone

Clone the repository and point use-package at it via :load-path.

Do not combine :load-path with :vc.

(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)

Via straight.el

(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)

Configuration

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.

jal-additional-agents Entry Format

Each entry: ("ARTIFACT-ID") to use defaults, or ("ARTIFACT-ID" :key value ...) to override.

PropertyTypeDescription
:paramsstringArguments appended after = in -javaagent:...=<params>
:jar-pathstringJAR location – behavior depends on leading character

:jar-path resolution rules:

Value starts withTreated asPlaceholders available
/ (slash)Absolute path, used as-is%a, %v expanded
anything elsePattern relative to Maven/Gradle local repository%a, %v, %g

Placeholder reference:

PlaceholderExpands to
%aartifactId
%vversion
%ggroupId

Example

The following example covers all entry forms in one block:

(setq jal-additional-agents
  '(
    ;; 1. Override a known agent's params only.
    ;;    JAR path is still resolved automatically from the local Maven/Gradle cache.
    ;;    Produces: -javaagent:/home/user/.m2/.../org.jacoco.agent-0.8.x-runtime.jar=destfile=build/jacoco.exec
    ("org.jacoco.agent" :params "destfile=build/jacoco.exec")

    ;; 2. Agent with an absolute JAR path.
    ;;    %a expands to the artifactId; the path is used as-is (not looked up in the cache).
    ;;    Produces: -javaagent:/opt/company-agents/my-profiler-1.0.jar=port=9999
    ("my-profiler"
     :jar-path "/opt/company-agents/%a-1.0.jar"
     :params "port=9999")

    ;; 3. Agent with a relative JAR pattern (no leading /), resolved inside the local repository.
    ;;    %g → groupId, %a → artifactId, %v → version
    ;;    Produces: -javaagent:/home/user/.m2/com/example/tracing-agent/2.3.1/tracing-agent-2.3.1.jar
    ("tracing-agent"
     :jar-path "%g/%a/%v/%a-%v.jar")
    ))

Usage with lsp-java

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.

Usage with eglot-java

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.

How agent detection works

  1. When the LSP/Eglot server starts, the advised function checks for a project-local cache file (.jal-config.el).
  2. 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-setup to t to skip this prompt and always run detection automatically.
  3. On confirmation, JAL runs mvn <phase> dependency:list (Maven) or a Gradle init script to resolve the agent JAR paths and versions.
    • IMPORTANT (Maven only): The Maven command prepends jal-maven-lifecycle-phase (default: "compile") so that multi-module reactors build their modules in dependency order first. This populates Maven’s reactor artifact map, allowing sibling modules to resolve without being installed in $HOME/.m2. **If you do not want JAL to trigger project compilation, set jal-maven-lifecycle-phase to ~nil~** for faster detection, at the cost of detection failures on multi-module reactors with uninstalled siblings or unbuilt projects.
  4. Results are written to .jal-config.el in the project root and reused on future startups.

You can also run detection manually: M-x jal-detect-java-agents.

Examples

Maven Project with Lombok

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.

Gradle Project with JaCoCo (custom output file)

(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)

Custom Agent with Absolute JAR Path

(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)

Interactive Detection

Manually detect a single agent in the current project: M-x jal-detect-agent-interactively -> enter the artifact ID (e.g., lombok).

Manual Configuration

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)

Running Tests

Automated Tests

make test

This byte-compiles all .el files and runs the ERT test suite in a clean batch Emacs session.

Manual Interactive Testing

For manual testing of LSP/Eglot integration across Maven and Gradle, JAL includes isolated development environments and sample projects. See the Test Projects README for instructions on launching these sandboxes.

Limitations

  • Secrets Management: There is no approach yet to allow .jal-config.el to avoid exposing agent options that contain sensitive values. Therefore, we recommend adding .jal-config.el to .gitignore to 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.

About

An Emacs loader for Java agents to be used with lsp-java and eglot-java

Resources

Contributing

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages