Sunday, June 29, 2025

(Mostly) Zero Trust: Sandboxing Java applications with GraalVM and Espresso

With the death of SecurityManager, isolating untrusted Java code execution (for example, in case of plugins or scripting) becomes even more difficult. But, luckily, the maturity of GraalVM, both from features and stability perspectives, offers a compelling solution to sandboxing Java code.

In today's post, we are going to explore GraalVM polyglot capabilities, more specifically GraalVM Espresso - an implementation of the Java Virtual Machine Specification built upon GraalVM as a Truffle interpreter. To make things more interesting, there are two runtime models that we are going to talk about:

  • out of process: isolated host/guest JVM with prebuilt Espresso runtime
  • out of process: isolated, potentially even different, host/guest JVM runtimes

At first, the amount of the details that GraalVM polyglot programming throws at you feels overwhelming, but the documentation really helps to navigate over them. The latest version of the GraalVM available as of this writing is 24.2.1 and this is what we are going to use. To keep things simple but meaningful, we are going to sandbox an application that:

  • exposes one interface ApplicationService
          public interface ApplicationService {
              ApplicationInfo getApplicationInfo();
              ApplicationInfo setApplicationInfo(ApplicationInfo info);
          }
          
  • exposes one custom data type (Java record)
          public record ApplicationInfo(String env, String version) {
          }
          
  • provides the default ApplicationService implementation
        public class AppRunner {
            public static void main(String[] args) {
                System.out.println(getApplication().getApplicationInfo());
            }
    
            public static ApplicationService getApplication() {
                return new ApplicationService() {
                    private ApplicationInfo info = new ApplicationInfo("unconstrained", "1.0.0");
    
                    @Override
                    public ApplicationInfo getApplicationInfo() {
                        return info;
                    }
    
                    @Override
                    public ApplicationInfo setApplicationInfo(ApplicationInfo info) {
                        this.info = info;
                        return info;
                    }
                };
            }
       }
          

What the host will be doing with this sandboxed application? Quite simple: it will get access to default ApplicationService implementation, query the ApplicationInfo and change it to its own provided value. Such an exercise would demonstrate the essential interoperability dance between host and guest in the GraalVM world. What is interesting, the guest (application we are going to sandbox) has no idea it is running inside the GraalVM Espresso managed runtime.

Let us start building with provided GraalVM Espresso runtime (which is based on JDK-21). Here are the dependencies that we need to get started.

        <dependency>
            <groupId>org.graalvm.polyglot</groupId>
            <artifactId>polyglot</artifactId> 
            <version>24.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.graalvm.espresso</groupId>
            <artifactId>espresso-language</artifactId> 
            <version>24.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.graalvm.espresso</groupId>
            <artifactId>polyglot</artifactId>
            <version>24.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.graalvm.espresso</groupId>
            <artifactId>espresso-runtime-resources-jdk21</artifactId>
            <version>24.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.graalvm.polyglot</groupId>
            <artifactId>java-community</artifactId> 
            <version>24.2.1</version>
            <type>pom</type>
        </dependency>

To have access to sandboxed application types, we would also need to include the dependency on its API (just for sake of simplicity, we include the application module itself):

        <dependency>
            <groupId>com.example.graalvmt</groupId>
            <artifactId>app</artifactId> 
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

The bootstrapping is pretty straightforward. The first thing the host application has to do is to create a new polyglot engine:

        final Engine engine = Engine
            .newBuilder()
            .build();

The next step establishes the polyglot context and is a bit more engaging. Please don't panic if some things are not clear from the start, we are going to look at all of them shortly.

        final Context context = Context
            .newBuilder("java")
            .option("java.Properties.java.class.path", application)
            .option("java.Polyglot", "true")
            .option("java.EnableGenericTypeHints", "true")
            .allowExperimentalOptions(true)
            .allowNativeAccess(true)
            .allowCreateThread(false)
            .allowHostAccess(HostAccess
                .newBuilder(HostAccess.ALL)
                .targetTypeMapping(
                        Value.class,
                        ApplicationInfo.class,
                        v -> true,
                        v -> new ApplicationInfo(
                            v.invokeMember("env").asString(), 
                            v.invokeMember("version").asString())
                    )
                .build()
            )
            .allowIO(IOAccess.NONE)
            .allowPolyglotAccess(PolyglotAccess
                .newBuilder()
                .allowBindingsAccess("java")
                .build())
            .engine(engine)
            .build();

Let us go over this snippet line by line. Since this is a polyglot context, we limit its languages to java only. We also provide the classpath to our sandboxed application JAR file using java.Properties.java.class.path property. We explicitly prohibiting thread creation with allowCreateThread(false) and I/O access with allowIO(IOAccess.NONE) but have to allow native access (allowNativeAccess(true)), this is a limitation of the GraalVM Espresso engine at the moment (please check [Espresso] Support running without native access. for more details).

The host access configuration needs some wider explanation. We do allow access to host from guest (sandboxed application) with HostAccess.ALL policy and also provide the type mapping for ApplicationInfo record class so we could reconstruct it from generic polyglot Value class. Last but not least, we allow polyglot access java bindings with allowBindingsAccess("java"). And by and large, this is it!

With the context bootstrapped, we are ready to interact with the sandboxed application (or in terms of GraalVM, guest) through polyglot bindings. To illustrate the host/guest isolation, let us make an inquiry about their JVM runtime versions.

        final Value runtime = context.getBindings("java").getMember("java.lang.Runtime");
        System.out.println("Host JVM version: " + Runtime.version());
        System.out.println("Guest JVM version: " + runtime.invokeMember("version"));
  

