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.

Thursday, May 30, 2024

JDK release cadence and the pain of endless technical debt

The shiny new code you are writing today is the legacy one tomorrow: this is probably the only flaw that new JDK release cadence brings to the table. Well, with all the respect, this is the price to pay to keep up with the pace of innovation, right? Yes, but your mileage may vary ... a lot.

To put things in perspective, let us start with a few examples, all taken from the open-source projects under Apache Software Foundation umbrella. The first one we are going to look at is switch statements, typically used like that:

@Override
protected int nextChild() {
    ElementFrame<Node, Node> frame = getCurrentFrame();
    if (frame.currentChild == null) {
        content = getCurrentNode().getFirstChild();
    } else {
        content = frame.currentChild.getNextSibling();
    }

    frame.currentChild = content;
    switch (content.getNodeType()) {
        case Node.ELEMENT_NODE:
            return START_ELEMENT;
        case Node.TEXT_NODE:
            return CHARACTERS;
        case Node.COMMENT_NODE:
            return COMMENT;
        case Node.CDATA_SECTION_NODE:
            return CDATA;
        case Node.ENTITY_REFERENCE_NODE:
            return ENTITY_REFERENCE;
        case Node.PROCESSING_INSTRUCTION_NODE:
            return PROCESSING_INSTRUCTION;
        default:
            throw new IllegalStateException("Found type: " + content.getClass().getName());
    }
}

Is it something you would type these days on modern JDK (and by modern, I would safely assume at least JDK-21), that has JEP-361: Switch Expressions incorporated? I doubt that, the switch expression is much better fit here:

@Override
protected int nextChild() {
    ElementFrame<Node, Node> frame = getCurrentFrame();
    if (frame.currentChild == null) {
        content = getCurrentNode().getFirstChild();
    } else {
        content = frame.currentChild.getNextSibling();
    }

    frame.currentChild = content;
    return switch (content.getNodeType()) {
        case Node.ELEMENT_NODE -> START_ELEMENT;
        case Node.TEXT_NODE -> CHARACTERS;
        case Node.COMMENT_NODE -> COMMENT;
        case Node.CDATA_SECTION_NODE -> CDATA;
        case Node.ENTITY_REFERENCE_NODE -> ENTITY_REFERENCE;
        case Node.PROCESSING_INSTRUCTION_NODE -> PROCESSING_INSTRUCTION;
        default -> throw new IllegalStateException("Found type: " + content.getClass().getName());
    };
}

If that is not convincing, another example would clear any doubts up. Here we have quite straightforward SimplePrincipal class (data class as we would normally call such classes):

public class SimplePrincipal implements Principal, Serializable {
    private static final long serialVersionUID = -5171549568204891853L;

    private final String name;

    public SimplePrincipal(String name) {
        if (name == null) {
            throw new IllegalArgumentException("Principal name can not be null");
        }
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public boolean equals(Object obj) {
        if (!(obj instanceof SimplePrincipal)) {
            return false;
        }

        return name.equals(((SimplePrincipal)obj).name);
    }

    public int hashCode() {
        return name.hashCode();
    }

    public String toString() {
        return name;
    }
}

With the record classes, introduced by JEP 395: Records, we have much better tool at our disposal to model data classes:

public record SimplePrincipal(String name) implements Principal, Serializable {
    public SimplePrincipal {
        if (name == null) {
            throw new IllegalArgumentException("Principal name can not be null");
        }
    }
    
    @Override
    public String getName() {
        return name;
    }
}

The reduction in boilerplate code required is just astonishing. The next power feature (that could be actually used along with records) we are going to look at is sealed classes, interfaces and records, introduced by JEP 409: Sealed Classes. To showcase it, let us take a look at this (simplified) hierarchy of classed:

/**
 * This class is the base class for SSL/TLS parameters that are common
 * to both client and server sides.
 */
public class TLSParameterBase {
    protected static final Collection<String> DEFAULT_HTTPS_PROTOCOLS = 
        Arrays.asList(
            "TLSv1", 
            "TLSv1.1", 
            "TLSv1.2", 
            "TLSv1.3" 
        ); 
        
    // Other members
}

/**
 * This class extends {@link TLSParameterBase} with client-specific
 * SSL/TLS parameters.
 *
 */
public class TLSClientParameters extends TLSParameterBase {
   // TLSClientParameters class members
}

/**
 * This class extends {@link TLSParameterBase} with service-specific
 * SSL/TLS parameters.
 *
 */
public class TLSServerParameters extends TLSParameterBase {
   // TLSServerParameters class members
}

The problem with such design is that anyone could subclass TLSParameterBase (yes, we could make it package private but that would lead to inability to reference it outside of the package), violating the designer's invariants that only server and client ones are expected to exist.

/**
 * This class is the base class for SSL/TLS parameters that are common
 * to both client and server sides.
 */
public sealed class TLSParameterBase permits TLSClientParameters, TLSServerParameters {
    protected static final Collection<String> DEFAULT_HTTPS_PROTOCOLS = 
        Arrays.asList(
            "TLSv1", 
            "TLSv1.1", 
            "TLSv1.2", 
            "TLSv1.3" 
        ); 
        
    // Other members
}

/**
 * This class extends {@link TLSParameterBase} with client-specific
 * SSL/TLS parameters.
 *
 */
public final class TLSClientParameters extends TLSParameterBase {
   // TLSClientParameters class members
}

/**
 * This class extends {@link TLSParameterBase} with service-specific
 * SSL/TLS parameters.
 *
 */
public final class TLSServerParameters extends TLSParameterBase {
   // TLSServerParameters class members
}

The new sealed keyword at the TLSParameterBase class declaration level along with permits clause solves this design flaw once and for all (we could have pushed the refactoring even further and convert TLSParameterBase to record).

Let us move on to a slightly different subject: XML/JSON/YAML/(you name it) as Java Strings.

public class RequestParserUnitTest {
    private static final String USE_KEY_X509_REFERENCE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        + "<wst:RequestSecurityToken xmlns:wst=\"http://docs.oasis-open.org/ws-sx/ws-trust/200512\">"
        + "<wst:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</wst:TokenType>"
        + "<wst:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issuelt;/wst:RequestType>"
        + "<wst:UseKey>"
        + "<wsse:SecurityTokenReference "
        + "xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">"
        + "<wsse:Reference URI=\"#x509\">lt;/wsse:Reference>lt;/wsse:SecurityTokenReference>"
        + "</wst:UseKey>" 
        + "</wst:RequestSecurityToken>";
        
    // Other members      
}

Admittedly, it looks messy and unmanageable (write once, never touch again). More to that, since this is the unit test case, troubleshooting any failing test cases caused by XML/JSON/... changes is a nightmare. How about using text blocks (multi-line strings) instead, the gem brought to Java language by https://openjdk.org/jeps/378:

public class RequestParserUnitTest {
    private static final String USE_KEY_X509_REFERENCE = """
            <?xml version="1.0" encoding="UTF-8"?>"
            <wst:RequestSecurityToken xmlns:wst="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
               <wst:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</wst:TokenType>
               <wst:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</wst:RequestType>
               <wst:UseKey>
                  <wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
                      <wsse:Reference URI="#x509"></wsse:Reference>
                  </wsse:SecurityTokenReference>
              </wst:UseKey>
          </wst:RequestSecurityToken>
      """;

