This tutorial will show you how to run a basic Java Maven project on the command line (as opposed to on an IDE).

We will learn how to run maven commands that:

  1. Create a new maven project
  2. Compile, package, and execute your Java code from an executable JAR file
  3. Run unit tests for your code
  4. Add external dependencies to your project

All of these tasks will be done on the command line, so that you can have a better idea on what’s going on under the hood, and how you can run a Java application without needing an IDE like Eclipse or IntelliJ.

If you just want to see the example code, you can view it on Github

Creating a New Maven Project

If you haven’t already, install OpenJDK on your system, after which you can install maven.

First, let’s create a new project folder using the maven command line tools:

mvn -B archetype:generate -DgroupId=com.sohamkamani -DartifactId=mvn-example -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4

I’ve used a groupID corresponding to my domain (sohamkamani.com), you should replace this with your own choice

This generates a new project folder with the following structure:

mvn-example
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── com
    │           └── sohamkamani
    │               └── App.java
    └── test
        └── java
            └── com
                └── sohamkamani
                    └── AppTest.java

App.java contains simple code that prints Hello World! when run.

Compiling and Packaging Our JAR File

Before running a Java application in production, we’ll need to compile the Java code into byte-code that can be run on the JVM.

If we have multiple classes and folder (which we most likely will), we have to package the compiled code into a common format (like a .jar file).

We can perform compilation and packaging by running the following command:

mvn compile
mvn package

We can combine these two commands by running mvn package.

Since compile and package are part of the same lifecycle, running mvn package will execute all lifecycle steps upto package, which includes the compile step

Running this command will create a bunch of files in a new target directory:

mvn-example
├── pom.xml
├── src/...
└── target
    ├── classes
    │   └── com
    │       └── sohamkamani
    │           └── App.class
    ├── test-classes
    │   └── com
    │       └── sohamkamani
    │           └── AppTest.class
    └── mvn-example-1.0-SNAPSHOT.jar

Some auxillary files are omitted from here for the sake of clarity

maven compiles and packages yor code

The JAR file is the final output that can be executed by the JVM. However, we still have to perform some additional steps before we can run our code.

Running Our Code

We can use the java command to execute our JAR file:

java -jar target/mvn-example-1.0-SNAPSHOT.jar

If we run this now, we will get the following error:

no main manifest attribute, in target/tmp-mvn-example-1.0-SNAPSHOT.jar

This is because the JAR file doesn’t know the entry point, so it has no idea where the main method is.

We can make use of the Maven JAR plugin, which gives us additional capabilities to build JAR files.

We can add the following configuration as a child of the <build> tag:

<!-- this goes within <build> -->
<plugins>
	<plugin>
		<!-- Build an executable JAR -->
		<groupId>org.apache.maven.plugins</groupId>
		<artifactId>maven-jar-plugin</artifactId>
		<version>3.1.0</version>
		<configuration>
			<archive>
				<manifest>
					<addClasspath>true</addClasspath>
					<!-- here we specify that we want to use the main method within the App class -->
					<mainClass>com.sohamkamani.App</mainClass>
				</manifest>
			</archive>
		</configuration>
	</plugin>
</plugins>
<!-- other properties -->

We can now rebuild the project by running:

mvn clean package

The clean subcommand removes previous artifacts in the target directory, such as the previous stale JAR file

Next, we can execute the JAR file by running:

java -jar target/mvn-example-1.0-SNAPSHOT.jar

Which will give us the output:

Hello World!

Adding More Classes

To organize code better, we can add more classes to our project. These will be compiled and packaged along with the rest of the code automatically.

To illustrate, let’s create a new class called Calculator within src/main/java/com/sohamkamani/Calculator.java:

package com.sohamkamani;

public class Calculator {
    public static int add(int n1, int n2) {
        return n1 + n2;
    }
}

This is a simple class with a single static method that adds two numbers.

We can now use this class within our App class:

public class App {
    public static void main(String[] args) {
        // Now, we will print "Hello World!" along with the result of the add method
        System.out.printf("Hello World! %d", Calculator.add(4, 5));
    }
}

