Skip to content

emacsmirror/jal

 
 

Repository files navigation

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.

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:

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 :config block) with (jal-lsp-java-mode 1) or (jal-eglot-java-mode 1). Do not add them to a buffer-local hook such as java-mode-hook. Use -1 to disable.

Using use-package with a local clone

Clone the repository to a local directory and point use-package at it:

(use-package jal
  :load-path "/path/to/java-agent-loader"
  :custom
  (jal-auto-setup t)
  :config
  (jal-lsp-java-mode 1)) ; or (jal-eglot-java-mode 1)

Using use-package with package-vc (Emacs 29+)

No clone needed: Emacs fetches the package directly from GitHub:

(use-package jal
  :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)

Using straight.el

(straight-use-package
 '(jal :type git :host github :repo "saulotoledo/java-agent-loader"))
(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.

Supported Properties for jal-additional-agents

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.

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 dependency:list or a Gradle init script to resolve the agent JAR paths and versions.
  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

make test

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

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

Java Agent Loader (JAL) for JDTLS

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages

  • Emacs Lisp 99.4%
  • Makefile 0.6%