Maven Execution Context for Custom Goals: How Apache Maven Builds and Runs Your Mojo
Maven constructs an execution context for every custom goal by combining a MojoExecution object, a MavenSession for global state, and a MojoExecutionScope for dependency injection, then runs the Mojo's execute() method within this isolated scope.
When you create a custom goal in Apache Maven, the core engine assembles a sophisticated execution context that manages everything from plugin resolution to component injection. Understanding how Maven builds this context—centered around MojoExecution, MavenSession, and MojoExecutionScope—is essential for writing robust plugins and debugging lifecycle issues. This article breaks down the exact source code paths and mechanisms that power the Maven execution context for custom goals.
The Three Pillars of the Execution Context
Every custom goal invocation relies on three interconnected components that define the what, where, and how of the build.
MojoExecution: The Goal Descriptor
The MojoExecution class (impl/maven-core/src/main/java/org/apache/maven/plugin/MojoExecution.java) encapsulates the specific configuration for a single goal execution. It stores the MojoDescriptor (metadata about the goal), the execution ID, the source (CLI, POM, or lifecycle), and the raw XML configuration (Xpp3Dom or XmlNode). When Maven prepares to run your custom goal, it instantiates this object using constructors defined at lines 105–108, which accept a MojoDescriptor and the configuration node.
MavenSession: The Build State
The MavenSession (impl/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java) provides the global execution context. It holds the current project, the full reactor graph, user properties, and the repository session. Custom goals access this session to read cross-project state or write to the shared pluginContext map.
MojoExecutionScope: The DI Container
The MojoExecutionScope (impl/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScope.java) is a Guice Scope that creates a fresh dependency-injection container for each goal execution. This scope caches components for the duration of the goal and propagates lifecycle events to WeakMojoExecutionListener instances, ensuring that plugins can observe execution boundaries without causing classloader leaks.
Step-by-Step Execution Flow
When you invoke a custom goal, Maven executes a precise sequence of resolution, construction, and invocation steps.
Goal Resolution and Descriptor Loading
Maven first reads the plugin's plugin.xml (generated by the maven-plugin-plugin) to create a MojoDescriptor. The DefaultMavenPluginManager#getMojoDescriptor(..) method at line 290 of impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java handles this lookup, returning a descriptor that specifies the implementation class, required parameters, and thread-safety flags.
Creating the MojoExecution Object
With the descriptor resolved, Maven constructs a MojoExecution object that wraps the descriptor, the goal name, and any user-supplied configuration. This object is created via the MojoExecution constructor that takes a MojoDescriptor and an XmlNode, binding the static plugin metadata to the dynamic runtime configuration.
Building the Execution Plan
The MojoExecution is inserted into a MavenExecutionPlan (impl/maven-core/src/main/java/org/apache/maven/lifecycle/MavenExecutionPlan.java). The DefaultLifecycleExecutionPlanCalculator generates this plan by calling MojoDescriptorCreator#getMojoDescriptor(..) at line 136 of impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoDescriptorCreator.java, resolving descriptors for each task in the build lifecycle.
Activating the Mojo Execution Scope
Before the goal runs, Maven opens a Mojo-execution scope via MojoExecutionScope. This scope initializes a per-execution cache and fires before callbacks to registered WeakMojoExecutionListener instances. The scope ensures that components annotated with @MojoExecutionScoped are instantiated fresh for each goal and disposed of afterward.
Component Injection and Goal Execution
Inside the active scope, Maven uses PlexusContainer (or the newer org.apache.maven.di) to instantiate the concrete Mojo class. Fields annotated with @Parameter are populated from the MojoExecution configuration using the PluginParameterExpressionEvaluator (impl/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluator.java) to resolve ${...} expressions. Finally, Maven invokes the Mojo's execute() method and records the outcome in MavenExecutionResult.
Key Architectural Concepts
Understanding these core abstractions helps you navigate the Maven source code and extend plugin functionality.
- MojoDescriptor (
compat/maven-plugin-api/src/main/java/org/apache/maven/plugin/descriptor/MojoDescriptor.java): Describes a goal's implementation class, required parameters, and thread-safety characteristics. - PluginDescriptor (
compat/maven-plugin-api/src/main/java/org/apache/maven/plugin/descriptor/PluginDescriptor.java): Holds the collection ofMojoDescriptors for a plugin and the plugin's artifact coordinates. - MojoExecutionScope (
impl/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScope.java): Guarantees a fresh DI container per goal execution and manages listener callbacks viaWeakMojoExecutionListener. - MavenExecutionPlan (
impl/maven-core/src/main/java/org/apache/maven/lifecycle/MavenExecutionPlan.java): An ordered list ofMojoExecutions that Maven iterates through during the build.
Practical Code Examples
Minimal Custom Maven Plugin
Create a plugin with a single goal that prints a configurable message:
<!-- pom.xml of the plugin -->
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>hello-maven-plugin</artifactId>
<version>1.0.0</version>
<packaging>maven-plugin</packaging>
<build>
<plugins>
<plugin>
<artifactId>maven-plugin-plugin</artifactId>
<version>3.12.0</version>
<configuration>
<goalPrefix>hello</goalPrefix>
</configuration>
<executions>
<execution>
<goals><goal>helpmojo</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
// src/main/java/com/example/HelloMojo.java
package com.example;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.*;
@Mojo(name = "sayhi", defaultPhase = LifecyclePhase.NONE, requiresProject = false)
public class HelloMojo extends AbstractMojo {
/** Message printed by the mojo – configurable from the command line */
@Parameter(property = "message", defaultValue = "Hello, Maven!")
private String message;
public void execute() throws MojoExecutionException {
getLog().info(message);
}
}
When you run the goal from the command line:
mvn com.example:hello-maven-plugin:1.0.0:sayhi -Dmessage="Hi from a custom goal"
Maven internally creates the execution context:
MojoExecution exec = new MojoExecution(
mojoDescriptor, // resolved from plugin.xml
"sayhi", // goal name
"default", // executionId
MojoExecution.Source.CLI); // source = CLI
This exec object is placed into the MavenExecutionPlan and executed inside a fresh MojoExecutionScope.
Accessing the Execution Context from Inside a Mojo
Inject session and project parameters to interact with the build state:
@Mojo(name = "inspect")
public class InspectMojo extends AbstractMojo {
@Parameter(defaultValue = "${session}", readonly = true)
private MavenSession session; // injected session
@Parameter(defaultValue = "${project}", readonly = true)
private MavenProject project; // current project
public void execute() {
getLog().info("Current project: " + project.getArtifactId());
getLog().info("User properties: " + session.getUserProperties());
getLog().info("All goals for this build: " + session.getGoals());
}
}
Because MojoExecutionScope caches the session and project objects, every custom goal sees a consistent view of the reactor. You can safely read from or write to the pluginContext map to share state between goals:
Map<String, Object> ctx = session.getPluginContext(pluginDescriptor, project);
ctx.put("myKey", "myValue"); // later goals in the same session can read this value
Summary
- Maven execution context for custom goals consists of three components:
MojoExecution(the configuration),MavenSession(the build state), andMojoExecutionScope(the DI container). - Resolution happens via
DefaultMavenPluginManager#getMojoDescriptorinimpl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java. - Construction involves creating a
MojoExecutionobject and adding it to theMavenExecutionPlanmanaged byDefaultLifecycleExecutionPlanCalculator. - Isolation is guaranteed by
MojoExecutionScope, which provides per-execution component caching and listener callbacks. - Injection of
@Parameterfields usesPluginParameterExpressionEvaluatorto resolve expressions beforeexecute()is called.
Frequently Asked Questions
How does Maven resolve the correct Mojo class for a custom goal?
Maven resolves the Mojo class by reading the plugin.xml descriptor bundled in the plugin JAR. The DefaultMavenPluginManager#getMojoDescriptor(..) method at line 290 of impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java loads this descriptor and returns a MojoDescriptor containing the fully qualified implementation class name.
What is the difference between MavenSession and MojoExecution?
MavenSession (impl/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java) provides global build state including the reactor projects and user properties, while MojoExecution (impl/maven-core/src/main/java/org/apache/maven/plugin/MojoExecution.java) represents the specific configuration and metadata for a single goal invocation. The session persists across the entire build, whereas a new MojoExecution is created for each goal.
Can custom goals share state between executions?
Yes, custom goals can share state using the pluginContext map accessible via session.getPluginContext(pluginDescriptor, project). Additionally, MojoExecutionScope ensures that components scoped to the execution are cached for the duration of the goal, while WeakMojoExecutionListener allows plugins to observe execution boundaries without retaining strong references that could cause memory leaks.
Where does Maven handle expression evaluation for @Parameter values?
Maven evaluates expressions like ${project.version} or ${session} using the PluginParameterExpressionEvaluator class located at impl/maven-core/src/main/java/org/apache/maven/plugin/PluginParameterExpressionEvaluator.java. This evaluation occurs during the component injection phase, immediately before the Mojo's execute() method is invoked.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →