DEV Community

Cover image for How to run Java without Maven, Gradle, or IDE
Aldo Wachyudi
Aldo Wachyudi

Posted on • Edited on

How to run Java without Maven, Gradle, or IDE

When you want to start programming using Java, you're most likely use Maven, Gradle, or an Integrated Development Environment (IDE) like IntelliJ or NetBeans. These tools has one thing in common, they're slow!

To start an empty Java project, Gradle takes more than 2 minutes.

BUILD SUCCESSFUL in 2m 38s
3 actionable tasks: 3 executed
Enter fullscreen mode Exit fullscreen mode

Maven is better, although 57 seconds isn't exactly fast either.

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  57.824 s
[INFO] ------------------------------------------------------------------------
Enter fullscreen mode Exit fullscreen mode

In a complex Java project, we could argue this overhead is worth it. But, what if you don't want to start a complex Java project? What if you just want to try a Java library, test your programming logic, or trying out a new Java syntax?

For a simple project, Java's build-in program like javac and java is enough. You can start write a Java program with a text editor and a terminal. That's it!

In this article, I'll show three examples of using JDK tools and a terminal to run a Java program.

1. Running a self-contained Java program

The first example is a class that takes a String as an input and produce hash as an output. This class doesn't need any third-party library. This class only use Java's build-in classes.

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Md5Util1 {
    public static void main(String[] args) throws NoSuchAlgorithmException {
        String plainText = args[0];

        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(plainText.getBytes());
        byte[] digest = md.digest();

        // Convert byte array to a hexadecimal String
        StringBuilder sb = new StringBuilder(2 * digest.length);
        for (byte b : digest) {
            sb.append(String.format("%02x", b));
        }

        System.out.println("MD5 Hash: " + sb);
    }
}
Enter fullscreen mode Exit fullscreen mode

Open a terminal to compile and run this program.

$ javac Md5Util1.java
$ java Md5Util1 "Hello, world!"
MD5 Hash: 6cd3556deb0da54bca060b4c39479839
Enter fullscreen mode Exit fullscreen mode

The compilation takes 0.7 second. No need to run an IDE that takes a lot of memory. Nor does waiting the build system finished configuring something. Just write the code!

As a refresher, javac is JDK's program that compile java code to bytecode.

$ javac Md5Util1.java
$ ls
Md5Util1.java Md5Util1.class
Enter fullscreen mode Exit fullscreen mode

java is JDK's program that run a Java class.

$ man java
NAME
       java - Java application launcher

SYNOPSIS
       java [ options ] class [ argument...  ]
Enter fullscreen mode Exit fullscreen mode

Note that we don't use .class extension, we just use the class name. Instead of java Md5Util1.class, we use java Md5Util1.

The argument(s) is passed to the Class's main method (public static void main(String[] args)). In this case, "Hello, world!" is passed on Md5Util1 main method.

We'll explore the java options in the next example.

Update:
Since Java 11, you can run java directly like this.

$ java Md5Util1 "Hello, world!"
MD5 Hash: 6cd3556deb0da54bca060b4c39479839
Enter fullscreen mode Exit fullscreen mode

2. Running a JUnit test

Even in a simple project, you need to use a third-party library. Like JUnit. JUnit is the de-facto library for testing Java code. Let's modify the Md5Util1 to make it easier to test.

import java.security.MessageDigest;

public class Md5Util2 {
    public static void main(String[] args) throws Exception {
        String plainText = args[0];
        String output = stringToMd5(plainText);

        System.out.println("MD5 Hash: " + output);
    }

    public static String stringToMd5(String input) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(input.getBytes());
        byte[] digest = md.digest();

        // Convert byte array to a hexadecimal String
        StringBuilder sb = new StringBuilder(2 * digest.length);
        for (byte b : digest) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}
Enter fullscreen mode Exit fullscreen mode

We've extracted the code for hashing in stringToMd5 method. Now, let's add the test class.

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class Md5Util2Test {

    @Test
    public void helloWorld() throws Exception {
        // Given
        String input = "Hello, world!";
        String expectedOutput = "6cd3556deb0da54bca060b4c39479839";

        // When
        String actualOutput = Md5Util2.stringToMd5(input);

        // Then
        assertEquals(expectedOutput, actualOutput);
    }
}
Enter fullscreen mode Exit fullscreen mode

Again, open a terminal to compile and run this program.

$ javac Md5Util2.java Md5Util2Test.java
Md5Util2Test.java:1: error: package org.junit does not exist
import org.junit.Test;
Enter fullscreen mode Exit fullscreen mode

This time, javac shows an error! javac doesn't know where to find the org.junit package. We need to inform javac where to find the the JUnit package and classes.

Java classes is bundled in a jar file. So, we should download the junit.jar file and attach it during compilation. We use -cp option to include the path of the class (hence, class path).

Download the junit's jar file in the mvnrepository.com (or the junit website). In this example, I use JUnit 4.12.