Depending on your system and settings, you may see something along these lines:

Host JVM version: 24.0.1+9-30
Guest JVM version: 21.0.2+13-LTS-jvmci-23.1-b33

Clearly, the host and guest JVMs are far apart. According to our plan, let us retrieve the instance of the ApplicationService and ask for ApplicationInfo:

        final ApplicationService service = context.getBindings("java")
            .getMember("com.example.AppRunner")
            .invokeMember("getApplication")
            .as(ApplicationService.class);
        System.out.println("ApplicationInfo? " + service.getApplicationInfo());

We should see in the console:

ApplicationInfo? ApplicationInfo[env=unconstrained, version=1.0.0]

Now let us change the ApplicationInfo by construction a new instance on the host and sending it back to the guest, using ApplicationService instance at hand:

        final ApplicationInfo newInfo = context.getBindings("java")
            .getMember("com.example.ApplicationInfo")
            .newInstance("sandboxed", "1.0.0")
            .as(ApplicationInfo.class);

        final ApplicationInfo info = service.setApplicationInfo(newInfo);
        System.out.println("ApplicationInfo? " + info);

This time, we should see in the console a different picture:

ApplicationInfo? ApplicationInfo[env=sandboxed, version=1.0.0]

With all details (hopefully) explained, we could see how things work in action. The easiest way to run the host application is by using Apache Maven and its Exec Maven Plugin (please change JAVA_HOME accordingly):

$ export JAVA_HOME=/usr/lib/jvm/java-24-openjdk-amd64/
$ mvn package exec:java -Dexec.mainClass="com.example.graalvm.sandbox.SandboxRunner" -Dexec.args="app/target/app-0.0.1-SNAPSHOT.jar" -f sandbox-bundled-jvm/

Here is how the complete output in the console looks like:

Host JVM version: 24.0.1+9-30
Guest JVM version: 21.0.2+13-LTS-jvmci-23.1-b33
ApplicationInfo? ApplicationInfo[env=unconstrained, version=1.0.0]
ApplicationInfo? ApplicationInfo[env=sandboxed, version=1.0.0]

Let us recap what we have done: we basically isolated the application we wanted to sandbox into separate JVM instance but preserved full access to its API, limiting some very key JVM capabilities (no threads, no I/O). This is very powerful but has one limitation: we are bounded by the GraalVM Espresso runtime. What if we could have used any other JVM? Good news, it is totally an option and this is what we are going to do next.

As of this writing, GraalVM Espresso allows running either Java 8, Java 11, Java 17, or Java 21 guest JVMs. However, the host could be running any other JVM, and why not latest JDK-24? There aren't actually too many changes that we need to do to the Context:

    final Context context = Context
            .newBuilder("java")
            .option("java.JavaHome", jdkHome)
            .option("java.Classpath", application)
            .option("java.Properties.java.security.manager", "allow")
            .option("java.PolyglotInterfaceMappings", getInterfaceMappings())
            .option("java.Polyglot", "true")
            .option("java.EnableGenericTypeHints", "true")
            .allowExperimentalOptions(true)
            .allowNativeAccess(true)
            .allowCreateThread(false)
            .allowHostAccess(HostAccess
                .newBuilder(HostAccess.ALL)
                .targetTypeMapping(
                        Value.class,
                        ApplicationInfo.class,
                        v -> true,
                        v -> new ApplicationInfo(
                            v.invokeMember("env").asString(), 
                            v.invokeMember("version").asString())
                    )
                .build()
            )
            .allowIO(IOAccess.NONE)
            .allowPolyglotAccess(PolyglotAccess
                .newBuilder()
                .allowBindingsAccess("java")
                .build())
            .engine(engine)
            .build();

Notice that we do provide own JDK installation (using java.JavaHome) and classpath (using java.Classpath). More to that, we have an opportunity to pass any additional JVM properties (for example, "java.Properties.java.security.manager", "allow" is equivalent to passing -Djava.security.manager=allow, however we don't really use it). The new option here is java.PolyglotInterfaceMappings:

    private static String getInterfaceMappings(){
        return "com.example.ApplicationService;";
    }

It allows to automatically constructing guest proxies for host objects that implement declared interfaces in the list (essentially, we could provide our own ApplicationService and push it to the guest). And one important note, we don't need espresso-runtime-resources-jdk21 dependency anymore. Everything else stays unchanged.

If we run the host application this time:

$ export JAVA_HOME=/usr/lib/jvm/java-24-openjdk-amd64/
$ mvn package exec:java -Dexec.mainClass="com.example.graalvm.sandbox.SandboxRunner" -Dexec.args="/usr/lib/jvm/java-17-openjdk-amd64/ app/target/app-0.0.1-SNAPSHOT.jar" -f sandbox-custom-jvm/

The output will be slightly different:

Host JVM version: 24.0.1+9-30
Guest JVM version: 17.0.15+6-Ubuntu-0ubuntu124.04
ApplicationInfo? ApplicationInfo[env=unconstrained, version=1.0.0]
ApplicationInfo? ApplicationInfo[env=sandboxed, version=1.0.0]

Sky is the limit! GraalVM with its powerful polyglot features and GraalVM Espresso engine offers an unique capability to isolate (sandbox) untrusted Java code execution, using a JDK of your choice (you can even run JDK-21 with full-fledged SecurityManager activated if dimed necessary). We have just looked into some foundational concepts (there are still some limitations) however there are much more to uncover here (and please, note that GraalVM evolves very fast, the limitations of today become features of tomorrow). Hope you find it useful.

The complete source code of the project is available on Github.

I πŸ‡ΊπŸ‡¦ stand πŸ‡ΊπŸ‡¦ with πŸ‡ΊπŸ‡¦ Ukraine.

Wednesday, March 26, 2025

JDK-24: the death of SecurityManager

It has happened: after many years of deprecation warnings and back-and-forth conversations, the SecurityManager is effectively dead. JDK-24, released just last week, sets a final point in this long story. But there is no time to grieve, so many new features in JDK-24 to talk about!

The JDK-24 is really packed with JEPs, nonetheless some of them are being dragged from the previous JDK releases. Let us kick off from the finalized features first, with the preview / experimental / incubating to follow right after.

  • JEP-472: Prepare to Restrict the Use of JNI: issues warnings about uses of the Java Native Interface (JNI) and adjusts the Foreign Function & Memory (FFM) API to issue warnings in a consistent manner. All such warnings aim to prepare developers for a future release that ensures integrity by default by uniformly restricting JNI and the FFM API. Application developers can avoid both current warnings and future restrictions by selectively enabling these interfaces where essential.

    Code that uses JNI is affected by native access restrictions if

    The following warnings are going to be issued:

         WARNING: A restricted method in java.lang.System has been called
         WARNING: java.lang.System::loadLibrary has been called by com.example.LoadLibraryRunner in an unnamed module (...)
         WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
         WARNING: Restricted methods will be blocked in a future release unless native access is enabled

    As per the hints above, to enable native access selectively, you could use the following command-line options:

    • $ java --enable-native-access=ALL-UNNAMED ... (all code on the class path)
    • $ java --enable-native-access=M1,M2, ... (specific modules on the module path)

    Alternatively, you could add Enable-Native-Access: ALL-UNNAMED to the manifest of an executable JAR file (MANIFEST.MF). The only supported value for the Enable-Native-Access manifest entry is ALL-UNNAMED; other values cause an exception to be thrown. For more details, please check Quality Outreach Heads-up - JDK 24: Prepares Restricted Native Access write-up.

  • JEP-498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe: issues a warning at run time on the first occasion that any memory-access method in sun.misc.Unsafe is invoked. All of these unsupported methods were terminally deprecated in JDK 23. They have been superseded by standard APIs, namely the VarHandle API (JEP 193, JDK 9) and the Foreign Function & Memory API (JEP 454, JDK 22).

    In case when usage of the sun.misc.Unsafe memory-access methods is detected, the following warnings are going to be issued:

         WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
         WARNING: sun.misc.Unsafe::allocateMemory has been called by com.example.UnsafeRunner (...)
         WARNING: Please consider reporting this to the maintainers of class com.example.UnsafeRunner
         WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
  • JEP-491: Synchronize Virtual Threads without Pinning: improves the scalability of Java code that uses synchronized methods and statements by arranging for virtual threads that block in such constructs to release their underlying platform threads for use by other virtual threads. This will eliminate nearly all cases of virtual threads being pinned to platform threads, which severely restricts the number of virtual threads available to handle an application's workload.

  • JEP-475: Late Barrier Expansion for G1: simplifies the implementation of the G1 garbage collector's barriers, which record information about application memory accesses, by shifting their expansion from early in the C2 JIT's compilation pipeline to later.

  • JEP-479: Remove the Windows 32-bit x86 Port: removes the source code and build support for the Windows 32-bit x86 port. This port was deprecated for removal in JDK 21 with the express intent to remove it in a future release.

  • JEP-490: ZGC: Remove the Non-Generational Mode: removes the non-generational mode of the Z Garbage Collector (ZGC), keeping the generational mode as the default for ZGC.

  • JEP-501: Deprecate the 32-bit x86 Port for Removal: deprecates the 32-bit x86 port, with the intent to remove it in a future release. This will thereby deprecate the Linux 32-bit x86 port, which is the only 32-bit x86 port remaining in the JDK. It will also, effectively, deprecate any remaining downstream 32-bit x86 ports. After the 32-bit x86 port is removed, the architecture-agnostic Zero port will be the only way to run Java programs on 32-bit x86 processors.

  • JEP-493: Linking Run-Time Images without JMODs: reduces the size of the JDK by approximately 25% by enabling the jlink tool to create custom run-time images without using the JDK's JMOD files. This feature must be enabled when the JDK is built; it will not be enabled by default, and some JDK vendors may choose not to enable it.

  • JEP-496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism: enhances the security of Java applications by providing an implementation of the quantum-resistant Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM). Key encapsulation mechanisms (KEMs) are used to secure symmetric keys over insecure communication channels using public key cryptography. ML-KEM is designed to be secure against future quantum computing attacks. It has been standardized by the United States National Institute of Standards and Technology (NIST) in FIPS 203.

  • JEP-497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm: enhances the security of Java applications by providing an implementation of the quantum-resistant Module-Lattice-Based Digital Signature Algorithm (ML-DSA). Digital signatures are used to detect unauthorized modifications to data and to authenticate the identity of signatories. ML-DSA is designed to be secure against future quantum computing attacks. It has been standardized by the United States National Institute of Standards and Technology (NIST) in FIPS 204.

  • JEP-484: Class-File API: provides a standard API for parsing, generating, and transforming Java class files. This JEP finalizes the Class-File API that was originally proposed as a preview feature by JEP 457 in JDK 22 and refined by JEP 466 in JDK 23.

  • JEP-486: Permanently Disable the Security Manager: removes the abilities to enable the Security Manager when starting the Java runtime (java -Djava.security.manager ...) or to install a Security Manager while an application is running (System::setSecurityManager).

    It is worth to mention that the impacted APIs changes are going way beyond just Security Manager, notably:

  • JEP-483: Ahead-of-Time Class Loading & Linking: improves startup time by making the classes of an application instantly available, in a loaded and linked state, when the HotSpot Java Virtual Machine starts. Achieve this by monitoring the application during one run and storing the loaded and linked forms of all classes in a cache for use in subsequent runs. Lay a foundation for future improvements to both startup and warmup time.

    This one is probably the first tangible deliverable of the Project Leyden, and an exciting one. The process to create a cache takes two steps.

    First, run the application once, in a training run, to record its AOT configuration, in this case into the file app.aotconf:

        $ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -cp app.jar com.example.App ...
        

    Second, use the configuration to create the cache, in the file app.aot (this step doesn’t run the application, it just creates the cache):

        $ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -cp app.jar
        

    Subsequently, in testing or production, run the application with the cache (if the cache file is unusable or does not exist then the JVM issues a warning message and continues):

        $ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...
        
  • The process is somewhat verbose at the moment, but no doubts, there are strong indications that the improvements are coming in the next release(s).

  • JEP-485: Stream Gatherers: enhances the Stream API to support custom intermediate operations. This will allow stream pipelines to transform data in ways that are not easily achievable with the existing built-in intermediate operations.

    From the API perspective, the changes include:

    Please check recently published The Gatherer API tutorial for more in-depth API design overview and different usage scenarios.

