How to Customize Maven Execution Behavior: CLI Flags, Embedder API, and Extension Points

You customize Maven execution behavior by intercepting and modifying the MavenExecutionRequest object before Maven.execute() processes it, using command-line options, programmatic setters, or custom EventSpy and CoreExtension implementations.

Apache Maven's architecture follows a strict request-response pattern that makes customization straightforward once you understand the pipeline. In the apache/maven repository, the MavenCli class orchestrates the build by constructing a DefaultMavenExecutionRequest, populating it via MavenExecutionRequestPopulator, and eventually delegating to Maven.execute(). Every major step in this flow exposes setters and extension points that allow you to customize Maven execution behavior without modifying core engine code.

Understanding the Execution Pipeline

Maven's build lifecycle follows a predictable sequence exposed in org.apache.maven.cli.MavenCli (compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java):

  1. CLI Parsing: MavenCli.doMain() reads command-line arguments, system properties, and the .mvn/maven.config file.
  2. Request Instantiation: A fresh DefaultMavenExecutionRequest is created (see the constructor at line 75 of DefaultMavenExecutionRequest.java).
  3. Default Population: MavenExecutionRequestPopulator.populateDefaults() enriches the request with settings from ~/.m2/settings.xml, environment variables, and active core extensions.
  4. EventSpy Injection: The EventSpyDispatcher is attached to the request so that every lifecycle phase fires observable events (configured in MavenCli.configure() around lines 1184-1190).
  5. Execution: Maven.execute() (around line 500 in impl/maven-impl/src/main/java/org/apache/maven/impl/Maven.java) transforms the request into a MavenExecutionResult.

Because each step exposes the request object publicly, you can inject custom logic at any point before the final execution call.

Customizing via the Embedder API

For embedded builds or CI/CD integrations, modify the DefaultMavenExecutionRequest programmatically before passing it to Maven.execute().

Setting Execution Flags

The DefaultMavenExecutionRequest class (impl/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java) provides setters for common execution parameters:

import org.apache.maven.DefaultMaven;
import org.apache.maven.execution.DefaultMavenExecutionRequest;
import org.apache.maven.execution.MavenExecutionResult;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.RepositorySystem;

public class CustomMavenEmbedder {
    public static void main(String[] args) throws Exception {
        // Build a Maven repository system (normally done by Plexus, omitted for brevity)
        RepositorySystem repoSystem = MavenRepositorySystemUtils.newRepositorySystem();

        // Create a request and customize it
        DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest();
        request.setBaseDirectory(new File("."));
        request.addGoal("clean");
        request.addGoal("install");
        request.setOffline(true);                      // run offline
        request.setDegreeOfConcurrency(8);             // parallel builds
        request.getUserProperties().setProperty("myProp", "value");

        // Create Maven core and execute
        DefaultMaven maven = new DefaultMaven();
        MavenExecutionResult result = maven.execute(request);

        if (result.hasExceptions()) {
            result.getExceptions().forEach(Throwable::printStackTrace);
        }
    }
}

This approach allows you to programmatically control offline mode, parallelism, repository locations, and user properties before the build starts.

Intercepting Lifecycle Events with EventSpy

To observe or modify behavior during execution, implement the org.apache.maven.eventspy.EventSpy interface. The EventSpyDispatcher (impl/maven-core/src/main/java/org/apache/maven/eventspy/internal/EventSpyDispatcher.java) forwards all lifecycle events to registered spies.

Implementing a Custom EventSpy

Create a spy that logs every mojo execution:

package com.example;

import org.apache.maven.eventspy.AbstractEventSpy;
import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.execution.ExecutionEvent.Type;

public class LoggingSpy extends AbstractEventSpy {
    @Override
    public void onEvent(Object event) {
        if (event instanceof ExecutionEvent) {
            ExecutionEvent ee = (ExecutionEvent) event;
            if (ee.getType() == Type.MojoStarted) {
                getLogger().info(">>> Starting mojo: " + ee.getMojoExecution().getArtifactId());
            }
        }
    }
}

