Table of Contents

Introduction

Java is a very popular programming language with a very long history. The technologies around it have helped to develop highly used and well-engineered enterprise applications. The purpose of this article is to describe a possible setup for the development environment of a professional software developer.

Let’s recap a few key features and terms:

  • the usual way to run Java applications is by compiling Java code in a low-level language called bytecode and use an interpreter, the so-called “Java Virtual Machine” or “JVM”, to execute the program
  • the main benefit of using the JVM is the portability of the compiled code since it will run properly on any processor and operating system for which a JVM exists
  • to execute the code we need at least the compiled code and the “Java Runtime Environment” (JRE)
  • for development we use the “Java Development Kit” (JDK) which includes a JRE, the compiler and all the necessary tools
  • other languages that compile to bytecode has been developed after Java, so the term “Java Virtual Machine” might not be very accurate anymore
  • probably as a response to the performance penalty paid by Java in comparison with GoLang, a very interesting project has been developed: GraalVM [1] which is able to compile Java code into a native executable

Smart Installation of Java (JDK)

There are multiple versions of the language and multiple implementations of the JVM. In an ideal world retro-compatibility and adherence to the language specifications would make us need only the latest version of any JDK implementation. However this is far from the reality so we need to be able to use multiple versions of Java and different implementations. Both requirements are met if we install each JDK in a different folder and we have a convenient way to switch from one JDK to another.

This problem is very common because it is not limited to the Java world and its solution provided by the operating system, looks very simple if we understand the concept of “Environment Variables” (in particular “PATH”). This is also cross-platform since it is the same for all the operating systems (Linux, Windows, MacOS, BSD, etc).

Let’s say we have written a hello-world program in “MyClass.java” and we have a JDK properly installed, we can compile the code with javac MyClass.java and execute the compiled code with java MyClass. The commands java and javac are actual programs and we can find the corresponding executable files in the filesystem in the directory of the JDK. If, for example, we install two JDKs, one for Java 17 and another for Java 21 we will have a file called java for each JDK and “using” Java 17 would mean runing the executable java (or java.exe in Windows) from the first directory. When we execute a Java program by clicking on “run” in the IDE or with a double click on the Jar file, the graphical user interface will run the java command under the hood. So if we understand how to set the desired JDK in the terminal we understand how to do it also in the IDE or in general in the operating system.

What does it happen when we run java MyClass? The operating system checks the content of the environment variable PATH which is a list of directories, then loops in the directory list until it finds an executable called “java”. At that point quit the search and run the program with the argument “MyClass”. In our example where we have two JDKs, if the second one is in the variable PATH before the first one, the operating system will execute Java 21; switching to Java 17 would mean adding the directory of the first JDK in the PATH at the beginning of the list.

The value of PATH is configured with an initial value during the bootstrap of the operating system but can be modified in each terminal. You can find documentation for your operating system where you can configure that variable if you want a default JDK to be automatically selected, however, to avoid confusion, I usually don’t have any default JDK my PATH. I then change the content of the PATH on a terminal base so I can work with Java 17 in a terminal and with Java 21 in a second terminal.

Let’s start by checking if the operating system already has a JRE installed:

  • open a new terminal and run java -version
  • if the output prints a version of Java it means there is at least a JRE installed (don’t worry if you have one, you can leave it there and keep following the guide without any issue; we will not harm that JRE and that will not create any trouble for us)
  • otherwise you should read an error message similar to “command not found”

If you are using Windows, please install Bash on Windows using MSYS2 [2], or Git Bash [3] or WSL (Windows Subsystem For Linux) [4].

Open a bash terminal (Git Bash, WSL or the native shell on Linux/MacOS/BSD) and proceed with this guide using this terminal. In case you are not sure you can check if it is “bash” by running echo $SHELL and expecting a result similar to /bin/bash.

Run the following command to create the folder structure we will use in the following:

cd $HOME
mkdir bsdrulez
cd bsdrulez
mkdir downloads apps projects

Choose a JDK

As mentioned previously there are multiple implementations of the Java Language Specifications. Some of them are open source, others are available only by purchases; however, unless you are working with a specific product that expressly says it supports only a closed-source JDK, the open-source JDK is preferable (ready for production even for the biggest and most secure enterprise application).

It is worth mentioning the following JDK:

  • Oracle JDK: due to changes in the software license you need to check the conditions of the version you want to use [5]; I personally haven’t used this one after Java 8
  • OpenJDK [6]: this is the open source reference implementation, but it receives support and updates only for 6 months (even the LTS version) [8]
  • Eclipse Temurin [7]: this is a production-grade implementation maintained by the Eclipse Foundation [9] and released with an Open Source License; this is my current default choice

Please read the full article at [8] if you want to have a more accurate picture. For our purposes you can proceed and download the archive of Eclipse Temurin [7] LTS for your platform (please note you need the Linux version if you are a Windows user using WSL). To demonstrate the usage of multiple JDKs, download both the archives for the LTS 17 and LST 21 and put those archives in the directory “downloads” we created at the end of the previous section.

Installing JDK “Eclipse Temurin”

Let’s proceed with the installation of our JDKs and extract the archive in the directory “apps” (you can do it manually or use the following commands):

cd $HOME/bsdrulez/downloads
cat OpenJDK21U-jdk_x64_linux_hotspot_*.tar.gz | tar xzv -C ../apps
cat OpenJDK17U-jdk_x64_linux_hotspot_*.tar.gz | tar xzv -C ../apps

Verify the correct installation with the following command and output:

$ ls $HOME/bsdrulez/apps
jdk-17.1.2+6    # your folder name can be different, but should start with jdk-17
jdk-21.0.3+9    # your folder name can be different, but should start with jdk-21

To use JDK 21 we can run:

export PATH=$HOME/bsdrulez/apps/jdk-21*/bin:$PATH
java -version  # this command should print "OpenJDK Runtime Environment Temurin-21"

If you open a new terminal and run the same commands with jdk-17 instead of jdk-21 you will see the other version in the output of the command java -version. As you can see to switch from one JDK to another is just a matter of “exporting” the environment variable PATH with the correct folder added at the beginning.

It is worth noting the general principle can be applied to install virtually any application. We will install Gradle, Maven and Eclipse (or IntelliJ) in the same way. This is also the same concept of CLASSPATH in Java.

Now that we have understood the basics, let’s move on to the next section where we will automate the switch from a JDK to another one.

Create Aliases to switch from a JDK to another

Instead of having to run the export command manually is is useful to define an alias so we just type use_jdk17 or use_jdk21 and it automatically switches from one JDK to another. In order to do that we will define the aliases in the file “$HOME/.bashrc”:

cat <<"EOF" >> $HOME/.bashrc

alias use_jdk21='export PATH=$HOME/bsdrulez/apps/jdk-21*/bin:$PATH'
alias use_jdk17='export PATH=$HOME/bsdrulez/apps/jdk-17*/bin:$PATH'
EOF

Please keep the first empty line so we are not breaking the .bashrc if the last line does not end with a new line character.

Now open a new terminal and check if everything is working:

# the following 7 lines create the file MyClass.java with that java code
cat <<"EOF" > MyClass.java
class MyClass {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
EOF

use_jdk21
java -version     # this command should print "OpenJDK Runtime Environment Temurin-21"
javac MyClass.java
java MyClass      # this should print "Hello, World!"
rm MyClass.class  # remove the compiled file

use_jdk17
java -version  # this command should print "OpenJDK Runtime Environment Temurin-17"
javac MyClass.java
java MyClass   # this should print "Hello, World!"
rm MyClass.*   # remove both MyClass.java and MyClass.class

Why to use Gradle and/or Maven

Even for the simplest real-world application you need to compile multiple classes and you want to use at least a third-party library (like JUnit to test your code). It is perfectly possible to compile each class manually, but the process can get time-consuming and super boring. This is one of the main reasons why they invented some tools like Maven or Gradle to automate the compilation of the code. Moreover what does it happen if I want to use a library that uses a second library and the second library uses three more libraries? I need all of them to successfully compile and run my project. This is a very well-known problem called “Dependency Hell”. The first time I heard about it was when I was a teenager playing with Linux distributions. At the time you had to download and install manually all the dependencies. They soon invented the package manager and the automatic dependency resolution. Back in the Java world Maven and Gradle are very useful also for this purpose: managing libraries and hiding the complexity of their dependencies.

Install Maven

Maven can be installed in a similar way as we installed the JDK (check [10] to find the latest stable release):

cd $HOME/bsdrulez/downloads
# at the time of writing the latest Maven is 3.9.7
wget https://dlcdn.apache.org/maven/maven-3/3.9.7/binaries/apache-maven-3.9.7-bin.tar.gz
cat apache-maven-3.9.7-bin.tar.gz | tar xz -C ../apps
cat <<"EOF" >> $HOME/.bashrc

alias use_maven='export PATH=$HOME/bsdrulez/apps/apache-maven-3.9.7/bin:$PATH'
EOF

source $HOME/.bashrc   # this reload the .bashrc so the new alias is available
use_jdk21
java -version
use_maven
mvn -version

The fastest way to generate a new project with Maven is to use Maven Archetypes [12]:

mkdir my-new-maven-project
cd my-new-maven-project
mvn archetype:generate \
    -DarchetypeGroupId=org.apache.maven.archetypes   \
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -DarchetypeVersion=1.4

mvn clean package
java -jar target/*.jar

If you want to include a new dependency in your project, go to Maven Central [15] and find your dependency. Instead of downloading the Jar archive, copy the snippet for Maven and put it in your pom.xml in the section .

Install Gradle

Similarly to what we did in the previous section we can install Gradle (find the latest version at [11]:

cd $HOME/bsdrulez/downloads
# at the time of writing the latest Gradle is 8.7
wget https://github.com/gradle/gradle-distributions/releases/download/v8.7.0/gradle-8.7-bin.zip
unzip -q gradle-8.7-bin.zip
mv gradle-8.7 ../apps
cat <<"EOF" >> $HOME/.bashrc

alias use_gradle='export PATH=$HOME/bsdrulez/apps/gradle-8.7/bin:$PATH'
EOF

source $HOME/.bashrc   # this reload the .bashrc so the new alias is available
use_jdk21
java -version
use_gradle
gradle -version

To set up a new project you can run:

mkdir my-new-gradle-project
cd my-new-gradle-project
gradle init

gradle tasks
gradle build
gradle run

Maven Central [15] can be used also for Gradle. Add to your file build.gradle the proper snippet found on Maven Central.

Spring Starter

The most useful framework for Java is Spring [13] and he simplest way of starting a new Spring application is to use the Spring Initializer [14]. There is a good UI where you can select the packages you want to include in your project and then you can download a zip archive with the Maven or Gradle project.

Depending on what you have chosen in the Spring Initializer, the project will include the “Maven Wrapper” or the “Gradle Wrapper”. There are a couple of scripts to let you use Maven or Gradle without the need to install them. However I always prefer to have full control of my tools, so I rarely use the Wrapper.

Integrated Development Environment (IDE)

Now let’s get to the interesting part ;)

Any IDE that can use Maven or Gradle will support projects created with the Spring Initializer. So, don’t worry if you read that only the paid version of IntelliJ supports the plugin for Spring or if you inadvertently tried to use the IDE Spring Tools and you found out it was as slow as hell. You will be able to use the IDE you prefer.

Let’s start by saying that IDE is a matter of personal preferences, choose whatever suits you. If you like Visual Studio Code or NetBeans that is completely fine. It is up to you.

When I started I learned how to use Eclipse and once you know its quirks and perks it really does all you need. It is also open source and still widely used by professionals in 2024.

The biggest downside of Eclipse, in my opinion, is its heaviness. Especially when we install Spring Tools from the Marketplace quickly becomes super slow to start and resource hungry. Since I am pretty comfortable at the command line, I don’t really need an IDE, however I wasn’t able to configure Vim or Emacs in a proper way to do what Eclipse or any other high-level IDE does out of the box. In particular I am referring to code navigation, automatic handling of imports, autocompletion and the debugger. All the “integrations” you can do with an IDE are really useless to me and they only add heaviness to my Eclipse, so I finally found out how to install just the core of Eclipse and add the only 3 features I need:

  • Java Development Tool (JDT)
  • Maven support
  • Gradle support

Lightweight Eclipse

Like most of the IDE and advanced text editors, there is a core of the application which is relatively lightweight (and relatively useless) and a bunch of plugins providing interesting functionalities. This is why it is possible to have different variants of Eclipse tailored to their specific purposes. Eclipse is used most often for Java development, but if you check the official download page of Eclipse [19] you will find packages for different flavors of the Java development and also for C/C++ and PHP. I also have successfully used Eclipse for Python development (you can download any package and then install a plugin from the “Marketplace” [20].

One of the most complaints I hear about Eclipse is that it is bloated. The reality is that how slow and resource-hungry it is depends on what you actually install inside it. At first I tried to find a way to uninstall plugins or I was looking at the most basic package from the download page. However the IDE was still filled with something I couldn’t care the less and I was wondering why they didn’t provide something more basic and customizable. Even if they did a great job in hiding it, I eventually was enlightened by a post on StackOverflow [16] which pointed me to the “Eclipse Platform Runtime Binary” [17] (which is the core of the IDE).

To install our lightweight version of Eclipse for Java:

  1. go to this page and find the latest version
  2. click on the desired build and scroll down to the next page until you reach the section “Platform Runtime Binary” (for example [18])
  3. download the version for your operating system (or Linux if you are using WSL under Windows) and install it following the same procedure we used for the JDKs, Maven and Gradle:
    cd $HOME/bsdrulez/downloads
    # at the time of writing the latest build is 4.30
    wget https://download.eclipse.org/eclipse/downloads/drops4/R-4.30-202312010110/download.php?dropFile=eclipse-platform-4.30-linux-gtk-x86_64.tar.gz
    cat eclipse-platform-4.30-linux-gtk-x86_64.tar.gz | tar xz
    mv eclipse* ../apps/eclipse
    echo '' >> $HOME/.bashrc
    echo 'export PATH=$HOME/bsdrulez/apps/eclipse:$PATH' >> $HOME/.bashrc
    source $HOME/.bashrc
    
  4. if you run the command eclipse in the terminal you will notice its complains about the missing JVM (unless you have a default one, so remember to type use_jdk21 before eclipse
  5. this core of Eclipse does not contain even the JRE to be able to run by itself, I like it
  6. the first time you open Eclipse it will ask where to put the namespace
  7. now, to set up Eclipse for Java development with the bare minimum, you need to navigate the main menu “Help -> Install New Software”
  8. in the pop-up window select “–All Available Sites–”
  9. search for JDT (Java Development Tool) and select the package (just the main package) which is “Eclipse Java development tools”
  10. from the same window search and select the main package for Maven: “M2E - Maven Integration for Eclipse”
  11. and the one for Gradle: “Buildship: Eclipse Plug-ins for Gradle”
  12. proceed with the installation
  13. now it is time to restart Eclipse
  14. open the Java Perspective: “Windows -> Perspective -> Open Perspective -> Java”
  15. (optional) open other “Views” of your interests in the current perspective (like the “Project Explorer”, “Gradle Tasks”, etc.)

We are ready to perform our initial tests to see if we have all the tools we need:

  • create a simple Java project and test the Hello World application from the IDE
  • since in the real world the bare minimum is to use Maven or Gradle with an external library for testing, go to the terminal and create a couple of projects: one with Maven archetype quickstart and the other with gradle init
  • get back to Eclipse and import those projects: “File -> Open Project from Filesystem”
  • open the sample app and add some lines of code or write a new test using one feature provided by the external library already included
  • then you have a couple of options: if you want to experiment with running the application from Eclipse you can go on, however I prefer to use the IDE just to write code and debug it (see the dedicated section to see how you can debug a program from Eclipse but run it from the terminal) so, if you like my setup, go to the terminal and run your modified sample app

Project Lombok

There is actually a missing very useful piece of the puzzle: Lombok [21]. This library is very useful because automates the writing of “boring” code. Instead of manually creating all the getters, we can just put an annotation on our class. Eclipse and all the other IDEs are perfectly capable of generating the getters and others very useful pieces of code, but what does it happen when you add a new attribute to a class? You need to create the new getter etc. However with Lombok you don’t have to do anything. No chance to forget about this simple boring step.

When you want to use a third-party library, in general, you just update the file pom.xml or build.gradle (run also “Gradle Refresh” from Eclipse) and you are good to go. However Lombok is a particular case: let’s say we add the annotation @Getter and then we use the getter in a second class. If you compile and run the code it will be fine because at compile time Lombok will generate the getter, however Eclipse is completely unaware of it so it will complain a lot. The solution to this is to download the jar of Lombok and let Eclipse load it during startup as a “javaagent”. This way, in our example, Eclipse will know what Lombok is going to generate the getter and will not signal the error. To properly install Lombok follow these steps:

  • close Eclipse
  • download the jar archive of Lombok in the same folder of Eclipse (do not follow the official instructions to install Project Lombok): https://projectlombok.org/downloads/lombok.jar
  • run the following command to add the proper line to the file “eclipse.ini”:
    echo "-javaagent:$HOME/bsdrulez/apps/eclipse/lombok*.jar" >> eclipse.ini
    
  • start Eclipse and test our example where Eclipse was complaining about the missing getter

Debug a Java application with Remote Debugging

Arguably one of the most useful features of an IDE is a Debugger. In Eclipse we can switch to the “Debug Perspective” to:

  • inspect threads, variables and the stack
  • create regular and conditional breakpoints
  • evaluate expressions on the spot
  • simulate different scenarios by dynamically changing values in variables during execution

For a simple Java application the debug session can be started directly from Eclipse, however the most generic way, useful for any Java application, is “Remote Debugging”. The idea is that the Debugger acts as a client and connects to the Java application under examination. Note that this technique does not apply only to webapp, but can be used also in a regular command line application. The only thing to remember is to start the JVM with the following parameters [22]: -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 where:

  • -X: give “Extra Options” to the “java” command, -Xdebug is just for retro-compatibility
  • -Xrunjdwp: enable the “Java Debug Wire Protocol” (JDWP)
  • transport: define the type of socket we want to open in mode “listening” (e.g. “dt_socket” is a regular TCP socket, but for example a Unix-domain socket can be chosen as well)
  • server=y: state the role of this JVM in the JDWP, in other words indicate this is the app and not the debugger
  • suspend=y: suspend the execution of the Java application until a debugger connect (this can be “n” it is fine to connect the application after the startup, e.g. for a webapp)
  • address=5005: TCP port of the listening socket (the debugger should use this port to connect to the application); use “*:5005” to listen for remote clients on 0.0.0.0 (i.e. when you need to debug an application running on a remote server)

Let’s test it with a HelloWorld app:

  • Create a new Java Project in Eclipse
  • Create the class Main.java with the main method that prints “Hello World”
  • open the command line in the folder of the Java file
  • compile with javac Main.java
  • execute with java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 Main
  • the application will be waiting for the debugger to connect
  • now it’s time to go to Eclipse in the Debug Perspective
  • select from the menu “Run -> Debug Configurations”
  • double click on “Remote Java Debugging” (this will create a new configuration)
  • select the project and change the port to 5005
  • click on “Apply” and then “Debug”
  • the debugger connects to the application
  • the execution proceeds and you can read “Hello World” in the terminal
  • in case the execution does not terminate naturally disconnect the debugger clicking on the red square
  • set a breakpoint on the print statement and restart the application
  • click on “Run -> Debug As -> select the previous Debug Configuration”
  • the debugger now connects and suspends the execution at the breakpoint

The same options to start the application can be given on the command line to run a Maven application, or can be configured in a “Run Configuration” in Eclipse.

For Gradle it is a little bit trickier since it uses a Daemon process. Basically when we run a gradle command we are using a Gradle client which contacts the Gradle server and instructs the Gradle server on what to is requested to do. This architecture allows Gradle to perform optimizations and to increase the performance of the development cycle caching recurring operations without changes.

From our perspective this means our previous options on the command line would put the Gradle client ready to be debugged, not the application. However Gradle supports our use case with a specific parameter --debug-jvm. For example:

gradle test --debug-jvm
gradle run  --debug-jvm
gradle bootRun --debug-jvm

The same result can be achieved in Eclipse with the creation of a new “Run Configuration” of type “Gradle Task”. In the window where you can configure it start putting just the command name (“test”, “run” or “bootRun”), then click on “Add” and put “–debug-jvm” as the second line (below the command name). Execute the application with “Run -> Run As -> select the configuration we just created”.

The part where we connect the debugger does not change.

Conclusions

If you arrived at the end of this article, congratulations!

We saw how to install different JDKs and how to switch from one to another. We spoke about how to set up the developer environment with Maven and Gradle, Project Lombok, Eclipse and how to run a debug session.

This is what I consider the bare minimum to have a professional setup for a Java Developer who likes lightweight solutions and wants to understand better his tools. From this foundation the sky is the limit.

References

[1] https://www.graalvm.org/

[2] https://www.msys2.org/

[3] https://git-scm.com/download/win

[4] https://learn.microsoft.com/en-us/windows/wsl/install

[5] https://www.oracle.com/java/technologies/javase/jdk-faqs.html

[6] https://openjdk.org/

[7] https://adoptium.net/temurin/releases/

[8] https://whichjdk.com/

[9] https://projects.eclipse.org/projects/adoptium

[10] https://maven.apache.org/download.cgi

[11] https://gradle.org/releases/

[12] https://maven.apache.org/archetypes/maven-archetype-quickstart/

[13] https://spring.io/

[14] https://start.spring.io/

[15] https://mvnrepository.com/repos/central

[16] https://stackoverflow.com/questions/2992749/getting-a-lightweight-installation-of-java-eclipse

[17] https://download.eclipse.org/eclipse/downloads/

[18] https://download.eclipse.org/eclipse/downloads/drops4/R-4.30-202312010110/#PlatformRuntime

[19] https://www.eclipse.org/downloads/packages/

[20] https://marketplace.eclipse.org/content/pydev-python-ide-eclipse

[21] https://projectlombok.org

[22] https://stackoverflow.com/questions/975271/remote-debugging-a-java-application