It was a lot but we are far from done yet, the list of preview / experimental / incubating features is as impressive:

  • JEP-478: Key Derivation Function API (Preview): introduces an API for Key Derivation Functions (KDFs), which are cryptographic algorithms for deriving additional keys from a secret key and other data. This is a preview API feature.

  • JEP-487: Scoped Values (Fourth Preview): introduces scoped values, which enable a method to share immutable data both with its callees within a thread, and with child threads. Scoped values are easier to reason about than thread-local variables. They also have lower space and time costs, especially when used together with virtual threads (JEP 444) and structured concurrency (JEP 480). This is a preview API feature.

  • JEP-489: Vector API (Ninth Incubator): introduces an API to express vector computations that reliably compile at runtime to optimal vector instructions on supported CPU architectures, thus achieving performance superior to equivalent scalar computations.

  • JEP-499: Structured Concurrency (Fourth Preview): simplifies concurrent programming by introducing an API for structured concurrency. Structured concurrency treats groups of related tasks running in different threads as a single unit of work, thereby streamlining error handling and cancellation, improving reliability, and enhancing observability. This is a preview API feature.

  • JEP-494: Module Import Declarations (Second Preview): enhances the Java programming language with the ability to succinctly import all of the packages exported by a module. This simplifies the reuse of modular libraries, but does not require the importing code to be in a module itself. This is a preview language feature that we have covered previously.

  • JEP-488: Primitive Types in Patterns, instanceof, and switch (Second Preview): enhances pattern matching by allowing primitive types in all pattern contexts, and extend instanceof and switch to work with all primitive types. This is a preview language feature that we have covered previously.

  • JEP-404: Generational Shenandoah (Experimental): enhances the Shenandoah garbage collector with experimental generational collection capabilities to improve sustainable throughput, load-spike resilience, and memory utilization. This experimental feature could be activated through the JVM command line options:

    $ java -XX:+UseShenandoahGC -XX:+UnlockExperimentalVMOptions -XX:ShenandoahGCMode=generational ... 
  • JEP-450: Compact Object Headers (Experimental): reduces the size of object headers in the HotSpot JVM from between 96 and 128 bits down to 64 bits on 64-bit architectures. This will reduce heap size, improve deployment density, and increase data locality. This experimental feature could be is activated through the JVM command line options:

    $ java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders ... 
  • JEP-492: Flexible Constructor Bodies (Third Preview): in constructors in the Java programming language, allows statements to appear before an explicit constructor invocation, i.e., super(..) or this(..). The statements cannot reference the instance under construction, but they can initialize its fields. Initializing fields before invoking another constructor makes a class more reliable when methods are overridden. This is a preview language feature.

  • JEP-495: Simple Source Files and Instance Main Methods (Fourth Preview): evolves the Java programming language so that beginners can write their first programs without needing to understand language features designed for large programs. Far from using a separate dialect of the language, beginners can write streamlined declarations for single-class programs and then seamlessly expand their programs to use more advanced features as their skills grow. Experienced developers can likewise enjoy writing small programs succinctly, without the need for constructs intended for programming in the large. This is a preview language feature.

The amount of features we just looked at is astonishingly large for just one release, but let us take a look on some other fixes and improvements that went into JDK-24:

The JDK-24 tooling updates bring some new capabilities and deprecations:

Sadly, there are few regressions to be aware of that sneaked into JDK-24 release:

Last but not least, let us look over the changes to the standard library:

To close up, a couple of security related enhancements that deserve closer look:

I think it is fair to say that JDK-24 is an outstanding (and at the same time, disruptive to some) release that prepares the ground for the next LTS version, JDK-25, which is expected to land later this year.