$ javac -cp junit-4.12.jar Md5Util2.java Md5Util2Test.java
$ ls -al | grep ".class"
Md5Util2.class
Md5Util2Test.class
Enter fullscreen mode Exit fullscreen mode

Now, let's run the test!

$ java -cp .:junit-4.12.jar org.junit.runner.JUnitCore Md5Util2Test
Exception in thread "main" java.lang.NoClassDefFoundError: org/hamcrest/SelfDescribing
    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1017)
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174)
Enter fullscreen mode Exit fullscreen mode

Oops, another error. This is why we should read the manual πŸ˜….

JUnit require hamcrest.jar to run the test. So, heads over to mvnrepository.com to download the hamcrest.jar. In this example, I use version 1.3.

The class path can be used to in java command too. Let's try to run the test again.

$ java -cp .:junit-4.12.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore Md5Util2Test
JUnit version 4.12
.
Time: 0.01

OK (1 test)
Enter fullscreen mode Exit fullscreen mode

Use colon to separate classes in the class path. .:junit-4.12.jar:hamcrest-core-1.3.jar means we add the current path (dot is the current path) and two other jar files to run the program.

When we run a JUnit test, we're actually running org.junit.runner.JUnitCore class. We pass the target test class to the JUnitCore arguments. In this case, Md5Util2Test is the target test class.

At this point, you might be wondering.. Why we don't need to add MessageDigest in the class path? Java already add JDK's standard classes in the class path. The classes is bundled as rt.jar file. It's located in the /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/jre/lib directory.

You can exract the rt.jar by using jar -xf rt.jar command.

$ tree -L 1
.
β”œβ”€β”€ META-INF
β”œβ”€β”€ apple
β”œβ”€β”€ com
β”œβ”€β”€ java
β”œβ”€β”€ javax
β”œβ”€β”€ jdk
β”œβ”€β”€ org
β”œβ”€β”€ rt.jar
└── sun

$ cd java

$ tree -L 1
.
β”œβ”€β”€ applet
β”œβ”€β”€ awt
β”œβ”€β”€ beans
β”œβ”€β”€ io
β”œβ”€β”€ lang
β”œβ”€β”€ math
β”œβ”€β”€ net
β”œβ”€β”€ nio
β”œβ”€β”€ rmi
β”œβ”€β”€ security
β”œβ”€β”€ sql
β”œβ”€β”€ text
β”œβ”€β”€ time
└── util
Enter fullscreen mode Exit fullscreen mode

Other than java.security.MessageDigest, you can see other useful Java classes, such as:

  • java.math.BigDecimal
  • java.time.Duration
  • java.util.ArrayList

3. Running a Java program with multiple JAR dependencies

As you see in example 2, some JAR may depends on other JARs. The more JAR we use in the project, the more complicated the compilation and running command is. This is why build tools like Maven or Gradle exist.

That being said, we can still use JDK's tool to run the program. Let's use Retrofit library as example. Retrofit is a popular REST API library that uses Interface as the API declaration. I use the sample code from Retrofit website verbatim to simplify the example.

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;

import java.util.List;

public interface GitHubService {
    @GET("users/{user}/repos")
    Call<List<Repo>> listRepos(@Path("user") String user);
}
Enter fullscreen mode Exit fullscreen mode

The Repo object from GitHub API is contains lots of information about a repository. In this case, we're only interested in Repo's id and name.

public class Repo {
    public String id;
    public String name;
}
Enter fullscreen mode Exit fullscreen mode

Next, create GitHubRetrofit class that build the Retrofit instance.

import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.util.List;

public class GitHubRetrofit {

    private Retrofit retrofit;
    private GitHubService service;

    public GitHubRetrofit() {
        retrofit = new Retrofit.Builder()
                .baseUrl("https://api.github.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        service = retrofit.create(GitHubService.class);
    }

    public Repo helloWorldRepo() throws Exception {
        Response<List<Repo>> response = service.listRepos("octocat").execute();
        return response.body()
                .stream()
                .filter(repo -> repo.name.equals("Hello-World"))
                .findFirst()
                .get();
    }
}

Enter fullscreen mode Exit fullscreen mode

Finally, we create the test class that test the GitHubRetrofit class.

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class GitHubRetrofitTest {

    @Test
    public void repos() throws Exception {
        // Given
        CallGitHub callGitHub = new CallGitHub();
        String expectedName = "Hello-World";

        // When
        Repo repo = callGitHub.helloWorldRepo();

        // Then
        assertEquals(repo.name, expectedName);
    }
}

Enter fullscreen mode Exit fullscreen mode

Even with this simple API call example, there are many classes and import statements. So, there should be several JAR files involved. Of course, we need the retrofit, junit, and hamcrest JAR. But, how do we know which JAR files to download in mavenrepository?

This is why we use Maven or Gradle. They manages dependencies for us. Maven uses Project Object Model (POM) to describe the dependencies. The POM file itself is available in the mvnrepository's Retrofit page. For example:

<dependencies>
    <dependency>
        <groupId>com.squareup.retrofit2</groupId>
        <artifactId>retrofit</artifactId>
        <version>2.9.0</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.8.5</version>
        <scope>compile</scope>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

You can use Gradle to list out dependencies too, by using ./gradlew dependencies command.

+--- com.squareup.retrofit2:converter-gson:2.9.0
|    +--- com.squareup.retrofit2:retrofit:2.9.0
|    |    \--- com.squareup.okhttp3:okhttp:3.14.9
|    |         \--- com.squareup.okio:okio:1.17.2
|    \--- com.google.code.gson:gson:2.8.5
\--- com.squareup.retrofit2:retrofit:2.9.0 (*)
Enter fullscreen mode Exit fullscreen mode

Since this article is about building and running Java without Maven or Gradle, I won't explain more about this topic.

After doing some trial and error, we know that we need the following JAR files. I put it under the /libs folder.

$ ls
GitHubRetrofit.java
GitHubRetrofitTest.java
GitHubService.java
Repo.java
libs

$ tree
.
β”œβ”€β”€ converter-gson-2.9.0.jar
β”œβ”€β”€ gson-2.8.5.jar
β”œβ”€β”€ hamcrest-core-1.3.jar
β”œβ”€β”€ junit-4.12.jar
β”œβ”€β”€ okhttp-3.14.9.jar
β”œβ”€β”€ okio-1.17.2.jar
└── retrofit-2.9.0.jar

0 directories, 7 files

Enter fullscreen mode Exit fullscreen mode

That's a lot of JARs!

To compile the class, run this command.

$ javac -d classes -cp libs/\* *.java
Enter fullscreen mode Exit fullscreen mode

The -d option describes the destination directory. Since we're dealing with multiple files, it's tidier to group the class in a separate folder.

$ tree -L 2
.
β”œβ”€β”€ GitHubRetrofit.java
β”œβ”€β”€ GitHubRetrofitTest.java
β”œβ”€β”€ GitHubService.java
β”œβ”€β”€ Repo.java
β”œβ”€β”€ classes
β”‚Β Β  β”œβ”€β”€ GitHubRetrofit.class
β”‚Β Β  β”œβ”€β”€ GitHubRetrofitTest.class
β”‚Β Β  β”œβ”€β”€ GitHubService.class
β”‚Β Β  └── Repo.class
└── libs
    β”œβ”€β”€ converter-gson-2.9.0.jar
    β”œβ”€β”€ gson-2.8.5.jar
    β”œβ”€β”€ hamcrest-core-1.3.jar
    β”œβ”€β”€ junit-4.12.jar
    β”œβ”€β”€ okhttp-3.14.9.jar
    β”œβ”€β”€ okio-1.17.2.jar
    └── retrofit-2.9.0.jar

2 directories, 15 files
Enter fullscreen mode Exit fullscreen mode

Finally, run the test by using this command.

$ java -cp classes:libs/\* org.junit.runner.JUnitCore GitHubRetrofitTest
JUnit version 4.12

Time: 1.355

OK (1 test)
Enter fullscreen mode Exit fullscreen mode

With several JAR files, we could simplify the command by grouping the Java files, Class files, and JAR files in a separate folder.

In this scenario, this trick works nicely. But for more complex project the build system can handle this in more elegant way. For example, Maven has build a standard directory layout for organizing a Java project. You can execute build and run with a simpler command.

Summary

Don't be scared by the last example. For simple use case, like solving an algorithmic problem or running a test, building and running Java manually is simple and powerful.

Not only you're more aware of the syntax (No autocomplete to help you), you'll also appreciate the blazing fast compilation speed. This enables you to focus more on the code rather than tweaking build system or IDE configuration.

As a bonus, if you're a laptop user like I am, you'll recognize your laptop temperature is colder 😁 (Yay typing!).

Use Maven/Gradle if you need few dependencies. Rather than typing and remembering Java commands, you just need to remember few build system commands like test or build.

Finally, use IDE if you need to manage many dependencies (and its transitive dependencies). IDE is also useful in some cases, like method completion (so, you don't have to find Java docs in the internet) or breakpoint for debugging (you can do it with jdb but it's more complicated).

In conclusion, use the right tool for the right job!

Credit

Top comments (3)

Collapse
 
rwitak profile image
RWitak

Thanks, just what I was looking for. I started out with IDEs and using Gradle and later Maven without ever learning the basics. Now I feel a little more comfortable just using barebones Java tools - and the build tools also make more sense!

Collapse
 
aldok profile image
Aldo Wachyudi

I'm glad you found the article helpful! Indeed, for basic use cases, built-in Java tools are often sufficient πŸ‘

Collapse
 
_siva_sankar profile image
Siva Sankaran S • Edited

Thanks @aldok . I am learning java from the scratch. I am coming from c#/.net background and worked in an angular project once. I always curious to understand how something works under the hood when I am encountering them. I haven't yet picked any IDE or any sophisticated VS code plugins intentionally for that. Thank you so much to explain the build process of java programs.

Note: Microsoft had spoiled us the dotnet programmers :D. Still I don't know much about the build process of dotnetπŸ˜‹