Running Unit Tests

Maven can also be used to run tests that we’ve defined in our project.

By convention, all tests reside within the src/test directory.

We can now create a unit test for the Calculator.add method within src/test/java/com/sohamkamani/CalculatorTest.java:

package com.sohamkamani;

// the JUnit library is used for testing
import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class CalculatorTest {

    @Test
    public void ShouldAdd() {
        assertEquals(2, Calculator.add(1, 1));
    }
}

By default, the maven project folder comes bundled with the JUnit library for running unit tests

To run tests, we can run the mvn test command - this will run all tests, tell us how many passed and failed, and give us more information about the failed tests.

In our case, if we run the tests, we will get the following output:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.sohamkamani.CalculatorTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.015 s - in com.sohamkamani.CalculatorTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.086 s
[INFO] Finished at: 2023-06-30T13:07:02+05:30
[INFO] ------------------------------------------------------------------------

And, if we change the test to fail:

public class CalculatorTest {

    @Test
    public void ShouldAdd() {
        // this is wrong, and should fail
        assertEquals(3, Calculator.add(1, 1));
    }
}

The we will get some additional information about the failed test:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.sohamkamani.CalculatorTest
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.017 s <<< FAILURE! - in com.sohamkamani.CalculatorTest
[ERROR] ShouldAdd(com.sohamkamani.CalculatorTest)  Time elapsed: 0.004 s  <<< FAILURE!
java.lang.AssertionError: expected:<3> but was:<2>
        at com.sohamkamani.CalculatorTest.ShouldAdd(CalculatorTest.java:11)

[INFO] 
[INFO] Results:
[INFO] 
[ERROR] Failures: 
[ERROR]   CalculatorTest.ShouldAdd:11 expected:<3> but was:<2>
[INFO] 
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.006 s
[INFO] Finished at: 2023-06-30T13:08:21+05:30
[INFO] ------------------------------------------------------------------------

Running Single Test Classes or Methods

When running tests in the previous section, we ran all tests in the project. However, we can also run a single test class or method by specifying the class name or method name:

# Running all tests in a single class
mvn test -Dtest=MyClassName

# Running a single test method
mvn test -Dtest=MyClassName#myTestMethod

For example, let’s say we had another test class called FailedTest in the same test package:

src/test
└── java
    └── com
        └── sohamkamani
            ├── CalculatorTest.java
            └── FailedTest.java  <-- new test class

This class contains a single test which always fails:

public class FailedTest {
  @Test
  public void alwaysFails() {
    Assert.fail("This test should fail");
  }
}

In this case, we only want to run the tests in CalculatorTest.java, and not FailedTest.java. We can do this by specifying the class name:

mvn test -Dtest=CalculatorTest

This will run all test methods within the CalculatorTest class, and ignore all other classes.

Now, let’s say we add a new test method called ShouldAddNegativeValues to CalculatorTest.java:

public class CalculatorTest {

    @Test
    public void ShouldAdd() {
        assertEquals(2, Calculator.add(1, 1));
    }

    @Test
    public void ShouldAddNegativeValues() {
        assertEquals(-2, Calculator.add(-1, -1));
    }
}

If we only want to test this single method, we can run:

mvn test -Dtest="CalculatorTest#ShouldAddNegativeValues"

Isolating tests in this way can be useful when debugging, since running all the tests every time can be time consuming and make it harder to find the issue when a test fails.

Adding Dependencies with the Maven Assembly Plugin

Let’s look at how to add dependencies and package them in our JAR file.

For most applications need external libraries (like Spring Boot or Apache Commons) to implement common functionality. Maven allows us to install these dependencies by specifying them in our pom.xml file.

For this example, let’s install the Cowsay library, which will display our output as a quote from a friendly cartoon figure of a cow.

First, we have to add Cowsay as a dependency in our pom.xml file:

<!-- ... -->

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <!-- We can add additional dependencies here -->
    <dependency>
      <groupId>com.github.ricksbrown</groupId>
      <artifactId>cowsay</artifactId>
      <version>1.1.0</version>
      <classifier>lib</classifier>
    </dependency>
  </dependencies>