Register your spy in META-INF/maven/extensions.xml:

<extensions>
    <extension>
        <groupId>com.example</groupId>
        <artifactId>logging-spy</artifactId>
        <version>1.0</version>
    </extension>
</extensions>

When Maven runs, MavenCli.configure() injects the EventSpyDispatcher into the request, ensuring your spy receives every ExecutionEvent.

Replacing Core Components

For deeper customization, you can replace the MavenExecutionRequestPopulator itself or register a CoreExtension.

Custom Request Populator

Override DefaultMavenExecutionRequestPopulator (impl/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequestPopulator.java) to inject defaults globally:

public class MyRequestPopulator implements MavenExecutionRequestPopulator {
    @Override
    public MavenExecutionRequest populateDefaults(MavenExecutionRequest request) {
        // Start from the defaults
        request = new DefaultMavenExecutionRequestPopulator().populateDefaults(request);
        // Add a default property for all builds
        request.getUserProperties().setProperty("my.default.prop", "123");
        return request;
    }
}

Package this as a core extension by implementing org.apache.maven.extension.CoreExtension (see api/maven-api-core/src/main/java/org/apache/maven/api/cli/extensions/CoreExtension.java) and listing it in ${user.home}/.m2/extensions.xml. This causes MavenCli to discover and use your populator instead of the built-in version.

Command-Line and Configuration File Customization

For simpler cases, you can customize Maven execution behavior without Java code:

  • Goals and phases: Pass directly to mvn (e.g., mvn clean install)
  • System properties: Use -Dkey=value (maps to request.setUserProperties())
  • Offline mode: --offline or -o (sets request.setOffline(true))
  • Custom settings: -s /path/to/settings.xml (affects the population step)
  • Parallel builds: -T 4 (sets request.setDegreeOfConcurrency(4))
  • Project-specific config: Place options in .mvn/maven.config in your project root

These options are parsed by MavenCli.doMain() and applied to the MavenExecutionRequest before population.

Summary

  • Maven execution behavior is controlled by the MavenExecutionRequest object created in MavenCli and processed by Maven.execute().
  • Programmatic customization uses the embedder API to set flags like offline, degreeOfConcurrency, and custom properties on DefaultMavenExecutionRequest.
  • Event interception is achieved by implementing EventSpy and registering it via extensions.xml, monitored by EventSpyDispatcher.
  • Core component replacement allows you to provide custom MavenExecutionRequestPopulator implementations or other extensions via the CoreExtension mechanism.
  • CLI customization modifies the same request object through command-line flags parsed by MavenCli.doMain().

Frequently Asked Questions

How do I run Maven builds programmatically with custom settings?

Use the embedder API by creating a DefaultMavenExecutionRequest, configuring it with setters like setOffline() and setDegreeOfConcurrency(), then passing it to DefaultMaven.execute(). This bypasses CLI parsing while respecting the same execution pipeline defined in org.apache.maven.Maven.

What is the difference between an EventSpy and a Maven plugin?

An EventSpy observes the entire build lifecycle across all projects and is registered via extensions.xml, while a plugin executes specific goals within the lifecycle. EventSpies run in the same container as Maven itself and can intercept events before any plugin executes, using the EventSpyDispatcher in impl/maven-core.

Can I change the default request population logic for all builds?

Yes. Implement a custom MavenExecutionRequestPopulator, package it as a core extension implementing CoreExtension, and register it in ~/.m2/extensions.xml. This replaces the default DefaultMavenExecutionRequestPopulator used by MavenCli to populate request defaults from settings and environment variables.

Where does Maven read project-specific execution options?

Maven reads the .mvn/maven.config file in the project root during MavenCli.doMain() parsing, applying those options to the execution request before the build begins. This is the recommended way to share custom flags (like --threads or specific profiles) across a team without wrapper scripts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →