Showing posts with label jdk. Show all posts
Showing posts with label jdk. Show all posts

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.

Wednesday, March 22, 2023

JDK-20: the most boring JDK release yet?

Still hot off the press, JDK-20 is out! These are terrific news, but what about exciting new features? Although a number of JEPs made it into the release, all of them are either preview or incubation features:

  • JEP-429: Scoped Values (Incubator): introduces scoped values, which enable the sharing of immutable data within and across threads. They are preferred to thread-local variables, especially when using large numbers of virtual threads.

  • JEP-432: Record Patterns (Second Preview): enhances the Java programming language with record patterns to deconstruct record values. Record patterns and type patterns can be nested to enable a powerful, declarative, and composable form of data navigation and processing.

  • JEP-433: Pattern Matching for switch (Fourth Preview): enhances the Java programming language with pattern matching for switch expressions and statements. Extending pattern matching to switch allows an expression to be tested against a number of patterns, each with a specific action, so that complex data-oriented queries can be expressed concisely and safely.

    If you keen to learn more, please check out Using Pattern Matching publication, a pretty comprehensive overview of this feature.

  • JEP-434: Foreign Function & Memory API (Second Preview): 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.

  • JEP-436: Virtual Threads (Second Preview): introduces virtual threads to the Java Platform. Virtual threads are lightweight threads that dramatically reduce the effort of writing, maintaining, and observing high-throughput concurrent applications.

  • JEP-437: Structured Concurrency (Second Incubator): simplifies multithreaded programming by introducing an API for structured concurrency. Structured concurrency treats multiple tasks running in different threads as a single unit of work, thereby streamlining error handling and cancellation, improving reliability, and enhancing observability.

  • JEP-438: Vector API (Fifth 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.

The pessimists may say this is the most boring release of JDK yet, but the optimist would argue that this is the calm before the storm (yes, I am talking about the next LTS release later this year, JDK-21). Nonetheless, there are quite a few notable changes to look at.

The standard library was the one benefited the most in JDK-20 release, let us take a closer look at what has changed:

The garbage collectors have got a considerable chunk of improvements (especially G1), covered by JDK 20 G1/Parallel/Serial GC changes in great details. To highlight just a few:

From the security perspective, it worth mentioning these changes:

  • JDK-8256660: Disable DTLS 1.0 by default: disables DTLS 1.0 by default by adding "DTLSv1.0" to the jdk.tls.disabledAlgorithms security property in the java.security configuration file.

  • JDK-8290368: Introduce LDAP and RMI protocol-specific object factory filters to JNDI implementation: introduces LDAP-specific factories filter (defined by jdk.jndi.ldap.object.factoriesFilter property) and RMI-specific factories filter (defined by jdk.jndi.rmi.object.factoriesFilter property). The new factory filters are consulted in tandem with the jdk.jndi.object.factoriesFilter global factories filter to determine if a specific object factory is permitted to instantiate objects for the given protocol.

This releases proves one more time that boring is not always bad. If you want to learn more about these (and other) features in JDK-20, I would highly recommend going over Java 20 πŸ₯± guide.

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

Thursday, October 27, 2022

JDK 19: revolution is looming!

The JDK-19 landed last month and may look like yet another routine release except it is not: Project Loom has finally delivered first bits! Alhough still in half-incubation / half-preview, it is a really big deal to the future of OpenJDK platform. Interestingly enough, a vast majority of the JDK-19 JEPs are either preview or incubating features. So, what JDK-19 has to offer?

From the security perspective, a few notable changes to mention (but feel free to go over much more detailed overview in the JDK 19 Security Enhancements post by Sean Mullan):

If you are one of the early adopters, please note that JDK-19 introduced a native memory usage regression very late in the release process so that the fix could not be integrated any more (JDK-8292654). The regression has already been addressed in JDK 19.0.1 and later.

With that, hope you are as excited as I am about JDK-19 release, the peaceful revolution is looming! And the JDK-20 early access builds are already there, looking very promising!

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

Wednesday, January 27, 2021

JEP-396 and You: strong encapsulation of the JDK internals is the default

Since the inception of the Project Jigsaw, one of its goals was to encapsulate most of the JDK internal APIs in order to give the contributors a freedom to move Java forward at faster pace. JEP-260, delivered along JDK 9 release was a first step in this direction. Indeed, the famous WARNING messages like the ones below

...
WARNING: An illegal reflective access operation has occurred
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
...

started to appear all over the place. Intended to give the developers and maintainers the time to switch to the alternative, publicly available APIs, they rather provoked the opposite: most just got used to them. Well, if nothing breaks, why bother?

But ... the things are going to change very soon. I think many of you have seen the code which tries to do clever things by gaining the access to the private method or fields of the classes from standard library. One of the notable examples I have seen often enough is overcoming ThreadPoolExecutor's core pool size / maximum pool size semantics (if curious, please read the documentations and complementaty material) by invoking its internal addWorker method. The example below is a simplified illustration of this idea (please, do not do that, ever).

final ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue>Runnable<());

final Runnable task = ...
executor.submit(task);
    
// Queue is empty, enough workers have been created
if (executor.getQueue().isEmpty()) {
    return;
}
    
// Still have some room to go
if (executor.getActiveCount() < executor.getCorePoolSize()) {
    return;
}
    
// Core pool is full but not maxed out, let us add more workers
if (executor.getActiveCount() < executor.getMaximumPoolSize()) {
    final Method addWorker = ThreadPoolExecutor.class.getDeclaredMethod(
        "addWorker", Runnable.class, Boolean.TYPE);
    addWorker.setAccessible(true);
    addWorker.invoke(executor, null, Boolean.FALSE);
}

To be fair, this code works now on JDK 8, JDK 11 or JDK 15: find a private method, make it accessible, good to go. But it will not going to work smoothly in soon to be out JDK 16 and onwards, generating the InaccessibleObjectException exception at runtime upon setAccessible invocation.

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make private boolean java.util.concurrent.ThreadPoolExecutor.addWorker(java.lang.Runnable,boolean) accessible: module java.base does not "opens java.util.concurrent" to unnamed module @72ea2f77
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:357)
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
        at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
        at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
        ...

So what is happening here? The new JEP-396 continues the endeavours of JEP-260 by strongly encapsulating JDK internals by default. It has been integrated into JDK 16 and JDK 17 early builds which essentially means, no abusive access is going to be allowed anymore. Arguably, this is the right move, though it is very likely to be a disruptive one.

Should you be worrying? It is a good question: if you do not use any internal JDK APIs directly, it is very likely one of the libraries you depend upon may not play by the rules (or may not be ready for rule changes). Hopefully by the time JDK 16 is released, the ecosystem will be in a good shape. There is never good time, we were warned for years and the next milestone is about to be reached. If you could help your favorite library or framework, please do.

The complete list of the exported packages that will no longer be open by default was conveniently made available here, a couple to pay attention to:

java.beans.*
java.io
java.lang.*
java.math
java.net
java.nio.*
java.rmi.*
java.security.*
java.sql
java.text.*
java.time.*
java.util.*

Last but not least, you still could overturn the defaults using the --add-opens command line options, but please use it with great caution (or better, do not use it at all):

$ java --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED ...

Please be proactive and test with latest JDKs in advance, luckily the early access builds (JDK 16, JDK 17) are promptly available to everyone.