I πŸ‡ΊπŸ‡¦ stand πŸ‡ΊπŸ‡¦ with πŸ‡ΊπŸ‡¦ Ukraine.

Thursday, December 26, 2024

Simple is finally easy: bootstrapping JAX-RS applications in Java SE environments

It has been a while since Jakarta EE 10 was released but the ecosystem is slowly (but steadily!) catching up. The Apache CXF project landed new 4.1.0 release very recently that delivers Jakarta EE 10 compatibility, specifically implementation of the Jakarta RESTful Web Services 3.1 specification (also known as JAX-RS).

One of the most exciting (in my option) features that Jakarta RESTful Web Services 3.1 includes is bootstrapping JAX-RS applications in Java SE environments. From now on, creating the full-fledged RESTful web services on the JVM becomes not only easy, but very straightforward! In today's post, we are going to build a sample RESTful web service and host it inside the Java SE application, with a catch - no boilerplate allowed.

The PeopleRestService, presented in the snippet below, is a minimalistic example of the typical Jakarta RESTful web service: for a sake of keeping things simple, it does not do anything useful besides returning the predefined data back.

import java.util.Collection;
import java.util.List;

import com.example.jakarta.restful.bootstrap.model.Person;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/people")
public class PeopleRestService {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Collection<Person> getPeople() {
        return List.of(new Person("a@b.com", "John", "Smith"));
    }
}

The Person class contains only three fields: email, firstName and lastName.

  
public class Person {
    private String email;
    private String firstName;
    private String lastName;
    // Skipping the getters and setters for brevity
}

Essentially, this is all we need at this point. Now the hardest part, how to expose the PeopleRestService to the outside world? Here is the moment for SeBootstrap to take the stage. Its entire purpose is allowing to startup a JAX-RS application in Java SE environments, without (explicitly) requiring the presence of the web container or application server. How does it look like in practice?

  
import java.util.Set;
import java.util.concurrent.CompletionStage;

import com.example.jakarta.restful.bootstrap.rs.PeopleRestService;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.SeBootstrap;
import jakarta.ws.rs.core.Application;

public class BootstrapRunner {
    @ApplicationPath("/api")
    public static final class JakartaRestfulApplication extends Application {
        @Override
        public Set<Object> getSingletons() {
            return Set.of(new PeopleRestService());
        }
    }

    public static void main(String[] args) {
        final SeBootstrap.Configuration configuration = SeBootstrap.Configuration
            .builder()
            .property(SeBootstrap.Configuration.PROTOCOL, "http")
            .property(SeBootstrap.Configuration.PORT, 10800)
            .property(SeBootstrap.Configuration.ROOT_PATH, "/")
            .build();
    
        SeBootstrap
            .start(new JakartaRestfulApplication(), configuration)
            .toCompletableFuture()
            .join();
    }
}

