ZetCode

Maven Exec Plugin

last modified November 8, 2026

In this article we show how to run Java programs and external commands from Maven with the Exec plugin. The examples use Maven 3.10, Java 25, and version 3.6.4 of the plugin.

The Exec plugin, maintained by the MojoHaus project, is handy during development: it runs a class with the project classpath without building a JAR file or typing a long java command. It has two goals that work very differently:

The example project

The plugin is declared in the POM with its version. The plugin-level configuration sets the main class for exec:java. The executions are explained later in the article.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>exec-example</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.6.4</version>
                <configuration>
                    <mainClass>com.example.App</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>with-config</id>
                        <goals>
                            <goal>java</goal>
                        </goals>
                        <configuration>
                            <arguments>
                                <argument>input.txt</argument>
                                <argument>output.txt</argument>
                            </arguments>
                            <systemProperties>
                                <systemProperty>
                                    <key>app.name</key>
                                    <value>ConfiguredApp</value>
                                </systemProperty>
                            </systemProperties>
                        </configuration>
                    </execution>
                    <execution>
                        <id>run-java</id>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>java</executable>
                            <arguments>
                                <argument>-Dapp.name=ExecDemo</argument>
                                <argument>-classpath</argument>
                                <classpath/>
                                <argument>com.example.App</argument>
                                <argument>alpha</argument>
                            </arguments>
                            <environmentVariables>
                                <APP_ENV>development</APP_ENV>
                            </environmentVariables>
                            <workingDirectory>${project.build.directory}</workingDirectory>
                        </configuration>
                    </execution>
                    <execution>
                        <id>status</id>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>java</executable>
                            <arguments>
                                <argument>-classpath</argument>
                                <classpath/>
                                <argument>com.example.Status</argument>
                                <argument>3</argument>
                            </arguments>
                            <successCodes>
                                <successCode>0</successCode>
                                <successCode>3</successCode>
                            </successCodes>
                        </configuration>
                    </execution>
                    <execution>
                        <id>debug</id>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>java</executable>
                            <arguments>
                                <argument>-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005</argument>
                                <argument>-classpath</argument>
                                <classpath/>
                                <argument>com.example.App</argument>
                            </arguments>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

The POM pins version 3.6.4 of the plugin. Without the declaration, Maven could still run mvn exec:java, because it resolves the exec prefix to the latest version of the plugin, but such a build is not reproducible.

src/main/java/com/example/App.java
package com.example;

import java.nio.file.Path;
import java.util.Arrays;

public class App {

    public static void main(String[] args) {
        System.out.println("Hello from " + App.class.getName());
        System.out.println("Arguments: " + Arrays.toString(args));
        System.out.println("app.name: " + System.getProperty("app.name", "not set"));
        System.out.println("APP_ENV: " + System.getenv().getOrDefault("APP_ENV", "not set"));
        System.out.println("Working directory: " + Path.of("").toAbsolutePath().getFileName());
        System.out.println("JVM started with: " + System.getProperty("sun.java.command"));
    }
}

The application prints its arguments, a system property, an environment variable, the name of the working directory, and the command that started the JVM. The sun.java.command property contains the main class and the arguments of the java command.

exec:java does not compile

The exec:java goal is not bound to any phase and does not compile anything. On a fresh project it fails, because there are no compiled classes yet.

