Maven DI
What is Maven DI?
Maven DI is the dependency injection framework of Maven, that is introduced in Maven 4.
It is the successor for Plexus DI (used in Maven 2) and JSR 330/Eclipse Sisu (used in Maven 3) in Maven.
How to use Maven DI
When you use Maven DI in Maven plugins or extensions, you want to have the dependency to maven-api-di
, so you can use the @org.apache.maven.api.di.Inject
, @org.apache.maven.api.di.Named
, and @org.apache.maven.api.di.Singleton
annotations in your plugins and extensions.
The annotations of Maven DI are similar to JSR 330, but they have different package name org.apache.maven.api.di
.
Implementation Details
TBD (need help)
How to use Maven DI in plugins
NOTE
If your plugin should also run with Maven 3.x, please look at Maven & JSR-330. Plugins that are using Maven DI are only compatible with Maven 4.
Let's take a look at an example plugin: If you want to look at this example project, you can find the code in Maven Core ITs.
The POM is set up for Maven DI as previously mentioned, with the maven-api-di
dependency
In addition, we add a Maven plugin dependency maven-api-core
to implement the interface Mojo
.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>org.apache.maven.its</groupId>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>mavendi-maven-plugin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<name>mavendi-maven-plugin Maven Plugin</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javaVersion>17</javaVersion>
<mavenVersion>4.0.0-beta-5</mavenVersion>
<mavenPluginPluginVersion>4.0.0-beta-1</mavenPluginPluginVersion>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-api-core</artifactId>
<version>${mavenVersion}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-api-di</artifactId>
<version>${mavenVersion}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-api-meta</artifactId>
<version>${mavenVersion}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>${mavenPluginPluginVersion}</version>
<configuration>
<extractors>
<extractor>java-annotations</extractor>
</extractors>
<skipErrorNoDescriptorsFound>true</skipErrorNoDescriptorsFound>
</configuration>
<executions>
<execution>
<id>default-descriptor</id>
<phase>process-classes</phase>
<configuration>
<internalJavadocBaseUrl>./apidocs/</internalJavadocBaseUrl>
</configuration>
</execution>
<execution>
<id>generate-helpmojo</id>
<goals>
<goal>helpmojo</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Now let's take a look at the plugin code.
We are using field injection to add the MavenDIComponent
to our Mojo.
import org.apache.maven.api.Lifecycle.Phase;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.plugin.Log;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;
@Mojo(name = "hello", defaultPhase = Phase.VALIDATE, projectRequired = false)
public class HelloMojo implements org.apache.maven.api.plugin.Mojo {
@Inject
private MavenDIComponent component;
@Inject
private Log logger;
@Parameter(property = "name")
private String name;
public void execute() {
//
// Say hello to the world, my little constructor injected component!
//
String message = component.hello(name);
logger.info(message);
}
}
The component that should be injected is annotated with @Named
and @Singleton
.
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton;
@Named
@Singleton
public class MavenDIComponent {
public String hello(String name) {
return "Hello " + name + "! I am a component that is being used via field injection!";
}
}
The next question is how to write a unit test for our Mojo. Maven DI introduces new helper classes for testing. They are packaged in Maven Plugin Testing Harness They have a good integration to de facto standard testing framework JUnit Jupiter.
So the next step is to add all dependencies that are needed for testing.
For the helper classes, we are adding maven-testing
.
The dependencies to maven-core
, maven-resolver-api
and maven-api-impl
are only needed for the test runtime.
junit-jupiter
is the used test framework, mockito-*
is the used mock framework and assertj
is the used annotation lib.
The last two mention dependencies are optional but helpful.
<!-- ... -->
<properties>
<!-- ... -->
<mavenPluginTestingVersion>4.0.0-beta-2</mavenPluginTestingVersion>
<mavenResolverVersion>2.0.2</mavenResolverVersion>
</properties>
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-testing</artifactId>
<version>${mavenVersion}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>${mavenVersion}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-api</artifactId>
<version>${mavenResolverVersion}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-api-impl</artifactId>
<version>${mavenVersion}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- ... -->
After you configure the test dependencies, you can write the first unit test for you mojo.
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Priority;
import org.apache.maven.api.di.Provides;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.plugin.testing.InjectMojo;
import org.apache.maven.api.plugin.testing.MojoParameter;
import org.apache.maven.api.plugin.testing.MojoTest;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@MojoTest
public class HelloMojoTest {
@Inject
private MavenDIComponent componentMock;
@Test
@InjectMojo(goal = "hello")
@MojoParameter(name = "name", value = "World")
public void testHello(HelloMojo mojoUnderTest) {
mojoUnderTest.execute();
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(componentMock).hello(captor.capture());
assertThat(captor.getValue()).isEqualTo("World");
}
@Singleton
@Provides
@Priority(10)
private MavenDIComponent createMavenDIComponent() {
return mock(MavenDIComponent.class);
}
}
If you want to mock your injected component, you have to write a method that creates the mock for this component.
This method has to be annotated with @Singleton
, @Provides
and @Priority(10)
, so that the DI framework selected it before the real component.