As simple as that: pass the port (10800), protocol (HTTP) and root path (/) through SeBootstrap.Configuration along with Application subclass (JakartaRestfulApplication) instance to SeBootstrap::start method. To complete the puzzle, here are all the dependencies that are required by our Java SE application (taken from project's Apache Maven pom.xml file).

  
<dependencies>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-frontend-jaxrs</artifactId>
		<version>4.1.0</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-rs-extension-providers</artifactId>
		<version>4.1.0</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-transports-http-jetty</artifactId>
		<version>4.1.0</version>
	</dependency>
	<dependency>
		<groupId>jakarta.json</groupId>
		<artifactId>jakarta.json-api</artifactId>
		<version>2.1.3</version>
	</dependency>
	<dependency>
		<groupId>jakarta.json.bind</groupId>
		<artifactId>jakarta.json.bind-api</artifactId>
		<version>3.0.1</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-classic</artifactId>
		<version>1.5.15</version>
	</dependency>
	<dependency>
		<groupId>org.eclipse</groupId>
		<artifactId>yasson</artifactId>
		<version>3.0.4</version>
	</dependency>
</dependencies>

Nothing special, except may be Eclipse Yasson, the JSON-B implementation provider. It is time to make sure everything actually works!

$ mvn clean package

...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...

$ java -jar target/cxf-jakarta-restful-3.1-bootstrap-0.0.1-SNAPSHOT.jar
Dec 24, 2024 2:43:45 P.M. org.apache.cxf.endpoint.ServerImpl initDestination
INFO: Setting the server's publish address to be http://:10800/api
14:43:46.129 [onPool-worker-1] INFO rg.eclipse.jetty.server.Server - jetty-12.0.15; built: 2024-11-05T19:44:57.623Z; git: 8281ae9740d4b4225e8166cc476bad237c70213a; jvm 23.0.1+8-FR
14:43:46.288 [onPool-worker-1] INFO jetty.server.AbstractConnector - Started ServerConnector@7b66a8d{HTTP/1.1, (http/1.1)}{:10800}
14:43:46.302 [onPool-worker-1] INFO rg.eclipse.jetty.server.Server - Started oejs.Server@7683694f{STARTING}[12.0.15,sto=0] @1701ms
14:43:46.349 [onPool-worker-1] INFO .server.handler.ContextHandler - Started oeje10s.ServletContextHandler@4b04a638{ROOT,/,b=null,a=AVAILABLE,h=oeje10s.ServletHandler@23ae88f1{STARTED}}

With the application up and running, we are ready to invoke the http://localhost:10800/api/people HTTP endpoint (the only one our Jakarta RESTful web service exposes).

$ curl http://localhost:10800/api/people -iv
* Host localhost:10800 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:10800...
*   Trying 127.0.0.1:10800...
* Connected to localhost (127.0.0.1) port 10800
* using HTTP/1.x
> GET /api/people HTTP/1.1
> Host: localhost:10800
> User-Agent: curl/8.11.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Server: Jetty(12.0.15)
Server: Jetty(12.0.15)
< Date: Tue, 24 Dec 2024 19:52:10 GMT
Date: Tue, 24 Dec 2024 19:52:10 GMT
< Content-Type: application/json
Content-Type: application/json
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
<

[{"email":"a@b.com","firstName":"John","lastName":"Smith"}]

And here we are, we get the response with our hardcoded list of people, no surprises! I hope you would agree, the bootstrapping process is very simple and easy to follow. Even more, you could integrate SeBootstrap in your test suites as well, thanks to its flexible configuration capabilities, for example:

  
final SeBootstrap.Configuration configuration = SeBootstrap.Configuration
    .builder()
    // Use random free port
    .property(SeBootstrap.Configuration.PORT, SeBootstrap.Configuration.FREE_PORT)
    ...
    .build();

// Start the instance
final Instance instance = SeBootstrap
    .start(new JakartaRestfulApplication(), configuration)
    .toCompletableFuture()
    .join();
    
final SeBootstrap.Configuration actual = instance.configuration();
// Use actual.port(), actual.host(), ...
...

// Stop the instance
instance
    .stop()
    .toCompletableFuture()
    .join();

It is worth to mention that bootstrapping secure Jakarta RESTful Web Services using HTTPS protocol is also supported, for example:

 
final SeBootstrap.Configuration configuration = SeBootstrap.Configuration
    .builder()
    .property(SeBootstrap.Configuration.PROTOCOL, "https")
    .property(SeBootstrap.Configuration.PORT, 10843)
    .property(SeBootstrap.Configuration.ROOT_PATH, "/")
    .sslContext(SSLContext.getDefault()) /* or supply your own */
    .build();

The complete source code of the project is available on Github.

I πŸ‡ΊπŸ‡¦ stand πŸ‡ΊπŸ‡¦ with πŸ‡ΊπŸ‡¦ Ukraine.

Friday, October 11, 2024

JDK-23: the messenger?

It's been a little less than few weeks since JDK-23 was released and we have not covered it yet! To be fair, this is not a big deal, each new release gets more and more attention, but nonetheless! And while this one in particular may not be looking too exciting (well, mostly all the features are in preview), it is a messenger of what is coming next (and that is really thrilling).

Anyway, there are quite a few things that JDK-23 delivers.

Along with the new features, there are some disruptive changes as well, notably:

On the bright side, there are a number of enhancements, bug fixes, tooling and GC improvements that are worth mentioning:

The changes on the standard library API side are quite moderate:

On the final note, it is good time to go over some security related updates that were introduced into JDK-23 (for more details, please check JDK 23 Security Enhancements):

If anything, JDK-23 is a solid release, ready for prime time. From the changes perfective, it looks low risk as well, so if you skipped JDK-22 for some reasons, JDK-23 may be the one.

I πŸ‡ΊπŸ‡¦ stand πŸ‡ΊπŸ‡¦ with πŸ‡ΊπŸ‡¦ Ukraine.

Friday, August 30, 2024

Apache CXF at speed of ... native!

GraalVM has been around for quite a while, steadily making big waves in OpenJDK community (looking at you, JEP 483: Ahead-of-Time Class Loading & Linking). It is wonderful piece of JVM engineering that gave birth to new generation of the frameworks like Quarkus, Helidon and Micronaut, just to name a few.

But what about the old players, like Apache CXF? A large number of applications and services were built on top of it, could those benefit from GraalVM, and particularly native image compilation? The answer to this question used to vary a lot, but thanks to steady progress, GraalVM strives to make it as frictionless as possible.

In today's post, we are going to build a sample Jakarta RESTful web service using Apache CXF and Jakarta XML Binding, and compile it to native image with GraalVM Community 21.0.2+13.1.

Let us start off with the data model, which consists of a single POJO, class Customer.

import jakarta.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "Customer")
public class Customer {
    private long id;
    private String name;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

The class CustomerResource, a minimal Jakarta RESTful web service implementation, exposes a few endpoints to manage Customers, for simplicity - the state is stored in memory.

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;

@Path("/")
@Produces(MediaType.APPLICATION_XML)
public class CustomerResource {
    private final AtomicInteger id = new AtomicInteger();
    private final Map<Long, Customer> customers = new HashMap<>();

    @GET
    @Path("/customers")
    public Collection<Customer> getCustomers() {
        return customers.values();
    }

    @GET
    @Path("/customers/{id}")
    public Response getCustomer(@PathParam("id") long id) {
        final Customer customer = customers.get(id);
        if (customer != null) {
            return Response.ok(customer).build();
        } else {
            return Response.status(Status.NOT_FOUND).build();
        }
    }

    @POST
    @Path("/customers")
    public Response addCustomer(Customer customer) {
        customer.setId(id.incrementAndGet());
        customers.put(customer.getId(), customer);
        return Response.ok(customer).build();
    }