<!-- ... -->

Next, we can use the Cowsay.say method within our main method to print the final output string:

package com.sohamkamani;

// import Cowsay class from the library
import com.github.ricksbrown.cowsay.Cowsay;

public class App {
    public static void main(String[] args) {
        int result = Calculator.add(4, 5);
        // We can specify the arguments and get the display
        // string from the `Cowsay.say` method
        String[] cowArgs = new String[] { String.valueOf(result) };
        String cowString = Cowsay.say(cowArgs);

        // print the final output string
        System.out.printf(cowString);
    }
}

However, there’s a problem - If we recompile our code and try to run the app now, we will get an error:

$ mvn clean compile package
$ java -jar target/mvn-example-1.0-SNAPSHOT.jar 
Exception in thread "main" java.lang.NoClassDefFoundError: com/github/ricksbrown/cowsay/Cowsay
        at com.sohamkamani.App.main(App.java:13)
Caused by: java.lang.ClassNotFoundException: com.github.ricksbrown.cowsay.Cowsay
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
        ... 1 more

It looks like the Java class loader couldn’t find the classes for the Cowsay library, even though we added it as a dependency in the pom.xml file.

This happens because by default, maven doesn’t bundle the dependency class files along with the application code. To enable this, we can use the maven-assembly-plugin.

This plugin includes all of our applications dependencies into the JAR file. This increases its overall size, but ensures that we can run it as a standalone executable using the java -jar command.

Let’s add the Maven assembly plugin in the pom.xml build definition:

<!-- ... -->
<build>
<plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <archive>
        <manifest>
            <addClasspath>true</addClasspath>
            <mainClass>com.sohamkamani.App</mainClass>
        </manifest>
        </archive>
    </configuration>
    </plugin>
    <!-- Add the assemble plugin with standard configuration -->
    <plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <configuration>
        <archive>
        <manifest>
            <mainClass>com.sohamkamani.App</mainClass>
        </manifest>
        </archive>
        <descriptorRefs>
        <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
    </configuration>
    </plugin>
</plugins>
<!-- ... -->
</build>
<!-- ... -->

To assemble our JAR file, we can run the assembly:single goal after the compile goal:

mvn clean compile assembly:single

This creates a new JAR file in the target directory that you can run using the java -jar command:

$ java -jar target/mvn-example-1.0-SNAPSHOT-jar-with-dependencies.jar
 ___
< 9 >
 ---
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Adding Command Line Arguments

We can pass in custom arguments directly from the command line when we run the java command.

For example, we can run:

java -jar target/mvn-example-1.0-SNAPSHOT-jar-with-dependencies.jar lorem ipsum

The lorem and ipsum strings can be accessed within the main method using the args array:

public static void main(String[] args) {
    // args is the array of command line arguments
    // in this case, args[0] = "lorem" and args[1] = "ipsum"
}

We can pass in any number of arguments, and access them in this way.

As an example, we can modify our code so that it prints the first argument as a famous quote, and the second argument as the author:

public class App {
    public static void main(String[] args) {
        // first, we should check if the user has provided the correct number of arguments
        if (args.length != 2) {
            // if not, we should print a usage message and exit
            System.out.println(
                    "Usage: java -jar target/cowsay-1.0-SNAPSHOT.jar \"quote\" \"author\"");
            System.exit(1);
        }
        String quote = args[0];
        String author = args[1];
        String[] cowArgs = new String[] {quote, " --" + author};
        String cowString = Cowsay.say(cowArgs);
        System.out.printf(cowString);
    }
}

Now, if we compile and run the application with the correct number of arguments, we will get the following output:

java -jar target/mvn-example-1.0-SNAPSHOT-jar-with-dependencies.jar "e=mc2" "Albert Einstein"

 _________________________
< e=mc2 --Albert Einstein >
 -------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

You can view the complete example code for this post on Github

How do you organize your Java projects? Do you ever use the command line for common tasks? Let me know in the comments!