skip to content

Compare exec-maven-plugin and maven-antrun-plugin for binding a custom step to a phase. When would you choose each, and what are the trade-offs?

level: seniorimportance: should knowfreq 40%

answer

  1. exec:exec forks external, exec:java same JVM
  2. antrun = inline Ant tasks
  3. PATH dependency hurts reproducibility
  4. antrun overuse = anti-pattern
  5. last resort vs real plugin

basics

~20 s

exec-maven-plugin runs an external program (exec:exec) or a Java main on the project classpath (exec:java). maven-antrun-plugin runs inline Ant tasks (copy, mkdir, echo, etc.). Use exec for one external command/Java, antrun for small file/scripting glue.

solid answer

~50 s

Both are 'glue' plugins for steps Maven has no first-class goal for, but they differ in nature. **exec-maven-plugin** has two goals: `exec:exec` forks an external process (any binary, e.g. npm, protoc, a shell script) with explicit `<executable>`/`<arguments>`; `exec:java` runs a Java `main()` *in the same JVM* on the project's classpath without forking. **maven-antrun-plugin** embeds an Ant `<target>` so you can use Ant's rich task library (copy, move, mkdir, replace, concat, conditionals) declaratively in the pom. Choose exec when you must invoke a real external tool or run application Java; choose antrun for portable filesystem/manipulation glue without depending on shell-specific commands. Trade-offs: antrun is OS-portable but verbose and a known anti-pattern when overused (it smuggles imperative logic into the pom); exec:exec is simple but ties the build to whatever is installed on PATH, hurting reproducibility. Both are last resorts versus a proper plugin.

code

xml · 19 lines
xml
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>3.5.0</version>
  <executions>
    <execution>
      <id>gen-proto</id>
      <phase>generate-sources</phase>
      <goals><goal>exec</goal></goals>
      <configuration>
        <executable>protoc</executable>
        <arguments>
          <argument>--java_out=${project.build.directory}/generated-sources</argument>
          <argument>api.proto</argument>
        </arguments>
      </configuration>
    </execution>
  </executions>
</plugin>

go deeper

for a junior

Know exec runs a program and antrun runs Ant tasks like copy/echo.

for a middle

Bind exec:exec / antrun:run to a phase with arguments and a target; know exec:java vs exec:exec exists.

for a senior

Weigh PATH dependence vs portability, JVM sharing in exec:java, and when antrun becomes a smell.

for a principal

Set policy: pin/vendor external tools for reproducible CI, cap antrun usage, and graduate recurring glue into a shared Mojo.

## The problem they solve Maven's lifecycle binds *goals* to *phases*. Sometimes you need a step with no existing goal — call a code generator, run `npm install`, copy a file, fail the build on a condition. Rather than write a plugin, two general-purpose plugins let you inline the step. ## exec-maven-plugin (org.codehaus.mojo) Two goals: - **exec:exec** — forks a *separate* OS process. You provide `<executable>` (e.g. `protoc`, `node`, `bash`) and `<arguments>`. It is language-agnostic. Because it forks, it does **not** get the Maven classpath unless you build it yourself; there is a special `%classpath` token to pass it. - **exec:java** — runs a Java `<mainClass>` in the *same* JVM as Maven, on the project's runtime/test classpath. No fork, fast, but a `System.exit` or rogue thread can affect the build, and it shares Maven's JVM. ```xml <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.5.0</version> <executions> <execution> <id>npm-build</id> <phase>generate-resources</phase> <goals><goal>exec</goal></goals> <configuration> <executable>npm</executable> <arguments><argument>run</argument><argument>build</argument></arguments> <workingDirectory>${project.basedir}/frontend</workingDirectory> </configuration> </execution> </executions> </plugin> ``` ## maven-antrun-plugin (org.apache.maven.plugins) One main goal, `antrun:run`, which executes an embedded Ant `<target>`. You get Ant's declarative task set: `<copy>`, `<move>`, `<mkdir>`, `<delete>`, `<replace>`, `<concat>`, `<echo>`, `<fail>`, plus conditionals. Maven properties are available, and you can export Ant-set properties back via `exportAntProperties`. ```xml <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <id>stamp-build</id> <phase>prepare-package</phase> <goals><goal>run</goal></goals> <configuration> <target> <mkdir dir="${project.build.directory}/meta"/> <echo file="${project.build.directory}/meta/build.txt">v=${project.version}</echo> </target> </configuration> </execution> </executions> </plugin> ``` ## Choosing - Need to run an installed external tool / shell script → **exec:exec**. - Need to run your own Java main on the project classpath quickly → **exec:java** (mind shared JVM / System.exit). - Need portable file shuffling, templating, conditional fail, with no external binary → **antrun**. ## Trade-offs and reproducibility - **exec:exec** depends on the host having the tool on PATH and the right version → builds become non-reproducible / machine-specific. Mitigate by pinning versions and documenting prerequisites, or using a wrapper (e.g. frontend-maven-plugin which downloads node). - **antrun** is portable (Ant ships with the plugin) but tempts you to embed sprawling imperative logic in XML — widely considered a smell. If a target grows, extract it into a real plugin or a script invoked via exec. - **exec:java** sharing the Maven JVM means heavy memory use or `System.exit(0)` can corrupt the reactor; for isolation prefer exec:exec with `java` as the executable. All three are pragmatic escape hatches; the cleaner long-term answer for repeated logic is a small Mojo (custom plugin).

  • What's the key runtime difference between exec:java and exec:exec?
    exec:java runs a Java main in Maven's own JVM on the project classpath (fast, no fork, but shares the JVM and is vulnerable to System.exit/threads). exec:exec forks a brand-new OS process with no Maven classpath unless you pass %classpath.
  • Why is heavy use of antrun considered an anti-pattern?
    It smuggles imperative build logic into the declarative pom as XML, which is hard to read, test, and reuse; the recommended path for non-trivial logic is a real Maven plugin (Mojo) or an external script.
  • How do exec:exec builds hurt reproducibility?
    They depend on a tool being present at a specific version on the machine's PATH; different developers/CI agents can get different results. You mitigate by pinning/vendoring the tool or using a plugin that downloads it.

saying these in an interview costs you the question

  • Claiming exec:java forks a new process
  • Saying antrun needs a separate Ant install (it bundles Ant)
  • Treating exec:exec as reproducible regardless of host tooling
  • Recommending antrun for large procedural logic

context