    @DELETE
    @Path("/customers/{id}")
    public Response deleteCustomer(@PathParam("id") long id) {
        if (customers.remove(id) != null) {
            return Response.noContent().build();
        } else {
            return Response.status(Status.NOT_FOUND).build();
        }
    }
}

The last piece we need is to have running web container to host the CustomerResource service. We are going to use Eclipse Jetty but any other HTTP transport supported by Apache CXF will do the job.

import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;

public class Server {
    public static org.apache.cxf.endpoint.Server create() {
        final JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
        sf.setResourceClasses(CustomerResource.class);
        sf.setResourceProvider(CustomerResource.class, new SingletonResourceProvider(new CustomerResource()));
        sf.setAddress("http://localhost:9000/");
        return sf.create();
    }
    
    public static void main(String[] args) throws Exception {
        var server = create();
        server.start();
    }
}

Literally, this is all we need from the implementation perspective. The Apache Maven dependencies list is limited to handful of those:

<dependencies>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-transports-http</artifactId>
		<version>4.0.5</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-transports-http-jetty</artifactId>
		<version>4.0.5</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-frontend-jaxrs</artifactId>
		<version>4.0.5</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-classic</artifactId>
		<version>1.5.7</version>
	</dependency>
</dependencies>

Cool, so what is next? The GraalVM project provides Native Build Tools to faciliate building native images, including dedicated Apache Maven plugin. However, if we just add the plugin into the build, the resulting native image won't be functionable, even if the build succeeds:

$./target/cxf-jax-rs-graalvm-server

Exception in thread "main" java.lang.ExceptionInInitializerError
        at java.base@21.0.2/java.lang.Class.ensureInitialized(DynamicHub.java:601)
        at com.example.jaxrs.graalvm.Server.create(Server.java:27)
        at com.example.jaxrs.graalvm.Server.main(Server.java:35)
        at java.base@21.0.2/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)
Caused by: java.util.MissingResourceException: Can't find bundle for base name org.apache.cxf.jaxrs.Messages, locale en
        at java.base@21.0.2/java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:2059)
        at java.base@21.0.2/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1697)
        at java.base@21.0.2/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1600)
        at java.base@21.0.2/java.util.ResourceBundle.getBundle(ResourceBundle.java:1283)
        at org.apache.cxf.common.i18n.BundleUtils.getBundle(BundleUtils.java:94)
        at org.apache.cxf.jaxrs.AbstractJAXRSFactoryBean.<clinit>(AbstractJAXRSFactoryBean.java:69)
        ... 4 more

Why is that? GraalVM operates under closed world assumption: all classes and all bytecodes that are reachable at run time must be known at build time. Since a majority of the frameworks, Apache CXF included, does not comply with such assumptions, GraalVM needs some help: tracing agent. The way we are going to let GraalVM capture all necessary metadata is pretty straightforward:

  • add test cases which exercise the service logic (more is better)
  • run test suite using tracing agent instrumentation
  • build the native image using the metadata collected by the tracing agent

If that sounds like a plan to you, let us add the test case first:

import java.io.InputStream;
import java.io.IOException;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.Response;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;

public class ServerTest {
    private org.apache.cxf.endpoint.Server server;
    
    @BeforeEach
    public void setUp() {
        server = Server.create();
        server.start();
    }
    
    @Test
    public void addNewCustomer() throws IOException {
        var client = ClientBuilder.newClient().target("http://localhost:9000/customers");
        try (InputStream in = getClass().getResourceAsStream("/add_customer.xml")) {
            try (Response response = client.request().post(Entity.xml(in))) {
                assertThat(response.getStatus(), equalTo(200));
            }
        }
    }

    @Test
    public void listCustomers() {
        var client = ClientBuilder.newClient().target("http://localhost:9000/customers");
        try (Response response = client.request().get()) {
            assertThat(response.getStatus(), equalTo(200));
        }
    }

    @AfterEach
    public void tearDown() {
        server.stop();
        server.destroy();
    }
}

Awesome, with tests in place, we could move on and integrate Native Build Tools into our Apache Maven build. It is established practice to have a dedicated profile for native image since the process could take quite a lot of time (and resources):

<profiles>
	<profile>
		<id>native-image</id>
		<activation>
			<property>
				<name>native</name>
			</property>
		</activation>
		<build>
			<plugins>
				<plugin>
					<groupId>org.graalvm.buildtools</groupId>
					<artifactId>native-maven-plugin</artifactId>
					<extensions>true</extensions>
					<version>0.10.2</version>
					<executions>
						<execution>
							<goals>
								<goal>compile-no-fork</goal>
							</goals>
							<phase>package</phase>
						</execution>
					</executions>
					<configuration>
						<agent>
							<enabled>true</enabled>
							<defaultMode>direct</defaultMode>
							<modes>
								<direct>config-output-dir=${project.build.directory}/native/agent-output</direct>
							</modes>
						</agent>
						<mainClass>com.example.jaxrs.graalvm.Server</mainClass>
						<imageName>cxf-jax-rs-graalvm-server</imageName>
						<buildArgs>
							<buildArg>--enable-url-protocols=http</buildArg>
							<buildArg>--no-fallback</buildArg>
							<buildArg>-Ob</buildArg>
						</buildArgs>
						<metadataRepository>
							<enabled>false</enabled>
						</metadataRepository>
						<resourcesConfigDirectory>${project.build.directory}/native</resourcesConfigDirectory>
					</configuration>
				</plugin>
			</plugins>
		</build>
	</profile>
</profiles>