$ mvn exec:java
[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.6.4:java (default-cli) on project exec-example: An exception occurred while executing the Java class. com.example.App -> [Help 1]
...

Running a class with exec:java

Therefore we combine the goal with the compile phase. The -q option hides the Maven log, so only the output of the application is shown. The last line reveals that the class runs in the JVM of Maven: the JVM was started with the Maven launcher class, and our class was called later from Maven's process. The working directory is the project directory.

$ mvn -q compile exec:java
Hello from com.example.App
Arguments: []
app.name: not set
APP_ENV: not set
Working directory: app
JVM started with: org.codehaus.plexus.classworlds.launcher.Launcher -q compile exec:java

Choosing the main class

The exec.mainClass user property sets the main class from the command line. However, user properties only fill in parameters that the POM does not set. In our project, the mainClass parameter is configured, so the property is ignored and App runs again. In a project without that configuration, here exit, the property selects the Tool class.

src/main/java/com/example/Tool.java
package com.example;

public class Tool {

    public static void main(String[] args) {
        System.out.println("Running the tool");
    }
}

A second class with a main method.

$ mvn -q exec:java -Dexec.mainClass=com.example.Tool
Hello from com.example.App
Arguments: []
app.name: not set
APP_ENV: not set
Working directory: app
JVM started with: org.codehaus.plexus.classworlds.launcher.Launcher -q exec:java -Dexec.mainClass=com.example.Tool
$ cd ../exit
$ mvn -q exec:java -Dexec.mainClass=com.example.Tool
Running the tool

Arguments and system properties

The exec.args property passes arguments to the program. Quotes group words into a single argument. Because exec:java runs in the Maven JVM, every -D option given to Maven is also a system property of our program; here it sets app.name. Environment variables come from the shell that started Maven; exec:java cannot change them.

$ mvn -q exec:java -Dexec.args="one two 'three four'" -Dapp.name=Demo
Hello from com.example.App
Arguments: [one, two, three four]
app.name: Demo
APP_ENV: not set
Working directory: app
JVM started with: org.codehaus.plexus.classworlds.launcher.Launcher -q exec:java -Dexec.args=one two 'three four' -Dapp.name=Demo

Configuring arguments in the POM

Arguments and system properties can be configured in the POM. The with-config execution of the java goal is not bound to a phase; we run it from the command line with the goal@execution-id syntax.

pom.xml (with-config execution)
<id>with-config</id>
<goals>
    <goal>java</goal>
</goals>
<configuration>
    <arguments>
        <argument>input.txt</argument>
        <argument>output.txt</argument>
    </arguments>
    <systemProperties>
        <systemProperty>
            <key>app.name</key>
            <value>ConfiguredApp</value>
        </systemProperty>
    </systemProperties>
</configuration>

The arguments element lists the arguments one by one, so no quoting is needed. The systemProperties element sets system properties for the duration of the run.

Arguments from exec.args replace the configured ones, as the second command shows.

$ mvn -q exec:java@with-config
Hello from com.example.App
Arguments: [input.txt, output.txt]
app.name: ConfiguredApp
APP_ENV: not set
Working directory: app
JVM started with: org.codehaus.plexus.classworlds.launcher.Launcher -q exec:java@with-config
$ mvn -q exec:java@with-config -Dexec.args="x y"
Hello from com.example.App
Arguments: [x, y]
app.name: ConfiguredApp
APP_ENV: not set
Working directory: app
JVM started with: org.codehaus.plexus.classworlds.launcher.Launcher -q exec:java@with-config -Dexec.args=x y

System.exit in exec:java

Since exec:java shares the JVM with Maven, a call to System.exit terminates Maven itself.

src/main/java/com/example/Status.java
package com.example;

public class Status {

    public static void main(String[] args) {
        int code = args.length > 0 ? Integer.parseInt(args[0]) : 0;
        System.out.println("Exiting with code " + code);
        System.exit(code);
    }
}

The program exits with the code given as an argument.

In the first run, Maven ends right after the program's output, without the usual build summary, and the shell gets the exit code of the program. With the exec.blockSystemExit option, the plugin intercepts the call: a non-zero code fails the build in a regular way, and a zero code lets the build continue.

$ mvn exec:java -Dexec.mainClass=com.example.Status -Dexec.args=3
[INFO] --- exec:3.6.4:java (default-cli) @ exec-exit ---
Exiting with code 3
$ echo $?
3
$ mvn exec:java -Dexec.mainClass=com.example.Status -Dexec.args=3 -Dexec.blockSystemExit
Exiting with code 3
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.6.4:java (default-cli) on project exec-exit: An exception occurred while executing the Java class. System::exit was called with return code 3 -> [Help 1]
...

Running programs with exec:exec

The exec:exec goal runs an executable in a new process. To run our class in a separate JVM, we call java and pass the project classpath.

pom.xml (run-java execution)
<id>run-java</id>
<goals>
    <goal>exec</goal>
</goals>
<configuration>
    <executable>java</executable>
    <arguments>
        <argument>-Dapp.name=ExecDemo</argument>
        <argument>-classpath</argument>
        <classpath/>
        <argument>com.example.App</argument>
        <argument>alpha</argument>
    </arguments>
    <environmentVariables>
        <APP_ENV>development</APP_ENV>
    </environmentVariables>
    <workingDirectory>${project.build.directory}</workingDirectory>
</configuration>

The <classpath/> element inside arguments expands to the runtime classpath of the project. Options placed before the class name, such as -Dapp.name=ExecDemo, are JVM options. The environmentVariables element sets variables for the new process, and workingDirectory sets its working directory. These parameters exist only for exec:exec.

This time the JVM was started directly with our class, the environment variable is set, and the working directory is target.

$ mvn exec:exec@run-java
[INFO] --- exec:3.6.4:exec (run-java) @ exec-example ---
Hello from com.example.App
Arguments: [alpha]
app.name: ExecDemo
APP_ENV: development
Working directory: target
JVM started with: com.example.App alpha
[INFO] BUILD SUCCESS
...

Exit codes

The exec:exec goal fails the build when the program returns a non-zero exit code. The successCodes parameter lists the codes that count as success.

pom.xml (status execution)
<id>status</id>
<goals>
    <goal>exec</goal>
</goals>
<configuration>
    <executable>java</executable>
    <arguments>
        <argument>-classpath</argument>
        <classpath/>
        <argument>com.example.Status</argument>
        <argument>3</argument>
    </arguments>
    <successCodes>
        <successCode>0</successCode>
        <successCode>3</successCode>
    </successCodes>
</configuration>

The Status program exits with code 3, which is listed as a success code, so the build succeeds.

$ mvn exec:exec@status
[INFO] --- exec:3.6.4:exec (status) @ exec-example ---
Exiting with code 3
[INFO] BUILD SUCCESS
...

Debugging

JVM options, such as the debugging agent, can be passed only to a separate JVM, so debugging is done with exec:exec.

pom.xml (debug execution)
<id>debug</id>
<goals>
    <goal>exec</goal>
</goals>
<configuration>
    <executable>java</executable>
    <arguments>
        <argument>-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005</argument>
        <argument>-classpath</argument>
        <classpath/>
        <argument>com.example.App</argument>
    </arguments>
</configuration>

The JDWP agent listens on port 5005, where a debugger such as IntelliJ IDEA, Eclipse, or jdb can attach. With suspend=n the program starts immediately; suspend=y would make it wait until a debugger connects. To debug code run by exec:java, we would have to start Maven itself with the agent, for example through the MAVEN_OPTS variable.

$ mvn -q exec:exec@debug
Listening for transport dt_socket at address: 5005
Hello from com.example.App
Arguments: []
app.name: not set
APP_ENV: not set
Working directory: app
JVM started with: com.example.App

Running a script during the build

An execution with a phase runs automatically as part of the build. The following project runs a shell script in the generate-resources phase.

pom.xml (exec plugin)
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>3.6.4</version>
    <executions>
        <execution>
            <id>build-info</id>
            <phase>generate-resources</phase>
            <goals>
                <goal>exec</goal>
            </goals>
            <configuration>
                <executable>bash</executable>
                <arguments>
                    <argument>${project.basedir}/scripts/build-info.sh</argument>
                    <argument>${project.build.outputDirectory}</argument>
                </arguments>
            </configuration>
        </execution>
    </executions>
</plugin>

The execution passes the script and the output directory for classes as arguments. The executable must be on the PATH.

scripts/build-info.sh
#!/usr/bin/env bash

out="$1/build-info.properties"
mkdir -p "$1"
echo "build.os=$(uname -s)" > "$out"
echo "build.date=$(date -u +%F)" >> "$out"
echo "Wrote $out"

The script writes a properties file into the directory given as the first argument.

The script runs before the resources are copied and the code is compiled. The file it created in target/classes is included in the JAR.

$ mvn package
[INFO] --- exec:3.6.4:exec (build-info) @ exec-script ---
Wrote /home/user/script/target/classes/build-info.properties
[INFO] --- resources:3.5.0:resources (default-resources) @ exec-script ---
[INFO] --- compiler:3.16.0:compile (default-compile) @ exec-script ---
[INFO] --- jar:3.5.1:jar (default-jar) @ exec-script ---
[INFO] BUILD SUCCESS
...
$ unzip -p target/exec-script-1.0.0.jar build-info.properties
build.os=Linux
build.date=2026-11-08

Best practices

Source

Exec Maven Plugin

In this article we have shown how to run Java programs and external commands from Maven with the Exec plugin.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.