Part 2 of 15

Setting Up Java 21: JDK Options, Tooling, and IDE Configuration

Choosing a JDK Distribution

Java 21 is open source (OpenJDK). Multiple vendors distribute binaries, all built from the same source with identical features. Choose based on your support requirements:

DistributionVendorFreeLTS SupportNotes
Eclipse TemurinAdoptiumYes2028+Recommended for most developers
OpenJDKOracle/CommunityYes6 months onlyReference implementation, no LTS
Oracle JDKOracleYes (NFTC)2031Identical to OpenJDK since Java 17
Amazon CorrettoAWSYes2030+Good for AWS deployments
Microsoft Build of OpenJDKMicrosoftYesLong-termGood for Azure deployments
Azul ZuluAzulYes/PaidLong-termIncludes Zing JVM variant
GraalVMOracleYesLong-termAdds native-image, polyglot

Recommendation: Eclipse Temurin for local development; match your cloud provider’s JDK for production (Corretto on AWS, Microsoft OpenJDK on Azure).


Installing Java 21

# Install SDKMAN! (Linux/macOS)
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"

# List available Java 21 distributions
sdk list java | grep 21

# Install Eclipse Temurin 21
sdk install java 21.0.3-tem

# Set as default
sdk default java 21.0.3-tem

# Switch to Java 21 in current shell only
sdk use java 21.0.3-tem

# Verify
java -version
# openjdk version "21.0.3" 2024-04-16 LTS

SDKMAN! is the best tool for local development — it lets you switch between Java 8, 11, 17, and 21 per project.

Option 2: Homebrew (macOS)

# Install Eclipse Temurin 21
brew tap homebrew/cask-versions
brew install --cask temurin21

# Verify
java -version

# If you have multiple JDKs, switch with:
export JAVA_HOME=$(/usr/libexec/java_home -v 21)

Option 3: Direct Download

Download the JDK from adoptium.net and set JAVA_HOME manually:

# Linux/macOS — add to ~/.bashrc or ~/.zshrc
export JAVA_HOME=/path/to/jdk-21
export PATH=$JAVA_HOME/bin:$PATH

Verify Installation

java -version
# openjdk version "21.0.x" ...

javac -version
# javac 21

# Check available GC options
java -XX:+PrintFlagsFinal -version | grep UseZGC

Maven Configuration

pom.xml — Compiler Plugin

<properties>
    <java.version>21</java.version>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <maven.compiler.release>21</maven.compiler.release>
</properties>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.12.1</version>
            <configuration>
                <release>21</release>
            </configuration>
        </plugin>
    </plugins>
</build>

Use <release>21</release> (not <source> + <target>) — release enforces the correct bootstrap classpath and prevents using APIs removed in Java 21.

Enabling Preview Features in Maven

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.12.1</version>
    <configuration>
        <release>21</release>
        <compilerArgs>
            <arg>--enable-preview</arg>
        </compilerArgs>
    </configuration>
</plugin>

<!-- Also needed for running tests with preview features -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
    <configuration>
        <argLine>--enable-preview</argLine>
    </configuration>
</plugin>

Spring Boot Maven Setup

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.5</version>
</parent>

<properties>
    <java.version>21</java.version>
</properties>

Spring Boot 3.2+ sets the compiler release automatically when java.version=21 is set. Enable virtual threads with one property:

# application.properties
spring.threads.virtual.enabled=true

Gradle Configuration

build.gradle (Groovy DSL)

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.withType(JavaCompile).configureEach {
    options.release = 21
}

build.gradle.kts (Kotlin DSL)

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
    }
}

tasks.withType<JavaCompile>().configureEach {
    options.release.set(21)
}

Enabling Preview Features in Gradle

tasks.withType<JavaCompile>().configureEach {
    options.release.set(21)
    options.compilerArgs.add("--enable-preview")
}

tasks.withType<Test>().configureEach {
    jvmArgs("--enable-preview")
}

tasks.withType<JavaExec>().configureEach {
    jvmArgs("--enable-preview")
}

Gradle Toolchains (Auto-Download JDK)

Gradle 7.6+ can download and use the correct JDK automatically:

// build.gradle.kts
plugins {
    id("java")
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
        vendor.set(JvmVendorSpec.ADOPTIUM)  // Eclipse Temurin
    }
}

Run with: ./gradlew build — Gradle downloads Java 21 if not found.


IntelliJ IDEA Configuration

Project SDK

  1. File → Project Structure → Project
  2. Set SDK to Java 21 (Add if not listed: +Add JDK → point to your JDK 21 path)
  3. Set Language level to 21 (or 21 - Preview for preview features)

Module Language Level

For preview features per-module:

  1. File → Project Structure → Modules
  2. Select module → Sources tab
  3. Set Language level to 21 - Preview

Compiler Settings

File → Settings → Build, Execution, Deployment → Compiler → Java Compiler:

  • Set Project bytecode version to 21
  • For preview: add -source 21 --enable-preview to Additional command line parameters
  • Java 21 Support — already built into IntelliJ IDEA 2023.2+
  • SonarLint — static analysis
  • CheckStyle-IDEA — code style enforcement

IntelliJ IDEA 2023.2 and later has native Java 21 support including virtual thread debugging.


VS Code Configuration

Extensions to Install

Java Extension Pack (Microsoft)
  ├── Language Support for Java (Red Hat)
  ├── Debugger for Java (Microsoft)
  ├── Java Test Runner (Microsoft)
  ├── Maven for Java
  └── Project Manager for Java

settings.json

{
    "java.configuration.runtimes": [
        {
            "name": "JavaSE-21",
            "path": "/path/to/jdk-21",
            "default": true
        }
    ],
    "java.compile.nullAnalysis.mode": "automatic",
    "java.format.settings.url": "https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml"
}

Verifying Your Setup

Create Hello.java:

// Hello.java — tests Java 21 features
import java.util.List;

void main() {   // Unnamed class + instance main (preview)
    // Sequenced Collections (final)
    var list = List.of("Java", "21", "rocks");
    System.out.println(list.getFirst());  // "Java"
    System.out.println(list.getLast());   // "rocks"

    // Pattern matching for switch (final)
    Object o = 42;
    String result = switch (o) {
        case Integer i when i > 0 -> "positive: " + i;
        case Integer i            -> "non-positive: " + i;
        default                   -> "not an integer";
    };
    System.out.println(result);  // "positive: 42"

    // Virtual thread (final)
    Thread.ofVirtual().start(() ->
        System.out.println("Hello from virtual thread!")
    );
}

Compile and run:

# Preview features needed for unnamed class (void main)
javac --enable-preview --release 21 Hello.java
java --enable-preview Hello

Expected output:

Java
rocks
positive: 42
Hello from virtual thread!

Virtual Thread Debugging in IntelliJ

IntelliJ IDEA 2023.2+ displays virtual threads in the debugger thread list. Set a breakpoint inside a virtual thread — the debugger shows the virtual thread, its carrier thread, and the full stack trace.

Enable virtual thread tracking:

# JVM flag for virtual thread monitoring
java -Djdk.tracePinnedThreads=full --enable-preview MyApp

This prints a stack trace whenever a virtual thread is pinned (stuck on its carrier thread due to a synchronized block or native call).


Common Setup Mistakes

Using <source> and <target> instead of <release>release correctly enforces the bootstrap classpath; source/target alone allow accidentally using APIs that don’t exist in Java 21.

Forgetting --enable-preview at runtime — classes compiled with --enable-preview also require --enable-preview at runtime. The JVM refuses to load them otherwise.

Running Java 21 bytecode on Java 17 — Java 21 .class files have bytecode version 65.0. Java 17’s JVM rejects them. Check your JAVA_HOME in production matches your compile target.

Using String Templates in new projects — JEP 430 was withdrawn from Java 23. It is unavailable in Java 23+ and will return in a redesigned form. Do not build on it.


Key Takeaways

  • Eclipse Temurin is the recommended free JDK distribution; SDKMAN! is the best tool for managing multiple JDKs
  • Use <release>21</release> in Maven, not <source> + <target> — it enforces the correct platform API constraints
  • Spring Boot 3.2+ fully supports Java 21; enable virtual threads with spring.threads.virtual.enabled=true
  • Gradle toolchains auto-download the correct JDK — no manual JAVA_HOME setup needed per project
  • IntelliJ IDEA 2023.2+ has native Java 21 support including virtual thread visualization in the debugger
  • Preview features require --enable-preview at both compile time AND runtime — wire it into your test runner too

Next: Pattern Matching for switch (JEP 441) — eliminate instanceof chains and manual casts with type-safe, exhaustive switch expressions.