It may look a bit complicated but fear not. The first thing to notice is that we configure tracing agent in the <agent> ... </agent> section. The captured metadata is going to be dumped into ${project.build.directory}/native/agent-output folder. Later on, the native image builder will refer to it as part of the <resourcesConfigDirectory> ... </resourcesConfigDirectory> configuration option. The profile is activated by the presence of native property.

Time to see each step in action! First thing first, run tests and capture the metadata:

$ mvn clean -Dnative test

...

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  6.977 s
[INFO] Finished at: 2024-08-30T15:50:55-04:00
[INFO] ------------------------------------------------------------------------

If we list the content of the target/native folder, we should see something like that:

$ tree target/native/

target/native/
└── agent-output
    ├── agent-extracted-predefined-classes
    ├── jni-config.json
    ├── predefined-classes-config.json
    ├── proxy-config.json
    ├── reflect-config.json
    ├── resource-config.json
    └── serialization-config.json    

If curious, you could inspect the content of each file, since it is just JSON, but we are going to proceed to the next step right away:

$ mvn -Dnative -DskipTests package

GraalVM Native Image: Generating 'cxf-jax-rs-graalvm-server' (executable)...
========================================================================================================================
Warning: Could not resolve org.junit.platform.launcher.TestIdentifier$SerializedForm for serialization configuration.
Warning: Could not resolve org.junit.platform.launcher.TestIdentifier$SerializedForm for serialization configuration.
[1/8] Initializing...                                                                                    (5.7s @ 0.10GB)
 Java version: 21.0.2+13, vendor version: GraalVM CE 21.0.2+13.1
 Graal compiler: optimization level: b, target machine: x86-64-v3
 C compiler: gcc (linux, x86_64, 9.4.0)
 Garbage collector: Serial GC (max heap size: 80% of RAM)
 2 user-specific feature(s):
 - com.oracle.svm.thirdparty.gson.GsonFeature
 - org.eclipse.angus.activation.nativeimage.AngusActivationFeature
------------------------------------------------------------------------------------------------------------------------
Build resources:
 - 9.99GB of memory (64.5% of 15.49GB system memory, determined at start)
 - 16 thread(s) (100.0% of 16 available processor(s), determined at start)
[2/8] Performing analysis...  [*****]                                                                   (31.2s @ 1.59GB)
   12,363 reachable types   (85.2% of   14,503 total)
   21,723 reachable fields  (63.2% of   34,385 total)
   61,933 reachable methods (57.6% of  107,578 total)
    3,856 types,   210 fields, and 2,436 methods registered for reflection
       62 types,    69 fields, and    55 methods registered for JNI access
        4 native libraries: dl, pthread, rt, z
[3/8] Building universe...                                                                               (4.8s @ 1.85GB)
[4/8] Parsing methods...      [**]                                                                       (3.0s @ 1.16GB)
[5/8] Inlining methods...     [***]                                                                      (2.2s @ 1.35GB)
[6/8] Compiling methods...    [*****]                                                                   (25.4s @ 1.89GB)
[7/8] Layouting methods...    [***]                                                                      (6.2s @ 1.49GB)
[8/8] Creating image...       [***]                                                                      (7.5s @ 2.03GB)
  32.41MB (49.57%) for code area:    38,638 compilation units
  30.78MB (47.08%) for image heap:  325,764 objects and 152 resources
   2.19MB ( 3.35%) for other data
  65.38MB in total

...

========================================================================================================================
Finished generating 'cxf-jax-rs-graalvm-server' in 1m 26s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:30 min
[INFO] Finished at: 2024-08-30T15:58:42-04:00
[INFO] ------------------------------------------------------------------------

And we should end up with a fully functional executable, let us make sure this is the case:

$ ./target/cxf-jax-rs-graalvm-server

Aug 30, 2024 4:03:22 PM org.apache.cxf.endpoint.ServerImpl initDestination
INFO: Setting the server's publish address to be http://localhost:9000/
16:03:22.987 [main] INFO  org.eclipse.jetty.server.Server -- jetty-11.0.22; built: 2024-06-27T16:27:26.756Z; git: e711d4c7040cb1e61aa68cb248fa7280b734a3bb; jvm 21.0.2+13-jvmci-23.1-b30
16:03:22.993 [main] INFO  o.e.jetty.server.AbstractConnector -- Started ServerConnector@5725648b{HTTP/1.1, (http/1.1)}{localhost:9000}
16:03:22.994 [main] INFO  org.eclipse.jetty.server.Server -- Started Server@1f9511a6{STARTING}[11.0.22,sto=0] @24ms
16:03:22.994 [main] INFO  o.e.j.server.handler.ContextHandler -- Started o.a.c.t.h.JettyContextHandler@375bd1b5{/,null,AVAILABLE}

If we open up another terminal window and run curl from the command line, we should be hitting the instance of our service and getting successful responses back:

$curl http://localhost:9000/customers -H "Content-Type: application/xml" -d @src/test/resources/add_customer.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Customer>
	<id>3</id>
    <name>Jack</name>
</Customer>

All the credits go to GraalVM team! Before we wrap up, you might be asking yourself if this the only way? And the short answer is "no": ideally, you should be able to add Native Build Tools and be good to go. The GraalVM Reachability Metadata Repository is the place that enables users of GraalVM native image to share and reuse metadata for libraries and frameworks in the Java ecosystem. Sadly, Apache CXF is not there just yet ... as many others.

The complete project sources are available on Github.

I πŸ‡ΊπŸ‡¦ stand πŸ‡ΊπŸ‡¦ with πŸ‡ΊπŸ‡¦ Ukraine.