    // Other members      
}

Although the usage of instanceof is often considered as an anti-pattern (and lack of proper design), it is pervasive and sneaks into most (if not all) Java codebases.

public static Long getLong(Message message, String key) {
    Object o = message.getContextualProperty(key);
    if (o instanceof Long) {
        return (Long)o;
    } else if (o instanceof Number) {
        return ((Number)o).longValue();
    } else if (o instanceof String) {
        return Long.valueOf(o.toString());
    }
    return null;
}

The introduction of JEP 394: Pattern Matching for instanceof did at least make the instanceof constructs less verbose and more readable:

public static Long getLong(Message message, String key) {
    Object o = message.getContextualProperty(key);
    if (o instanceof Long l) {
        return l;
    } else if (o instanceof Number n) {
        return n.longValue();
    } else if (o instanceof String s) {
        return Long.valueOf(s);
    }
    return null;
}

If we want to make it even more compact, switch expression could help us once again, thanks to enhancements introduced by JEP 441: Pattern Matching for switch:

public static Long getLong(Message message, String key) {
    return switch (message.getContextualProperty(key)) {
        case Long l -> l;
        case Number n ->  n.longValue();
        case String s -> Long.valueOf(s);
        default -> null;
    };
}

The Java language is changing fast, some may say very fast (and we haven't even talked about JVM and standard library changes!). That brings a lot of benefits but at the same time, poses many challenges. Thankfully, the strict compatibility policy that Java language designers prioritize over everything else means that you don't need to rewrite your code, it will still work. It just becomes legacy ... it becomes yet another kind of technical debt to pay ...

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

Saturday, March 23, 2024

JDK-22: The JNI's grave?

Another six months passed and it is about time for a new JDK release: without further ado, please meet JDK-22. The theme of this release is obviously Foreign Function & Memory API that becomes generally available after numerous preview cycles. So what else is there?

  • JEP-454: Foreign Function & Memory API: introduces an API by which Java programs can interoperate with code and data outside of the Java runtime. By efficiently invoking foreign functions (i.e., code outside the JVM), and by safely accessing foreign memory (i.e., memory not managed by the JVM), the API enables Java programs to call native libraries and process native data without the brittleness and danger of JNI.

    By all means, Foreign Function & Memory API (or FFM shortly), was eagerly awaited. For a long time JNI was the only mechanism to access native code on JVM and it has gained pretty infamous reputation of being brittle and outdated. FFM has emerged as a (substantially) better alternative and, after many rounds of previews, the API is finalized in JDK-22.

  • JEP-423: Region Pinning for G1: reduces latency by implementing region pinning in G1, so that garbage collection need not be disabled during Java Native Interface (JNI) critical regions.

  • JEP-456: Unnamed Variables & Patterns: enhances the Java programming language with unnamed variables and unnamed patterns, which can be used when variable declarations or nested patterns are required but never used. Both are denoted by the underscore character, _.

    To be fair, the positive impact of this small language change on the readability of Java programs is paramount. Familiar to Scala developers for years, it is here for Java developers now.

            if (content instanceof String s && s.startsWith("{")) {
                parseJson(s);
            } else if (content instanceof String s && s.startsWith("<")) {
                parseXml(s);
            } else if (content instanceof String _) {
                throw new IllegalArgumentException("The content type is not detected");
            };
      

    It becomes even more evident with records:

    
         sealed interface Response permits NoContentResponse, ContentResponse {}
        record NoContentResponse(int status) implements Response {}
        record ContentResponse(int status, byte[] content) implements Response {}
    
        public int status(final Response response) {
            return switch(response) {
                case NoContentResponse(var status) -> status;
                case ContentResponse(var status, _) -> status;
            };
        }
          

    And lamba functions:

        BiFunction<String, Number, String> f = (_, n) -> n.toString();
        
  • JEP-458: Launch Multi-File Source-Code Programs: enhances the java application launcher to be able to run a program supplied as multiple files of Java source code. This will make the transition from small programs to larger ones more gradual, enabling developers to choose whether and when to go to the trouble of configuring a build tool.

    This is a logical continuation of the JEP 330: Launch Single-File Source-Code Programs, delivered in JDK-11. With this change, Java could seriously challenge the status quo of the established scripting languages.

Couple of new and refined preview language features and APIs are also made into release, just briefly mentioning them here (and we would get back to them once finalized).

Every new JDK release comes with a long list of bugfixes, and in case of JDK-22, there are quite a few worth mentioning:

Moving off from bugs and regressions, let us take a look at the interesting new features that JDK-22 delivers across the board:

Last but not least, let us talk about the API changes (standard library) that went into this release.

From the security perspective, there are couple of notable changes to highlight (but please, do not hesitate to check out JDK 22 Security Enhancements for more details):

To finish up, it will be useful to mention a few regressions that ended up in JDK-22 release, the fixes are already scheduled for the upcoming major or patch releases:

Some may say that JDK-22 is a boring release, but I personally disagree: FFM APIs and formalizing _ usage are all but not boring features.

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