JVM Advent: Recent Episodes

None

The JVM Programming Advent Calendar

View Details

Just like that it happened. You, a disciplined Java developer, are now installing Python. Like everything in 2025, it just arrived. One day, you were running a tidy mvn install, the next, you’re learning about virtual environments and fighting an unfriendly pip install that won’t explain what it just pulled from the internet.

Good news: staying safe in Python’s world isn’t complicated. It’s just not obvious. Think of this as a friendly reminder to check the extension cable before plugging in another set of lights.

Pip Install vs. Mvn Install: A World of DifferenceJava’s packaging has its flaws, but predictability isn’t one of them. Maven Central is curated. Group IDs enforce a namespace. When you fetch com.fasterxml.jackson.core:jackson-databind, you get that specific artefact from that particular organisation.

Python is radically different:

  • A Flat Namespace: Package names are first-come, first-served on PyPI. Name collisions and typosquatting are primary attack vectors.
  • A Trusting Resolver: The resolver is extremely trusting. It will often prioritise a higher version number from any available index, which is the core of dependency confusion attacks.

If you’re used to Java’s rules, Python’s model will trip you up. Please treat it with extreme care.

Be Suspicious of “pip install …”Java developers might fall for an occasional fake install script; Python encourages them. The internet is full of “copy this pip install and trust me.” You shouldn’t accept that for a Maven dependency. Don’t accept it here.

If you see pip install my-cool-tool, ask the same questions you would for a new Maven dependency:

  1. Who owns this package? (Check PyPI, GitHub activity, and developer reputation).
  2. Does the name look close to something legitimate? (Guard against typosquatting: requests vs. requessts).
  3. Is this the actual source, or a mirror?

Dependency Confusion in Python is embarrassingly easy. A malicious public package with the same name can hijack a private internal package called company-utils simply by having a higher version number. This is the default behaviour if not explicitly mitigated.

Your Checklist: Prefer explicit version pins. (e.g., requests==2.28.1). * Prefer known sources. Use a private package registry like Artifactory or Nexus to proxy and cache PyPI, allowing your organisation to blacklist known bad packages and prioritise your internal packages. (It may sound like ‘corporate’, but it’s essential) * Prefer tooling that uses a lock file.*

Take Five Minutes to Learn Virtual EnvironmentsYou’ve probably ignored Python’s advice about virtual environments already. Don’t. This is the difference between a clean setup and a machine that slowly accumulates mysterious, conflicting modules.

A virtual environment gives you the isolation you take for granted in Java’s classpath model. Without it, you are installing every dependency into your system path.

Create one:

*python3 -m venv .venv**source .venv/bin/activate* Now every pip install lives inside .venv, not your machine. This gives you reproducibility and enables cleaner scanners, policy checks, and SBOM generation. (In fact, once you start using Python in more depth, you’ll realise you can’t live without this approach – so do the right thing and you’re future 2026 self will thank you)

Pin Your Dependencies. Really! Pin your dependencies.In Java, you pin dependencies by default in your POM. Since transitive dependencies on Maven Central are immutable, you know the complete set upfront and permanently. In Python, if you don’t pin, you get drift. Packages can silently update, pulling in new transitive dependencies.

Most critically: Malicious actors publish higher-numbered versions to trick the resolver. If you use a loose constraint like package>=1.0.0, an attacker publishing package==99.99.99 can compromise your build.

  • Use pip-tools, Poetry, or uv.
  • Generate a lock file (your pom.xml equivalent).
  • Add the lock file to source control.
  • Use Hashes: Generate hashes for all dependencies. They are your final line of defence against tampered packages.

The Out-of-Support Trap: The Real Threat of Old PackagesAttackers don’t rely solely on brand-new exploits; they often compromise systems by manipulating developers (i.e., you) into installing old, vulnerable packages that are frequently out of support.

The workflow is:

  1. Bad Actor publishes an old, vulnerable version (perhaps with malicious code added) or relies on an unmaintained package with a known CVE.
  2. The vulnerable version is pulled in due to a loose dependency constraint (e.g., a sub-dependency requires old-library<2.0.0).
  3. Your vulnerability scanner may catch the known CVE, but since the package is long out of support (its maintainers have moved on, or the version is too old), no patch exists.

The Cost:

  • Vulnerability: You have a known exploit in your stack.
  • Technical Debt: You now have to either fork and patch the unmaintained library yourself (probably a massive undertaking) or re-architect your application to use a different, supported library. You’ve essentially built a stack that’s immediately due for a costly rebuild.

Mitigation: Automated Scanners are Non-Negotiable: Use tools like Syft, Safety, Sonatype or Snyk to generate an SBOM and scan for known CVEs on every build. (Do all security companies start with ‘S’?) * Policy Checks: Block builds that rely on packages with severe, unpatched CVEs, or that use versions marked as “end-of-life” by the maintainer. Check for third-party maintainers too. Not all open-source is abandoned. Sometimes it gets picked up and brushed down. * Audit Your Transitives: The lock file is key. It lets you see the entire dependency graph, not just your direct requirements, and vet for ancient, risky components. * Look for commercial support.* Strangely there are companies who work to keep you safe. End-of-life support (not really what it sounds like) can often be found for those pesky out-of-support open source components you’ve just installed and now discovered need a fix.

Know What “Local Install” Actually MeansPython encourages local installs with instructions like pip install -e .. This installs the package “editable” from your working directory. While convenient for development, it means the installed code changes instantly whenever someone edits files on disk.

  • Avoid in Production: This mutability is the last thing you want in a production flow.
  • Understand the Implications: Use editable installs only for local, feature-branch development, where mutability is a feature, not a bug.

If you don’t want to be on the leading edge, don’t do this!

Keep Your LLM Tools ContainedMany developers install Python only for LLMs. That’s fine, but keep those tools contained. LLM agents and model servers often run their own complex Python interpreters and download additional files.

  • Isolation: Do not let your LLM workspace share a Python environment with your production scripts.
  • Sandboxing: Do not run tools that download models or plugins without sandboxing (e.g., in a container or a dedicated VM).
  • No curl | bash: Do not assume that “AI tool = harmless CLI.” Do not run shell-piped installers for model servers.
  • No curl | bash: Hey, it’s the holidays, we can count things twice. Seriously, while pip install requirements.txt is a rich source of malware and compromised systems so is curl | bash. Be very, very careful about using it to install software. Even if you think you trust the website or organisation involved.

Keep on the ‘nice’ list. Python gives you rope. Your established Java discipline is your best defence :

  • Trust Nothing: Treat PyPI packages like third-party artefacts: verify their origin and security posture.
  • Pin Everything: Go beyond version pinning to use lock files and dependency hashes.
  • Automate Scans: Make SBOM generation and CVE scanning mandatory in your CI/CD pipeline.
  • Environments as Cattle: Use virtual environments or containers, and frequently rebuild them to ensure a clean, reproducible state.

If you stick to these Java security principles, Python becomes far less chaotic and much more manageable.

Have a great holiday season and keep your software safeThe post Santa’s Python Pitfalls: A Java Developer’s Guide to Staying Safe This Christmas appeared first on JVM Advent.

View Details

Introduction: the promise of “embed once, run anywhere” reimaginedWhat if you could embed native‑level capabilities into your Java applications, think of database engines, scripting runtimes, policy interpreters, even compilers and still remain pure Java, with no JNI, no native binaries, no concerns about OS or architecture compatibility? That is the promise of combining WebAssembly (Wasm) with the zero dependencies runtime Chicory.

The year 2025 feels like a turning point for this model. Chicory is no longer just an advanced exercise or a runtime prototype. It is powering a growing ecosystem of real‑world Java libraries. “Embed once, run anywhere” is no longer a slogan, it is becoming practical. In this post, we will explore why Chicory matters, what has been built upon it so far, and why you (as a Java developer or architect) should care today.

What is Chicory and why it mattersChicory is a JVM‑native WebAssembly runtime. It allows you to run Wasm modules with zero native dependencies or JNI. As long as the JVM runs, the Wasm‑derived code runs.

Most existing Wasm runtimes (for example V8, Wasmtime, Wasmer, Wasmedge) are often written in C/C++/Rust. Embedding them in a Java application requires shipping native binaries for every supported platform, complicating distribution and deployment.

By contrast, Chicory eliminates that burden. You just depend on a JAR. This design brings structural advantages:

  • Portability: if the JVM can run, the same Wasm module can run everywhere. No need to build per OS or architecture.
  • Sandboxing and JVM integration: the Wasm module executes inside the JVM sandbox, on a managed isolated heap, preserving JVM memory safety, garbage collection, observability, tooling.
  • Simplicity: no external native dependencies, no packaging matrix across OS/architecture, no JNI trouble.

Chicory re-imagines the classic “write once, run anywhere” Java promise. It extends it beyond Java bytecode, enabling WebAssembly modules that let you embed seamlessly powerful native features, while staying inside the JVM.

Chicory execution modes: flexibility for development and productionOne of the strengths of Chicory is that it supports multiple ways of executing Wasm modules, letting you choose the trade‑off between portability and performance.

Here is an overview:

| Mode | Description | When to Use | | Interpreter | Default mode. Executes Wasm modules directly, without compilation. It requires no extra dependencies and is maximally portable. | During development, dynamic loading, or when you want maximum flexibility. | | Runtime Compilation | On-the-fly compilation of Wasm modules into Java bytecode (in‑memory) and dynamic loading. This requires an additional dependency (ASM) but improves execution performance significantly. | Useful when you need better performance while still allowing dynamic module loading. | | Build‑time Compilation | Compile Wasm modules into plain Java bytecode at build time (via Maven/Gradle plugin). You get standard class/JAR artifacts and avoid runtime compilation overhead altogether. | For production deployments with static modules and where performance is important. |

With the release of Chicory 1.4.0, the compiler (both runtime and build‑time) and the annotations system became stable.

More recently, Chicory 1.6.0 added support for the Java Platform Module System (JPMS), a directory‑backed runtime‑compiler cache (improving startup for repeated module loads), enhanced support for the Wasm “Threads” proposal(atomic fence instructions and atomic ops), increased Wasm spec conformance and verified compatibility with Java 25.

Thanks to this flexibility, libraries and applications can choose the mode that fits their use case: from dynamic scripting in dev to high‑performance embedded Wasm in production.

Real‑world “native‑free” tools built on ChicoryOne of the strongest signals that the Wasm‑on‑JVM model is maturing is the growing number and quality of real libraries now using Chicory.

Here are some of the most notable:

QuickJs4j: a sandboxed JavaScript runtime for Java. QuickJS (originally a C engine) is compiled to WebAssembly, then Chicory compiles that Wasm into pure Java bytecode. The result is a small, self‑contained JAR runtime.

QuickJs4J’s is already powering the Microcks JavaScript dispatcher and Apicurio’s Registry custom artifact types.

SQLite4j: a pure‑Java SQLite JDBC driver. SQLite (originally written in C) is compiled to WebAssembly, then translated via Chicory to JVM bytecode. This allows embedding the full power of the most used lightweight SQL database inside Java without the need to ship native binaries.

Opa‑java‑wasm: is the WebAssembly-powered version of the policy engine Open Policy Agent (OPA), delivered for Java via Chicory. It powers in-process OPA policy evaluation removing the network calls required by more traditional integrations and, again, no native dependencies.

Beyond these, the “Who uses Chicory?” list includes many other use cases: user-defined functions for data engines, plugin systems, scripting inside data frameworks and more.

These are not toy examples. They deliver widely relevant capabilities: a JavaScript runtime, an embedded database and a policy engine. They show that Wasm + JVM is not just a thought experiment; it is already powering production‑ready tools.

Why this matters for Java developers and enterprisesFor years, Java developers have accepted a painful tradeoff: whenever you needed to interact with libraries originally written in system programming languages, you ended up either rewriting the full thing or using JNI, FFI, native binaries, and OS/architecture‑specific builds. Packaging, deployment complexity, native-library hell became a recurring burden.

Chicory changes that math. With Wasm + Chicory + appropriate libraries, you can embed powerful functionality without leaving the JVM or shipping native dependencies.

New architectural patterns are emerging: plugin systems, embedded scripting engines, user-provided logic, sandboxed extensions, policy engines can now run within a JVM and with a controlled ABI surface.

For enterprise and cloud‑native applications, this significantly reduces operational complexity, simplifies deployment across environments and improves maintainability enabling wider code re-use.

Where things stand: momentum, community and next stepsThe ecosystem around Chicory is gaining tangible momentum. With stable compiler support, modularity, improved performance, cache support, and a lot of emerging libraries and integrations, the year 2025 feels like the moment when this technology shifted from “experimental runtime” to a useful building block.

Now is a great time to try things out!
Try embedding a Wasm‑compiled module into your Java project using Chicory and help us shape the future of native‑free, polyglot tooling for the JVM.

Conclusion: reimagining “write once, run anywhere” for modern needsChicory re-imagines Java’s classic “write once, run anywhere” promise:extending it beyond Java bytecode to WebAssembly modules compiled into JVM bytecode. With Chicory, you can embed powerful, native‑level capabilities inside Java applications, while retaining the safety, portability and simplicity of pure Java.

If you are a Java developer or architect still wrestling with native dependencies or painfully shipping platform‑specific binaries perhaps it is time to look again. Chicory and the growing ecosystem of libraries around it shows that you can have your cake and eat it too.

Give it a try now. The future of native‑free, polyglot-powered Java tooling may depend on it.

The post Bring WebAssembly to the JVM. How Chicory Is Powering a New Generation of Java Libraries appeared first on JVM Advent.

View Details

During the second part of this year, Anysphere, the company behind Cursor IDE, released 2 new products that could help you in 2026 increase the level of automation in your software operations. The names of both products are: Cursor Agent CLI and Cursor Cloud Agents. The article will explain the features that both products share and the unique capabilities of each. Finally, the article will share some insights for creating great supervised AI Dev pipelines.

What is Cursor Agent CLI in a Pipeline context?In August 2025, Anysphere released Cursor Agent CLI, a new way to interact with frontier models but not coupled with a particular IDE. With this local development approach, the software engineer added a new way to enrich the development experience, but what happens if we use this product in a pipeline? In that case, we will add new capabilities.

Let’s review the following pipeline to understand the concept:

``` name: Run Cursor Agent on Demand

on:

workflow_dispatch:

jobs:

agent-on-demand:

runs-on: ubuntu-latest
timeout-minutes: 5

permissions:

contents: write

pull-requests: write

steps:

- name: Checkout repository

uses: actions/checkout@v6

with:

token: ${{ secrets.GITHUB_TOKEN }}

fetch-depth: 0

- name: Install Cursor CLI

run: |

curl https://cursor.com/install -fsS | bash

echo "$HOME/.cursor/bin" >> $GITHUB_PATH

- name: Run Cursor Agent

env:

CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}

run: |

echo "=== User Prompt:===";

PROMPT="Develop a classic Java class HelloWorld.java program that print Hello World in the console only"

echo "$PROMPT";

echo "=== Cursor Agent Execution:===";

echo "";

cursor-agent -p "$PROMPT" --model auto

- name: Create PR with changes

env:

GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

PAT_TOKEN: ${{ secrets.PAT_TOKEN }}

GITHUB_REPOSITORY: ${{ github.repository }}

GITHUB_ACTOR: ${{ github.actor }}

run: |

chmod +x .github/scripts/create-pr.sh

.github/scripts/create-pr.sh ``` In a few lines of code, a pipeline is able to execute a task with the help of Frontier models and at the end of the process, submit a PR to be reviewed by the team.

Once you have a clear idea about how to start working with this product, let’s jump to the second product released, Cursor Cloud Agent.

What are Cursor Cloud Agents?In October 2025, Cursor Cloud Agents was released, and it provides a collection of REST endpoints to handle the service. The different resources are organized into 3 categories:

  • Agent Management (Launch, Follow up, Stop & Delete)
  • Agent Information (Status, Conversation & List of Agents)
  • General Information (Models, Repositories & API keys)

Using this service, you can delegate tasks to frontier models, but all operations run on Cursor cloud infrastructure, not in your pipelines like with Cursor Agent CLI.

As the service provides different REST endpoints, it is important to understand the minimum concepts to orchestrate tasks with them.

Understanding the lifecycle of a Cursor Cloud Agent requestStep 1: Launching a new AI Agent When a user want to use this service, launch a HTTP POST request to provision a new cloud AI agent, the service will require the following information:

  • A valid Github/Gitlab repository to operate with permissions
  • A user prompt with the clear goal to be achieved
  • An available frontier model to process your user prompt
  • A Cursor API Key to authenticate the request and validate if the user has permissions to operate with the required Git repository.

Note: In this article we will put focus on Prompts based on Text plain, not images.

Once the User sends the request, the service will return a HTTP response with status code 201 indicating that the request was received and the service will be processed soon, an Agent-ID which is pretty useful to be used with other REST resources to track the progress and an Agent State, in this case, CREATING.

Note: You could track the whole process here in a visual way: https://cursor.com/agents

What happens under the hood?

Once the service receives the request, it will provision an EC2 instance running in AWS region us-east-1 with the following features:

  • OS: Linux Distro
  • Cores: 4 cores
  • Memory: 16GB
  • Disk: 126GB HDD
  • Java: Java 21

Inside this Linux container, the service will perform a git checkout operation of the git repository described in the request, and after that, it will start working on the details described in the user prompt.

As you can observe, the request receives a fast response, but the whole process is asynchronous. So how do you track the progress of your user prompt as it works on your repository?

Step 2: What is the status of my AI Agent?An AI Agent has the following states:

  • RUNNING
  • FINISHED
  • ERROR
  • CREATING
  • EXPIRED

If you remember from the first step, the AI Agent returned the state CREATING, and if everything goes well, the current state should now be RUNNING. But how do you know what the real status is? For that purpose, there exists a GET endpoint to receive the status from an Agent ID.

By calling the status endpoint periodically, the user/process can know when the AI Agent has changed the state to FINISHED.

Once the AI Agent is in a FINISHED state and the process has changed anything in the git repository, it will execute internally a git commit & git push to a feature branch and will create a PR to be reviewed.

Finally we have our lovely Hello World in the repository:

``` package info.jab.examples;

public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
} ``` Step 3: Review the pull requestOnce Cursor Cloud Agent reaches the goal specified in the user prompt, the service will create a PR in the repository to be reviewed by your team, independent of your Git branch strategy like Trunk-Based Development, Gitflow, or similar.

When to choose Cursor Agent CLI and when to choose Cursor Cloud Agents?Exploring new technologies always has a cost. Let’s list a few factors to help in your decision-making:

  • Complexity of the scenario to automate: If the scenario to delegate is easy, like a simple task or a sequence of operations, Cursor Agent CLI could be the first option. On the other hand, if you need to model a process that behaves like a state machine or a directed graph, Cursor Cloud Agents offer better options because they provide more granular control of the execution.
  • Experience with AI agents in the team: If you are starting with AI Agents, Cursor Agent CLI could be the first option to run a pilot because it requires less effort to start and receive results.
  • Pipeline capacity: Depending on the pipeline capacity, you might consider whether to run the operations in your pipelines or outside.
  • Costs: Both products use the same subscription, so the cost structure is the same today.
  • Tooling: Frontier models use tools to interact in the environments where they operate. Currently, Cursor Agent CLI has support for MCP, but this feature is not available in Cursor Cloud Agents. An important question might be: Why do you need MCP for everything? A CLI interface might be enough.
  • Observability: If observability is a critical factor, Cursor Cloud Agents provides a specific endpoint to retrieve the internal agent conversation. This approach is useful for analysis in case of errors.

Until now, we have reviewed the way to execute user prompts by comparing 2 products, but in both cases, you can decouple the location of your prompts from the location of the execution. In the next sections, we will explore aspects of user prompts that will help you be more efficient and maintain them with less effort.

Developing great user promptsUntil now, we have only explained the output of the service—in the previous case, the creation of a Java class that writes to the terminal’s standard output. But how do you increase efficiency in the process? It’s simple: send a request with a user prompt that minimizes ambiguity to reach the defined goals.

An initial Hello World user promptYou might think that a good user prompt could be:

Develop a classic Java class HelloWorld.java program that prints "Hello World" in the console only. And nothing more. But in practice, this idea—which apparently seems very easy—could be interpreted by frontier models in several ways, independent of which frontier model is used, because frontier models have non-deterministic behavior and may have doubts about:

  • The location of the Java class
  • The approach to compile the class (javac or using a build system)
  • Whether they need to commit .class files
  • Whether they need to use System.out.println or IO.println (Java 25+)

If you understand the potential problems on the frontier model side, let’s iterate on this user prompt.

Moving away from plain text user promptsWhen you use modern IDEs with AI features and the frontier model doesn’t return the expected result, you continue the conversation, and after a few iterations, the result is as expected. But when using this kind of service running in your pipelines where you expect accurate results, you need to define restrictions and other details clearly to achieve your goals. So little by little, that user prompt will require some structure to operate accurately.

Encoding your User prompts in PML formatPML is the acronym for Prompt Markup Language, an XML Schema designed to help software engineers describe user prompts accurately.

Take a look at the evolution from plain text to PML with the new sections:

Text plain:

Develop a classic Java class HelloWorld.java program that prints "Hello World" in the console only. XML with PML Schema:

```

<role>  
    You are a Senior software engineer with extensive         experience in Java software development  
</role>

<goal>  
    Develop a classic Java class HelloWorld.java program  
    that print "Hello World" in the console only  
</goal>

<constraints>  
    <constraint-list>  
        <constraint>The develop the class in the Maven module `sandbox`</constraint>  
        <constraint>The develop the class in the package info.jab.examples</constraint>  
        <constraint>Do not create any test class</constraint>  
        <constraint>Do not touch the build file (pom.xml)</constraint>  
    </constraint-list>  
</constraints>

<output-format>  
    <output-format-list>  
        <output-format-item>Don not explain anything</output-format-item>  
    </output-format-list>  
</output-format>

<safeguards>  
    <safeguards-list>  
        <safeguards-item>Build the solution with Maven Only</safeguards-item>  
    </safeguards-list>  
</safeguards>

<acceptance-criteria>  
    <acceptance-criteria-list>  
        <acceptance-criteria-item>The solution is compiled successfully with `./mvnw clean compile -pl sandbox`</acceptance-criteria-item>  
        <acceptance-criteria-item>The solution only prints "Hello World" in the console</acceptance-criteria-item>  
        <acceptance-criteria-item>Only commit java sources only and push the changes to the branch to create the PR</acceptance-criteria-item>  
    </acceptance-criteria-list>  
</acceptance-criteria>

``` Although we have increased the number of lines, now the user prompt look robust and we have mitigated the ambiguity and now it has a better structure and it will be easier to maintain in the future with new refinements.

  • User prompt
    • Role
    • Goal
    • Restrictions
    • Output format
    • Safeguards
    • Acceptance criteria

Once you have created the document, it can be validated with the XML Schema and later transformed to another format like Markdown.

Here is the result converted into Markdown:

```

Role

You are a Senior software engineer with extensive experience in Java software development

Goal

Develop a classic Java class HelloWorld.java program
that print "Hello World" in the console only

Constraints

  • The develop the class in the Maven module sandbox
  • The develop the class in the package info.jab.examples
  • Do not invest time in planning
  • Do not create any test class
  • Do not touch the build file (pom.xml)

Output Format

  • Don not explain anything

Safeguards

  • Build the solution with Maven Only

Acceptance Criteria

The goal will be achieved if the following criteria are met:

  • The solution is compiled successfully with ./mvnw clean compile -pl sandbox
  • The solution only prints "Hello World" in the console
  • The solution is committed and pushed to the branch to create the PR
  • Only commit java sources only and push the changes to the branch to create the PR ``` Using XML as the source format for your user prompts, you could use the composability features that XML includes. On the other hand, when creating or updating PML files, you always create files with the same syntax, so your prompts will be homogeneous at scale.

What happen if something goes wrong?Don’t be naive—even the most complex systems in the world, like nuclear plants, have incidents in different ways, so why wouldn’t this kind of integration have them too? Let’s explore different types of issues that your threat model plan should cover in your projects using this kind of technology.

Scenario: using Cursor Agent cli from a PipelineImagine the scenario where you delegate a task to Cursor Agent CLI in the execution of your pipeline. What issues could happen?

Scenario: Using Cursor Agent CLi from the pipeline

Issues at Pipeline Level

  • Third-party dependencies: If your pipeline is too complex, the global reliability may suffer. Review the chain of dependencies to simplify your pipeline and avoid runtime issues. Log runtime issues with third-party dependencies. Using a predefined Docker image could reduce the number of runtime issues.
  • Unexpected files included in the PR: When frontier models try to resolve the goals described in the user prompt, they sometimes need to create scripts, extra files, or simply store files for analysis and debugging. Refine your user prompts to specify exactly what files and file extensions are valid; another alternative is to combine this with .gitignore files.

Issues at Cursor Agent CLI Level

  • Cursor Agent CLI doesn’t make progress: This is rare, but you may not see execution progress due to different runtime issues. For such cases, it is important to define realistic timeouts for the pipeline step that involves this integration.
  • Cursor Agent CLI enters a loop: Sometimes, if you send an unclear or unrealistic user prompt, the process may enter a loop. To avoid this, review your user prompts, and if this happens, define clear timeouts to reduce costs and finish the process sooner.

Scenario: Orchestrating Cursor Cloud Agent from a PipelineImagine the scenario where you try to orchestrate an integration with the service Cursor Cloud Agent from a popular Pipeline. What issues could happen?

Scenario: Orchestrating Cursor Cloud Agents from the Pipeline

Issues at Pipeline level

  • Third-party dependencies: If your pipeline is too complex, the global reliability may suffer. Review the chain of dependencies to simplify your pipeline and avoid runtime issues. Log runtime issues with third-party dependencies. Using a predefined Docker image build could reduce the number of runtime issues.
  • The task related to Cursor Cloud Agent failed: It is not common, but the service can fail. In that case, I recommend logging the Agent ID returned from the launch operation (POST /v0/agents) and writing the internal conversation using the REST endpoint GET /v0/agents/{id}/conversation.
  • I am not able to create more Cursor Cloud Agents: If you use the service and submit the PR, at the end of the process, you should delete the agent to release resources at the Cursor level—the resources are not infinite. Use the endpoint DELETE /v0/agents.

Issues at Cursor Cloud Agent level

  • Cursor Cloud Agent finished with an ERROR state: Yes, it is not common, but sometimes it happens for different reasons. On the user side, for example, if you change a customized Cursor environment described in .cursor/environment.json and the associated Dockerfile, you could encounter this issue, but there could also be unknown runtime issues at the Cursor level. Logging the Agent ID and the conversation can be useful.
  • Cursor Cloud Agent doesn’t make progress: This is rare, but you may not see execution progress due to different runtime issues. For such cases, it is important to define realistic timeouts for the pipeline step that involves this integration.
  • Cursor Cloud Agent enters a loop: Sometimes, if you send an unclear or unrealistic user prompt, the process may enter a loop. To avoid this, review your user prompts, and if this happens, define clear timeouts to reduce costs and finish the process sooner.
  • Cursor Cloud Agent includes files not requested in the PR: When frontier models try to resolve the goals described in the user prompt, they sometimes need to create scripts, extra files, or simply store files for analysis and debugging. Refine your user prompts to specify exactly what files and file extensions are valid; another alternative is to combine this with .gitignore files.

In general, it is a good practice to log the Agent ID for potential Cursor support and log the internal frontier model conversation for further analysis in order to improve the user prompt. Do not miss creating a threat model in your projects.

Real world scenarioSIf you have doubts about what scenarios could be used for this new cloud service, I’ll share a few scenarios that you might find interesting.

  • Continuous documentation: Not everyone loves documenting solutions, and sometimes the documentation is outdated over time. You could use this service to update the documentation at different levels for the team, externally, or simply to train people.
  • Continuous coding standard refactoring: People come and go on your team, and everyone has a different programming style. In Java, you can establish format rules with plugins like Spotless or similar, but there is no tooling to unify the programming paradigm or style. Google and other companies have published Java guides—why not refactor your software using your style? If you have good tests, you’re safe.
  • Fix changes if a third party breaks the contract: Oh my god, that team changed the contract again, and the product owner didn’t estimate the task in the sprint. Okay, let’s monitor the contract—if something changes, let’s delegate the action to evaluate the level of change and determine if it’s acceptable, then adapt the anticorruption layer to the new change.
  • Continuous Sonar cleanup: Oh my god, the Sonar gate is blocked again, and the product owner doesn’t allow us to release the product. Okay, let’s run the pipeline that retrieves the failing security hotspots and issues with blocker & high severity to be fixed today.
  • Continuous profiling: Not everyone on the team has good skills to understand files like flamegraphs, thread dump files, GC logs, etc. But why not delegate that task to the service to discover new opportunities to improve your products?
  • Simplify complexity: Periodically, you could run a pipeline that reviews the current implementation to simplify architecture, implementation, data types used, etc. Simple systems are maintained better.
  • Empower people based on team issues: Periodically, issues reported in the ticket system or similar platforms could be a good source of ideas to train the squad and sharpen the axe.
  • Solve the Advent of Code 2025 with a scheduled pipeline every day.

https://adventofcode.com/2025

Creativity and your monthly budget mark the limit.

LIMITATIONSThis technology is awesome, but you should consider the following factors:

  • Cognitive load: When adding these new virtual hands to your squad, you need to ensure that everyone is able to review the new PRs with quality. Apart from using this technology for delivery, review the new opportunities to strengthen the team based on the issues identified as input.
  • Budget: This technology is not free. It is another input for your engineering manager in terms of cost and resources to maintain the prompts and pipelines.
  • Resources: You could create several pipelines, but you need to calibrate with the current cadence of PR reviews to avoid saturating the process.

ExampleS in actionusing cursor agent cli in action: Orchestrating Cursor Cloud Agent from a PipelineReview the following step to understand how to run a pipeline with Cursor Agent CLI using user prompts based on PML.

- name: Run Cursor Agent env: CURSOR\_API\_KEY: ${{ secrets.CURSOR\_API\_KEY }} run: | echo "=== User Prompt:==="; jbang trust add https://github.com/jabrena/ PROMPT=$(jbang pml-to-md.0.4.0-SNAPSHOT@jabrena convert pml-hello-world-java.xml) echo "$PROMPT"; echo "=== Cursor Agent Execution:==="; echo ""; cursor-agent -p "$PROMPT" --model auto In the previous example, the Cursor agent processes a user prompt in Markdown which was originally created in XML (using a PML schema).

Orchestrating Cursor Cloud Agent from a PipelineA picture is worth a thousand words. You can see a service that monitors Cursor Cloud Agent runtime at the following address: https://jabrena.github.io/cursor-cloud-agent-rest-api-status/

Cursor Cloud Agent REST API Status

Every hour, the service tests the execution to verify different aspects of the solution. After a month of running the service, I can assert that latencies are stable, and this fact is important when designing AI solutions that don’t require near real-time feedback. Further information about the pipeline here: https://github.com/jabrena/cursor-cloud-agent-rest-api-status/blob/main/.github/workflows/scheduled-ping-agent.yaml

Note: The service has been running for more than 1 month (30 × 24 × 4 samples stored). Under the hood, the pipeline uses Churrera CLI, an Open source Java CLI tool designed to orchestrate Cursor Cloud Agents and measure latencies.

Takeaways* Cursor Agent CLI is a nice way to start using frontier models in pipelines. * Cursor Cloud Agents is a nice way to use frontier models for parallel and complex scenarios because it offers an easy REST interface with fine-grained control over the process. * Running a pilot in non-critical services could be a good way to understand the possibilities and train people. * PML is a great way to write robust user prompts based on XML, which can be verified and transformed later to other hierarchical formats like Markdown. * Decouple User prompts from Agent systems. It will be considered as another IT asset in the future. * You can combine User prompts plus System prompts like Cursor rules or Claude Skills to enrich the final result. * Both alternatives create pull requests, the team has the last word on accepting the new code added to the main/develop branch. This model is also compatible if you use Extreme Programming. * Review your engineering processes to include a threat modeling task for new operations that include AI. * Models can interact with the environment using CLI tools or MCP tools. * You can assign the operation of these pipelines to junior profiles because these problems were modeled with user prompts, so the risk is very limited. However, there exists a nice opportunity to improve the solutions with ideas like adding functional programming patterns, improving the OOP design, improving performance based on multiple factors, etc.

References* https://cursor.com/docs/cloud-agent * https://cursor.com/docs/cloud-agent/api/endpoints * https://editor.swagger.io/?url=https://cursor.com/docs-static/cloud-agents-openapi.yaml * https://cursor.com/agents * https://github.com/jabrena/pml * https://github.com/jabrena/churrera-cli * https://github.com/jabrena/cursor-rules-java * https://github.com/jabrena/cursor-cloud-agent-rest-api-status * https://contextmapper.org/docs/examples/ * https://modelcontextprotocol.io/docs/getting-started/intro

The post Delegating Java tasks to Supervised AI Dev Pipelines appeared first on JVM Advent.

View Details

If you’re reading this, you’re probably already using some LLM for coding. Maybe it’s Copilot, maybe Claude Code, maybe Cursor with Gemini enabled (or Cursor’s own model).

You know the drill. Do you truly expect the announcement “We are worst than competitors?”

The problem is that when someone asks, “Which model is best for Java?”, most answers are based either on a vibes check and opinions, or on re-published benchmarks that, frankly, hardly anyone truly understands – especially what they actually test and how that translates to everyday engineering work.

Today, we’re changing that. We’ll walk through how LLM benchmarking for code actually works, which models are available on the market at the end of 2025, and – most importantly – how they perform in benchmarks that are specific to the JVM, including Java, Kotlin, and Scala.

How Do You Even Measure an LLM’s Ability to Write Code?The Evolution: from “Can You Code?” to “Can You Be an Engineer?”The history of LLM coding benchmarks is a story of steadily rising expectations. It all started with a very basic question: can the model generate working code at all?

HumanEval described in the paper Evaluating Large Language Models Trained on Code is the foundation of almost everything that came afterward. It consists of 164 hand-written Python problems, each with a set of unit tests. The model is given a function signature and a docstring and must generate the function body. Sounds simple? In 2021, even GPT-3 struggled with it.

The key innovation of HumanEval was the pass@k metric. Instead of measuring textual similarity (like BLEU score), we check whether the generated code actually passes the tests. This is a fundamental shift in thinking: we don’t care whether the code looks good – we are interested whether it works.

So what’s the problem with HumanEval? It’s too easy. By the end of 2024, top models were achieving 90%+ on this benchmark. When all the leading models cluster around 90%, it becomes very hard to say which one is actually better.

A similar alternative to HumanEval is MBPP (Mostly Basic Python Problems) – 974 entry-level problems. More data, but roughly the same level of difficulty.

The Modern Standard: SWE-benchThe real revolution arrived with SWE-bench (Software Engineering Benchmark). Instead of asking a model to fill in a missing function or solve an isolated coding puzzle, SWE-bench puts it in a situation that looks much closer to real work. The model is given the full source code of a real GitHub repository, along with the description of an issue taken directly from GitHub Issues, and is asked to produce a patch that actually fixes the problem.

At this point, we’re no longer testing whether a model can “code” in the narrow sense. We’re testing whether it can behave like a software engineer. To succeed, the model has to understand an existing codebase, navigate across multiple files, and reason about how different components interact. It needs to interpret the often messy, incomplete, or ambiguous business context hidden in an issue description, follow the project’s established conventions, and produce a change that solves the problem without breaking existing tests. This is software engineering, not algorithm trivia.

SWE-bench Verified raises the bar even further. It is a curated subset of 500 tasks that have been manually verified by humans, and it has effectively become the industry’s gold standard. When vendors talk about their models’ real-world coding capabilities, this is the benchmark they usually point to.

However, SWE-bench also has a fundamental limitation. It is exclusively focused on Python, which immediately makes it less useful for large parts of the industry. On top of that, it has been around long enough that there is a growing suspicion that some models may have been trained on it, at least partially. That doesn’t make the benchmark useless, but it does mean we should treat impressive scores with a healthy dose of skepticism – especially if we care about JVM languages like Java, Kotlin, and Scala.

What’s particularly striking about SWE-bench is the scale of the solutions it expects. The mean lines of code per solution is just 11, with a median of only 4 lines. Amazon’s analysis found that over 77.6% of the solutions touch only one function. This tells us something important: SWE-bench is testing surgical precision on isolated problems, not the kind of sprawling, multi-component changes that often dominate real engineering work. Additionally, over 40% of the problems come from the Django repository alone, which introduces significant bias toward one project’s patterns and conventions.

Additionally, as it has become the most widely recognized, it regularly appears in new model announcements and marketing materials. The results are… interesting

Scale AI has attempted to address these limitations with SWE-bench Pro, a significantly improved successor.

Instead of 500 Python-only problems, it offers 1,865 tasks drawn from 41 repositories across Python, Go, JavaScript, and TypeScript. The solutions are substantially larger – averaging 107 lines of code with a median of 55 lines, typically spanning 4 files. The benchmark also covers a more diverse range of software types: consumer applications with complex UI logic, B2B platforms with intricate business rules, and developer tools with sophisticated APIs. Crucially, humans rewrote the problem descriptions based on issues, commits, and PRs to ensure no missing information, and they added explicit requirements grounded in the unit tests used for validation. All environments are dockerized with dependencies pre-installed, so the benchmark explicitly does not test repository setup – just the engineering work itself.

New Benchmarks: BigCodeBench and LiveCodeBenchBigCodeBench emerged as a response to the growing criticism that existing coding benchmarks had simply become too easy. Instead of testing toy problems, it raises the bar by introducing 1,140 tasks that require real interaction with 139 different libraries. On average, each task comes with 5.6 tests and achieves 99% branch coverage. At this level, knowing the syntax is no longer enough. The model needs to understand how to actually use libraries like pandas, numpy, requests, and dozens of others in realistic ways—exactly the kind of knowledge developers rely on in day-to-day work.

LiveCodeBench, on the other hand, tackles a completely different but equally important problem: data contamination. Its tasks are sourced from weekly programming contests on platforms like LeetCode, AtCoder, and Codeforces, and each task is tied to a specific publication date. This allows evaluators to check whether a model could realistically have seen the problem during training. If a model was trained before a given date, it simply couldn’t have memorized that task. In practice, this makes LiveCodeBench one of the most credible attempts so far to measure genuine generalization rather than benchmark recall.

Multilinguality: Where Is Java?And this is where we get to the heart of the problem. A review of 24 major coding benchmarks reveals a rather uncomfortable statistic. An overwhelming 95.8% of existing benchmarks focus on Python, while only five of them include Java at all.

This imbalance isn’t accidental. Python dominates machine learning research, so benchmarks are naturally designed around the language researchers themselves use every day. The result is a benchmarking ecosystem that tells us a lot about how well models perform in Python, but surprisingly little about their real capabilities in languages like Java – or, by extension, the broader JVM world.

MultiPL-E is an effort to address this imbalance by translating the HumanEval and MBPP benchmarks into 18 additional programming languages, including Java, Kotlin, and Scala.

On paper, this sounds like exactly what the ecosystem needs. In practice, however, there’s a catch. Automatic translation doesn’t always capture the idioms of a given language. A Java test mechanically translated from Python may compile and run, but it often fails to exercise what actually matters in an object-oriented context. Instead of testing real JVM-style design, it may still be implicitly testing Pythonic assumptions.

HumanEval-XL takes a slightly different approach by expanding the benchmark to cover 12 programming languages, including Python, Java, Go, Kotlin, PHP, Ruby, Scala, JavaScript, C#, Perl, Swift, and TypeScript. It also introduces 80 problems written in 23 natural languages, which makes it more diverse than earlier efforts. That said, while it is certainly better than nothing, it still falls short when it comes to evaluating realistic enterprise Java scenarios.

The problems remain small and isolated, far removed from the kinds of codebases and architectural concerns that dominate real-world JVM development.

Aider Polyglot deserves special attention for JVM developers because it actually includes Java. The benchmark consists of 225 hard-level Exercism problems distributed across six languages: JavaScript (49), Java (47), Go (39), Python (34), Rust (30), and C++ (26). Solutions typically range from 30 to 200 lines of code and span at most 2 files. The evaluation allows one round of feedback before final assessment – mimicking a realistic back-and-forth with a coding assistant. While this is far from enterprise-scale Java work, it remains one of the few benchmarks that can tell us anything concrete about model performance on JVM languages in a standardized way.

JavaBench: The First Benchmark Dedicated to OOPJavaBench is a direct response to Python’s dominance in coding benchmarks. Instead of abstract problems or toy functions, it is built around four real Java projects, covering 389 methods across 106 classes, with an impressive 92% test coverage. The benchmark was additionally validated by 282 students, who achieved an average score of 90.93 out of 100, giving us a meaningful human baseline.

What truly sets JavaBench apart is its focus on object-oriented programming features. It explicitly evaluates concepts such as encapsulation, inheritance, and polymorphism – areas that benchmarks like HumanEval do not even attempt to measure. This makes it far more representative of how Java is actually used in practice.

JavaBench is nice try, but not the most active supported thing. We have better alternatives at the market.

CoderUJB: Benchmarking Real-World Java WorkCoderUJB pushes realism even further. It is built on 17 real open-source Java projects and contains 2,239 programming questions spanning multiple task types. These include not only code generation, but also test generation, bug fixing, and defect detection. The point here is no longer just to check whether a model can produce syntactically correct code, but whether it can perform the kinds of activities a real Java developer deals with every day.

Brokk Power Ranking: A Java Benchmark for 2025The Brokk Power Ranking, created by Jonathan Ellis, co-creator of Apache Cassandra, is one of the freshest additions to the benchmarking landscape and directly addresses some of the core weaknesses of SWE-bench. Unlike most existing benchmarks, it is not Python-only. Instead, it draws tasks from real Java repositories such as Brokk, JGit, LangChain4j, Apache Cassandra, and Apache Lucene.

Equally important, it is genuinely fresh. The tasks are derived from commits made within the last six months, which significantly reduces the risk that models were trained on the benchmark data. It is also intentionally challenging, featuring 93 tasks with contexts reaching up to 108k tokens. Ellis positions it deliberately between AiderBench, which tends toward toy problems, and SWE-bench, which often throws entire repositories at the model and says “good luck.”

Results as of November 2025 paint a much clearer picture of how models actually perform in Java-centric, real-world scenarios. At the very top, the S tier is occupied by GPT-5.1 and Claude Opus 4.5, which clearly separate themselves from the rest of the field. Just below them, the A tier includes GPT-5, GPT-5 Mini, and Grok Code Fast 1, forming a strong upper-middle group with solid but slightly less consistent performance. The B tier is represented by Claude Sonnet 4.5 and GLM 4.6, while the C tier includes Grok 4.1 Fast, Gemini 2.5 Flash, Gemini 3 Pro (Preview), and DeepSeek-V3.2. At the bottom, in the D tier, we find Kimi K2 Thinking and MiniMax M2.

Several patterns stand out immediately. Chinese models such as DeepSeek-V3, Kimi K2, and Qwen3 Coder perform noticeably worse here than they do on benchmarks like SWE-bench or AiderBench. This suggests that their apparent strength on more generic or Python-heavy evaluations does not translate well to Java-heavy, object-oriented codebases.

Another clear takeaway is GPT-5’s dominance across every price tier. No matter whether you look at premium or more cost-conscious options, GPT-5-based models consistently lead in terms of raw capability. The trade-off, however, is speed: GPT-5 remains relatively slow compared to its competitors.

Finally, Claude Sonnet 4 stands out for a very different reason. While it does not top the absolute performance charts, it is screaming fast – faster than all models in tiers A and B. For workflows where latency matters as much as correctness, this makes it a surprisingly compelling choice despite not sitting at the very top of the ranking.

Kotlin is in interesting position in one important respect: specialization. Mellum, used inside JetBrains AI Assistant, is currently the only model with dedicated fine-tuning specifically for Kotlin, which gives it a noticeable edge in understanding Kotlin idioms and conventions. Beyond that, Claude models tend to handle Kotlin surprisingly well, especially when it comes to expressive syntax and functional-style constructs.

Scala, unfortunately, remains the most challenging case. No mainstream model has dedicated fine-tuning for Scala, which puts the language at a structural disadvantage. That said, the best reported results so far come from Claude Opus 4.5, which benefits from a strong grasp of functional programming concepts, and GPT-5, which shows solid performance on Scala tasks in benchmarks such as MultiPL-E. These models can be effective, but they still require more guidance and validation than their Java or Kotlin counterparts.

Practical TakeawaysBefore diving into model recommendations, it is worth stepping back to consider what benchmarks actually measure – and what they do not. When we say an agent scores 25% on SWE-bench Pro, we are saying: in a problem set of well-defined issues with explicit requirements and specified interfaces, 25% of the agent’s solutions pass the relevant unit tests. This is useful for tracking progress, but it is not software engineering as most practitioners understand it. The high-leverage parts of real SWE work – collaborating with stakeholders to develop specifications, translating ambiguous requirements into clean interfaces, writing secure and maintainable code – remain entirely unmeasured. We know the code passes tests; we have no idea if it is maintainable, secure, or well-crafted. The UTBoost paper goes further, demonstrating that many SWE-bench solutions pass unit tests without actually resolving the underlying issues. Keep this gap in mind when interpreting any benchmark results.

When choosing a model, the most important rule is not to trust any single benchmark blindly. SWE-bench, while influential, is Python-only, and HumanEval is simply too trivial to say much about real-world engineering in 2025. Benchmarks that span multiple languages- such as Aider Polyglot or the Brokk Power Ranking – are far more informative for JVM developers. Even then, no benchmark can replace testing a model directly on your own codebase, with your own architectural constraints and conventions.

Looking at broader trends for 2025, Java is finally starting to receive more focused attention. The emergence of JavaBench, CoderUJB, and the Brokk Power Ranking is a clear signal that the ecosystem is moving beyond Python-centric evaluation. At the same time, specialized models are becoming more prominent, with Mellum for Kotlin and tools like Codestral for code completion pointing toward a future of narrower but deeper optimization. Another clear pattern is that reasoning increasingly matters: models with explicit “thinking” or extended reasoning modes tend to perform better on complex, multi-step tasks. Context size also plays a critical role, with models like Gemini 3 Pro – offering up to one million tokens – making it feasible to work with entire codebases in a single session.

There are also some pitfalls worth actively avoiding. “Benchmark gaming” is becoming more visible, particularly when models show suspiciously strong results on a single benchmark like SWE-bench but fail to replicate that performance elsewhere. Relying on outdated benchmarks is equally misleading—HumanEval from 2021 tells us very little about model capabilities in 2025. Finally, one-off tests are unreliable by nature, since LLM performance is probabilistic. Meaningful evaluation requires repeated runs and consistent patterns, not a single lucky output.

So, the state of LLM benchmarking for JVM languages in 2025 is… complicated. Python still dominates research and evaluation, but genuinely JVM-focused benchmarks are finally emerging, especially for Java, with Brokk Power Ranking leading the way. Kotlin benefits from an steward in JetBrains and experiments like in Mellum, while Scala is still waiting for truly dedicated tooling.

For everyday development work, Claude Sonnet 4.5 and GPT-5 are both safe, well-rounded options that handle the vast majority of typical tasks reliably. When the work shifts toward long, complex debugging sessions – where reasoning across many files and iterations really matters – Claude Opus 4.5 clearly pulls ahead. On the other end of the spectrum, if speed and cost efficiency are the primary concerns, lighter models such as Gemini 2.5 Flash or GPT-5 Mini offer a reasonable trade-off between performance and latency.

Note for the end: The difficulty of designing good benchmarks… actually makes me optimistic about coding agents. Current state-of-the-art benchmarks fall woefully short of capturing the nuance and messiness of real engineering work – yet the agents we have are already remarkably capable. There is substantial low-hanging fruit in benchmark design: validating with property-based testing instead of unit tests, using formal methods where possible, starting from product-level documents like PRDs and technical specifications, and creating benchmarks that test information acquisition and clarification skills rather than assuming perfect problem statements.

As these improvements arrive, we should expect corresponding improvements in agent capabilities through better training signals.

Please remember, the current solutions are the worst we will ever get

The post How to really measure LLMs for JVM Code? A Benchmarking guide for late 2025 appeared first on JVM Advent.

View Details

Migrating an existing application to a new version of Java or a framework such as Spring Boot involves much more than simply updating a version number in a file. Each new release of a library or language brings new features, deprecations, behavioral changes, and sometimes complete API redesigns. When legacy code is involved, these upgrades may require revisiting old implementations and correcting patterns that no longer work. Even the slightest change can trigger a cascading effect, modifying other parts of the application in unexpected ways.

These migration challenges require significant effort even for a single application. The difficulty grows exponentially when a company needs to upgrade dozens of systems at once. And once the migration is complete, a new question arises: how can we keep applications consistently up to date without repeating the same painful process?

CONTEXT OF THE SITUATIONImagine a team responsible for several microservices that use different versions of Java or Spring Boot. This happens because not all microservices change frequently; at some point, a problem occurs with a library they all use, and when someone tries to update the version, another problem arises, such as the new version not supporting all versions of Java.

To see this situation graphically, check the following table, which represents a possible scenario that could occur in any company:

| API | Compilation | Code Style | Framework | | api-a | 8 | 8 | Spring Boot 1.5.7 | | api-b | 11 | 8 | Spring Boot 2.1.4 | | api-c | 17 | 17 | Spring Boot 3.0.0 | | api-d | 14 | 11 | Spring Boot 2.3.3 |

With this problem in mind, the only alternative is to create a custom solution for each application that does not use the minimum version of Java that supports this library, but this could have many implications, such as performance issues and more time to fix the problem across all the microservices.

The Problems with Legacy CodeLegacy code does not always need to be updated. For example, a desktop application that operates in isolation and does not interact with external services may continue to function without requiring significant changes. In these scenarios, updates are optional.

However, in many other cases, applications must be updated for several reasons, mainly when they rely on outdated Java versions or obsolete dependencies. Running outdated software introduces risks and limitations that can directly affect stability, security, and maintainability.

Some of the risks associated with outdated code are:

  • Security vulnerabilities: Older versions often contain unpatched exploits.
  • Compatibility issues: Modern libraries, tools, and operating systems may no longer support obsolete versions.
  • Performance inefficiencies: Newer versions frequently include optimizations that older releases lack.

HOW TO SOLVE THESE PROBLEMS?The problem could be split into two situations: one is an application that contains legacy code and needs to be updated, and the other is an application that uses a relatively recent version of a language or framework but has some dependencies on older versions.

There is a set of tools or libraries to solve these problems, the most popular are:

  • Renovate: Automates dependency updates and creates pull requests across many languages.
  • OpenRewrite: Automatically refactors source code to migrate between Java versions or frameworks.
  • Maven Versions Plugin: Detects and updates dependency versions in Maven projects without code changes.
  • Dependabot: GitHub’s built-in tool that creates pull requests for outdated dependencies.

Let’s see in the following table a brief comparison of all of them:

| Main differences | Reno vate | Open Rewrite | Maven plugin | Dependa Bot | | It has good documentation. | | | | | | Support multiple languages. | | | | | | Refactoring the source code to a specific version. | | | | | | Automates dependency updates. | | | | | | It’s possible to generate pull/merge requests. | | | | | | It has a large user community. | | | | |

In this comparison, the best option for addressing migration problems is OpenRewrite. If you need to keep dependencies up to date, the winners could be Dependabot or Renovate. However, given the cost of these tools, the winner is the Maven Plugin.

This article uses a source from a GitHub repository; feel free to clone it and use it to learn about how to keep the application updated.

Solution #1: Leveraging OpenRewriteOpenRewrite provides a comprehensive solution to code migration challenges with automated, reliable refactoring tools that streamline the process. The platform effectively highlights necessary code changes, implements consistent transformations, and significantly minimizes the manual effort required for application updates. By automating repetitive tasks and guiding developers through essential modifications, OpenRewrite improves the manageability of large-scale upgrades while reducing the risk of errors.

The migration process with OpenRewrite follows a precise, structured flow, offering a series of recipes that include the steps and resources needed at each stage. This tool is not limited to migrating Spring Boot or Java; it offers a comprehensive catalog of recipes for different languages and frameworks.

Let’s see how the process of migrating an application to Java 21 and Spring Boot 3.4 is. To do that, add the OpenRewrite plugin or dependency to your build system, along with the recipes to be used, as shown in the following code block.

```

 <groupId>org.openrewrite.maven</groupId>

 <artifactId>rewrite-maven-plugin</artifactId>

 <version>6.24.0</version>

 <configuration>

     <activeRecipes>

         <recipe>org.openrewrite.java.OrderImports</recipe>

   <recipe>org.openrewrite.java.migrate.UpgradeToJava21</recipe>

org.openrewrite.java.spring.boot3.SpringBootProperties_3_4

     </activeRecipes>

 </configuration>

 <dependencies>

     <dependency>

         <groupId>org.openrewrite.recipe</groupId>

         <artifactId>rewrite-spring</artifactId>

         <version>6.19.0</version>

     </dependency>

     <dependency>

         <groupId>org.openrewrite.recipe</groupId>

         <artifactId>rewrite-migrate-java</artifactId>

         <version>3.9.0</version>

     </dependency>

</dependencies>

``` As a recommendation, check the latest version of this plugin on the official webpage or a repository like this regularly.

The plugin only contains the core logic to execute the receipts that are necessary to include as external dependencies, as shown in the previous code block. Also, it’s possible to create custom receipts that are not part of the official library.

The next step after the modifications on the project is to execute the changes using the following command:

``` $ mvn rewrite:run

[INFO] Using active recipe(s) [org.openrewrite.java.OrderImports, org.openrewrite.java.migrate.UpgradeToJava21, org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_4]
[INFO] Using active styles(s) []
[INFO] Validating active recipes...
[INFO] Project [api-reservations] Resolving Poms...
[INFO] Project [api-reservations] Parsing source files
[WARNING] locking FileBasedConfig[/home/asacco/.config/jgit/config] failed after 5 retries
[INFO] Running recipe(s)...
[WARNING] Changes have been made to api-reservations/pom.xml by:
[WARNING] org.openrewrite.java.migrate.UpgradeToJava21
[WARNING] org.openrewrite.java.migrate.UpgradeBuildToJava21
[WARNING] org.openrewrite.java.migrate.UpgradeJavaVersion: {version=21}
[WARNING] org.openrewrite.maven.UpdateMavenProjectPropertyJavaVersion: {version=21}
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_4
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springframework.boot, artifactId=, newVersion=3.1.x, overrideManagedVersion=false}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springdoc, artifactId=
, newVersion=2.2.x}
[WARNING] org.openrewrite.java.testing.mockito.Mockito4to5Only
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.mockito, artifactId=, newVersion=5.x}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springframework.boot, artifactId=
, newVersion=3.2.x, overrideManagedVersion=false}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springdoc, artifactId=, newVersion=2.5.x}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springframework.boot, artifactId=
, newVersion=3.3.x, overrideManagedVersion=false}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springdoc, artifactId=, newVersion=2.6.x}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springframework.boot, artifactId=
, newVersion=3.4.x, overrideManagedVersion=false}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springdoc, artifactId=*, newVersion=2.8.x}
[WARNING] Changes have been made to api-reservations/src/main/java/com/twa/reservations/connector/CatalogConnector.java by:
..... ``` If everything works as expected and the migration was successful, the changes to the POM file that add the openRewrite plugin will be removed.

Suppose it’s necessary to understand and see all the changes that could affect the application before doing so. In that case, another command shows that information by simulating execution and displaying the results.

$ mvn rewrite:dryRun .... [INFO] Using active recipe(s) [org.openrewrite.java.OrderImports, org.openrewrite.java.migrate.UpgradeToJava21, org.openrewrite.java.spring.boot3.SpringBootProperties\_3\_4] [INFO] Using active styles(s) [] [INFO] Validating active recipes... [INFO] Project [api-reservations] Resolving Poms... [INFO] Project [api-reservations] Parsing source files .... [WARNING] These recipes would make changes to api-reservations/src/main/java/com/twa/reservations/controller/ReservationController.java: [WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot\_3\_4 [WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot\_3\_3 [WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot\_3\_2 [WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot\_3\_1 [WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot\_3\_0 [WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot\_2\_7 [WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot\_2\_6 [WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot\_2\_5 [WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot\_2\_4 [WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot\_2\_3 [WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot\_2\_2 [WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot\_2\_1 [WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot\_2\_0 [WARNING] org.openrewrite.java.spring.boot2.SpringBoot2BestPractices [WARNING] org.openrewrite.java.spring.NoAutowiredOnConstructor [WARNING] Patch file available: [WARNING] /home/asacco/Code/Talks/out-with-the-old/api-reservations/target/rewrite/rewrite.patch [WARNING] Estimate time saved: 20m [WARNING] Run 'mvn rewrite:run' to apply the recipes. [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 20.671 s [INFO] Finished at: 2025-12-01T15:22:34-03:00 [INFO] ------------------------------------------------------------------------ On the file rewrite.patch that was created in the target folder, all the changes that will be executed in the application will appear.

Solution #2: Dependency – Updates VersionsThe versions-maven-plugin is a powerful Maven tool designed to help developers keep project dependencies, plugins, and parent versions up to date. With simple commands, it can identify outdated components, suggest the latest compatible versions, and even update itself pom.xml automatically. This reduces the manual effort required to track version changes and helps ensure applications remain secure, stable, and aligned with the latest improvements in their ecosystems.

The first step to use this plugin is to include it on the pom file like appears on the following block:

<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>versions-maven-plugin</artifactId> <version>2.18.0</version> </plugin> As a recommendation, check the latest version of this plugin on the official webpage or a repository like this regularly.

The next and last step is to run a command that checks all dependencies and the project and suggests which are outdated. The command and the output looks like the following:

$ mvn versions:display-dependency-updates ... [INFO] --- versions:2.18.0:display-dependency-updates (default-cli) @ api-reservations --- [INFO] The following dependencies in Dependency Management have newer versions: [INFO] biz.aQute.bnd:biz.aQute.bnd.annotation ................ 7.0.0 -> 7.1.0 [INFO] co.elastic.clients:elasticsearch-java ................ 8.15.5 -> 9.2.1 [INFO] com.couchbase.client:java-client ..................... 3.7.9 -> 3.10.0 [INFO] com.datastax.oss:native-protocol ...................... 1.5.1 -> 1.5.2 [INFO] com.fasterxml.jackson.core:jackson-annotations ..... 2.18.5 -> 3.0-rc5 [INFO] com.fasterxml.jackson.core:jackson-core ............. 2.18.5 -> 2.20.1 [INFO] com.fasterxml.jackson.core:jackson-databind ......... 2.18.5 -> 2.20.1 [INFO] com.fasterxml.jackson.dataformat:jackson-dataformat-avro ... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.970 s [INFO] Finished at: 2025-12-01T16:43:32-03:00 [INFO] ------------------------------------------------------------------------ Consider that with this command, all the dependencies related to Spring Boot will appear; for example, they could be updated by simply incrementing the framework’s version. This scenario is a good candidate to use another approach which only show the version of the dependencies that are declared on the pom file. The command is quite similar to the previous one, but the output is entirely different, as shown in the following block.

$ mvn versions:display-property-updates ... [INFO] The following version properties are referencing the newest available version: [INFO] ${maven-failsafe-plugin.version} .............................. 3.5.4 [INFO] ${mockito.version} ........................................... 5.20.0 [INFO] The following version property updates are available: [INFO] ${datafaker.version} ................................. 2.3.0 -> 2.5.3 [INFO] ${formatter-maven-plugin.version} .................. 2.23.0 -> 2.29.0 [INFO] ${instancio-junit.version} ........................... 5.2.1 -> 5.5.1 [INFO] ${junit-platform-launcher.version} ................ 1.8.2 -> 6.1.0-M1 [INFO] ${junit.version} ................................. 5.10.1 -> 6.1.0-M1 [INFO] ${mapstruct.version} ........................... 1.5.5.Final -> 1.6.3 [INFO] ${maven-compiler-plugin.version} ............. 3.14.1 -> 4.0.0-beta-3 [INFO] ${maven-enforcer-plugin.version} ..................... 3.4.1 -> 3.6.2 [INFO] ${maven-surefire-plugin.version} ..................... 3.1.2 -> 3.5.4 [INFO] ${spring-boot-starter.version} ...................... 3.4.12 -> 4.0.0 [INFO] ${springdoc-openapi-starter-webmvc-ui.version} ...... 2.8.14 -> 3.0.0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.970 s [INFO] Finished at: 2025-12-01T16:43:32-03:00 [INFO] ------------------------------------------------------------------------ This plugin offers a series of other commands to check specific parts of the POM file, such as plugins, and many others, so it’s recommended to use the appropriate one depending on the context.

WHAT’S NEXT?There are many resources for the evolution of a platform or application. The following is just a short list of resources:

  • Modernizing Enterprise Java: A Concise Cloud Native Guide for Developers by Markus Eisele
  • Building Evolutionary Architectures: Automated Software Governance by Neal Ford

Other resources could be great for understanding some concepts related to the evolution of a platform in depth:

  • Software Architecture: The Hard Parts: Modern Trade-Off Analyses for Distributed Architectures by Neal Ford
  • Refactoring: Improving the Design of Existing Code (2nd Edition) by Martin Fowler
  • Software Developer To Software Architect by Mark Richards

Consider this just a small list of available resources. If something is unclear, find another video or resource.

CONCLUSIONThere is no silver bullet for keeping an application permanently up to date, but combining the proper practices with the right tools can dramatically simplify the process. Tools such as OpenRewrite or the Maven Versions Plugin automate much of the Java and Spring Boot migration process. Still, they cannot fix everything, especially when a library does not support newer Java versions. For example, Orika, a widely used mapping library, does not support Java 17 or later, so developers must manually migrate to an alternative before they can benefit from automation.

Because of these limitations, maintaining a clean codebase, adopting a strong testing culture, and updating dependencies regularly are essential. By combining these practices with the tools discussed in this article, development teams can reduce migration risks and ensure that future upgrades become far less painful.

The post Out with the Old, In with the New: A Guide to Application Upkeep appeared first on JVM Advent.

View Details

A Hands-On Guide to Text SummarizationOver the past year, the Java ecosystem has made significant strides in making Generative AI development enterprise-ready.

For Spring developers, SpringAI has emerged as the go-to toolkit for seamlessly integrating enterprise data and APIs with AI models.

Are you curious in developing enterprise grade AI applications with Spring AI? Then read on.

Setting up SpringAILoading documents and evaluating them with Generative AI is a fundamental use case that you will encounter when working with Large Language Models (LLMs) in the industry.

Therefore, to get started with SpringAI, we are using the practical example of summarizing Wikipedia articles with LLMs. We are going to create a springai-wikipedia-demo project that you can find on GitHub.

The project has been build with Spring Boot v3.5.8 running on Java 21. As key ingredients we are using the Anthropic API integration that uses the Claude models. To extract text from PDF documents we are using the Apache Tika document reader.

The key dependencies are:

implementation("org.springframework.boot:spring-boot-starter-webflux")implementation("org.springframework.ai:spring-ai-starter-model-anthropic")implementation("org.springframework.ai:spring-ai-tika-document-reader") Note that we have added a dependency on spring-webflux. For SpringAI to work, we need the Netty Client on the classpath to carry out synchronous and reactive HTTP requests (using its RestClient and WebClient). So even if you only run SpringAI from the CLI, you also need to include a dependency to spring-webflux.

As said, we use Claude from Anthropic as LLM for demo purpose, but you can of course use any of your choice.

Note that you need to provide an Anthropic API key for the Claude Console as an environment variable to authenticate your requests to the model. See the application.properties file:

spring.ai.anthropic.api-key=${ANTHROPIC\_API\_KEY:missing} The Document reader infrastructureOne of the key abstractions when processing media content in SpringAI is the org.springframework.ai.document.Document interface. A Document contains the plain content and metadata about the document. The content can be textual, or optionally audio or video. The most important interface methods are:

String getText();Media getMedia();Map<String,Object> getMetadata(); The Document abstraction is closely tied to the context of Extract, Transform, Load (ETL) processes for Retrieval Augmented Generation (RAG). ETL is a three-step data integration process to collect data from various sources, clean and reshape it into a usable format, here a Document.

In simple terms, RAG is needed to feed your own private data to the LLM in order to take it into account when answering prompts. In the enterprise, this is of very high value. For privacy purposes, proprietary and open source LLMs won’t be trained on your specific company data.

In our example case, we are going to load five random articles of Wikipedia that have been saved as PDF. The articles have been placed in the resources folder:

├── application.properties└── articles ├── 2023\_Asia\_Contents\_Awards\_&\_Global\_OTT\_Awards.pdf ├── Anthony\_Wonke.pdf ├── Chang\_Tzi-chin.pdf ├── Indera\_SC.pdf └── Neant-sur-Yvel.pdf In the real world, you can imagine this content being Confluence docs, JIRA tickets, internal reports, literature and other kinds of publications that you want to provide to your LLM. You might want to let coworkers ask questions about your internal documentation. Or you want provide customers with answers about your products taking into account your own knowledge base.

To use the Tika document reader, we introduce a simple DocumentReader component; I am going to skip the import declarations in my examples:

package com.slissner.springai.infrastructure.document;@Componentpublic class DocumentReader { public List<Document> loadText(final Resource resource) { final TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(resource); return tikaDocumentReader.read(); }} As you can see, SpringAI has nice abstractions. You just throw in some Resource reference, be it TXT, HTML, PDF, XLSX and so on, and the Tika Reader will answer with the appropriate Document.

Next, we are defining a repository reading the articles:

@Repositorypublic class ArticleRepository { private final DocumentReader documentReader; public ArticleRepository(final DocumentReader documentReader) { this.documentReader = documentReader; } private static final List<String> DOCUMENT\_PATHS = Stream.of( "2023\_Asia\_Contents\_Awards\_&\_Global\_OTT\_Awards.pdf", "Anthony\_Wonke.pdf", "Chang\_Tzi-chin.pdf", "Indera\_SC.pdf", "Neant-sur-Yvel.pdf") .map(path -> "/articles/" + path) .toList(); public List<Document> getAll() { return DOCUMENT\_PATHS.stream() .map(ClassPathResource::new) .map(documentReader::loadText) .flatMap(Collection::stream) .toList(); }} We simply load all articles with the List<Document> getAll() method, by first loading the classpath Resource and then sending it to the Tika reader.

Processing Documents with the ChatClientAs we can now load List<Document> from the PDFs, we can carry out a first prompt to let the LLM summarize the content of the first article in the list 2023_Asia_Contents_Awards_&_Global_OTT_Awards.pdf.

For this purpose, we have introduced the ArticleService application service. Note that we have not introduced yet a separate infrastructure class for the ChatClient. Our use case is so simple that we did not want to introduce a separate class for it.

package com.slissner.springai.application;@Servicepublic class ArticleService { private final ChatClient chatClient; private final ArticleRepository articleRepository; public ArticleService( final ArticleRepository articleRepository, final ChatClient.Builder chatClientBuilder) { this.articleRepository = articleRepository; this.chatClient = chatClientBuilder.build(); } public String summarizeArticles() { final List<Document> articles = articleRepository.getAll(); // Use the first article as an example final Document articleContent = articles.getFirst(); return chatClient .prompt() .user("Provide a summary of the following article:\n\n" + articleContent) .call() .content(); }} We can now run the String summarizeArticles() application service method on the command line, with the following CommandLineRunner:

@Bean public CommandLineRunner commandLineRunner(final ArticleService articleService) { return args -> { log.info("Okay, I am going to summarize articles..."); final String summary = articleService.summarizeArticles(); log.info("The summary is:"); log.info(summary); log.info("Done!"); }; } Great, that worked! Here is the answer from the LLM:

2025-11-28T16:26:02.457+01:00 INFO 4936 --- [springAI] [ main] c.slissner.springai.SpringAiApplication : The summary is:2025-11-28T16:26:02.457+01:00 INFO 4936 --- [springAI] [ main] c.slissner.springai.SpringAiApplication : # 2023 Asia Contents Awards & Global OTT Awards SummaryThe 2023 Asia Contents Awards & Global OTT Awards was held on October 8, 2023, at the BIFF Theater in Busan Cinema Center, South Korea. This event represents a rebranding and expansion of the previous Asia Contents Awards, now including global OTT (Over-The-Top) content and services.[...] The ChatClient offers a fluent API when communicating with the AI models.

chatClient .prompt() .user("Provide a summary of the following article:\n\n" + articleContent) .call() .content(); You declare to send a .prompt() to the model and its .user() input.

Note that we are passing the prompt here as a String text. You can also pass a Resource text handle. However, if you need full control over the user prompt, you can pass a well-defined Prompt to the ChatClientRequestSpec prompt(Prompt prompt) method. A Prompt gives you full control over the ChatOption such as the concrete model, max tokens or the temperature of the model.

Ultimately, the chat model can be synchronously called with the .call() method. There exists also a .stream() method that offers a reactive Flux<String> via calling the .content() method.

Memorizing Chat History with AdvisorsSo far, we have only sent a single prompt. What if we want to send a sequence of prompts, memorize the model answers and then run a final prompt on the memory?

For this purpose, SpringAI introduced the Advisors API. Think of the Advisors API as a plugin system for your AI calls.

Each advisor can intercept a request, add context, or modify the prompt before it reaches the model. Advisors are small middleware that automatically add missing context, such as previous messages or app-specific data, so the AI model always has what it needs.

The most common use cases are to add your own data to the conversation (see RAG); or to to add conversational history to the otherwise stateless chat model API.

Note that the SpringAI team warns that the order in which advisors are added to the advisor chain is crucial, like for other middleware. An advisor that has been added before another advisor to the advisor chain is executed first.

Interestingly, if you are having a look into the DefaultChatClient implementation, you can see that calling or streaming the chat model is realized bz just two Advisors that have been added at the end of the chain:

private BaseAdvisorChain buildAdvisorChain() { // At the stack bottom add the model call advisors. // They play the role of the last advisors in the advisor chain. this.advisors.add(ChatModelCallAdvisor.builder().chatModel(this.chatModel).build()); this.advisors.add(ChatModelStreamAdvisor.builder().chatModel(this.chatModel).build()); return DefaultAroundAdvisorChain.builder(this.observationRegistry) .observationConvention(this.advisorObservationConvention) .pushAll(this.advisors) .build();} Now, let’s enhance our current ArticleService implementation with the standard MessageChatMemoryAdvisor. First we need to inject a ChatMemory into our ArticleService:

@Servicepublic class ArticleService { private static final Logger log = LoggerFactory.getLogger(ArticleService.class); private final ArticleRepository articleRepository; private final ChatClient chatClient; private final ChatMemory chatMemory; public ArticleService( final ArticleRepository articleRepository, final ChatClient.Builder chatClientBuilder, final ChatMemory chatMemory) { this.articleRepository = articleRepository; this.chatClient = chatClientBuilder.build(); this.chatMemory = chatMemory; } As we have not declared any other bean, SpringAI will bind it with the default InMemoryChatMemoryRepository. There exists other ChatMemoryRepository implementations. For example, you can store your ChatMemory to a relational database with the help of a JdbcChatMemoryRepository.

This ChatMemory instance we are going to pass to our MessageChatMemoryAdvisor middleware:

public String summarizeArticles() { final MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory).build(); final List<Document> articles = articleRepository.getAll(); articles.stream() .filter(article -> StringUtils.isNotBlank(article.getText())) // Max length of 8000 characters to avoid API limits .map(abbreviateArticleContent()) .forEach( articleContent -> { try { log.info("Calling AI API with article. [id={}]", articleContent.id()); chatClient .prompt() .advisors(chatMemoryAdvisor) .user("Provide a summary of the following article:\n\n" + articleContent.text()) .call() // We need to call .content() in order to actually retrieve the answer and store // it // in the chat memory. .content(); log.info( "Successfully summarized article content with AI model. Sleeping now... [id={}]", articleContent.id()); // Sleep for 60 seconds after each API call to avoid rate limiting Thread.sleep(60000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Thread was interrupted during sleep", e); } }); It is important that you call the .content() method and not only the .call() method.

Neither the .call() nor the .stream() method do actually trigger the AI model execution. Instead, they only instruct Spring AI whether to use synchronous or streaming calls. The actual terminal operations are .content(), .chatResponse(), and .responseEntity(). In our example, only by calling .content() method thus, we are actually storing the answers to the MessageChatMemoryAdvisor.

Furthermore, note the ugly Thread.sleep() call. The Anthropic API may return with a HTTP 429 error because of a 20,000 input tokens per minute rate limit. We circumvent this limit here with the help of the sleep. However, this gives you a first glimpse of the difficulties to scale AI model usage in a high data volume context, such as for enterprise applications.

With a final prompt, we will ask the AI to work with the previous answers. Let’s assume we build an editorial agent for a travel magazine. We want it to evaluate whether we can recommend arbitrary news articles as a travel destination.

return chatClient .prompt() .advisors(chatMemoryAdvisor) .user( "Imagine you are a journalist in a news outlet. You work for a travel magazine that is based in Europe, " + "and thus its readers are European travelers. A colleague of yours has summarized five articles for " + "you. Now it is your turn to pick a subject out of these five articles and write a short travel " + "recommendation. The idea is that you write a single paragraph that praises a destination or activity " + "that you want to recommend to your readers." + "\n\n" + "Given the summaries provided, what is the most interesting topic?") .call() .content(); The idea here is that the LLM correctly picks the article about Néant-sur-Yvel, a village of a thousand inhabitants in Brittany, France.

Let’s run it through the model…

@Bean public CommandLineRunner commandLineRunner(final ArticleService articleService) { return args -> { log.info("Okay, I am going to summarize articles and provide travel recommendations..."); final String recommendation = articleService.summarizeArticles(); log.info("The travel recommendation is:"); log.info(recommendation); log.info("Done!"); }; } And here comes its answer:

Tucked away in the enchanting landscape of Brittany, the commune of Néant-sur-Yvel offers discerning travelers a perfect escape from the well-trodden tourist paths of France. This picturesque village, nestled along the banks of the Yvel river, embodies authentic rural French charm that has remained largely undiscovered by mass tourism. With its medieval architecture, verdant countryside perfect for cycling and hiking, and proximity to the legendary Brocéliande Forest—steeped in Arthurian legends—Néant-sur-Yvel provides a genuine glimpse into traditional Breton life. The village makes an ideal base for exploring the wider Morbihan department, with its megalithic monuments and stunning coastline just a scenic drive away. Visit in late spring when the countryside bursts with wildflowers, and don't miss sampling local Breton specialties like galettes and cider in the village's unassuming but delightful eateries. For travelers seeking to experience the France that exists beyond the postcard views of Paris, Néant-sur-Yvel delivers an authentic slice of Brittany that will leave you enchanted. With such a beautiful answer, who wouldn’t love to travel to Néant-sur-Yvel now?

Final wordThe example has shown that Spring AI is a strong choice for enterprise AI applications. It offers a modular, modern, and well‑integrated feature set. However, scaling AI requires careful planning: token limits, API rate limits, and operational costs must all be considered. This is were the true challenge lies.

The post Getting Started with SpringAI appeared first on JVM Advent.

View Details

IntroductionWhen the first technical solutions based on Artificial Intelligence began to be created, Python was the language and runtime platform of choice. It was not a total surprise as Python was the choice of the great majority of data scientists to analyse data, perform experiments and create AI models, so it was kind of natural that AI solutions were also based on Python and its rich ecosystem for data-intensive applications.

However, with the rise of Generative AI, organisations worldwide reconsider that approach given that the vast majority of GenAI-based solutions are just leveraging existing Large Language Models consumed “as a service” via web APIs. For doing that, Python does not have the same lead over the others. We must balance other key aspects of enterprise-grade solutions, such as resilience, scalability, observability, as well as the leverage of existing skills in the organisation. That means that other key languages and platforms can play a leading role in delivering and running GenAI-based solutions. Platforms such as Node.js, Go, and, of course, Java.

In Java we already have multiple valid approaches to be considered.In this article I will cover Langchain4j, one of the most popular choices in the Java ecosystem for building and running AI solutions at scale.

Why Langchain4j? Key outsdanding aspects are:

  1. Framework-agnostic: As a library it imposes no constraints about how you design and build your solutions, so it can be easily integrated with any existing solution, either Spring-based, Jakarta-based, Quarkus-based, Micronaut-based, or with no framework at all.
  2. Simple yet powerful API: Based on well-known patterns such as the Builder pattern, and with simple API constructs so its learning curve is simple and rewarding:
  3. It works with both cloud-based “as a service” models such as OpenAI or Google Vertex, and with local/owned models via Ollama.

NOTE: The following examples are based on Langchain4j 0.36.2.

The first Langchain4j programTo demonstrate these concepts, let’s look at a “hello world” Langchain4j program.

The main interface that we need to learn about is ChatLanguageModel (from package dev.langchain4j.model.chat). This interface has the simple API that we need to send messages to an LLM and get its response. To instantiate specific models to interact with them we need the specific implementation depending on who provides them:

  • For OpenAI, we leverage OpenAiChatModel from package dev.langchain4j.model.openai.OpenAiChatModel.
  • For Vertex, we leverage VertexAiGeminiChatModel from package dev.langchain4j.model.vertexai.
  • For Ollama, we leverage OllamaChatModel from package dev.langchain4j.model.ollama.OllamaChatModel.

As well as others. Every implementation of ChatLanguageModel has its own builder pattern to be able to add any specific configuration setting that is needed. Let’s see three brief examples of how this looks once we put it together:

OpenAI Hello World import dev.langchain4j.model.chat.ChatLanguageModel;import dev.langchain4j.model.openai.OpenAiChatModel;class OpenAIHelloWorld { void main() { // OpenAI model ChatLanguageModel model = OpenAiChatModel.builder() .apiKey(System.getenv("OPENAI\_API\_KEY")) .modelName("gpt-4o") .build(); // the first prompt String message = "Hello world!"; System.out.println("\n>>> " + message); String answer = model.generate(message); System.out.println(answer); }} As can be seen above, to connect with OpenAI services you must provide your own API key. You can also add explicitly the model that you want to be used.

Vertex Hello World import dev.langchain4j.model.chat.ChatLanguageModel;import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;class VertexAIHelloWorld { void main() { // Vertex AI model ChatLanguageModel model = VertexAiGeminiChatModel.builder() .project(System.getenv("VERTEXAI\_PROJECT\_ID")) .location("us-central1") .modelName("gemini-2.5-flash") .build(); // the first prompt String message = "Hello world!"; System.out.println("\n>>> " + message); String answer = model.generate(message); System.out.println(answer); }} The pattern is similar to the previous but the settings that must be provided are different: the project id in Vertex, the cloud region and the model name.

Ollama Hello World import dev.langchain4j.model.chat.ChatLanguageModel;import dev.langchain4j.model.ollama.OllamaChatModel;class OllamaGptOssHelloWorld { void main() { // gpt-oss:20b model running locally with Ollama ChatLanguageModel model = OllamaChatModel.builder() .baseUrl("http://localhost:11434") .modelName("gpt-oss:20b") .build(); // the first prompt String message = "Hello world!"; System.out.println("\n>>> " + message); String answer = model.generate(message); System.out.println(answer); }} Again, the pattern is similar. In this case we are running Ollama in the local computer and using the gpt-oss:20b model, quite competent and runnable in many personal computers.

Managing the context (a.k.a. short-term chat memory)While the previous examples work, they lack a critical feature that any GenAI solution would need. The first important concept that we need to understand is how to manage the context of the conversation with the LLMs, also known as the short-term memory.

In essence, what we must do is to track the whole conversation with the LLM (a.k.a. “the chat”). After every interaction, the pair question and answer are saved to be sent with the next request payload, typically models expect that under a specific history entry in the request body in Json format. Fortunately, Langchain4j deals with those details and we just focus on keeping track of the conversation. The simplest way to do that is to use a in-memory store, as we can see in the following example:

ChatLanguageModelmodel = OllamaChatModel.builder() .baseUrl(baseUrl) .modelName(modelName) .timeout(Duration.ofSeconds(300)) .temperature(0.0) .build();// define context windowChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);// initial prompt with name and what I'm doingString message = "Hello world! My name is Jorge and I'm writing this for Java Advent 2025.";chatMemory.add(userMessage(message));AiMessage answer = model.generate(chatMemory.messages()).content();System.out.println(answer.text());chatMemory.add(answer);// ask for the namemessage = "What is my name?";chatMemory.add(userMessage(message));answer = model.generate(chatMemory.messages()).content();System.out.println(answer.text());chatMemory.add(answer); ChatMemory (from package dev.langchain4j.memory) is the interface that abstracts the different implementations of short-term memory. In this example, we just use the implementation provided by MessageWindowChatMemory (from package dev.langchain4j.memory.chat) with a maximum capacity of 10 messages. Other implementations may have other ways to set the maximum capacity, e.g., using the token count.As can be seen in the example, the whole chat memory is passed to the model. As we keep adding every message and answer into the memory, the LLM will leverage the whole conversation (up to the limits of the memory or its own internal context window, whatever comes first) to come up with the best possible answer.

The static function userMessage from class dev.langchain4j.data.message.UserMessage helps to simplify keeping the history up to date by putting the user prompt in the right place.

Enriching answers with Retrieval Augmented Generation (RAG)No matter how big is the LLM we use for our solutions it lacks something key: the business-specific data. Facts and figures, business processes, knowledge bases… Every bit and piece of internal information of the organisation that is therefore not part of the public data sets used to train LLMs.

If we want to create really useful GenAI-based solutions we need them to be aware of the potential user context: what do they need, what they must know.

RAG is a very simple and time- and cost-effective pattern to have our AI agents better prepared for their assigned tasks, as compared with training your own models or fine-tuning existing ones.To augment the LLM answer the RAG pattern enriches the context with pieces of information that are connected to the user’s problem or request. This is done by querying a vector search database or a graph database and obtain the documents or document portions that seem to be related to the user’s prompt.

RAG can be seen as a form of long-term memory: we prepare the knowledge bases or graphs before an agent is first released into the public, and is suitable for continuous improvement as knowledge base sources are not read-only. RAG pattern also plays well with continuous feedback, as users report about our agents performance (e.g., correctness, completeness, relevance of results, etc.) and that feedback leads to refining prompts and the content in knowledge bases.

RAG is a two part process, then:

  1. The existing know-how is processed: parse, tokenization, vectorization, store in KB store. This process can be done periodically or even continuously if needed.
  2. When the user asks for something the prompt is used to search for the relevant information in the KB store, the best ranked results are used to augment the prompt, and the whole set of data is sent to the LLM to get the final response.

It is important to note that as the relevant pieces of information go within the context, and are subject to context limits, it is important to balance the quantity of data that is retrieved and ranked, as we cannot simply add every piece of the KB into the context.

RAG can be seen graphically in this diagram:

Implementing RAG with Langchain4jIn Langchain4j we can implement both parts of the pattern:

  1. Ingest documents containing the organisation knowledge to build up the knowledge base. For simple use cases, this KB can be maintained even in memory (e.g., for a bunch of PDF documents) and is pretty convenient to build many specialised agents with minimal dependencies (and investment).
  2. Access the knowledge base when users asks for something to get the best possible results.

Let’s see how this works in practice.

Building the KNowledge BaseTo build the knowledge base with Langchain4j we need the following abstractions:

  • EmbeddingModel from package dev.langchain4j.model.embedding: This is responsible for converting text into embeddings, that is, numerical representations (vectors) of pieces of text (tokens). In the example below, that can also be used in simple use cases, we will leverage the popular MiniLM-L6-V2 model.
  • EmbeddingStore from package dev.langchain4j.store.embedding: This is responsible for abstracting the actual store, e.g. a vector search database. In the example below, that can also be used in simple use cases, we will leverage an in-memory store.
  • DocumentSplitter from package dev.langchain4j.data.document: This is responsible for chunking the know-how documents. To parse documents in binary formats into text, in the example we leverage the popular Apache Tika library.
  • EmbeddingStoreIngestor from package dev.langchain4j.store.embedding: This is responsible to ingest every parsed document into the embedding store with the provided document splitter and embedding model.

A simple example code with Langchain4j would be like this:

// an embedding model good for simple documentsEmbeddingModel embModel = new AllMiniLmL6V2EmbeddingModel();// an in-memory embedding storeEmbeddingStore<TextSegment> embStore = new InMemoryEmbeddingStore<>();// load a PDF file from the classpathPath path = Path.of(ClassLoader.getSystemResource("acme-know-how.pdf").toURI());Document document = FileSystemDocumentLoader.loadDocument(path, new ApacheTikaDocumentParser());DocumentSplitter splitter = DocumentSplitters.recursive(256, 0);// ingest the document into the embedding storeEmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() .documentSplitter(splitter) .embeddingModel(embModel) .embeddingStore(embStore) .build();ingestor.ingest(document); Augmenting the ResponsesNo matter if the knowledge base is created at runtime or it is a persistence enterprise-grade vector search database, the abstraction needed to augment the response during retrieval are the same:

  • ContentRetriever frompackage dev.langchain4j.rag.content.retriever: This is responsible to abstract the embedding store and model that will be used to look for the relevant data in the KB.
  • AiServices from package dev.langchain4j.service.AiServices: This is a very convenient abstraction to create AI agents combining a given chat model and chat memory (as seen in the previous examples) with the content retriever.

The retrieval example with Langchain4j is quite straightforward:

// define the content retriever connecting everything togetherContentRetriever retriever = EmbeddingStoreContentRetriever.builder() .embeddingModel(embModel) .embeddingStore(embStore) .maxResults(1) .minScore(0.8) .build(); // llama3:8b model running locally with OllamaChatLanguageModel chatModel = OllamaChatModel.builder() .baseUrl("http://localhost:11434") .modelName("llama3:8b") .build();// define context windowChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(100);Agent agent = AiServices.builder(Agent.class) .chatLanguageModel(chatModel) .chatMemory(chatMemory) .contentRetriever(retriever) .build(); String message1 = "Could you summarize in 50 words the main concepts about the Java Platform?";String answer1 = agent.answer(message1); The interface Agent is a simple abstraction of our agent and its system prompt:

interface Agent { @SystemMessage(""" You are an expert in information technologies and software engineering. """) String answer(String inputMessage);} ConclusionsThe integration of Generative AI into enterprise software is no longer solely the domain of Python scripts or expensive cloud APIs. As we can see, Langchain4j offers a production-grade library for building agentic solutions at scale:

  • Decoupling: Langchain4j acts as a robust anti-corruption layer. By coding against interfaces like ChatLanguageModel and EmbeddingModel, applications can remain up to a certain degree agnostic to the underlying provider.
  • Simplicity: The AiServices API brings the familiarity of aspect-oriented programming (similar to Spring Data) to AI. Complex orchestration involving RAG retrieval, history management, and prompt engineering is abstracted behind clean Java interfaces and annotations.
  • Local Inference Viability: With the optimization of models (quantization) and the efficiency of modern hardware, running capable small-sized or medium-sized models augmented with the organization know-how on your own hardware is not just possible but practical for development cycles, CI/CD pipelines, privacy-sensitive edge deployments, and cost-effective deployments.

Knowing moreIf you want to know more and explore Langchain4j in deep, the following resources will be helpful:

  • I created a step by step workshop with lots of examples here: https://github.com/deors/workshop-langchain4j
  • The Langchain4j project tutorials: https://docs.langchain4j.dev/category/tutorials/ The post Making Java a first-class AI citizen with Langchain4j appeared first on JVM Advent.

View Details

As the year comes to a close, turn your focus to boosting your Java application performance by applying Ahead-of-Time (AOT) cache features in recent JDK releases. This article guides you through using AOT cache optimizations in your application, thereby minimizing startup time and achieving faster peak performance.

What is the Ahead-of-time cache in the jdkJDK 24 introduced the Ahead-Of-Time (AOT) cache, a HotSpot JVM feature that stores classes after they are read, parsed, loaded, and linked. Creating an AOT cache is specific to an application, and you can reuse it in subsequent runs of that application to improve the time to the first functional unit of work (startup time).

To generate an AOT cache, you need to perform two steps:

  1. Training by recording observations of the application in action. You can trigger a recording by setting an argument for the -XX:AOTMode option and giving a destination for the configuration file via -XX:AOTConfiguration: java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -cp app.jar com.example.App ... This step aims to answer questions like “Which classes does the application load and initialize?”, “Which methods become hot?” and store the results in a configuration file (app.aotconf).
  2. Assembly that converts the observations from the configuration file into an AOT cache (app.aot). java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -cp app.jar

To benefit from a better startup time, run the application by pointing the -XX:AOTCache flag to the resulting AOT cache.

java -XX:AOTCache=app.aot -cp app.jar com.example.App ... The improved startup time is the result of shifting work, usually done just-in-time when the program runs, earlier to the second step, which creates the cache. Thereafter, the program starts up faster in the third phase because its classes are available from the cache immediately.

The three-step workflow (train+assemble+run) became available starting with JDK 24, via JEP 483: Ahead-of-Time Class Loading & Linking, the first feature merged from the research done by Project Leyden. A set of benchmarks prove the effectiveness of this feature and other Leyden performance-related ones, as displayed by Figure 1.

Figure 1: AOT Cache Benchmarks as of JDK 24

In JDK 25, the changes in JEP 515 – Ahead-of-Time Method Profiling enabled frequently executed method profiles to be part of the AOT cache. This addition improves application warm up by allowing the JIT to start generating native code immediately at application startup. The new AOT feature does not require you to add more constraints to your application execution; just use the existing AOT cache creation commands. Moreover, benchmarks showed improved startup time too (Figure 2).

Figure 2: AOT Cache Benchmarks as of JDK 25

JDK 25 also simplified the process for generating an AOT cache by making it possible to do it in a single step, through setting the argument for -XX:AOTCacheOutput flag:

```

Training Run + Assembly Phasejava -XX:AOTCacheOutput=app.aot \ -cp app.jar com.example.App ...

`` Upon passing-XX:AOTCacheOutput=[cache location]`, the JVM creates the cache on its shutdown. JEP 514 – Ahead-of-Time Command-Line Ergonomics introduced the two-step process for creating and using the AOT cache.

```

Training Run + Assembly Phasejava -XX:AOTCacheOutput=app.aot \ -cp app.jar com.example.App ...# Deployment Runjava -XX:AOTCache=app.aot -cp app.jar com.example.App ...

`` The two-step workflow may not work as expected in resource-constrained environments. The sub-invocation that creates the AOT cache uses its own Java heap with the same size as the heap used for the training run. As a result, the memory needed to complete the one-step AOT cache generation is double the heap size specified on the command line. For example, if the one-step workflowjava -XX:AOTCacheOutput=...is accompanied by-Xms2g -Xmx2g`, specifying a 2GB heap, then the environment needs 4GB to complete the workflow.

A division of steps, as in a three-phase workflow, may be a better choice if you intend to deploy an application to small cloud tenancies. In such cases, you could run the training on a small instance while creating the AOT cache on a larger one. That way, the training run reflects the deployment environment, while the AOT cache creation can leverage the additional CPU cores and memory of the large instance.

Regardless of which workflow you choose, let’s take a closer look at AOT cache requirements and how to set it up to serve your application needs best.

How to Craft the AOT Cache Your Application NeedsTraining and production runs should produce consistent results, just faster in deployment runs. To achieve that, the assembly phase intermediates what happens between training and production runs (Figure 3).

Figure 3: Training / Assembly / Deployment

For consistent training runs and the subsequent ones, make sure that:

  • Your JARs preserve their timestamp across training runs.
  • Your training runs and the production one use the same JDK release for the same hardware architecture and operating system.
  • Provide the classpath for your application as a list of JARs, without any directories, wildcards or nested JARs.
  • Production run classpath must be a superset of the training one.
  • Do not use use JVMTI agents that call the AddToBootstrapClassLoaderSearchand AddToSystemClassLoaderSearch APIs.

To check if your JVM is correctly configured to use the AOT cache, you can add the option -XX:AOTMode=on to the command line:

java -XX:AOTCache=app.aot -XX:AOTMode=on \ -cp app.jar com.example.App ... The JVM will report an error if the AOT cache does not exist or if your setup disregards any of the above requirements. Furthermore, the features introduced in JDK 24 and 25 did not support the Z Garbage Collector (ZGC). Yet, this limitation no longer applies as of JDK 26, with the introduction of JEP 516: Ahead-of-Time Object Caching with Any GC.

To ensure the AOT cache works effectively in production, the training run and all following runs must be essentially identical. Training runs are a way of observing what an application is doing across different runs and are primarily two types:

  • integration tests, which run at build time
  • production workloads, which require training in production.

Avoid loading unused classes during the training step and skip rich test frameworks to keep the AOT cache minimal. Mock external dependencies in training to load needed classes, but be aware that this may introduce extra cache entries.

AOT cache effectiveness depends on how closely the training run matches production behavior. If you rebuild the application or upgrade its JDK, you must regenerate the AOT cache. Otherwise, you risk crashes or undefined behavior (methods missing from cache).

In case you need to debug the performance of your application, run it with -Xlog:aot,class+path=info to monitor what it loads from cache.

Tips for Efficient Training RunsThere is a trade-off between performance and how easy it is to run the training. Using a production run for training is not always practical, especially for server applications, which can create log files, open network connections, access databases, etc. For such cases, it is better to make a synthetic training run that closely resembles actual production runs.

Aligning the training run to load the same classes as production helps to achieve an optimized startup time. To determine which classes are loaded by your training run, you can append the -verbose:class flag upon launching it. Or observe the loaded classes by enabling the jdk.ClassLoad JFR event and profiling your application with it:

```

configure the eventjfr configure jdk.ClassLoad#enabled=true# profile as soon as your application launchesjava -XX:StartFlightRecording:settings=custom.jfc,duration=60s,filename=/tmp/AOT.jfr# profile on a running application identified through llvmidjcmd llvmid JFR.start settings=custom.jfc duration=60s filename=/tmp/AOT.jfr

`` On the recording file, you may check the loaded classes, but also which methods your application frequently uses by running the followingjfr` commands:

```

print jdk.ClassLoad events from a recording filejfr print --events "jdk.ClassLoad" /tmp/AOT.jfr# view frequently executed methodsjfr view hot-methods /tmp/AOT.jfr

``` If you determine that there are methods frequently used but not detected by your training run, exercise them. You can work out the standard modes of your application using a temporary file directory, a local network configuration, and a mocked database, if needed.
Avoid loading unused classes during training and skip rich test frameworks to keep the AOT cache minimal. Instead, use smoke tests to cover typical startup paths; avoid extensive suites and stress/regression tests.

TakewaysTo conclude, crafting an AOT cache for better performance requires you to look over:

  • Cache validity or staleness; if you rebuild the application or upgrade the JDK, you must regenerate the AOT cache.
  • Portability, as the AOT cache is JVM and platform-specific.
  • Startup path coverage; the training run must cover typical application startup paths. If your training run is shallow, you will not warm up enough, and the benefits of the cache will be limited.
  • Operational setup as both the application JAR and the AOT cache must run with least privilege and according to immutable infrastructure practices.

Application performance is an ongoing task because software evolves: new features are added, libraries change, workloads grow, and infrastructure shifts (e.g., to the cloud, container orchestration, etc.). Depending on those evolutions, your application performance goals evolve as well. Invest in training your application today and keep up with JDK releases to unlock available optimizations, as performance improves with each of them!

The post Run Into the New Year with Java’s Ahead-of-Time Cache Features appeared first on JVM Advent.

View Details

The Pi4J project is a Java library that allows you to control the GPIO pins and electronic components connected to a Raspberry Pi with pure Java code. It removes the complexity of using native libraries and the Java Native Interface (JNI), allowing you to focus on your application logic.

In the Java Advent of 2020, I published “Light up your Christmas lights with Java and Raspberry Pi“, using Java 11 and Pi4J V1.2. Wow, things have changed a lot: this article uses Java 25 and a snapshot of the soon-to-be-released Pi4J V4!

I became a contributor to Pi4J while working on my book “Getting Started with Java on the Raspberry Pi” around 2020. But even after many years of working on Pi4J’s code, I get puzzled when I dive deep into its sources. Please help me, do you understand what’s happening in this piece of code?

JNIEXPORT jobject JNICALL Java\_com\_pi4j\_library\_gpiod\_internal\_GpioD\_c\_1gpiod\_1chip\_1open (JNIEnv* env, jclass javaClass, jstring path) { struct gpiod\_chip* chip; const char* nativeString = (*env)->GetStringUTFChars(env, path, NULL); chip = gpiod\_chip\_open(nativeString); (*env)->ReleaseStringUTFChars(env, path, nativeString); if(chip == NULL) { return NULL; } jclass cls = (*env)->FindClass(env, "java/lang/Long"); jmethodID longConstructor = (*env)->GetMethodID(env, cls, "<init>", "(J)V"); return (*env)->NewObject(env, cls, longConstructor, (jlong) (uintptr\_t) chip);} It’s one of the “connection points” between Java and the native libraries to communicate with the GPIO pins. Using JNI and Java Native Access (JNA), Docker build environments are used to compile native libraries for use from Java. This complexity makes it easy for the end user to interact with Java, but difficult for the Pi4J developers to maintain and debug.

In this post, I’ll explain how the Foreign Function & Memory API (FFM API) has revolutionized how Java developers interact with native libraries and memory, significantly simplifying the Pi4J project. This article is based on a talk I gave at the Devoxx and JFall conferences about the history and evolution of this addition to OpenJDK.

A Quick History LessonMy Java journey started 15 years ago when I switched from C# to Java and never looked back. That was right in the middle of Java’s 30-year history, and I’ve been following its evolution closely ever since. I even had the chance this year to talk to James Gosling, the “Father of Java”, for the Foojay Podcast.

In recent years, we have seen many evolutions in the Java language and virtual machine, such as improved switch-case, virtual threads, performance improvements, and more. One of the most significant recent developments has been Project Panama and the Foreign Function & Memory (FFM) API that emerged from it.

Foreign Function & Memory (FFM) APIThe FFM API was officially released in Java 22 as a finalized feature. It represents years of work within Project Panama and has three main goals:

  1. Memory safety: Safe access to off-heap memory while maintaining proper cleanup.
  2. Easy interaction: Simple ways to call native libraries.
  3. High performance: Matching or exceeding JNI’s performance.

The Problem With JNIJNI has been around since Java 1.1, and while it works, it has a few critical drawbacks:

  • Manual memory management with a steep learning curve.
  • Complex implementation requiring C headers and compilation steps.
  • Hard to use by design: As I learned from Simon Ritter, a Sun engineer once said, JNI was deliberately made difficult to discourage people from using it!

There were attempts to improve this situation with libraries such as JNA and Java Native Runtime (JNR), but they came with their own overhead and limitations.

How The FFM API EvolvedThe development of the FFM API is a fascinating story that shows how OpenJDK evolves through careful iteration. The project was broken down into separate JEPs (JDK Enhancement Proposals), which started being delivered in Java 14. As you will see, most of the JEPs were incubator or preview features, which can only be used with the --enable-preview flag, as you can learn in this detailed explanation.

Foreign Memory Access APIAs a first step, the OpenJDK team created a new API to access off-heap memory, enabling safe, efficient access to foreign memory. None of these were finalized, as they got integrated into OpenJDK as incubator features, preparing for something bigger.

  • OpenJDK 14: JEP 370: Foreign-Memory Access API (Incubator)
  • OpenJDK 15: JEP 383: Foreign-Memory Access API (Second Incubator)
  • OpenJDK 16: JEP 393: Foreign-Memory Access API (Third Incubator)

Foreign Linker APIIn the next step, statically typed pure-Java access to native code got integrated, again as an incubator feature.

  • OpenJDK 16: JEP 389: Foreign Linker API (Incubator)

Foreign Function & Memory APIFinally, the FFM API was finalized in Java 22. It’s a combination of the two previous APIs, and it went through incubator and preview phases across multiple Java releases.

  • OpenJDK 17: JEP 412: Foreign Function & Memory API (Incubator)
  • OpenJDK 18: JEP 419: Foreign Function & Memory API (Second Incubator)
  • OpenJDK 19: JEP 424: Foreign Function & Memory API (Preview)
  • OpenJDK 20: JEP 434: Foreign Function & Memory API (Second Preview)
  • OpenJDK 21: JEP 442: Foreign Function & Memory API (Third Preview)
  • OpenJDK 22: JEP 454: Foreign Function & Memory API

This iterative approach allowed the OpenJDK team to gather community feedback and ensure the API was stable, performant, and truly useful.

Simple Code ExamplesLet me show you how much simpler things have become with a few examples.

Accessing Memory DirectlyHere’s a basic example of accessing memory directly:

void main() { // Open a confined Arena that manages off-heap memory // and will release it automatically. try (Arena arena = Arena.ofConfined()) { // Allocate 5 ints in the arena MemorySegment segment = arena .allocate(ValueLayout.JAVA\_INT, 5); // Fill the segment with random values for each int. System.out.print("Setting values: "); for (int i = 0; i < 5; i++) { int randomValue = new Random().nextInt(100); segment.setAtIndex(ValueLayout.JAVA\_INT, i, randomValue); System.out.print(randomValue + " "); } System.out.println(""); // Print the values back from memory. System.out.print("Reading values: "); for (int i = 0; i < 5; i++) { System.out.print(segment .getAtIndex(ValueLayout.JAVA\_INT, i) + " "); } System.out.println(""); }} Notice a few things about this code:

  • I execute it with Java 25, which means I can use the new simplified main method introduced by JEP 512: Compact Source Files and Instance Main Methods. As a result, I don’t need to use a class and package, and a lot of the main method “clutter” is no longer needed in a simple example like this.
  • The Arena manages memory cleanup automatically in the try block.
  • The MemorySegment is a simple wrapper around a native memory address.
  • No manual memory management is needed.

$ java ArenaDemo.javaSetting values: 16 7 27 50 80 Reading values: 16 7 27 50 80 A more extended example is available here in FFMMemoryManagement.java, with a Java 11 example here Java11MemoryManagement.java illustrating how much more complex this was before the FFM API. The Java 11 version must be executed with JBang as described in the comments, to force the use of Java 11.

Calling Native FunctionsTo illustrate how to call a native library function, we’ll make the most complex String.length() implementation A perfect example of how this can be done with FFM API within one simple-to-read method:

void main() throws Throwable { // The text we will use in the demo. var text = "Hello JVM Advent!"; // Obtain a Linker that knows how to call // native (C) functions on this platform. Linker linker = Linker.nativeLinker(); // Create a SymbolLookup that can find symbols // (like C functions) in the default native libraries. SymbolLookup lookup = linker.defaultLookup(); // Create a MethodHandle to call the native C function MethodHandle strlen = linker.downcallHandle( // Look up the address of the `strlen` symbol, // or throw if it isn't found. lookup.find("strlen").orElseThrow(), // Describe the C function: // - returns long // - takes one pointer (address) argument FunctionDescriptor.of(ValueLayout.JAVA\_LONG, ValueLayout.ADDRESS) ); // Open a confined Arena that manages off-heap memory // and will release it automatically. try (Arena arena = Arena.ofConfined()) { // Allocate native memory for a C-style string // and copy "Hello World!" into it. MemorySegment str = arena.allocateFrom(text); // Call the native `strlen` function // via the MethodHandle, // passing the string's memory address, // and cast the result to a long. long length = (long) strlen.invoke(str); // Print the result. System.out.println("Length with native library: " + length); } System.out.println("Length with String.length(): " + text.length());} No JNI headers, no C-compilation, just pure Java code!

$ java LinkerDemo.javaLength with native library: 17Length with String.length(): 17 Also, for this example, you can find a more extended example here in FFMNativeCalls.java, with a Java 11 in Java11NativeCalls.java.

Performance Comparison ExampleI like demos that have a visual output. So I created a simple benchmark that generates moving gradients, which involves many memory writes. In both Java 11 and 25, I try to refresh the gradient every 5 milliseconds.

The results speak for themselves (tested on a MaOS M2 with Azul Zulu 25):

$ jbang Java11PixelBuffer.java[jbang] Building jar for Java11PixelBuffer.java...Interval: 57 - Generated 250000 pixelsInterval: 56 - Generated 250000 pixels$ java FFMPixelBuffer.javaInterval: 5 - Generated 250000 pixelsInterval: 5 - Generated 250000 pixels * Java 11 approach using BufferedImage and arrays: ~50-60 milliseconds per frame. * FFM API approach with direct memory access: ~5 milliseconds per frame.

That’s up to 11x performance improvement just by avoiding the overhead of Java object management!

Why the FFM API Matters for Raspberry Pi ProjectsLet me first answer a more general question I get asked a lot: “Why Java on a Raspberry Pi?” The answer is simple: Java is the language that allows me to do everything. I can build user interfaces with JavaFX, make API calls to services, and leverage the extensive library ecosystem. When I started experimenting with Java on Raspberry Pi over five years ago, I didn’t want to learn a new language. I wanted to learn how to use and interact with electronic components, using the tools I already knew and loved.

This is where the Pi4J project comes in: it’s a Java library that lets you control the GPIO pins and electronic components connected to a Raspberry Pi. But here’s the catch: this library has always relied on native C/C++ code to communicate with the hardware. Until now, that meant dealing with the complexity of JNI and JNA.

Pi4J ArchitecturePi4J uses a plugin architecture in which different “providers” handle communication with GPIO pins. Previously, these providers relied on native libraries and JNI/JNA, which meant:

  • Complex multi-layered code with up to 5 levels before executing GPIO operations.
  • Need for Docker builds to compile native libraries.
  • Cryptic JNI header generation and wizard-like C code.

These plugins get loaded dynamically at runtime, in the “black bar” in this architecture diagram:

The FFM TransformationPi4J now has an almost-ready FFM-based provider, which will be available in V4. The improvements are dramatic:

  • Performance Benchmarks
    • Getting GPIO input state: 10x faster!.
    • SPI communication: Significant improvement, though less dramatic.
  • Code Simplification
    • Direct Java-to-kernel communication.
    • No more complex JNI layers.
    • Readable, maintainable Java code.
    • No need for native library compilation with a complex Docker build process.

A Community Success StoryWhat makes this even better is that the FFM implementation came from the community. Nick Gritsenko (aka @DigitalSmile) had already created a Java 22 library for GPIO interaction using FFM. When I discovered his work, I asked if we could use it. But even better, he contributed it directly to Pi4J himself and made it fit perfectly into the plugin architecture! This resulted in a major pull request that’s now merged into the Pi4J V4 snapshot.

I’m very proud that this happens again and again. In the past, Alexander Liggesmeyer added support for the Raspberry Pi 5 with a new plugin. And at this moment, Stefan Haustein, Stephen More, Tom Aarts, and others are actively working on a new Pi4J drivers library and example implementations to make it much easier for everyone to create applications that interact with more complex electronic components, such as joysticks, LCD screens, LED strips, and more…

Beyond Raspberry PiThe FFM-based implementation opens up exciting possibilities. Since it’s based on standard Linux kernel methods available in Debian, we believe Pi4J could potentially work on:

  • Orange Pi boards and other Linux-based SBCs (Single Board Computers).
  • RISC-V processors running Debian.

I’m looking forward to experimenting with these different hardware platforms! Maybe you’ll read more about that in next year’s JVM Advent…

Pi4J Examples Using the FFM APII often demo with a CrowPi – a neat kit with a Raspberry Pi and pre-wired components in a single box. It’s great for learning because you can’t wire things incorrectly!

Here’s a simplified example using JBang to blink an RGB LED, using a snapshot build of Pi4J V4. Once this version is released, you can remove the line with //REPOS and update the version.

/// usr/bin/env jbang "$0" "$@" ; exit $?//REPOS mavencentral,pi4j-snapshots=https://oss.sonatype.org/content/repositories/snapshots//DEPS com.pi4j:pi4j-core:4.0.0-SNAPSHOT//DEPS com.pi4j:pi4j-plugin-linuxfs:4.0.0-SNAPSHOTimport com.pi4j.Pi4J;import com.pi4j.io.gpio.digital.*;/** * Execute with `jbang Pi4JExample.java` */void main() throws InterruptedException { var pi4j = Pi4J.newAutoContext(); var red = pi4j.digitalOutput().create(17); var green = pi4j.digitalOutput().create(27); var blue = pi4j.digitalOutput().create(22); // Blink pattern for (int i = 0; i < 10; i++) { red.high(); Thread.sleep(500); red.low(); green.high(); Thread.sleep(500); green.low(); blue.high(); Thread.sleep(500); blue.low(); }} More examples like this, which can be executed with JBang, are available in the Pi4J JBang repository.

Important NotesWhile working on the presentation about the FFM API and this article, I noticed a few points worth mentioning.

  • Memory Safety Warning: When using FFM, you’re stepping outside the “safe JVM-managed garbage collector”. You’ll see a warning about enabling native access with a few of the examples from this article. This is intentional as the OpenJDK team wants to ensure developers understand they’re working with potentially dangerous operations. Use the --enable-native-access flag to acknowledge this and suppress the warning. This may change in future versions of OpenJDK.
  • JEPs Are Worth Reading: If you’re curious about how Java evolves, I highly recommend reading the JDK Enhancement Proposals. They’re not just technical specifications! They’re well-written documents that explain the thinking behind design decisions, include examples, and provide deep insights from the architects of Java. Similarly, read more about the OpenJDK projects, for instance, by subscribing to their mailing list to follow what is happening within such a project.
  • Keep Up With Java Releases: Java has a six-month release cycle, and every release is a good one! Each brings many bug fixes, improvements, and evolutions (from projects and JEPs). If you can update your systems, especially your development systems, you should! The FFM API was introduced in Java 22, but I learned at Devoxx that Java 24 brought significant performance improvements to the FFM implementation, without any API changes! This shows that the OpenJDK team continues to optimize and improve existing features.

ConclusionThe FFM API has been a significant improvement for developers working with native code in recent years. It removes the complexity of JNI while maintaining, and even improving, performance. It opens new possibilities for Java in embedded systems and hardware interfacing. It will also drive closer integration of Artificial Intelligence (AI), Machine Learning (ML), and Large-Language Model (LLM) development with Java.

Feel free to experiment and contribute. OpenJDK and other open source projects, like Pi4J, thrive on community involvement!

If you’re interested in Java on Raspberry Pi, check out:

  • The Pi4J website.
  • My blog posts on webtechie.be and Foojay.io.
  • My ebook “Getting Started with Java on the Raspberry Pi“, which I keep updating with new Java versions, Pi4J improvements, etc.

Remember: We must thank OpenJDK and all its contributors for amazing features like the Garbage Collector, JIT compilation, Lambdas, Virtual Threads, and now the FFM API. Java keeps getting better, and that’s worth celebrating!

The post The FFM API: How OpenJDK Changed the Game for Native Interactions (And Made Pi4J Better!) appeared first on JVM Advent.

View Details

It’s great to write another entry for Java Advent this year! Last year, I wrote about how you can use Timefold Solver, an optimization library written in Java, to solve planning problems such as employee scheduling or, in true holiday spirit, optimizing Santa’s travel route.

This year, as the world is now all going “agentic”, I was curious to learn more about agents and decided to start tinkering myself. I started a pet project with a basic idea:

“Could I generate a fully working optimization application purely from a simple problem description?”

Here’s where that curiosity led.

Basics: FrameworkI like to approach pet projects in a way that they incorporate some ideas/frameworks/concepts I’ve played with before, while still challenging me to learn some new things.

There was no doubt in my mind about using Java, so I started looking for Java compatible agentic frameworks. I ran into Langchain4J Agentic, the Agent Development Kit and a few others. Since I had the most experience with Langchain4J already, that became my framework of choice.

I scoured through the docs to get a sense of what building an agent was like and then just got to work. I just needed 1 more thing: a benchmark / test scenario.

Let’s Sing!I was looking for an example scenario. Something I would love to optimize that was also in my typical fashion:

  • Silly (can’t be too serious when messing around with technology).
  • Relatively simple (so everyone could understand it).
  • Somewhat practical.

After talking to Wouter Bauweraerts from the Belgian Java Community (hi Wouter ), I settled on a karaoke bar scheduler. Yes. Karaoke.

Here’s the prompt I wanted the agent to digest:

**I run a Karaoke bar. I have 2 stages and I want to schedule songs following these rules:

– Avoid having the same singer 2 times in a row
– Avoid having songs from the same artist 2 times in a row

A singer can sign up with a song. The duration of the performance depends on the lenght of the song.
I want to be able to create a clear clock of when the next song will start, so include timing.**

Time to get to work on the agent!

(Note: All example prompts shown here are abbreviated. The real ones are as long as a 10 year old’s wishlist for Santa.)

Agent v0.1: Just an LLM prompt in disguiseThe first version of my agent was pretty simple. Just take the input prompt, add a bit of information in the @SystemMessage and @UserMessage to steer the result a bit and hope for the best. After all, with all the hype, this was surely supposed to work? I even scoped it to only write the Timefold related code, not bothering with any sort of user interface.

https://gist.github.com/TomCools/c227ab745e1ae5a9c6f145cfe7e680e1Then I needed to choose the underlying LLM model. Langchain4j makes it trivial to change the LLM the Agent uses. As I was still in the exploratory phase, I decided to use a locally hosted LLM, qwen3:8b, which I run on my macbook using Ollama.

https://gist.github.com/TomCools/625c5c2c88b3e6a046f10bc3fd4dd20aAs I was patiently waiting for my brand new agent to give its very first result, I was already dreaming of what awesome result would come out of that writeCode method. As some of you might expect… it’s not that simple, unless the end result you wanted is a dumpster fire of mangled code.

https://gist.github.com/TomCools/c424652a075624b879c57e61174b7536Hey, at least it did output something! So let’s see how we can improve this.

Agent v0.2: Limiting scope even moreI felt I was asking too much from my single agent (or maybe this is just my human bias talking), so I decided to split it up into 2 separate coding agents: 1 for the Java domain code, 1 for the Java constraint stream code.

Constraint Streams is the syntax you use with Timefold to write constraints, such as the “Avoid having the same singer 2 times in a row” constraint.

The result was slightly better. The agents were more narrowly focussed and seemed able to keep it together. The output however is still nowhere near being workable. It was at least starting to look a bit more like workable Java code.

Agent v0.3: Better base modelAgents are only as good as their underlying model. Given that I’ve had more success with Claude and ChatGPT when asking similar coding questions, I felt it was time to replace the model used by my agent to Claude Sonnet 4.5 (20250929).

With this change in model I now have to start paying for my tokens. I started proceeding a bit more carefully and sometimes moved back to Ollama just for basic runs. Langchain4j makes this a 1 line code change .

Changing the model immediately brought my agent to life. The returned code at least looked like it was written by someone who had seen Java before. However, copy pasting the code into my IDE just showed how glaringly wrong it actually was. It’s the sort of code that would never pass a code-review… oh wait!

Agent v0.4: Review and FeedbackI had seen a subject header on the agentic langchain documentation about sub-agents and agentic workflows. So I decided to try it. Instead of a single “in and out” workflow, I added a Review agent, which reviews the written code, provides feedback and then puts the coding agent to work again with that feedback.

Agentic coder and code reviewer work in a loop. Instead of continuing after X reviews, you could also let the reviewer score the solution and continue once a certain score has been reached.The cool thing is that we can actually give these agents tools to work with… and what better tool to give an agent which needs to write compilable code than an actual Java Compiler.

https://gist.github.com/TomCools/72342a51c40b92b5a5dcd4ad81b02f1bThe resulting code was pretty ok and usually compiled just fine. But it did make some very obvious mistakes from a Timefold perspective (e.g. adding both @ShadowVariable and @PlanningVariable to a single field).

Agent v0.5: Avoiding the same mistakes (easyRAG)To avoid these similar mistakes, I tried to give the agents a bit more information to work with by using RAG (Retrieval Augmented Generation). As I just wanted a simple solution here, I used the easyRAG features of Langchain4j. This allows me to just point to a directory which had some documentation included so the agent would be able to use the contained documents.

https://gist.github.com/TomCools/1159f3c86fc8f91ec62e5da3fc4be63eWe have a lot of documentation for Timefold Solver, generated to pdf, it’s a good 400 pages. Not all of that is relevant for every agent, so I added some simplified documentation in different subdirectories for each of agents and then added those to the agent with a ContentRetriever.

I ran it a couple of times to see the impact these documents had. Whenever a new mistake seemed to become prevalent, I added more information to the relevant documents to improve the result.

This also led me to make a couple updates to our actual documentation. Turns out that if an LLM can’t understand your documentation humans will struggle with it as well.

At this point, it was getting slightly painful to see my API credits decline. With the subagents in a loop, this was burning through a lot of tokens, so I tried to figure out: how can I get a better result, without burning through so many tokens?

Agent v0.5: (Plant)UML to the rescue!Upon inspection, I noticed a lot of the comments by the review agents were not about code details at all, but about the structure of the classes and the placement of the annotations Timefold Solver needs. When solving these problems myself, these are all things I’d add to a diagram before even opening my editor… so why not do the same here?

Instead of skipping directly to the code, I introduced 1 new agent. This “modelling” agent would “model” how the solution should look and format it in PlantUML. I gave this agent the documentation we have for modelling problems with Timefold Solver and explicitly asked it to add some details about design choices it made to the PlantUML diagram.

Generated Diagram, notes contain the design decisions. It did a pretty decent job here!Now I not only made “thinking about the structure of our solution” way more explicit, I also have a reviewable asset that is passed between agents: the PlantUML diagram. As it turns out, PlantUML is an excellent format if you want to pass information about the structure of your classes between agents without giving it the entire codebase.

I adjusted the coding agents to accept the PlantUML diagram as input, so they did not get overloaded with residual information about the problem statement.

This massively improved consistency and reduced hallucinations. It now worked well enough that I ventured into the much harder part.

Agent v0.6: UI and BackendHaving a nice scheduling core is nice, hosting a Karaoke with hardcoded Java will probably not be very conductive for a great evening. So we still need a UI.

I don’t think that having GenAI scaffold an entire project is a good move here, so I looked into tools that would help me create a project structure.

In the Java world, one potential solution is to rely on JHipster and more specifically the JHipster Domain Language (JDL) . JDL allows you to describe a project: entities, relationships and some other properties.

https://gist.github.com/TomCools/bdda56e9826fcf37779a781fda05c1e5These are all things we can let an LLM fill in, based on the PlantUML and the problem description. Then you can create a full application with a single command (jhipster import-jdl model.jdl), which I wrapped into a Tool so I can have an agent create the JDL and generate a simple application for my problem.

And now we have a fully working application (Angular + Spring Boot), including login screen, metrics and CRUD pages for every entity involved entity, all at a fraction of the token cost we’d have if we let GenAI do everything.

ConclusionWhile I still have to resolve some gaps, like actually connecting Timefold Solver to the rest of the JHipster generated code and creating a better scheduling UI, I think I’ll stop this pet project here for now. For me, the real value of these tinkering projects isn’t what I end up building. It’s everything I learn while fumbling toward it.

Here are my main lessons.

  • Having inspectable intermediate formats makes the development process much easier and the end result way more auditable.
  • At the moment of writing, it’s still much better to have a human in the loop. Even tiny mistakes made in one of the first steps can lead to a failed result in the end. Being able to intervene and correct makes the whole thing so much easier than this “fully autonomous” thing I was trying.
  • Better models = better results. We may not want to spend the money, but it did change my results from absolute garbage to something that quite often works. I was very happy with Langchain4J in this regard, I only needed to change 1 line of code to try a different model.
  • Learning is still so reinvigorating! It’s been a while since I’ve felt a big jolt of love for a pet project but this one struck like a lightning bolt. Yes, it can get annoying sometimes to get GenAI to do what you want, but as a dad raising a small 2 year old kid, I have been trained to be patient and steer it in the right direction.

Happy Holidays!

The post Tinkering with a “hands-off” agent appeared first on JVM Advent.

View Details

AI is changing how we build software, but many teams still work as if nothing has changed. They treat code as the only reliable artifact. Everything else slowly gets outdated:

  • Requirements documents drift away from reality
  • Diagrams do not match the current architecture
  • Tests only cover part of the behavior
  • Business logic hides deep in service classes

AI code generation tools can make this even worse if used without structure. They produce code fast, but the process behind the code remains the same.

Spec-Driven Development (SDD) solves this by starting from clear specifications instead of starting from code. AI then uses these specifications to generate consistent code, tests, and documentation.

The AI Unified Process (AIUP) is a practical way to apply SDD in real projects. It keeps requirements, code, and documentation in sync. In this article, we look at AIUP in the context of a full-stack Java application built with Spring Boot, jOOQ, and Vaadin.

From Code-Centered To Spec-DrivenIn most teams, development is code-centered:

  • A ticket is created
  • A developer writes code
  • Documentation and tests come later

This leads to long-term problems:

  • You lose track of why a rule exists
  • Business decisions hide in commit messages
  • Refactoring becomes risky
  • The architecture stops matching the documentation

Spec-Driven Development flips this:

  1. You start with a clear specification.
  2. This specification becomes the single source of truth.
  3. AI generates code, tests, and diagrams from it.
  4. Developers review and adjust the generated results.

When requirements change, you update the specification first, then regenerate.

The AI Unified Process (AIUP)AIUP is an iterative process with a simple but powerful structure.
It is built on three core specification artifacts:

  1. Requirements Catalog
  2. Entity Model
  3. System Use Cases

These artifacts are always updated in this order. Everything else flows from them.

  1. Requirements CatalogThe Requirements Catalog is the foundation of AIUP.
    It defines what the system must do and under which conditions.

It contains three types of requirements:

Functional RequirementsThese describe what the system should do. In AIUP, they are usually written as user stories.

Examples:

  • As a user, I want to create a customer with name and email.
  • As an admin, I want to deactivate customers who have no open orders.
  • As a sales agent, I want to see all active customers sorted by name.

Functional requirements describe user goals and expected behavior, not code.

Non-Functional RequirementsThese describe qualities of the system, like:

  • Performance
  • Security
  • Availability
  • Logging
  • Observability
  • Scalability

Example:

  • All write operations must be audited with a timestamp, user, and change details.

ConstraintsThese describe rules that must always be enforced.

Examples:

  • Customer email must be unique.
  • Only authenticated users may access customer data.
  • Orders cannot be deleted once invoiced.

The Requirements Catalog is written in simple, precise language. It is reviewed frequently and kept under version control. Everything in AIUP starts with this catalog.

  1. Entity ModelThe Entity Model describes the domain data structure.
    It answers:

Which concepts exist, and how do they relate?

The Entity Model is based on the Requirements Catalog. Functional requirements describe what users want to do. Constraints often describe the domain rules. From both, you extract stable domain objects.

For example, from the requirements above, you define:

Entities:

  • Customer
  • Order

Attributes:

  • Customer: id, name, email, active
  • Order: id, status, totalAmount, customerId

Relationships:

  • Customer 1..* Order

The Entity Model lets AI generate:

  • Database schema or migrations
  • jOOQ meta model
  • Domain classes and DTOs
  • Validation rules
  • Basic UI bindings in Vaadin

It ensures that back end, database, and UI use the same domain structure.

  1. System Use CasesAIUP uses only System Use Cases. There is no separation between business and system use cases.

A System Use Case describes how the system behaves when triggered by a user or external system. It defines:

  • Actors
  • Preconditions
  • Steps
  • Decision logic
  • Postconditions
  • Errors

System Use Cases transform requirements into explicit logic.

Example System Use Case: Deactivate CustomerName: Deactivate Customer
Actor: Administrator
Preconditions:

  • User is authenticated
  • User has role ADMIN

Main Flow:

  1. Load customer
  2. Load related orders
  3. Check for open orders
  4. If open orders exist, return error
  5. Set customer.active = false
  6. Write audit entry

Postconditions:

  • Customer is inactive
  • Operation is logged

How AIUP Runs In IterationsAIUP uses short iterations. Each cycle follows this sequence:

  1. Update Requirements Catalog
  2. Update Entity Model
  3. Update System Use Cases
  4. Generate code, diagrams, and tests with AI
  5. Review and improve the generated output
  6. Run tests
  7. Refine the specifications

This creates a stable, controlled development loop.

Example: A Java Feature With AIUPImagine you add a “Deactivate Customer” feature. All documentation is code and under version control.
The example uses Markdown, but you could also use AsciiDoc to get a richer Markdown syntax.

Step 1: Requirements Catalog | Category | Description ||---------------------------|------------------------------------------------------------|| Functional requirement | As an admin, I want to deactivate a customer who has no open orders. || Non-functional requirement| All write operations must be audited. || Constraint | Customer email must be unique. Step 2: Entity Model Entity: Customer with id, name, email, active Entity: Order with id, status, customerId Relation: Customer 1..* Order Rule: Status can be OPEN, COMPLETED, or CANCELLED AI generates:

  • Migration script for unique email
  • jOOQ code
  • Domain classes

Step 3: Use Cases ```

UC-001: Deactivate CustomerName Deactivate CustomerActor AdministratorPreconditions - User is authenticated - User has role ADMINMain Flow 1. Load customer 2. Load related orders 3. Check for open orders 4. If open orders exist, return error 5. Set customer.active = false 6. Write audit entryPostconditions - Customer is inactive - Operation is logged

``` AI generates:

  • Vaadin UI
  • Service method implementing logic
  • jOOQ queries
  • Unit, integration tests, and E2E tests

You review, adjust, and commit.

Why This Fits Full-Stack Java So WellAIUP fits the Java ecosystem because:

  • The Spring ecosystem is very mature
  • jOOQ is built around generated code
  • Vaadin UI components are superior
  • Java is strongly typed
  • Tests are easy to generate from System Use Cases

AIUP connects these elements with a straightforward process.

Benefits For Teams Clear Requirements
Rules, constraints, and user stories are explicit. *
Consistent Architecture
UI, Backend, and database reflect the same model. *
Faster Onboarding
New developers read the specs instead of guessing *
Safer Use Of AI
AI follows the spec and avoids random patterns. *
Higher Speed
AI handles repetitive tasks *
Lower Risk*
System Use Cases generate strong test coverage.

AI Is A Partner, Not A ReplacementAIUP assumes a skilled development team. Developers still:

  • Make architectural decisions
  • Review the generated code
  • Ensure security
  • Ensure performance
  • Adjust implementation details

AI accelerates work. Developers ensure correctness.

ConclusionSpec-Driven Development with AIUP provides teams with a structured approach to using AI. AI can generate code, tests, and diagrams that stay consistent with the specification.

In a Java stack using Spring Boot, jOOQ, and Vaadin, this approach is a natural fit. It increases speed, improves quality, and keeps the system aligned with business needs.

To get started:

  • Write the Requirements Catalog
  • Define the Entity Model
  • Add System Use Cases
  • Let AI generate code and tests
  • Review, refine, and repeat

This is how AIUP makes full-stack Java development faster, safer, and more consistent.

See it in ActionTwo webinar recordings show how to work with the AIUP: Spec-driven Development and Spec-driven Testing

The post Spec-Driven Development in Practice: How AI Simplify Full-Stack Java appeared first on JVM Advent.

View Details

Eclipse Collections is an open source Java Collections framework. In this blog I am going to demonstrate four lesser known features of the framework. I have published similar blogs in Java Advent Calendars of 2018, 2019, 2020, 2021, 2022, 2023, and 2024. Please refer to the resources at the end of the blog for more information about the framework. The newly published Eclipse Collections Categorically: Level up your programming game is a great book to dive deep in the design and methodology behind the iteration patterns of Eclipse Collections.

  1. groupByUniqueKey(): Eclipse Collections offers a way to transform and collect the output in a map, where the key is the transformed value. The groupByUniqueKey() API ensures that the generated keys must each be unique, or else an exception is thrown. This is an easy way to create a map from a collection and guarantee that no keys are overridden. @Testpublic void groupByUniqueKey() { MutableList list = Lists.mutable.of(1, 2, 3, 4); MutableMap<Integer, Integer> groupByUniqueKeyMap = list.groupByUniqueKey( each -> -1 * each); // Negate each integer MutableMap<Integer, Integer> expectedMap = Maps.mutable.of( -1, 1, //key, value pairs -2, 2, -3, 3, -4, 4); assertEquals(expectedMap, groupByUniqueKeyMap);}@Testpublic void groupByUniqueKey\_throws() { MutableList list = Lists.mutable.of(1, 2, 2, 3); // Throws exception because the element 2 is a duplicate assertThrows( IllegalStateException.class, () -> list.groupByUniqueKey(each -> each));}
  2. countByEach(): Eclipse Collections offers a way to count the number of occurrences of each value after transforming each element of a collection. The countByEach() API does it by iterating through the collection and applying the function to each element and then counting the number of occurrences of the transformed value. This API returns a Bag which is an optimized data structure for counting number of objects. This is similar to the countBy() API that I covered in the 2019 blog with the difference that in case of countByEach() the transformation function returns an Iterable @Testpublic void countByEach() { MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4); MutableBag<Integer> counts = list .countByEach( each -> Lists.mutable.of(each, each + 1)); assertEquals(1, counts.occurrencesOf(1)); assertEquals(2, counts.occurrencesOf(2)); assertEquals(2, counts.occurrencesOf(3)); assertEquals(2, counts.occurrencesOf(4)); assertEquals(1, counts.occurrencesOf(5));}
  3. sumBy*(): Eclipse Collections offers a way to group and sum elements of the collection using sumByInt(), sumByLong(), sumByDouble(), and sumByFloat() methods. A salient point to note is that the return types are up-casted i.e. sumByInt() returns a ObjectLongMap and sumByFloat() returns a ObjectDoubleMap. This avoids overflow issues in the results. // Common function to classify even and odd numbersFunction<Integer, String> mappingFunction = each -> { if (each % 2 == 0) { return "EVEN"; } return "ODD"; };@Testpublic void sumByInt() { MutableList list = Lists.mutable.of(1, 2, 3, 4); MutableObjectLongMap sumByInt = list.sumByInt( mappingFunction, each -> each); MutableObjectLongMap expected = ObjectLongMaps.mutable.of( "EVEN", 6L, //key, value pairs "ODD", 4L); assertEquals(expected, sumByInt);}@Testpublic void sumByLong() { MutableList list = Lists.mutable.of(1, 2, 3, 4); MutableObjectLongMap sumByLong = list.sumByLong( mappingFunction, Integer::longValue); MutableObjectLongMap expected = ObjectLongMaps.mutable.of( "EVEN", 6L, //key, value pairs "ODD", 4L); assertEquals(expected, sumByLong);}@Testpublic void sumByDouble() { MutableList list = Lists.mutable.of(1, 2, 3, 4); MutableObjectDoubleMap sumByDouble = list.sumByDouble( mappingFunction, Integer::doubleValue); MutableObjectDoubleMap expected = ObjectDoubleMaps.mutable.of( "EVEN", 6.0, //key, value pairs "ODD", 4.0); assertEquals(expected, sumByDouble);}@Testpublic void sumByFloat() { MutableList list = Lists.mutable.of(1, 2, 3, 4); MutableObjectDoubleMap sumByFloat = list.sumByFloat( mappingFunction, Integer::floatValue); MutableObjectDoubleMap expected = ObjectDoubleMaps.mutable.of( "EVEN", 6.0, //key, value pairs "ODD", 4.0); assertEquals(expected, sumByFloat);}
  4. aggregateBy(): Eclipse Collections offers a way to aggregate and group results into a map using a grouping function. Please note the API signature — the first input is the grouping function, second input is the zero value function, and the last input is the aggregation function. The code below shows how each of these inputs impacts the behavior. @Testpublic void aggregateBy() { MutableList list = Lists.mutable.of(1, 2, 3, 4); MutableMap<String, Integer> aggregateBy1 = list.aggregateBy( mappingFunction, () -> 0, Integer::sum); // Note that because the zero value is in this case 0, // the result is that // sum of even numbers is 0 + 2 + 4 = 6; // sum of odd numbers is 0 + 1 + 3 = 4 MutableMap<String, Integer> expected1 = Maps.mutable.of( "EVEN", 6, "ODD", 4); assertEquals(expected1, aggregateBy1); MutableMap<String, Integer> aggregateBy2 = list.aggregateBy( mappingFunction, () -> 10, Integer::sum); // Note that because the zero value is in this case 10, // the result is that // sum of even numbers is 10 + 2 + 4 = 16; // sum of odd numbers is 10 + 1 + 3 = 14 MutableMap<String, Integer> expected2 = Maps.mutable.of( "EVEN", 16, "ODD", 14); assertEquals(expected2, aggregateBy2); MutableMap<String, Integer> aggregateBy3 = list.aggregateBy( mappingFunction, () -> 0, (before, each) -> before + each + 10); // Note that because the aggregation function adds 10 to // every computation, the computation becomes: // for even numbers: 0 + (2 + 10) + (4 + 10) = 26; // for odd numbers: 0 + (1 + 10) + (3 + 10) = 24 MutableMap<String, Integer> expected3 = Maps.mutable.of( "EVEN", 26, "ODD", 24); assertEquals(expected3, aggregateBy3);}

Summary:In this blog I explained a few lesser known features of Eclipse Collections groupByUniqueKey(), countByEach(), sumBy*(), and aggregateBy(). I hope you found the post informative. If you have not used Eclipse Collections before, give it a try. There are few resources below. Make sure you show us your support and put a star on our GitHub Repository.Eclipse Collections ResourcesEclipse Collections comes with it’s own implementations of List, Set and Map. It also has additional data structures like Multimap, Bag and an entire Primitive Collections hierarchy. Each of our collections have a fluent and rich API for commonly required iteration patterns. Website * Source code on GitHub (Make sure to star the Repository*) * Contribution Guide * Reference Guide * Eclipse Collections Categorically: Level up your programming game The post Hidden Treasures of Eclipse Collections 2025 Edition appeared first on JVM Advent.

View Details

According to Wikipedia, a software supply chain is the components, libraries, tools, and processes used to develop, build, and publish a software artifact. The Java space provides thousands upon thousands of libraries that may be consumed as dependencies for building projects. Many of these libraries rely on Apache Maven as their build tool of choice, followed by Gradle and Apache Ant. There are of course a few other more, the Java ecosystem provides plenty of options (bld, JBang, etc).

While these build tools continue to improve their own development practices, their use alone is not enough to ensure that your artifacts, build pipelines, compiler tool chains, and other working parts of your software supply chain are secure and resistant to tampering or attacks. The Supply-chain Levels for Software Artifacts project, or SLSA (pronounced “salsa”) is a it’s a security framework, a checklist of standards and controls to prevent tampering, improve integrity, and secure packages and infrastructure. In its landing page you’ll find the following description regarding places where potential attacks may occur

© 2025 The Linux Foundation

Yes, attacks may come from many places but also happen at many locations within the software supply chain.

We must take action at different locations, fortunately the previously mentioned build tools provide some support but we need a few more things. Do you recall the news that shocked the IT world in December 2021? That’s right, it was Log4Shell. Or the failed XZ Backdoor from 2024? These and many other attacks could have been prevented or have their impact lessened if specific techniques, additional metadata, and tools were also available and applied on time.

Let’s begin with the straight forward ones, then move on with more complex options. But before we do, I would like to remark that JReleaser, while not strictly a build tool but rather a release tool, provides support for all of the features we’ll cover. Onward.

Reproducible ArtifactsReproducible artifacts come from Reproducible Builds, a software development practice where building the exact same source code with the same tools and instructions always results in a bit-for-bit identical binary output. If you happen to have a compatible reproducible environment (similar or identical OS, Java distribution, environment variables, etc) then you may rebuild a given codebase at an specific point of its history (say a tagged release) and obtain a bit-by-bit identical set of artifacts that may be compared at different build dates.

Luckily for many Java developers, Maven makes it quite easy to generate reproducible artifacts (JARs, ZIPs, TARs) by simply setting a fixed timestamp. Gradle offers similar capabilities although the value of the timestamp can’t be changed. JReleaser also lets you assemble reproducible archives with any of its assemblers.

For example, this repository contains a simple Helloworld project. Its build creates an executable JAR file, while its release configuration defines a binary distribution as a ZIP file. Here’s a short snippet of said configuration

assemble: javaArchive: helloworld: active: ALWAYS formats: [ ZIP ] fileSets: - input: '.' includes: [ 'LICENSE' ] mainJar: path: target/{{distributionName}}-{{projectVersion}}.jar Invoking the assemble command results in archive containing the following entries:

Archive: java-archive/helloworld-1.0.0.zipLength Date Time Name--------- ---------- ----- ---- 11357 01-06-2025 19:30 helloworld-1.0.0/LICENSE 4533 01-06-2025 19:30 helloworld-1.0.0/bin/helloworld 1889 01-06-2025 19:30 helloworld-1.0.0/bin/helloworld.bat 3777 01-06-2025 19:30 helloworld-1.0.0/lib/helloworld-1.0.0.jar--------- ------- 21556 4 files That timestamp may look arbitrary but its guaranteed to be reproducible, as it happens to match the tagged commit for that release, which is

commit eaa60a9314e3555db6e44da8442808d46f2fcd35Author: Andres Almiray <aalmiray@gmail.com>Date: Mon Jan 6 19:30:58 2025 +0100 Neat, isn’t it? Let’s continue.

Digital SignaturesAnother way to add an extra layer of security is by providing digital signatures for a given set of artifacts. We’ve been using PGP for decades since its inception. Pretty much every build tool supports generating these kind of signatures with either explicit mechanism (plugins) or calling out to external commands (gnupg for example).

This works fine, except when it doesn’t, which is when key management comes into play. Some developers are lazy and set their keys to never expire, while some are too concerned that they rotate their keys quite often. No matter where you stand in the key management spectrum, it requires performing additional tasks as a burden. For this reason, a new idea emerged a bit more than a decade ago: Sigstore.

Sigstore provides an alternative for automating artifact signing and signature verification. While the signing tool was originally created for signing container images (hence the name cosign) and written in Go, nowadays there’s a Java API that enables Maven and Gradle support. JReleaser in turn also supports generating signatures for all artifacts to be released as either PGP or Sigstore, making it easier to provide such signatures as it doesn’t matter how those artifacts were built/assembled or which tool did it.

Enabling digital signatures for a release, whether with PGP or Sigstore, is quite easy with JReleaser, for example, the minimum configuration would be something similar to

signing: active: ALWAYS armored: true Where’s the rest? Well, the values of the public & secret keys, as well as an associated passphrase (when required) may be supplied via environment variables, to keep these value as secrets.

SBOMs and SWID TagsThe next level of protection is comprised of additional metadata files that may be used to inspect an artifact for its set of dependencies, or in the case of an archive, for its contained files. By now I hope you’ve heard of SBOMs and how to procure them during a build. SBOMs have become more important these days as software vendors and makers will be required to provide them upon request, according to CRA and DORA.

Both Maven and Gradle provide plugins that generate SBOMs in different formats. You may also use Syft to create such files. These SBOMs should provide a list of all dependencies required for building the matching artifact, a JAR file in this case. But what about ZIPs and TARs, or other kind of archives? In this case JReleaser can generate SBOMs for all archives created via any of its assemblers.

Here’s a snippet showing how you can configure SBOM generation for artifacts using both cyclonedx and Syft. The resulting SBOMs will be packaged in a single ZIP and will be available to be uploaded as release assets

catalog: sbom: cyclonedx: active: ALWAYS pack: enabled: true name: '{{projectName}}-{{projectVersion}}-cyclonedx-sboms' syft: active: ALWAYS pack: enabled: true name: '{{projectName}}-{{projectVersion}}-syft-sboms' You only need one format or the other, this particular example shows how easy is to configure either format.

Additionally to SBOMs there’s also SWID Tags, created by the National Institute of Standards and Technology (NIST). This format provides a thorough list of all entries found inside a given archive, with a checksum per entry. Computing both SBOMs and SWID Tags provides a level of redundancy as the same checksum values should appear in both files, making it harder for an attacker to fake values. Also, you may be contractually obligated to deliver such files depending on the type of projects and customers you may be working with. As far as I recall there’s a Maven plugin for generating SWID tags but no Gradle plugin.

Activating SWID tag generation for assembled archives is as easy as configuring the following snippet in your JReleaser configuration file

catalog: swid: swid-tag: active: ALWAYS Now, adding SWID tag to a given assembly is done as follows

assemble: javaArchive: helloworld: active: ALWAYS formats: [ ZIP ] fileSets: - input: '.' includes: [ 'LICENSE' ] mainJar: path: target/{{distributionName}}-{{projectVersion}}.jar swid: tagRef: swid-tag Notice the reference to swid-tag, which is the custom name that we defined in a previous snippet.

Attestation FilesAttestations bind some subject (a named artifact along with its digest) to a predicate (some assertion about that subject) using the in-toto format. Predicates consist of a type URI and a JSON object containing type-dependent parameters.

Attestation files are usually digitally signed, and guess what, this is where Sigstore appears again. Attestation providers also offer a command line tool that may be used to verify the signatures of the attestation files, as well as their contents.

If you happen to use GitHub (whether free or enterprise) then you have access to GitHub Attestations. Making use of this feature is straight forward and requires you to use the actions/attest action. There are a few ways to supply the list of files to be included for attestation, one of them is a checksums file. It so happens that JReleaser automatically calculates such a file when a release is performed, or when the checksum command is explicitly invoked. This makes it quite easy to combine it with the actions/attest action, like so

- name: Release uses: jreleaser/release-action@v2 with: arguments: release env: # change this value to your own version number JRELEASER\_PROJECT\_VERSION: 1.2.3- name: Attestations uses: actions/attest-build-provenance@v1 with: subject-checksums: out/jreleaser/checksums/. checksums\_sha256.txt predicate-type: 'https://example.com/predicate/v1' predicate: '{}' The SLSA framework also provides its own attestation feature. You may run it directly from GitHub via the slsa-framework/slsa-github-generator, for which you’d need to configure more things depending on your build requirements. Luckily the SLSA team made things easier by enabling other tools to create their own sanctioned SLSA builders (BYOB), as explained in this link.

JReleaser is an early adopter of the BYOB feature, providing a seamless integration with GitHub Actions. The jreleaser/helloworld-java-slsa repository showcases how this integration works. There is one thing to take into consideration, the SLSA builder must be provided with both build and release instructions, as they must happen within a controlled environment. I said earlier that JReleaser is a release tool, not a build tool, however it does allow custom commands to be invoked at certain stages of its execution. Thus if we’re able to supply build instructions that are guaranteed to be invoked before a release then we should be good to go. And that’s exactly what we can do.

The release configuration file in the helloworld-java-slsa repository has a section specifying the required build instructions, and that they should be invoked during the assemble step.

hooks: script: before: - run: './mvnw -ntp verify' condition: '"{{ Env.CI }}" == true' verbose: true filter: includes: ['assemble'] Now, the next bit of configuration is found in a GitHub Actions workflow such as this one

release: name: Release needs: [ precheck ] permissions: contents: write id-token: write actions: read packages: write uses: jreleaser/jreleaser-slsa/.github/workflows/builder\_slsa3\_java.yml@v1.1.0 with: project-version: ${{ needs.precheck.outputs.VERSION }} rekor-log-public: true secrets: github-token: ${{ secrets.GITHUB\_TOKEN }} This snippet defines specific permissions granted to the default GITHUB_TOKEN, such that the invoked trusted workflow (builder_slsa4_java.yml) can not access more than it shouldn’t. This trusted workflow will in turn invoke the SLSA generator and make sure that the attestation file is attached as a release asset. It all just happens automagically as seen in this release:

And tough this is a Java Advent entry, I would be remiss it I did not mention that JReleaser can be used with any kind of project independent of its source code, its not just for Java. Likewise, the SLSA support offered by JReleaser may be used with Go, Rust, and Zig, with additional languages coming later.

SummarySecuring your Software Supply Chain requires a paradigm shift, adapting build and releases processes, learning new tools and techniques. The build tools you’re already used to may provide answers to some of these concerns, while JReleaser provides additional support, specifically during the release portion of the chain.

The post Strengthening your Software Supply Chain appeared first on JVM Advent.

View Details

It seems like it’s become a tradition that I announce I have joined a new company for the Java Advent of Code. At least this time it’s actually an old friend: I am excited to be back at Red Hat, in the llm-d team! Does that mean I forgot about Java? Of course not. If anything, this is an opportunity to learn more about GPU programming, and since Java is my comfort-zone language, what better occasion than looking into TornadoVM?

Recently, the TornadoVM team released gpullama3, a proof-of-concept demonstrating LLM inference on GPUs using pure Java. Let’s explore this together!

What is TornadoVM?TornadoVM is a plugin for the OpenJDK that enables Java programs to automatically run on heterogeneous hardware (GPUs, FPGAs, and multi-core CPUs) using standard Java code annotated for parallel compute.

Under the hood, TornadoVM:

  1. takes your Java bytecode
  2. compiles it to GPU-specific kernels (via OpenCL, PTX, or SPIR-V)
  3. manages data transfers between CPU and GPU memory
  4. executes the computation on the GPU
  5. returns the results back to your Java program

Transformer-based language models are computationally expensive but highly-parallelizable. At inference time, generating each token requires matrix multiplications across billions of parameters. GPUs excel at these operations because they can perform thousands of computations in parallel. Thus, TornadoVM is the perfect tool for this kind of workload.

Installing TornadoVMInstalling GPULlama3.java was surprisingly straightforward. Make sure you have your favorite flavor of JDK 21 installed. I use sdkman, so I made sure I had Temurin 21 installed:

sdk install java 21.0.9-tem Then you’ll want to make sure you have installed cmake, a C/C++ toolchain, Python and pip. Now you can clone the repo:

git clone https://github.com/beehive-lab/GPULlama3.java and follow the instructions on the README; for instance, on macOS/Linux:

```

Enter the TornadoVM submodule directorycd external/tornadovm# Optional: Create and activate a Python virtual environment if neededpython3 -m venv venvsource ./venv/bin/activate# Install TornadoVM with a supported JDK 21 and select the backends (--backend opencl,ptx).# To see the compatible JDKs run: ./bin/tornadovm-installer --listJDKs# For example, to install with OpenJDK 21 and build the OpenCL backend, run: ./bin/tornadovm-installer --jdk jdk21 --backend opencl# Source the TornadoVM environment variablessource setvars.sh

``` You can verify the installation was successful by running one example:

cd tornado-examplesmvn packagecd ..tornado -cp tornado-examples/target/tornado-examples-1.1.2-dev-e1d2d12.jar uk.ac.manchester.tornado.examples.compute.MatrixVectorRowMajor Of course, make sure you replace tornado-examples-1.1.2-dev-e1d2d12.jar with the right jar name! Your output should look something like this:

WARNING: Using incubator modules: jdk.incubator.vectorMatrix-Vector Multiplication Benchmark======================================Configuration:- Input dimension (columns): 8192- Output dimension (rows): 2048- Local work group size: 32- Backend: OPENCL- DP4A benchmarks enabled: false- Warmup iterations: 140- Benchmark iterations: 120Initializing data...Setting up TornadoVM execution...Warming up sequential implementation...Benchmarking sequential implementation...Warming up parallel implementation...Benchmarking parallel implementation...Validating results...Validation PASSED ✓Performance Results:====================Matrix size: 2048 x 8192Sequential Implementation: Average time: 15.892 ms Min time: 15.673 ms Max time: 16.795 ms Performance: 2.11 GFLOP/sParallel Implementation (TornadoVM): Average time: 2.405 ms Min time: 2.030 ms Max time: 5.069 ms Performance: 13.95 GFLOP/sPure TornadoVM @Parallel Implementation (TornadoVM): Average time: 4.931 ms Min time: 3.745 ms Max time: 8.357 ms Performance: 6.81 GFLOP/sParallel Implementation FP16 (TornadoVM): Average time: 1.840 ms Min time: 1.575 ms Max time: 3.090 ms Performance: 18.24 GFLOP/sQ8 Vectorized: Average time: 1.746 ms Min time: 1.459 ms Max time: 6.097 ms Performance: 19.22 GFLOP/sSpeedup: KernelContext vs Java 6.61xSpeedup: @Parallel vs Java 3.22xSpeedup: KernelContext vs @Parallel 2.05xSpeedup: Q8 Vectorized vs KernelContext 1.38xSpeedup: Q8 Vectorized vs KernelContext FP16 1.05x Baby’s First GPU KernelHere’s a simple Example.java:

import uk.ac.manchester.tornado.api.annotations.*;import uk.ac.manchester.tornado.api.*;import uk.ac.manchester.tornado.api.enums.DataTransferMode;import uk.ac.manchester.tornado.api.types.arrays.FloatArray;public class Example { public static void vectorMul(FloatArray a, FloatArray b, FloatArray result) { for (@Parallel int i = 0; i < result.getSize(); i++) { result.set(i, a.get(i) * b.get(i)); } } public static void main(String... args) { int size = 1024; var a = new FloatArray(size); var b = new FloatArray(size); var result = new FloatArray(size); for (int i = 0; i < size; i++) { a.set(i, i * 2.0f); b.set(i, i + 1.0f); } var taskGraph = new TaskGraph("multiply") .transferToDevice(DataTransferMode.FIRST\_EXECUTION, a, b) .task("vectorMul", Example::vectorMul, a, b, result) .transferToHost(DataTransferMode.EVERY\_EXECUTION, result); var snapshot = taskGraph.snapshot(); new TornadoExecutionPlan(snapshot).execute(); System.out.println("\nVector Multiplication Results (first 10):"); for (int i = 0; i < 10; i++) { System.out.printf("result[%d] = %.2f (a[%d]=%.2f * b[%d]=%.2f)\n", i, result.get(i), i, a.get(i), i, b.get(i)); } }} The @Parallel annotation tells TornadoVM this loop can be parallelized. The TaskGraph API manages data movement and execution scheduling. You can compile it with the following (if you followed the installation guide correctly $TORNADO_SDK will point to the right path):

javac -g --enable-preview -source 21 -cp "$TORNADO\_SDK/share/java/tornado/*" Example.java Notice that -g is required for this to work correctly. Now you can run it with:

tornado Example It will print the first 10 items in the resulting vector.

Playing with GPULlama3.javaThe gpullama3 project demonstrates running a Llama 3 model entirely in Java with GPU acceleration. Assuming you are back at the root of the repo, continue with the setup procedure.

```

Navigate back to the project root directorycd ../../# Source the project-specific environment paths -> this will ensure the correct paths are set for the project and the TornadoVM SDK# Expect to see: [INFO] Environment configured for Llama3 with TornadoVM at: /home/YOUR_PATH_TO_TORNADOVMsource set_paths# Build the project using Maven (skip tests for faster build)# mvn clean package -DskipTests or just makemake

``` Now let’s download a compatible model using the HuggingFace CLI:

```

download and install the hugging face CLIpip install -U huggingface_hub# download a model to ./models/hf download beehive-lab/Llama-3.2-1B-Instruct-GGUF-FP16 --include '*.gguf' --local-dir models

``` Try it! Even on my poor MacBook Air with 8 GB RAM (provided I don’t have too many applications open) this returns:

❯ python llama-tornado --gpu --verbose-init --opencl --model models/Llama-3.2-1B-Instruct-FP16.gguf --prompt "tell me a joke"WARNING: Using incubator modules: jdk.incubator.vectorLoading model weights in TornadoVM format (loading F16)Starting TornadoVM initialization...TornadoVM GPU execution plan creation: 1011.56 msJava to GPU JIT compiler warmup: 4994.25 msTransfer read-only weights to GPU: 13958.97 msFinished TornadoVM initialization...Here's one:What do you call a fake noodle?(wait for it...)An impasta!Hope that made you laugh!achieved tok/s: 3.00. Tokens: 42, seconds: 13.98 Disclaimer: even if you have better CPU/GPUs at your disposal, they are unlikely to affect the quality of the joke.

What just happened?GPULlama3.java currently supports a few FP16 (16-bit floating point) and 8-bit quantized models:

  • Llama 3.2 (1B) – FP16
  • Llama 3.2 (3B) – FP16
  • Llama 3 (8B) – FP16
  • Mistral (7B) – FP16
  • Qwen3 (0.6B) – FP16
  • Qwen3 (1.7B) – FP16
  • Qwen3 (4B) – FP16
  • Qwen3 (8B) – FP16
  • Phi-3-mini-4k – FP16
  • Qwen2.5 (0.5B)
  • Qwen2.5 (1.5B)
  • DeepSeek-R1-Distill-Qwen (1.5B)

Depending on the model being selected, a different execution plan will be built. The execution plan corresponds to the model architecture. In our case, we picked the unquantized Llama 3.2 1B FP16. Let’s take a look at the setupTornadoForwardPlan() method in FP16LayerPlanner, used by LLama 3.2:

abstract class FP16LayerPlanner ... { ... protected final void setupTornadoForwardPlan() { List<ImmutableTaskGraph> allTaskGraphs = new ArrayList<>(); GridScheduler masterScheduler = new GridScheduler(); // 1. Activation layer (common to all models) allTaskGraphs.add(activationLayer.getImmutableTaskGraph()); activationLayer.updateGridScheduler(masterScheduler); // 2. FFN layers (N transformer layers - model-specific) allTaskGraphs.addAll(ffnLayers.getFfnLayerTaskGraphs()); ffnLayers.updateGridScheduler(masterScheduler); // 3. Logits layer (common to all models) allTaskGraphs.add(logitsLayer.getTaskGraph().snapshot()); logitsLayer.updateGridScheduler(masterScheduler); // Cache for future retrievals this.immutableTaskGraphs = allTaskGraphs; this.gridScheduler = masterScheduler; }} In the Activation layer we mostly look up token embeddings and apply an initial normalization step, while the Logit layer is where we convert the model’s internal representation into token predictions. So let’s concentrate a bit more on the Feed-Forward Network layer (FFN), and in particular on the Attention implementation. The LlamaFP16FFNLayers#setupSingleFFNLayer method is a bit cryptic at a first glance; let’st start from its signature:

TaskGraph setupSingleFFNLayer(LlamaTornadoWeights weights, Configuration config, int layerIndex) The method is building a TaskGraph, essentially describing the data flow of our GPU kernels. Let’s focus on QKV and attention, using Sebastian Raschka1‘s excellent Python Notebook as a reference. The following is the architecture diagram of the Llama 3.2 1B model:

For obvious reasons of brevity, we aren’t going to explore this in detail, but we do want to take a look at the implementation of the attention heads. In particular, let’s take a look at how we compute the Query, Key, Value matrices (Q,K,V = project(x) in the Python version):

.task("qmatmul", TransformerComputeKernelsLayered::matrixVectorGeneric, context, state.wrapXb, state.wrapQ, weights.wqLayered[layerIndex].asHalfFloatArray(), config.dim(), config.dim(), LOCAL\_WORK\_GROUP\_SIZE\_ALLOC).task("kmatmul", TransformerComputeKernelsLayered::matrixVectorGeneric, context, state.wrapXb, state.wrapK, weights.wkLayered[layerIndex].asHalfFloatArray(), config.dim(), config.kvDim(), LOCAL\_WORK\_GROUP\_SIZE\_ALLOC).task("vmatmul", TransformerComputeKernelsLayered::matrixVectorGeneric, context, state.wrapXb, state.wrapV, weights.wvLayered[layerIndex].asHalfFloatArray(), config.dim(), config.kvDim(), LOCAL\_WORK\_GROUP\_SIZE\_ALLOC) This is followed by the RoPE rescaling to encode token positions:

.task("rope", TransformerComputeKernelsLayered::ropeRotation, context, state.positionHolder, state.wrapQ, state.wrapK, config.kvDim(), config.headSize()) Now we are ready to compute attention. The generic version (there is also an NVidia-specific implementation) is:

unifiedLayer.task("parallel-attention", TransformerComputeKernelsLayered::processHeadsParallel, state.wrapQ, state.wrapKeyCache, state.wrapValueCache, state.wrapXb, config.numberOfHeads(), config.headSize(), config.kvDim(), config.kvMul(), config.contextLength(), state.positionHolder, state.wrapAtt, layerIndex, config.contextLength()); Let’s drill down into TransformerComputeKernelsLayered::processHeadsParallel to see how that is performed. The following is one of the GPU kernels. It essentially computes:

You will notice that the method:

  1. takes the Q,K (Query, Key) vectors that we computed earlier and it computes the attention score up to the current position pos (scaled by the square root of the head size), filling the wrapAtt vector
  2. next, it applies softmax to the wrapAtt, turning the scores into attention weights (steps 2-4), accumulating partial results onto the same wrapAtt vector
  3. finally (step 5) it computes a weighted sum of the Value vectors (value_cache) up to pos, using the calculated Attention Weights (wrapAtt).

/** * Computes attention for a single head. Implements scaled dot-product attention with softmax normalization. * * Steps: 1. Compute attention scores: Q·K / sqrt(head\_size) 2. Apply softmax (with max subtraction for numerical stability) 3. Compute weighted sum of values * * @param allQ * All query vectors * @param key\_cache * Cached keys * @param value\_cache * Cached values * @param allXb * Output buffer * @param h * Head index to process * @param headSize * Dimension per head * @param kvDim * Key/value dimension * @param kvMul * Key multiplier for grouped attention * @param loff * Layer offset in cache * @param pos * Current position * @param wrapAtt * Attention weights buffer */ private static void processHeadTornado(FloatArray allQ, FloatArray key\_cache, FloatArray value\_cache, FloatArray allXb, int h, int headSize, int kvDim, int kvMul, long loff, int pos, FloatArray wrapAtt) { // Base index for this head's attention weights int headOffset = h * (pos + 1); // STEP 1: Calculate attention scores for all timesteps for (int t = 0; t <= pos; t++) { int kvHeadIdx = h / kvMul; int keyOffset = (int) (loff + t * kvDim + kvHeadIdx * headSize); float score = 0.0f; for (int i = 0; i < headSize; i++) { score += allQ.get(h * headSize + i) * key\_cache.get(keyOffset + i); } score = score / TornadoMath.sqrt(headSize); // Store in attention buffer wrapAtt.set(headOffset + t, score); } // STEP 2: Find max score for softmax stability float maxScore = wrapAtt.get(headOffset); for (int t = 1; t <= pos; t++) { float val = wrapAtt.get(headOffset + t); if (val > maxScore) { maxScore = val; } } // STEP 3: Compute exponentials and sum float sum = 0.0f; for (int t = 0; t <= pos; t++) { int idx = headOffset + t; float expScore = TornadoMath.exp(wrapAtt.get(idx) - maxScore); wrapAtt.set(idx, expScore); sum += expScore; } // STEP 4: Normalize float normFactor = (sum > 0.0f) ? (1.0f / sum) : (1.0f / (pos + 1)); for (int t = 0; t <= pos; t++) { int idx = headOffset + t; wrapAtt.set(idx, wrapAtt.get(idx) * normFactor); } // STEP 5: Compute weighted sum of values for each dimension for (int i = 0; i < headSize; i++) { float weightedSum = 0.0f; for (int t = 0; t <= pos; t++) { int kvHeadIdx = h / kvMul; int valueOffset = (int) (loff + t * kvDim + kvHeadIdx * headSize); weightedSum += wrapAtt.get(headOffset + t) * value\_cache.get(valueOffset + i); } allXb.set(h * headSize + i, weightedSum); } } After the attention mechanism computes relationships between tokens, the result is added to the original input, normalized, and passed through a feed-forward network. This process repeats across multiple layers before finally producing the next-token prediction (the logit layer).

Because it’s an autoregressive model, this entire process repeats for each token, using the previously generated sequence as input.

In short, TornadoVM handled GPU compilation and execution transparently, allowing a pure Java program to perform LLM inference!

ConclusionsWe’ve completed our whirlwind tour of Llama3GPU.java and TornadoVM. If your head is still spinning, don’t worry, you’re not alone! It’s a lot to take in, but I hope this post has sparked your interest and inspired you to dig deeper: I know I will!

  1. Sebastian Raschka is the author of Build a Large Language Model from Scratch ↩︎

The post A Glance at GPU Goodness in Java: LLM Inference with TornadoVM appeared first on JVM Advent.

View Details

— Preparing for the Quantum Shift

Table of Contents The EU’s Post-Quantum Plan: Recommendation (EU) 2024/1101 + What will upcoming EU cybersecurity legislation require? And how can Java help you prepare? + Why this is significant for architects? + Java advisory note * NIS2 — Cryptography as Organizational Governance + Engineering Implications + Java advisory note * DORA — Operational Resilience + Engineering Implications + Java advisory note * Cyber Resilience Act — Secure-by-Design Software + Core Objectives + Engineering Implications + Java advisory note * Standards & Certification — ENISA EUCC, ACM v2, ETSI TS 103 744 + ENISA & EUCC + ETSI TS 103 744 — The Foundation of Hybrid TLS + Java advisory note * National Implementations — Germany and France * Practical Checklist for Java-Based Systems + For CTOs and Security Architects + For Platform Teams + For Java Developers * Conclusion * References Over the next decade, European cryptographic systems will undergo a significant transformation similar to the change from DES to AES and SSL to TLS. The converging forces of regulatory requirements, post-quantum research, and market pressure are set to redefine software security. This is particularly important for organizations in sectors such as finance, critical infrastructure, cloud services, public administration, and large-scale digital platforms. This is not optional:* it marks the beginning of a significant architectural shift that will impact nearly every system with a security boundary.

At the center of this shift lies a simple but powerful reality: The cryptography we rely on today will not be sufficient for the systems we build for tomorrow.

The European Union is preparing for this future through a combination of high-impact legislation (NIS2, DORA, the Cyber Resilience Act), forward-looking strategic documents (the Post-Quantum Cryptography Roadmap), and emerging standards (ETSI hybrid key-exchange profiles, ENISA’s EUCC and ACM mechanisms). Together, these frameworks make one expectation unmistakable: systems must be crypto-agile, updatable, and robust in the face of long-term cryptographic threats—including those posed by quantum computing.

For technical leaders, this means the timeline for learning about post-quantum transition and cryptographic lifecycle management is not “sometime in the 2030s”—it is right now. System designs created today must remain defensible five, ten, or fifteen years from now. Cryptographic decisions made today will determine whether your systems remain compliant, secure, and operational in the era of hybrid and post-quantum algorithms.

This article is written to help you navigate what comes next. It does three things:

  1. It explains the major EU legislative and strategic changes that will require system upgrades over the coming years, what they mean, why they matter, and how they will influence long-term architecture.
  2. It provides a practical advisory note for Java practitioners, showing how the JVM has quietly been preparing for this future through a series of foundational security enhancements.
  3. It highlights recent Java features—KEM/KDF APIs, modern PEM support, hybrid TLS efforts, and PQC-related proposals and explains how they enable crypto agility and long-term resilience. This is not about compliance checklists or abstract policy. It is about engineering: how to build systems today that will still be secure and supportable a decade from now.

Java, with its modern cryptographic architecture, provider-based extensibility, and emerging support for hybrid and PQC operations, offers a powerful foundation for this transition. Many of the tools you need—structured KEM/KDF APIs, improved certificate handling, modular cryptographic providers, and simplified security models—already exist in current JDKs or are in active development.

The coming years will challenge our assumptions about how cryptography is integrated into software systems. But with the right understanding—and the right platform choices—you can position your systems to not just meet regulatory expectations, but to be resilient in a security landscape that is about to change more rapidly than any time in recent memory.

The EU’s Post-Quantum Plan: Recommendation (EU) 2024/1101What will upcoming EU cybersecurity legislation require? And how can Java help you prepare?Between the Post-Quantum Cryptography (PQC) transition, the NIS2 Directive, the DORA Regulation, and the Cyber Resilience Act (CRA), the EU is moving toward a future in which:

  • Cryptography must be upgradable,
  • Systems must be resilient against long-term threats, and
  • Software products must be secure-by-design and maintainable for years. If you design, operate, or oversee systems that must endure well into the 2030s, these legal changes are not mere theoretical concepts; they are integral components of your engineering strategy. They affect protocol choices, PKI design, cryptographic libraries, update processes, compliance documentation, and runtime platform capabilities.

On 11 April 2024, the European Commission issued Recommendation (EU) 2024/1101 on a “Coordinated Implementation Roadmap for the transition to Post-Quantum Cryptography” [1]. It instructs Member States to “develop and implement a harmonised approach as the Union transitions to post-quantum cryptography.” [2]

Furthermore, “Within two years of this Recommendation, the NIS Cooperation Group should develop a coordinated implementation roadmap for the transition to post-quantum cryptography.” [3]

This Roadmap was subsequently published on 23 June 2025 [4].

The Roadmap defines phases, not hard deadlines.

These phases include guidance such as:

  • By the mid-2020s: build governance, inventory crypto assets, identify exposure to “harvest-now-decrypt-later”, and launch PQC pilots [5].
  • “Around 2030”: high-risk and high-value systems should use quantum-safe or hybrid cryptography—this phrasing appears in public commentary, not as an exact quote from the official text [6].
  • “Beyond 2030”: wider system migration—again, this is interpretive, based on analyses that infer the long-tail implications of the roadmap [6]. Note: To be precise, the recommendation does not explicitly state “2030” or “2035”.

Why this is significant for architects?Even without binding deadlines, the roadmap clearly signals that:

  • PQC migration will be expected for critical systems,
  • Hybrid cryptography will act as the transition strategy,
  • Long-term systems must be crypto-agile, not locked into fixed primitives. Java advisory noteJava offers several essential building blocks that are crucial for this transition:

  • JEP 452 — Key Encapsulation Mechanism (KEM) API (Delivered: Java 21): clean provider-based integration point for PQC KEMs.

  • JEP 510 — KDF API (Delivered: Java 25): necessary for combining classical and PQ secrets in hybrid schemes.
  • JEP 524 — PEM Encodings (Preview / In development: Integrated: Java 26): modern handling of keys/certificates required for PQC formats.
  • JEPs 496 / 497 — Quantum-Resistant Cryptography (Delivered: Java 24): Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism & Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm
  • Hybrid TLS Key Exchange (Under development: Candidate; JEP 527 — Post-Quantum Hybrid Key Exchange for TLS 1.3): draft efforts to enhance the security of Java applications that require secure network communication by implementing hybrid key exchange algorithms for TLS 1.3. Back to top

NIS2 — Cryptography as Organizational GovernanceThe NIS2 Directive—Directive (EU) 2022/2555—was adopted on 14 December 2022 and published on 27 December 2022 [7]. Its purpose is “to achieve a high common level of cybersecurity across the Union.” [7]

Member States were required to transpose NIS2 by 17 October 2024 [8].

NIS2 applies to essential and important entities across sectors such as:

  • Energy, transport, water, healthcare, banking,
  • Financial market infrastructures,
  • Digital infrastructure, managed services,
  • Public administration, and more. Most relevant to system architects is NIS2’s requirement to define “policies and procedures regarding the use of cryptography and, where appropriate, encryption.” [7]

Engineering ImplicationsNIS2 does not name algorithms. Instead, it demands:

  • Documented cryptographic policies,
  • Upgradeability strategies,
  • Lifecycle management for keys and algorithms,
  • Demonstrable resilience against known risks. This means:

  • Fixed, non-upgradable TLS stacks become a liability.

  • Systems must prepare for algorithm deprecation cycles.
  • Quantum-vulnerable algorithms must be assessed as part of risk management. Java advisory noteRecent Java features support NIS2 expectations:

  • The KEM/KDF APIs make algorithm agility feasible.

  • Modern PEM support simplifies adoption of evolving certificate profiles.
  • Forward-looking hybrid TLS implementations allow for controlled, gradual migration.
  • Removal of the Security Manager (JEP 486) reduces the number of brittle, hard-to-audit legacy security layers. Back to top

DORA — Operational ResilienceRegulation (EU) 2022/2554 (DORA) became applicable on 17 January 2025 [9]. It states that “ICT systems support complex systems used for everyday activities.” [9]

DORA mandates:

  • Continuous ICT risk management,
  • ICT incident reporting,
  • Operational resilience testing,
  • Oversight of critical ICT third-party providers. Engineering ImplicationsDORA requires institutions to demonstrate:

  • Resilience even under cryptographic degradation,

  • Preparedness for long-term confidentiality threats,
  • Robust key management and secure communications,
  • Crypto agility and migration planning. Many financial infrastructures rely heavily on Java. This means:

  • Java-based systems will be evaluated as part of ICT operational resilience.

  • Quantum-resilience will increasingly be seen as part of ICT risk, not theoretical research. Java advisory noteJava’s modern crypto stack helps architects meet DORA’s expectations:

  • KEM/KDF abstractions enable controlled PQ/HKDF migration designs.

  • Hybrid TLS (draft features—exact JEP number requires verification) reduces exposure to “harvest now, decrypt later” attacks.
  • Proposed PQ algorithms (JEPs 496/497) enable early integration testing. Back to top

Cyber Resilience Act — Secure-by-Design SoftwareThe Cyber Resilience Act—Regulation (EU) 2024/2847—entered into force on 10 December 2024. Most obligations take effect from 11 December 2027 [11]. CRA declares, “Cybersecurity is one of the key challenges for the Union.” [11]

Core ObjectivesIt applies to all products with digital elements, demanding:

  1. Secure-by-design and secure-by-default development practices
  2. Mandatory vulnerability management and disclosure processes
  3. Obligatory security update mechanisms
  4. Regulation of cybersecurity risks across the entire lifecycle of digital products
  5. Increase transparency and accountability for vendors and manufacturers Engineering ImplicationsCRA influences platform choices due to its impact on user experience and engagement:

  6. Cryptography must be updatable throughout the product’s lifecycle.

  7. Security controls must be auditable.
  8. The architecture must be maintainable for years to come. Java advisory noteJava provides:

  9. Removal of the outdated Security Manager, improving auditability;

  10. Standardized crypto APIs that enable algorithms to evolve;
  11. Modern TLS and PEM handling for future profiles. These do not “make you CRA-compliant,” but they make compliance architecturally achievable.**

Back to top

Standards & Certification — ENISA EUCC, ACM v2, ETSI TS 103 744ENISA & EUCCThe EU Cybersecurity Act (Regulation (EU) 2019/881) established the EU cybersecurity certification framework [13]. Under it, ENISA published the EUCC Guidelines on Cryptography in July 2024 [14].

These guidelines recommend that developers use the Agreed Cryptographic Mechanisms version 2 (ACM v2) for certified products [15].

Public analyses confirm that ACM v2 introduces hybrid and quantum-safe mechanisms for high-assurance contexts [16].

ETSI TS 103 744 — The Foundation of Hybrid TLSETSI’s Quantum-Safe Cryptography working group authored TS 103 744, which defines “quantum-safe hybrid key establishment schemes pairing a high-assurance but quantum-vulnerable key exchange with a quantum-safe key encapsulation mechanism.” [17]

The updated version 1.2.1 describes itself as offering “a clear, testable, and interoperable path to deploy hybrid key establishment today.” [19]

Java advisory noteDraft and prototype implementations of hybrid TLS in Java follow this ETSI pattern:

  • Classical ECDHE + PQC KEM → combined via a KDF
  • Using provider abstractions defined in JEP 452/510. Back to top

National Implementations — Germany and FranceGermany: Draft law NIS2UmsuCG for NIS2 implementation [20] and FinmadiG for DORA and MiCAR alignment [21].

France: The Projet de loi relatif à la résilience des infrastructures critiques et au renforcement de la cybersécurité was adopted in first reading by the Senate on 12 March 2025 [23].

These national laws that dictate how supervisory authorities will assess systems in real-world scenarios transform EU obligations into auditable operational requirements:

  • Supervisory authorities can request evidence that systems support cryptographic evolution.
  • Algorithms must be replaceable without requiring significant code rewrites.
  • TLS configurations must be compatible with hybrid and post-quantum cryptographic mechanisms.
  • PKI infrastructures must be able to handle new certificate profiles.
  • Legacy security designs must be replaced with maintainable and testable architectures. Back to top

Practical Checklist for Java-Based SystemsFor CTOs and Security Architects Map your systems to NIS2, DORA, CRA applicability. * Identify sensitive data requiring long-term confidentiality. * Build a crypto-agility roadmap aligned with PQC phases. For Platform Teams Plan pilots of hybrid TLS and roadmap-aligned cryptography. * Transition away from legacy, hard-coded crypto. * Establish internal standards for provider-based crypto. For Java Developers Familiarize yourself with KEM/KDF APIs and PEM updates. * Avoid bespoke crypto; rely on JCA/JCE where possible. * Track PQC-related JEPs* (especially those still in proposal form). Back to top

ConclusionEU cybersecurity legislation is setting the direction for the next decade: resilience, agility, post-quantum readiness, and security-by-design. These requirements affect protocols, libraries, build pipelines, PKI, update mechanisms, and runtime platforms.

Java’s recent and proposed features—modern crypto APIs, hybrid-TLS work, improved certificate handling, simplified security architecture—provide solid foundations for architects who need to prepare for this future. The mandates are coming. The engineering must begin now.

Back to top

References [1] European Commission. Recommendation (EU) 2024/1101 of 11 April 2024 on a Coordinated Implementation Roadmap for the transition to Post-Quantum Cryptography. 2024. url * [2] European Commission. Communication accompanying Recommendation (EU) 2024/1101 (quotation referenced). 2024. * [3] European Commission. Recommendation (EU) 2024/1101, Article 2: Tasking the NIS Cooperation Group. 2024. * [4] NIS Cooperation Group. A Coordinated Implementation Roadmap for the Transition to Post-Quantum Cryptography.* 23 June 2025. url * [5] NIS Cooperation Group. PQC Roadmap – Phases and Timeline Guidance. 2025. * [6] Industry and policy analyses summarising PQC milestones (2026 planning, 2030 critical systems, ~2035 broader migration). 2025. * [7] European Parliament & Council. Directive (EU) 2022/2555 (NIS2 Directive) on measures for a high common level of cybersecurity across the Union. 2022. url * [8] European Commission. NIS2 Transposition Overview – Transposition by 17 October 2024. 2024. * [9] European Parliament & Council. Regulation (EU) 2022/2554 (DORA). 2022. url * [10] Legal and compliance analyses confirming DORA applicability from 17 January 2025. 2024–2025. * [11] European Parliament & Council. Regulation (EU) 2024/2847 (Cyber Resilience Act). 2024. * [12] Regulatory summaries detailing CRA’s entry into force (10 December 2024) and applicability (11 December 2027). 2024–2025. * [13] European Parliament & Council. Regulation (EU) 2019/881 (Cybersecurity Act). 2019. * [14] ENISA. EUCC Cryptography Guidelines. 17 July 2024. * [15] ENISA & ECCG. Agreed Cryptographic Mechanisms version 2 (ACM v2). 2024–2025. * [16] Keysight & other vendors. Analyses of ACM v2 and the introduction of hybrid/PQC mechanisms. 2024–2025. * [17] ETSI TC CYBER QSC. ETSI TS 103 744 – Quantum-Safe Hybrid Key Exchanges. 2020–2024. url * [18] ETSI. TS 103 744 – Technical definition of hybrid KEX (Classical KEX + PQC KEM). 2020–2024. * [19] ETSI. TS 103 744 v1.2.1 – “Clear, testable, and interoperable path to deploy hybrid key establishment today.” 2024. * [20] Germany (BMI). Draft NIS2UmsuCG – NIS2 Implementation and Cybersecurity Strengthening Act. 2024–2025. * [21] Germany. Finanzmarktdigitalisierungsgesetz (FinmadiG). 2024–2025. * [22] German legal commentaries discussing FinmadiG’s alignment with DORA/MiCAR. 2024–2025. * [23] France. Projet de loi relatif à la résilience des infrastructures critiques et au renforcement de la cybersécurité – Senate Adoption 12 March 2025. 2025. * [24] French legal analyses on NIS2/CER/DORA transposition. 2025. Back to top

Ixchel RuizKarakun AG – Basel, Switzerland

Ixchel Ruiz has been developing software applications and tools since 2000. Her research interests include Java, dynamic languages, client-side technologies, and testing. As a member of the JCP Executive Committee, Java Champion, Oracle ACE Pro, Testcontainers Community Champion, CDF Ambassador, Hackergarten enthusiast, Open Source advocate, public speaker, and mentor, Ixchel is deeply committed to fostering inclusive and collaborative tech communities. She actively mentors aspiring developers and champions initiatives aimed at increasing diversity and accessibility in the technology sector.

Ixchel’s work is characterised by a relentless pursuit of innovation, a deep understanding of user needs, and an unwavering commitment to ethical technology development.

The post Breaking keys and building trust: The JAVA way! appeared first on JVM Advent.

View Details

Just another Static Site Generator (SSG)? Honestly, yes — but Roq is a little different. It’s a thin layer on Quarkus, which gives it a different kind of potential.

I’ve spent time looking at other SSGs in the JavaScript ecosystem (Gatsby, Next.js, Nuxt) and in other languages (Hugo, Jekyll, JBake…). Roq borrows many of their popular features and conventions.

What really stands out, though, is that these SSGs have to re-implement most of the core building blocks inside their framework.

With Quarkus, we already get almost everything we need out of the box — and that’s a key distinction:

  • Quarkus has Qute as Type-Safe template engine, some sugar for Roq.
  • Roq Plugins and Themes are Quarkus extensions.
  • Quarkus allow to serve files statically and dynamically.
  • CDI allows to extend and bind data and templates together.
  • Quarkus extensions can be used with Roq, the most use-full is the Quarkus Web-Bundler (to bundle script, styles and web deps without any config).
  • Quarkus test framework.
  • And Quarkus Dev Mode !

Roq is a rock on top of Quarkus:

  • Create endpoints for all your static site based on conventions (dir structure and Frontmatter data).
  • Allow to define data files (yml or json) and consume them in templates.
  • Provide plugins and themes.
  • Add a command to export you Quarkus app as a static site.
  • A GitHub Action for automation.
  • Soon: A CMS to manage article and pages from the Quarkus Dev-UI.

In this demo, we will install Quarkus and clone a repository, change a few things to see how it reacts.

SetupMake sure you have the JDK 17+ on your machine and install the Quarkus CLI using the command bellow:

```

Install the Quarkus CLIcurl -Ls https://sh.jbang.dev | bash -s - trust add https://repo1.maven.org/maven2/io/quarkus/quarkus-cli/curl -Ls https://sh.jbang.dev | bash -s - app install --fresh --force quarkus@quarkusio‎

``` NOTE: We started working on a Quarkus Wrapper to allow starting dev-mode and soon also editor mode without anything to install on the machine.

TIP: You can optionally install Quarkus IDE tooling to make the xp even smoother.

I cooked a demo repo with Quarkus, Roq and Tailwind extensions in the pom.xml:

```

Clone the starter repo (or download):git clone https://github.com/ia3andy/the-code-site.gitcd the-code-site‎

``` You should be all set for the whole journey

What did I clone ?

the-coder-site/├── content/│ ├── index.html # Website index page and metadata│ └── ** # Articles and pages├── public/images/ # Images for your site├── web/│ ├── *.js # Scripts (auto-bundled)│ └── *.css # Styles (auto-bundled)├── templates/│ ├── layouts/│ │ ├── default.html # Base HTML structure│ │ ├── post.html # Layout for a blog post│ │ └── page.html # Layout for a page│ └── partials/│ ├── header.html # Site header│ └── footer.html # Site footer├── config/application.properties # Site config ├── pom.xml # Quarkus setup (Roq, TailwindCSS)└── ... # Gitignore, Maven Wrapper Let’s start Quarkus Dev-Mode:

quarkus dev‎ When Quarkus starts — after the initial download of its dependencies, press w on you keyboard and let the magic happen!

I suggest you put your browser on your second screen if you have one, this content is also available in your new blog (or in content/posts/2025-01-02-demo.md)

Episode 1 – The Index Page and Live-ReloadLet’s open content/index.html and have a look.

The first part is the FrontMatter header, it allows to set up the site and provide data for the templates:

---layout: defaulttitle: Your Namedescription: >- Personal blog - A programmer sharing thoughts on software development, Java, and web technologies.greeting: Hi, I'm Your Name!tagline: Just a codernavigation: - title: Blog url: / - title: Tags url: /tags - title: About url: /aboutpaginate: posts--- Change the title: with your name

Switch to the browser and see the change, Live reload should be real quick

The layout: default is the template which will wrap this page content, they are defined in template/layouts/ but we will see that later.

The content part is in html (because it a .html file), it is pretty straightforward. You can see how pagination on posts happens.

Episode 2 – Web-BundlingThe Quarkus Web Bundler, takes the web/ dir stuff and use the mvnpm dependencies, to create a production ready “bundle” for your page. {#bundle /} is included in the default layout and add the resulting script and style html tags.

Let’s give it a ride:

In the web/styles.css, change the @theme { ... } part by this:

@theme { --font-sans: 'Atkinson Hyperlegible', system-ui, -apple-system, sans-serif; --color-primary: #7c2d12; --color-secondary: #9a3412; --color-tertiary: #ea580c; --color-surface: #fff7ed; --color-surface-2: #ffedd5; --color-surface-3: #fed7aa; --color-card: #ffffff; --color-card-border: #fdba74; --color-border: #fdba74; --color-border-strong: #fb923c; --color-code-bg: #fff7ed; --color-code-text: #c2410c; --color-pre-bg: #ffedd5; --color-pre-text: #7c2d12; --color-accent-300: #fdba74; --color-accent-400: #fb923c; --color-accent-500: #f97316; --color-accent-600: #ea580c; --color-accent-700: #c2410c;} Slick right? (you also have a dark mode button in the site if you want to give it a shot)

Note: The design is using TailwindCSS which is supported by Roq and Quarkus using the quarkus-web-bundler-tailwindcss extension.

In web/app.js, add this in the bottom:

alert('Hello Roq'); Check the browser (and then remove it )

If you have a look to the pom.xml, you’ll see mvnpm deps for hightlightjs and the font Atkinson Hyperlegible used in the css (dependabot will take a good care of them).

Episode 3 – Writing Posts and PagesTo Create a new page:

  • Create a new file in content/ with .md or .html extension.
  • Add it to the menu in the index page (it will be under [filename]/ by default).

To Create a new post:

  • Create a new file in content/posts with .md or .html extension.
  • It is already available in the blog!
  • Using a FrontMatter header (in yaml between ---), add a title, description, some tags (the path is based on a slug of the title by default).
  • Feel free to also add content to your post.

TIP: You can also create a directory with an index file instead if you want to access relative static files in your page or post.

Ok, let’s have a bit of fun:

open config/application.properties, uncomment the line (remove the #) and go back to the “Blog” page.

I didn’t know you could write articles that fast

This is faker data generation to help you with pagination and tagging (it’s only enabled in dev mode thanks to %dev).

Episode 4 – DataWe already covered a lot, let’s quickly cover the rest.

Create data/navigation.yml with the index.html FrontMatter navigation content:

items: - title: Blog url: / - title: Tags url: /tags - title: About url: /about In templates/partials/header.html change this:

- {#for item in site.data.navigation}+ {#for item in cdi:navigation.items} Congratulation, you didn’t change a thing

Tip You can also map this data to a structure (Java class or record) for type-safety and making sure your data is meeting expectations.

Episode 5 – Templates: Layouts, Partials, Tags and ExtensionsThis is a bit boring but important to know.

Layouts let you share and reuse parts of the HTML around your content — headers, footers, wrappers — so pages only provide the unique content while layouts handle the surrounding structure.

In templates/layouts/default.html replace the {#insert /} this in the <main> by this:

```

One template to rule them all

``` All pages in the site is now showing this message

Partials (located in templates/partials/) let you reuse small HTML/Qute snippets—like a header, a footer, a card, a pagination block, or a meta block. Instead of repeating the same HTML everywhere, you include them with {#include partials/… /}, keeping layouts and pages clean and consistent.

Tags (located in templates/tags/) are small, self-contained components you can call from any template. They behave like mini-templates with parameters, useful for things like buttons, cards, or repeated UI fragments. You invoke them using Qute’s {#your-tag foo="bar"} syntax, and they keep your templates much cleaner by replacing boilerplate HTML with a reusable tag definition.

@TemplateExtension methods can be used to extend the data classes with new functionality from Java (to extend the set of accessible properties and methods). For example, it is possible to add computed properties and virtual methods.

Episode 6 – ThemesIf you create a Roq app using Code Quarkus, you’ll notice that you get a fully styled, well-structured website without writing any template or CSS yourself. That’s because Roq allow to use themes, which provide all the building blocks: layouts, components, styles, scripts, and templates.

Roq themes are deeply overridable, letting you replace or extend only what you need while keeping the rest intact. This keeps your project focused on the content, not the design system. Whenever you want to adjust a layout, change a component, or tweak the styling, you simply override that part in your project, and the rest of the theme continues to work seamlessly.

You can also create your own. The process is very similar to what we’ve seen in this demo: you define your layouts, templates, components, and styles, and Roq takes care of wiring everything together. This gives you full freedom to shape the look and feel of your site while still benefiting from Roq’s structure and conventions.

Season Finale – PublishingUp to this point, you haven’t actually generated anything — you’ve just been using Quarkus to build and render your app. Add a Java service, plug in a database with Quarkus extensions, and it works fine. That’s a different path, though, because you’ll need a server to run it.

For static site generation, you only need static files to run on a static server. Roq makes this simple by providing a command for your CI or a GitHub Action. Learn more about publishing with Roq here.

ConclusionI hope you enjoyed the demo and consider using Roq for your next site. If you want to show your support, give Roq a star on GitHub.

The Roq users and community are growing, and I hope to see you there soon .

Plenty of new things are coming in the next few months — a CMS, i18n for collections, a dead-link checker, an mkdocs theme, and more.

If you spot issues, have ideas for cool features, or want to contribute, you’re more than welcome

The post Discover Roq, the Quarkus Way for Static Site Generation in Java appeared first on JVM Advent.

View Details

Since Java 14, the Java switch and instanceof statements have been enhanced, in multiple phases, to support pattern matching and a “data-oriented” programming style. In this article, I explore when this programming style is beneficial, and why. I look at the sweet spot of perfect pattern usage, absolute antipatterns where it should not be used, no matter how many examples you see in blogs and conference presentations, and corner cases where the switch syntax clashes with legacy behavior.

Sealed Hierarchies and Records are NicePattern matching is one of the shiny new objects in the Java language. Maybe not that new anymore—it started with Java 14. And maybe not that shiny. How many times have you used it in your code?

There may be a reason. Pattern matching works best with a sealed hierarchy of interfaces and record types. (If you want to show off, you can call them “algebraic data types”.) Everywhere that you have such a hierarchy, pattern matching is a natural tool.

How many such hierarchies do you have in your code base? Well, that may explain why you are not often reaching for that tool.

Some people argue that you should actively organize your data into such a form. This is sometimes called data-oriented programming, and it can be a good idea when it fits the problem domain. For examples with a business context, I can recommend this book by Chris Kiehl, currently in early access. He discusses real-life scenarios, such as

public sealed interface Lifecycle { record Pending() implements Lifecycle {} record Billed(String invoiceId) implements Lifecycle {} record Rejected(Reason reason) implements Lifecycle {} record InReview(ApprovalId approvalId) implements Lifecycle {}} Since these scenarios require a fair amount of domain knowledge, let me use a simple and familiar example: JSON values. There are four kinds of primitive values, and arrays and objects.

Make the leaves of the inheritance tree into records, or, if they only have finitely many instances, into enums. And all other types into sealed interfaces:

sealed interface JSONValue {}sealed interface JSONPrimitive extends JSONValue {}enum JSONBoolean implements JSONPrimitive { FALSE, TRUE; }enum JSONNull implements JSONPrimitive { INSTANCE; }record JSONNumber(double value) implements JSONPrimitive {}record JSONString(String value) implements JSONPrimitive {}record JSONArray(List<JSONValue> values) implements JSONValue {}record JSONObject(Map<String, JSONValue> entries) implements JSONValue {} Now we can use pattern matching:

static String quote(String s) { return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";}static String stringify(JSONValue j) { return **switch (j)** { case JSONNumber(var v) -> "" + v; case JSONString(var s) -> quote(s); case JSONBoolean.TRUE -> "true"; case JSONBoolean.FALSE -> "false"; case JSONNull.INSTANCE -> "null"; case JSONArray(var values) -> values.stream() .map(this::stringify) .collect(Collectors.joining(",", "[", "]")); case JSONObject(var entries) -> entries.entrySet() .stream() .map(e -> quote(e.getKey()) + ":" + stringify(e.getValue())) .collect(Collectors.joining(",", "{", "}")); };} This is a switch expression. Each case yields a value (after the -> token). The expression switch (j) { ... } yields the value of the matching case. The return statement returns that value.

The value in parentheses in switch (**j**) is called the selector. In our case, the type of the selector j is the JSONValue interface.

Note the record patterns, such as:

case JSONNumber(**var v**) -> "" + v; If j is a JSONNumber, the variable v is set to the record component. The type of v is the component type, in this case double.

Also note that some cases are enum instances, such as case JSONBoolean.TRUE -> .... These are called constant patterns.

Finally, note that the switch is exhaustive. It covers all possible values for the selector. All switch expressions must be exhaustive. Because no matter what the selector, the expression must have a value.

Ok, not all values are covered. What if the selector j is null? Then a NullPointerExpression is thrown. If j is new JSONString(null), then s is null, also causing an NPE. That is just to be expected. Generally, null is exempted from exhaustiveness checking because it would be too exhausting to check for them, particularly in nested positions.

Why is pattern matching nice? The object-oriented alternative would have been to add a stringify method to all levels of the hierarchy:

  • as an abstract or default method in each interface
  • as a concrete method in each record or enum

That is easy enough to do—after all, it is a sealed hierarchy. But it has two drawbacks. First, only the owner of the hierarchy can add methods. And the logic of the action, here, stringification, is sprinkled over multiple classes.

By using an external method and pattern matching, the logic is all in one place. And anyone, not just the hierarchy owner, can go forth and pattern match. Without the need of a visitor pattern. This is good.

Future NicenessOptional could have been declared as a sealed interface whose subtypes are a record Optional.Of and an enum Optional.Empty. Then you could use code like this:

var result = switch(stream.max(comparator)) { // Not actually case Optional.Of(x) -> x; case Optional.Empty.INSTANCE -> someDefault;}; Of course, that is not how Optional actually works. But there are plans to make deconstruction work with arbitrary classes. Then you will be able to write something like this:

var result = switch(stream.max(comparator)) { // Maybe soon case Optional.of(x) -> x; case Optional.empty() -> someDefault;}; The details are in flux, so I won’t belabor them. Once available, such “member patterns” (or whatever they will end up being called) will make pattern matching practical for a wider set of classes.

Naughty FallthroughThe classic switch statement, which came to Java via C and C++, has a single raison d’être: to allow the compiler to construct a jump table.

If the labels fall in a compact range, the jump addresses can be in an array. Otherwise, the table is an array of pairs (label, address), sorted by label. Binary search finds the matching case.

As of Java 5, labels can also be strings. Then the jump table contains the hash, and each jump target checks if the string actually matches.

The jump table also explains the fallthrough behavior. After jumping to the code of the case, the program keeps running, until a break causes a jump to the end of the statement. Or, if there is no break, it keeps running with the instructions of the next case. Which is almost always unintended, and a common error. Stay away from it.

Java 14 gave us four forms of switch. The classic statement. A lovely new switch expression. And a switch statement without fallthrough. Also a switch expression with fallthrough—very naughty. That was only added in an effort to make the language more regular.

My advice: If you want a jump table, use the new switch statement without fallthrough. Simply use -> tokens instead of :, and drop the break statements.

switch (c) { case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> { ndigit[c-'0']++; } case ' ', '\n', '\t' -> { nwhite++; } default -> { nother++; }} If you want pattern matching, use a switch expression. For sure without fall-through.

Type PatternsYou have seen record patterns (matching a record and extracting its components), as well as the more general member patterns of the future.

Another pattern, called type pattern, checks whether the selector expression has a particular type. It then binds a variable to the type cast:

Object x = ...;Object doubled = switch (x) { case String s -> s + s; // s is a String case Number n -> n.doubleValue() * 2; // n is a Number default -> List.of(x, x);} Of course, this example is completely artificial. When is the last time you had business logic that made type tests like this?

The blogosphere is full of examples like this, in order to illustrate the finer points of switch. Or to gleefully present puzzles that explore cruel conflicts between classic and modern syntax and semantics. Not that I would ever do such a thing.

It is not common to write code that starts with an Object and then narrows down the type. If that is important for you, go ahead and learn more about type patterns. But if you feel the need for the occasional instanceof, just stick with it.

In fact, instanceof has gotten better. A classic code snippet such as

if (x instanceof String) { String s = (String) x; Do something with s} is more easily expressed in modern Java as

if (x instanceof String s) { Do something with s} Primitive PatternsThe classic switch statement in Java 1.0 permitted selector types int, short, char, and byte. Why not long, float, double, or boolean? They aren’t all that useful with jump tables.

As of Java 25, the selector type can be any type, except for those four types. A proposal, now in its fourth preview, aims to remedy this anomaly. To make the language more regular.

If you just use constant case labels, this is unsurprising.

double x = ...;String result = switch (x) { // JEP 530 allows selector of type double case 3.141592653589793 -> "π"; case 1.4142135623730951 -> "√2"; default -> "something else";}; But there are also primitive patterns:

result = switch (x) { case **int n** when n % 2 == 0 -> "an even integer"; case **float \_** -> "a float"; default -> "something else";}; The selector is a 64-bit double. The first case tests whether it actually represents a 32-bit int. The second case checks if fits into a 32-bit float without losing any bits of information. To fully understand the latter, you need to be familiar with the internals of the IEEE 754 floating-point standard.

What if you also toss in some wrapper types? Of course, I would never do this. Just kidding, I certainly would in the interest of creating yet another naughty puzzler. But Simon Ritter beat me to it:

int x = ...;switch (x) { case Integer i -> System.out.println("int"); case byte b -> System.out.println("byte");} This should not compile. After all, the second case can never happen, and pattern matching is generally good about flagging such dominance.

But it does compile. In this instance, poor switch is getting overwhelmed. There is so much historical baggage that must be respected. And sometimes the results are counterintuitive.

Do not mix primitive patterns and type patterns in the same switch. They do completely different things. A primitive pattern checks whether a value can be converted to a different type. A type pattern checks whether a value belongs to a different type.

The conversion tests can be useful, but in many practical situations, they work better with instanceof:

int x = ...;if (x instanceof byte b) { out.write(b);} else { // x is not between -128 and 127 ...} Ok, maybe it’s not that useful. Normally you have bytes between 0 and 255. But that’s another story.

Project Valhalla promises to let us define our own types that act like primitive types, such as long double, short float, unsigned byte. It is not yet clear how pattern matching will work with those types, but I would not be surprised if it was complex and a fertile ground for nasty puzzlers.

Right now, there is a lot of noise about primitive patterns, because it is a new feature. But it is unlikely to impact many programmers. Except as pitfalls. Consider this:

JSONObject o = ...;var result = switch (o) { case JSONNumber(int x) -> x; case JSONNull.INSTANCE -> 0; default -> throw new IllegalArgumentException();}; Did the programmer really mean case JSONNumber(**int** x)? It is an easy mistake to accidentally write int instead of double. Before primitive patterns, the compiler rejected this. Now it has an exciting new meaning: Is o an instance of JSONNumber whose value component is actually an integer? This can be useful, of course, if intended. But what if it isn’t?

Tip: Get into the habit of always using var with record patterns. Then you don’t run into this issue.

Constant PatternsIn our sealed JSON hierarchy, we had a mixture of record patterns and enum constant patterns:

case JSONNumber(var v) -> "" + v;case **JSONBoolean.TRUE** -> "true"; // a constant pattern And that’s fine.

A classic jump table switch only has constant patterns. That’s fine to.

For now, constant patterns have an unfortunate limitation: they don’t nest.

record Point(int x, int y) {}Point p = ...;var result = switch (p) { case Point(**0**, \_) -> "on x-axis"; // ERROR, can't nest constant pattern ...} You have to write:

var result = switch (p) { case Point(x, \_) **when x == 0** -> "on x-axis"; ...} This limitation may get fixed at some point in the future.

Even with top-level constant patterns, the rules can get pretty arcane. For example, what is wrong with this?

Object x = ...;String result = switch (x) { case "" -> "empty"; case 0 -> "zero"; case JSONNull.INSTANCE -> "null"; default -> "something else";}; You can only use string cases when the selector type is String, and integer cases when the selector type is int or Integer. Or short or char or byte. But not long, double, or float. Here the selector type is Object, so they are not allowed. Yet, with an Object selector, enum constants are ok.

You are unlikely to run into real-life switch expressions with Object or Integer selectors, so this too is more of an issue for puzzlers than real-life scenarios.

ConclusionThere is a lot going on with modern switch, and some usages are nicer than others.

The sweet spot is the “sealed hierarchies of records and enums” use case. For bragging rights, call it “algebraic data types”.

In the future, that convenience will be extended to other classes such as Optional.

For other type tests, prefer the modern form of instanceof over a switch with type patterns.

Also, if you want to convert between primitive types, try the new instanceof form first.

If you use jump tables, that’s totally fine. But refactor without fallthrough.

Blogs and puzzler presentations will delight in exploring the interactions between classic and “enhanced” switches, which can get arcane and complex. (Nobody could have predicted that.)

Don’t let that scare you away from using pattern matching. It is a truly useful feature, and it is well worth organizing appropriate parts of your code with pattern matching in mind.

The post Nice and Naughty Cases of Pattern Matching appeared first on JVM Advent.

View Details

After exploring Java bytecode in previous years (2022, 2023, 2024), this year we’ll take an unexpected detour for a Java advent: instead of generating Java bytecode, we’ll use Java to build and execute LLVM IR, the intermediate language behind compilers like clang.

Using Java’s Foreign Function & Memory (FFM) API, we’ll call the LLVM C API, generate a “Hello, World!” program, and even JIT-compile it to native code – all from Java.

The task is simple: create a program that simply prints “Hello, World!”. But we must do this from Java via LLVM.

What is LLVM?The LLVM Project, a collection of modular compiler and toolchain technologies, began as a research project over 20 years ago at the University of Illinois. It has grown significantly, underpinning many compilers and tools like clang.

The core libraries provide a source & target independent optimizer along with code generation for a multitude of target machines. They are built around the LLVM IR, an intermediate representation, which we’ll generate & execute from Java.

Installing LLVMTo use the LLVM C API from Java, we’ll need LLVM’s shared libraries and headers installed locally. There is an automatic installation script available to easily install LLVM on Ubuntu/Debian systems, for example to install LLVM 20:

$ wget https://apt.llvm.org/llvm.sh$ chmod +x llvm.sh$ ./llvm.sh 20 Once we have LLVM installed we can use the LLVM tooling to execute textual-form LLVM IR and we’ll also be able to use the LLVM C API in Java via the FFM API.

LLVM IRLLVM IR is a strongly-typed, SSA-based intermediate language. It abstracts away most machine-specific details, making it easier to represent high-level constructs in a compiler-friendly format. There are three equivalent representations of the IR: an in-memory format, a bitcode format for serialisation and a human readable assembly language representation.

The textual form of the LLVM IR for our “Hello, World!” looks like this:

@str = private constant [14 x i8] c"Hello, World!\00"declare i32 @puts(ptr)define i32 @main() { call i32 @puts(ptr @str) ret i32 0} Eventually, we’ll generate this via Java but, for now, if you save this in a file called helloworld.ll you can try executing it with the LLVM interpreter, lli:

$ lli helloworld.llHello, World! There are a few types of entities used in the helloworld.ll example:

  • A global variable containing the string “Hello World!”
  • A declaration of the external libc puts function
  • A definition of the main function
  • Instructions to call puts and return an integer exit code

You can dive deeper into the LLVM “Hello, World!” example here if you like before continuing to the next section, where we’ll start using the Java FFM API.

What is the Java FFM API?The Foreign Function and Memory (FFM) API enables Java programs to interoperate with code and data outside the Java runtime. The API is a replacement for the older JNI API that enables Java programs to call native libraries in a safer way. The API can be used to call foreign functions and safely access foreign memory that is not managed by the JVM.

A companion to the FFM API is a tool named jextract that can automatically generate Java bindings from a C header file. jextract parses C header files and automatically generates the Java source code with method handles and type-safe FFM bindings.

We’ll use the jextract tool to generate bindings for the LLVM C API and those bindings will allow us to call the LLVM API from Java.

Getting startedFirst, let’s create a simple project to start. We’ll use maven to build our project but you can use another build tool if you like, it’s not important:

$ mvn archetype:generate -DgroupId=com.example -DartifactId=jvm-llvm-helloworld -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false Once you have a project skeleton, update the pom.xml file to set the Java version >= 22:

<properties> <maven.compiler.source>25</maven.compiler.source> <maven.compiler.target>25</maven.compiler.target> </properties> Then build and run the program to check everything is OK:

$ mvn clean install$ java -cp target/jvm-llvm-helloworld-1.0-SNAPSHOT.jar com.example.AppHello World! The maven generated sample already printed “Hello, World!” but that’s too easy! We’ll remove that and generate it via LLVM in the following sections.

Let’s now create the LLVM bindings using jextract so that we can use the LLVM API.

Creating LLVM bindingsWe’ll use jextract to generate bindings from the LLVM C API header files. Make sure LLVM is available on your system (see Installing LLVM above) and you’ll also need to download jextract.

The following jextract command (on Linux) will create Java bindings for the specified LLVM C headers, placing the generated code into the com.example.llvm package within the src/main/java directory, with the main header class named LLVM.

$ jextract -l LLVM-20 -I /usr/include/llvm-c-20 \ -I /usr/include/llvm-20 \ -t com.example.llvm \ --output src/main/java \ --header-class-name LLVM \ /usr/include/llvm-c-20/llvm-c/Core.h \ /usr/include/llvm-c-20/llvm-c/Support.h \ /usr/include/llvm-c-20/llvm-c/ExecutionEngine.h \ /usr/include/llvm-c-20/llvm-c/Target.h \ /usr/include/llvm-c-20/llvm-c/TargetMachine.h To test the generated bindings, let’s print the LLVM version using the static method generated for LLVM version string constant: edit the sample’s App.java file to print the version using the following:

package com.example;import static com.example.llvm.LLVM.LLVM\_VERSION\_STRING;/** * LLVM Hello world! * */public class App { public static void main(String[] args) { var version = LLVM\_VERSION\_STRING(); System.out.println("LLVM version: " + version.getString(0)); }} If you run this, you’ll see the LLVM version printed:

$ java -cp target/jvm-llvm-helloworld-1.0-SNAPSHOT.jar --enable-native-access=ALL-UNNAMED com.example.AppLLVM version: 20.0.0 Note the use of --enable-native-access=ALL-UNNAMED to prevent warnings about native code access; I’ll omit this for brevity in later commands.

Memory SegmentsThe LLVM_VERSION_STRING method returns a MemorySegment rather than a Java String. In the FFM API, a MemorySegment represents a contiguous region of memory—either on or off the Java heap—enabling safe, structured access to native memory.

Let’s take a look at the implementation in the generated source file:

public static MemorySegment LLVM\_VERSION\_STRING() { class Holder { static final MemorySegment LLVM\_VERSION\_STRING = LLVM.LIBRARY\_ARENA.allocateFrom("20.0.0"); } return Holder.LLVM\_VERSION\_STRING; } This method allocates memory containing the version string that contains the version number. The allocated MemorySegment is returned from the method and to get the String back into Java-land we need to call getString(0) on the memory segment which reads a null-terminated string at the given offset (0), using the UTF-8 charset.

Memory segments are managed through arenas (such as the LLVM.LIBRARY_ARENA in the code above), which bridge Java’s managed heap and foreign memory spaces by applying familiar resource management patterns like try-with-resources.

Since we’ll need to allocate native memory, let’s declare an Arena:

public static void main(String[] args) { try (Arena arena = Arena.ofConfined()) { // TODO } } Creating an LLVM moduleAs a reminder, we need to recreate the following LLVM IR via the LLVM C API:

declare i32 @puts(ptr)@str = constant [14 x i8] c"Hello, World!\00"define i32 @main() { call i32 @puts(ptr @str) ret i32 0} Let’s start by creating an LLVM module – the container for all functions and globals – and print it so that we can run it through the LLVM interpreter:

public static void main(String[] args){ try (Arena arena = Arena.ofConfined()) { var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // TODO: Fill in the module var llvmIrCharPtr = LLVMPrintModuleToString(module); try { System.out.println(llvmIrCharPtr.getString(0)); } catch (Exception e) { System.err.println("Failed to write LLVM IR: failed to get error message: " + e.getMessage()); } // Clean up LLVM resources LLVMDisposeMessage(llvmIrCharPtr); LLVMDisposeModule(module); }} If we execute this now, we’ll see an empty IR module:

$ java -cp target/jvm-llvm-helloworld-1.0-SNAPSHOT.jar com.example.App; ModuleID = 'hello'source\_filename = "hello" If you pass this output through the LLVM interpreter, you’ll see that it tries to execute the module but cannot find the entry point main function:

$ java -cp target/jvm-llvm-helloworld-1.0-SNAPSHOT.jar com.example.App | lliSymbols not found: [ main ] We now have an LLVM module, but it has no executable code – the interpreter rightly complains that main is missing; so let’s add the main function.

Adding a main functionThe entry point to our program is the function named main which takes no parameters and returns an integer exit code, where a non-negative integer denotes success. We can add a function to the module using the LLVMAddFunction function, along with the LLVMFunctionType and LLVMInt32Type functions to create the function type.

Notice that all of these functions return a MemorySegment and all 3 LLVMAddFunction parameters are MemorySegments.

public static void main(String[] args){ try (Arena arena = Arena.ofConfined()) { var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // Create main function signature: int main() var int32Type = LLVMInt32Type(); var mainType = LLVMFunctionType(int32Type, NULL, 0, 0); var mainName = arena.allocateFrom("main"); var mainFunc = LLVMAddFunction(module, mainName, mainType); // TODO: Add the code var llvmIrCharPtr = LLVMPrintModuleToString(module); try { System.out.println(llvmIrCharPtr.getString(0)); } catch (Exception e) { System.err.println("Failed to write LLVM IR: failed to get error message: " + e.getMessage()); } // Clean up LLVM resources LLVMDisposeMessage(llvmIrCharPtr); LLVMDisposeModule(module); }} If you execute this now you’ll see a declaration of the main function but it has no body so the LLVM interpreter will produce the same error:

$ java -cp target/jvm-llvm-helloworld-1.0-SNAPSHOT.jar com.example.App; ModuleID = 'hello'source\_filename = "hello"declare i32 @main()$ java -cp target/jvm-llvm-helloworld-1.0-SNAPSHOT.jar com.example.App|lliSymbols not found: [ main ] Next we’ll add some instructions to the body of the function.

Adding an entry basic blockIn order to add code to a function we need to add at least 1 basic block – the entry block. A basic block is a sequence of instructions within a function that executes straight through from start to finish, with no branches in the middle. These blocks form the nodes of the Control-Flow Graph (CFG), and they connect to each other based on how control flows between them.

Basic blocks can be added to a function with the LLVMAppendBasicBlock function:

public static void main(String[] args){ try (Arena arena = Arena.ofConfined()) { var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // Create main function signature: int main() var int32Type = LLVMInt32Type(); var mainType = LLVMFunctionType(int32Type, NULL, 0, 0); var mainName = arena.allocateFrom("main"); var mainFunc = LLVMAddFunction(module, mainName, mainType); var entry = LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry")); // TODO: Add the instructions var llvmIrCharPtr = LLVMPrintModuleToString(module); try { System.out.println(llvmIrCharPtr.getString(0)); } catch (Exception e) { System.err.println("Failed to write LLVM IR: failed to get error message: " + e.getMessage()); } // Clean up LLVM resources LLVMDisposeMessage(llvmIrCharPtr); LLVMDisposeModule(module); }} If you run the program through lli now, you’ll see a different error:

$ java -cp target/jvm-llvm-helloworld-1.0-SNAPSHOT.jar com.example.App | llilli: <stdin>:6:1: error: expected instruction opcode} That makes sense, we don’t yet have any instructions in our function!

Building instructionsTo add instructions, we first create an instruction builder using the LLVMCreateBuilder function. This gives us an LLVMBuilder that we can use to insert new instructions into a basic block.

We’ll also use the LLVMPositionBuilderAtEnd function to position the builder at the end of the entry block and LLVMBuildRet to build a return instruction:

public static void main(String[] args){ try (Arena arena = Arena.ofConfined()) { var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // Create main function signature: int main() var int32Type = LLVMInt32Type(); var mainType = LLVMFunctionType(int32Type, NULL, 0, 0); var mainName = arena.allocateFrom("main"); var mainFunc = LLVMAddFunction(module, mainName, mainType); var entry = LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry")); var builder = LLVMCreateBuilder(); LLVMPositionBuilderAtEnd(builder, entry); // TODO: Call puts “Hello, World!” // Return 0 LLVMBuildRet(builder, LLVMConstInt(int32Type, 0, 0)); var llvmIrCharPtr = LLVMPrintModuleToString(module); try { System.out.println(llvmIrCharPtr.getString(0)); } catch (Exception e) { System.err.println("Failed to write LLVM IR: failed to get error message: " + e.getMessage()); } // Clean up LLVM resources LLVMDisposeMessage(llvmIrCharPtr); LLVMDisposeBuilder(builder); LLVMDisposeModule(module); }} If you run the program and pass the output through lli now, you’ll see nothing happen:

$ java -cp target/jvm-llvm-helloworld-1.0-SNAPSHOT.jar com.example.App | lli Great news – the errors are gone! Checking the return code confirms the program exited successfully, returning 0.

$ echo $?0 Try changing the 0 to some other number to confirm that the value is indeed coming from the exit code returned by the LLVM IR program!

Global variablesA global variable, defined at the top-level in LLVM IR, defines a region of memory with a fixed address that is allocated when the program is loaded, rather than dynamically at runtime. Globals can be declared as constant if their values will never change.

We’ll add the string “Hello, World!” to our LLVM program as a global constant.

public static void main(String[] args){ try (Arena arena = Arena.ofConfined()) { var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // Create main function signature: int main() var int32Type = LLVMInt32Type(); var mainType = LLVMFunctionType(int32Type, NULL, 0, 0); var mainName = arena.allocateFrom("main"); var mainFunc = LLVMAddFunction(module, mainName, mainType); var entry = LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry")); var builder = LLVMCreateBuilder(); LLVMPositionBuilderAtEnd(builder, entry); // Create a global string constant containing "Hello, World!" var helloStr = LLVMBuildGlobalStringPtr(builder, arena.allocateFrom("Hello, World!"), arena.allocateFrom("hello\_str")); // TODO: Call puts “Hello, World!” // Return 0 LLVMBuildRet(builder, LLVMConstInt(int32Type, 0, 0)); var llvmIrCharPtr = LLVMPrintModuleToString(module); try { System.out.println(llvmIrCharPtr.getString(0)); } catch (Exception e) { System.err.println("Failed to write LLVM IR: failed to get error message: " + e.getMessage()); } // Clean up LLVM resources LLVMDisposeMessage(llvmIrCharPtr); LLVMDisposeBuilder(builder); LLVMDisposeModule(module); }} We don’t use the hello_str yet so running lli would produce the same as before, but you can see the string is now declared in the LLVM IR (prefixed with @ because it is a global, like the main function):

$ java -cp target/jvm-llvm-helloworld-1.0-SNAPSHOT.jar com.example.App ; ModuleID = 'hello'source\_filename = "hello"@hello\_str = private unnamed\_addr constant [14 x i8] c"Hello, World!\00", align 1define i32 @main() {entry: ret i32 0} Let’s add the final instruction next – a call to puts to print the string.

Calling functionsBefore we can call the libc puts function we must declare it in the module by first building the function type and then calling LLVMAddFunction to add it to the module:

public static void main(String[] args){ try (Arena arena = Arena.ofConfined()) { var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // Create main function signature: int main() var int32Type = LLVMInt32Type(); var mainType = LLVMFunctionType(int32Type, NULL, 0, 0); var mainName = arena.allocateFrom("main"); var mainFunc = LLVMAddFunction(module, mainName, mainType); var entry = LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry")); var builder = LLVMCreateBuilder(); LLVMPositionBuilderAtEnd(builder, entry); // Create a global string constant containing "Hello, World!" var helloStr = LLVMBuildGlobalStringPtr(builder, arena.allocateFrom("Hello, World!"), arena.allocateFrom("hello\_str")); // Create puts function type: int puts(char*) var putsParamTypes = arena.allocate(ADDRESS, 1); var charPtrType = LLVMPointerType(LLVMInt8Type(), 0); putsParamTypes.set(ADDRESS, 0, charPtrType); var putsType = LLVMFunctionType(int32Type, putsParamTypes, 1, 0); // Add puts function to the module var putsFunc = LLVMAddFunction(module, arena.allocateFrom("puts"), putsType); // TODO: Call puts “Hello, World!” // Return 0 LLVMBuildRet(builder, LLVMConstInt(int32Type, 0, 0)); var llvmIrCharPtr = LLVMPrintModuleToString(module); try { System.out.println(llvmIrCharPtr.getString(0)); } catch (Exception e) { System.err.println("Failed to write LLVM IR: failed to get error message: " + e.getMessage()); } // Clean up LLVM resources LLVMDisposeMessage(llvmIrCharPtr); LLVMDisposeBuilder(builder); LLVMDisposeModule(module); }} Now that we’ve declared the function we can call it with the @hello_str global as a parameter using the LLVMBuildCall2 function:

public static void main(String[] args){ try (Arena arena = Arena.ofConfined()) { var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // Create main function signature: int main() var int32Type = LLVMInt32Type(); var mainType = LLVMFunctionType(int32Type, NULL, 0, 0); var mainName = arena.allocateFrom("main"); var mainFunc = LLVMAddFunction(module, mainName, mainType); var entry = LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry")); var builder = LLVMCreateBuilder(); LLVMPositionBuilderAtEnd(builder, entry); // Create a global string constant containing "Hello, World!" var helloStr = LLVMBuildGlobalStringPtr(builder, arena.allocateFrom("Hello, World!"), arena.allocateFrom("hello\_str")); // Create puts function type: int puts(char*) var putsParamTypes = arena.allocate(ADDRESS, 1); var charPtrType = LLVMPointerType(LLVMInt8Type(), 0); putsParamTypes.set(ADDRESS, 0, charPtrType); var putsType = LLVMFunctionType(int32Type, putsParamTypes, 1, 0); // Add puts function to the module var putsFunc = LLVMAddFunction(module, arena.allocateFrom("puts"), putsType); // Create puts function call var callArgs = arena.allocate(ADDRESS, 1); callArgs.set(ADDRESS, 0, helloStr); LLVMBuildCall2(builder, putsType, putsFunc, callArgs, 1, arena.allocateFrom("puts")); // Return 0 LLVMBuildRet(builder, LLVMConstInt(int32Type, 0, 0)); var llvmIrCharPtr = LLVMPrintModuleToString(module); try { System.out.println(llvmIrCharPtr.getString(0)); } catch (Exception e) { System.err.println("Failed to write LLVM IR: failed to get error message: " + e.getMessage()); } // Clean up LLVM resources LLVMDisposeMessage(llvmIrCharPtr); LLVMDisposeBuilder(builder); LLVMDisposeModule(module); }} Running the program’s output through lli will finally display the expected result: “Hello, World!”:

$ java -cp target/jvm-llvm-helloworld-1.0-SNAPSHOT.jar com.example.App | lliHello, World! Congratulations, you’ve successfully used the Java FFM API to call the LLVM C API to build an LLVM module that contains code to print “Hello, World!”.

Just-in-time (JIT) CompilationSo far, we’ve been printing LLVM IR and letting lli execute it. But LLVM also exposes a JIT compiler API, allowing us to generate and execute machine code in-memory. Let’s see how to JIT our “Hello, World!” directly from Java.

LLVM IR is target independent but once we start compiling to native code we must know which machine we are targeting. We’ll target x86 Linux in the following code; if you’re using ARM, Mac or Windows you’ll need to adjust the code for your machine.

The first step is to initialise and create an LLVM JIT compiler for the target machine:

public static void main(String[] args){ try (Arena arena = Arena.ofConfined()) { var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // Create main function signature: int main() var int32Type = LLVMInt32Type(); var mainType = LLVMFunctionType(int32Type, NULL, 0, 0); var mainName = arena.allocateFrom("main"); var mainFunc = LLVMAddFunction(module, mainName, mainType); var entry = LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry")); var builder = LLVMCreateBuilder(); LLVMPositionBuilderAtEnd(builder, entry); // Create a global string constant containing "Hello, World!" var helloStr = LLVMBuildGlobalStringPtr(builder, arena.allocateFrom("Hello, World!"), arena.allocateFrom("hello\_str")); // Create puts function type: int puts(char*) var putsParamTypes = arena.allocate(ADDRESS, 1); var charPtrType = LLVMPointerType(LLVMInt8Type(), 0); putsParamTypes.set(ADDRESS, 0, charPtrType); var putsType = LLVMFunctionType(int32Type, putsParamTypes, 1, 0); // Add puts function to the module var putsFunc = LLVMAddFunction(module, arena.allocateFrom("puts"), putsType); // Create puts function call var callArgs = arena.allocate(ADDRESS, 1); callArgs.set(ADDRESS, 0, helloStr); LLVMBuildCall2(builder, putsType, putsFunc, callArgs, 1, arena.allocateFrom("puts")); // Return 0 LLVMBuildRet(builder, LLVMConstInt(int32Type, 0, 0)); // Initialize LLVM JIT + x86 Target LLVMLinkInMCJIT(); LLVMInitializeX86Target(); LLVMInitializeX86TargetInfo(); LLVMInitializeX86TargetMC(); LLVMInitializeX86AsmPrinter(); LLVMInitializeX86AsmParser(); // Create JIT execution engine var jitCompiler = arena.allocate(ADDRESS); var jitErrorMsgPtrPtr = arena.allocate(ADDRESS); LLVMCreateJITCompilerForModule(jitCompiler, module, /* optimization level = */ 2, jitErrorMsgPtrPtr); // Disable the IR printing now // var llvmIrCharPtr = LLVMPrintModuleToString(module); // // try { // System.out.println(llvmIrCharPtr.getString(0)); // } catch (Exception e) { // System.err.println("Failed to write LLVM IR: failed to get error message: " + e.getMessage()); // } // Clean up LLVM resources // LLVMDisposeMessage(llvmIrCharPtr); LLVMDisposeBuilder(builder); LLVMDisposeModule(module); }} LLVMCreateJITCompilerForModule sets up a JIT execution engine to compile an LLVM module to native machine code. LLVMCreateJITCompilerForModule will return a 1 upon failure and then we can check the error message string for more information but to simplify things we’ll ignore error handling for now.

Requesting the address of the main function triggers its compilation – LLVM generates the machine code only when it’s first needed, hence the name Just-In-Time compilation. We can retrieve a pointer to the compiled function using LLVMGetPointerToGlobal:

public static void main(String[] args){ try (Arena arena = Arena.ofConfined()) { var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // Create main function signature: int main() var int32Type = LLVMInt32Type(); var mainType = LLVMFunctionType(int32Type, NULL, 0, 0); var mainName = arena.allocateFrom("main"); var mainFunc = LLVMAddFunction(module, mainName, mainType); var entry = LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry")); var builder = LLVMCreateBuilder(); LLVMPositionBuilderAtEnd(builder, entry); // Create a global string constant containing "Hello, World!" var helloStr = LLVMBuildGlobalStringPtr(builder, arena.allocateFrom("Hello, World!"), arena.allocateFrom("hello\_str")); // Create puts function type: int puts(char*) var putsParamTypes = arena.allocate(ADDRESS, 1); var charPtrType = LLVMPointerType(LLVMInt8Type(), 0); putsParamTypes.set(ADDRESS, 0, charPtrType); var putsType = LLVMFunctionType(int32Type, putsParamTypes, 1, 0); // Add puts function to the module var putsFunc = LLVMAddFunction(module, arena.allocateFrom("puts"), putsType); // Create puts function call var callArgs = arena.allocate(ADDRESS, 1); callArgs.set(ADDRESS, 0, helloStr); LLVMBuildCall2(builder, putsType, putsFunc, callArgs, 1, arena.allocateFrom("puts")); // Return 0 LLVMBuildRet(builder, LLVMConstInt(int32Type, 0, 0)); // Initialize LLVM JIT + x86 Target LLVMLinkInMCJIT(); LLVMInitializeX86Target(); LLVMInitializeX86TargetInfo(); LLVMInitializeX86TargetMC(); LLVMInitializeX86AsmPrinter(); LLVMInitializeX86AsmParser(); // Create JIT execution engine var jitCompiler = arena.allocate(ADDRESS); var jitErrorMsgPtrPtr = arena.allocate(ADDRESS); LLVMCreateJITCompilerForModule(jitCompiler, module, /* optimization level = */ 2, jitErrorMsgPtrPtr); var executionEngine = jitCompiler.get(ADDRESS, 0); var addressOfMainFunc = LLVMGetPointerToGlobal(executionEngine, mainFunc); // Disable the IR printing now // var llvmIrCharPtr = LLVMPrintModuleToString(module); // // try { // System.out.println(llvmIrCharPtr.getString(0)); // } catch (Exception e) { // System.err.println("Failed to write LLVM IR: failed to get error message: " + e.getMessage()); // } // Clean up LLVM resources // LLVMDisposeMessage(llvmIrCharPtr); LLVMDisposeBuilder(builder); LLVMDisposeModule(module); }} Now that we’ve compiled the function, we need a way to invoke it from Java. To do this, we use the foreign linker to create a MethodHandle for the JIT-compiled main function. This handle acts as a callable reference to the native code:

public static void main(String[] args){ try (Arena arena = Arena.ofConfined()) { var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // Create main function signature: int main() var int32Type = LLVMInt32Type(); var mainType = LLVMFunctionType(int32Type, NULL, 0, 0); var mainName = arena.allocateFrom("main"); var mainFunc = LLVMAddFunction(module, mainName, mainType); var entry = LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry")); var builder = LLVMCreateBuilder(); LLVMPositionBuilderAtEnd(builder, entry); // Create a global string constant containing "Hello, World!" var helloStr = LLVMBuildGlobalStringPtr(builder, arena.allocateFrom("Hello, World!"), arena.allocateFrom("hello\_str")); // Create puts function type: int puts(char*) var putsParamTypes = arena.allocate(ADDRESS, 1); var charPtrType = LLVMPointerType(LLVMInt8Type(), 0); putsParamTypes.set(ADDRESS, 0, charPtrType); var putsType = LLVMFunctionType(int32Type, putsParamTypes, 1, 0); // Add puts function to the module var putsFunc = LLVMAddFunction(module, arena.allocateFrom("puts"), putsType); // Create puts function call var callArgs = arena.allocate(ADDRESS, 1); callArgs.set(ADDRESS, 0, helloStr); LLVMBuildCall2(builder, putsType, putsFunc, callArgs, 1, arena.allocateFrom("puts")); // Return 0 LLVMBuildRet(builder, LLVMConstInt(int32Type, 0, 0)); // Initialize LLVM JIT + x86 Target LLVMLinkInMCJIT(); LLVMInitializeX86Target(); LLVMInitializeX86TargetInfo(); LLVMInitializeX86TargetMC(); LLVMInitializeX86AsmPrinter(); LLVMInitializeX86AsmParser(); // Create JIT execution engine var jitCompiler = arena.allocate(ADDRESS); var jitErrorMsgPtrPtr = arena.allocate(ADDRESS); LLVMCreateJITCompilerForModule(jitCompiler, module, /* optimization level = */ 2, jitErrorMsgPtrPtr); var executionEngine = jitCompiler.get(ADDRESS, 0); var addressOfMainFunc = LLVMGetPointerToGlobal(executionEngine, mainFunc); // Create method handle to the int main() function that // we just created and compiled. var functionHandle = Linker.nativeLinker().downcallHandle( addressOfMainFunc, FunctionDescriptor.of(/* returnType = */ JAVA\_INT) ); // Disable the IR printing now // var llvmIrCharPtr = LLVMPrintModuleToString(module); // // try { // System.out.println(llvmIrCharPtr.getString(0)); // } catch (Exception e) { // System.err.println("Failed to write LLVM IR: failed to get error message: " + e.getMessage()); // } // Clean up LLVM resources // LLVMDisposeMessage(llvmIrCharPtr); LLVMDisposeBuilder(builder); LLVMDisposeModule(module); }} The downcallHandle method tells Java how to interpret the native function’s signature – in this case, a function that takes no arguments and returns an int.

Now we can invoke the compiled native function directly from Java, just like a regular method call:

public static void main(String[] args){ try (Arena arena = Arena.ofConfined()) { var module = LLVMModuleCreateWithName(arena.allocateFrom("hello")); // Create main function signature: int main() var int32Type = LLVMInt32Type(); var mainType = LLVMFunctionType(int32Type, NULL, 0, 0); var mainName = arena.allocateFrom("main"); var mainFunc = LLVMAddFunction(module, mainName, mainType); var entry = LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry")); var builder = LLVMCreateBuilder(); LLVMPositionBuilderAtEnd(builder, entry); // Create a global string constant containing "Hello, World!" var helloStr = LLVMBuildGlobalStringPtr(builder, arena.allocateFrom("Hello, World!"), arena.allocateFrom("hello\_str")); // Create puts function type: int puts(char*) var putsParamTypes = arena.allocate(ADDRESS, 1); var charPtrType = LLVMPointerType(LLVMInt8Type(), 0); putsParamTypes.set(ADDRESS, 0, charPtrType); var putsType = LLVMFunctionType(int32Type, putsParamTypes, 1, 0); // Add puts function to the module var putsFunc = LLVMAddFunction(module, arena.allocateFrom("puts"), putsType); // Create puts function call var callArgs = arena.allocate(ADDRESS, 1); callArgs.set(ADDRESS, 0, helloStr); LLVMBuildCall2(builder, putsType, putsFunc, callArgs, 1, arena.allocateFrom("puts")); // Return 0 LLVMBuildRet(builder, LLVMConstInt(int32Type, 0, 0)); // Initialize LLVM JIT + x86 Target LLVMLinkInMCJIT(); LLVMInitializeX86Target(); LLVMInitializeX86TargetInfo(); LLVMInitializeX86TargetMC(); LLVMInitializeX86AsmPrinter(); LLVMInitializeX86AsmParser(); // Create JIT execution engine var jitCompiler = arena.allocate(ADDRESS); var jitErrorMsgPtrPtr = arena.allocate(ADDRESS); LLVMCreateJITCompilerForModule(jitCompiler, module, /* optimization level = */ 2, jitErrorMsgPtrPtr); var executionEngine = jitCompiler.get(ADDRESS, 0); var addressOfMainFunc = LLVMGetPointerToGlobal(executionEngine, mainFunc); // Create method handle to the int main() function that // we just created and compiled. var functionHandle = Linker.nativeLinker().downcallHandle( addressOfMainFunc, FunctionDescriptor.of(/* returnType = */ JAVA\_INT) ); // Execute the main function via the method handle. try { int result = (int) functionHandle.invoke(); System.out.println("main() returned: " + result); } catch (Throwable e) { System.err.println("Error calling JIT function: " + e.getMessage()); } // Disable the IR printing now // var llvmIrCharPtr = LLVMPrintModuleToString(module); // // try { // System.out.println(llvmIrCharPtr.getString(0)); // } catch (Exception e) { // System.err.println("Failed to write LLVM IR: failed to get error message: " + e.getMessage()); // } // Clean up LLVM resources // LLVMDisposeMessage(llvmIrCharPtr); LLVMDisposeBuilder(builder); LLVMDisposeModule(module); }} When functionHandle.invoke() runs, Java crosses into the native world and calls the machine code that was just compiled by the LLVM JIT compiler.

And that’s it, you can now run the Java application without the LLVM interpreter and see the resulting “Hello, World!”:

$ java -cp target/jvm-llvm-helloworld-1.0-SNAPSHOT.jar com.example.App Hello, World! Congratulations, you’ve now JIT-compiled Hello World, with the help of Java’s FFM API calling LLVM’s C API.

Next stepsIn this Java advent we built and executed native machine code from pure Java and a little help from LLVM – no JNI, no C glue, just memory segments, method handles, and a modern FFI. By the end, we had just a simple program that prints “Hello, World!” but it shows the potential of the Java FFM API and the things you can do when Java and native code work together.

Now see what else you can do, for example, try generating other instructions: print more text, do simple calculations, or even build tiny programs entirely in LLVM from Java.

The full code for this post is available on GitHub over here.

The post Java Hello World, LLVM Edition appeared first on JVM Advent.

View Details

You learn by comparing to what you already know. I was recently bitten by assuming Rust worked as Java regarding transitive dependency version resolution. In this post, I want to compare the two.

Dependencies, transitivity, and version resolutionBefore diving into the specifics of each stack, let’s describe the domain and the problems that come with it.

When developing any project above Hello World level, chances are you’ll face problems that others have faced before. If the problem is widespread, the probability is high that somebody was kind and civic-minded enough to have packaged the code that solves it, for others to re-use. Now you can use the package and focus on solving your core problem. It’s how industry builds most projects today, even if it brings other problems: you sit on the shoulders of giants.

Languages come with build tools that can add such packages to your project. Most of them refer to packages you add to your project as dependencies. In turn, projects’ dependencies can have their own dependencies: the latter are called transitive dependencies.

In the above diagram, C and D are transitive dependencies.

Transitive dependencies have issues on their own. The biggest one is when a transitive dependency is required from different paths, but in different versions. In the diagram below, A and B both depend on C, but on different versions of it.

Which version of C should the build tool include in your project? Java and Rust have different answers. Let’s describe them in turn.

Java transitive dependency version resolutionReminder: Java code compiles to bytecode, which is then interpreted at runtime (and sometimes compiled to native code, but this is outside of our current problem space). I’ll first describe runtime dependency resolution and build time dependency resolution.

At runtime, the JVM offers the concept of a classpath. When having to load a class, the runtime searches through the configured classpath in order. Imagine the following class:

public static Main { public static void main(String[] args) { Class.forName("ch.frankel.Dep"); }} Let’s compile it and execute it:

java -cp ./foo.jar:./bar.jar Main The above will first look in the foo.jar for the ch.frankel.Dep class. If found, it stops there and loads the class, regardless of whether it might also be present in the bar.jar; if not, it looks further in the bar.jar class. If still not found, it fails with a ClassNotFoundException.

Java’s runtime dependency resolution mechanism is ordered and has a per class granularity. It applies whether you run a Java class and define the classpath on the command line as above, or whether you run a JAR that defines the classpath in its manifest.

Let’s change the above code to the following:

public static Main { public static void main(String[] args) { var dep = new ch.frankel.Dep(); }} Because the new code references Dep directly, new code requires class resolution at compile-time. Classpath resolution works in the same way:

javac -cp ./foo.jar:./bar.jar Main The compiler looks for Dep in foo.jar, then in bar.jar if not found. The above is what you learn at the beginning of your Java learning journey.

Afterwards, your unit of work is the Java Archive, known as the JAR, instead of the class. A JAR is a glorified ZIP archive, with an internal manifest that specifies its version.

Now, imagine that you’re a user of foo.jar. Developers of foo.jar set a specific classpath when compiling, possibly including other JARs. You’ll need this information to run your own command. How does a library developer pass this knowledge to downstream users?

The community came up with a few ideas to answer this question: The first response that stuck was Maven. Maven has the concept of POM, where you set your project’s metadata, as well as dependencies. Maven can easily resolve transitive dependencies because they also publish their POM, with their own dependencies. Hence, Maven can trace each dependency’s dependencies down to the leaf dependencies.

Now back to the problem statement: how does Maven resolve version conflicts? Which dependency version will Maven resolve for C, 1.0 or 2.0?

The documentation is clear: the nearest.

In the above diagram, the path to v1 has a distance of two, one to B, then one to C; meanwhile, the path to v2 has a distance of three, one to A, then one to D, then finally one to C. Thus, the shortest path points to v1.

However, in the initial diagram, both C versions are at the same distance from the root artifact. The documentation provides no answer. If you’re interested in it, it depends on the order of declaration of A and B in the POM! In summary, Maven returns a single version of a duplicated dependency to include it on the compile classpath.

If A can work with C v2.0 or B with C 1.0, great! If not, you’ll probably need to upgrade your version of A or downgrade your version of B, so that the resolved C version works with both. It’s a manual process that is painful–ask me how I know. Worse, you might find out there’s no C version that works with both A and B. Time to replace A or B.

Rust transitive dependency version resolutionRust differs from Java in several aspects, but I think the following are the most relevant for the sake of our discussion:

  • Rust has the same dependency tree at compile-time and at runtime
  • It provides a build tool out of the box, Cargo
  • Dependencies are resolved from source

Let’s examine them one by one.

Java compiles to _bytecode, then you run the latter. You need to set the classpath both at compilation time and at runtime. Compiling with a specific classpath and running with a different one can lead to errors. For example, imagine you compile with a class you depend on, but the class is absent at runtime. Or alternatively, it’s present, but in an incompatible version.

Contrary to this modular approach, Rust compiles to a unique native package the crate’s code and every dependency. Moreover, Rust provides its own build too, thus avoiding having to remember the quirks of different tools. I mentioned Maven, but other build tools likely have different rules to resolve the version in the use case above.

Finally, Java resolves dependencies from binaries: JARs. On the contrary, Rust resolves dependencies from sources. At build time, Cargo resolves the entire dependency tree, downloads all required sources, and compiles them in the correct order.

With this in mind, how does Rust resolve the version of the C dependency in the initial problem? The answer may seem strange if you come from a Java background, but Rust includes both. Indeed, in the above diagram, Rust will compile A with C v1.0 and compile B with C v2.0. Problem solved.

ConclusionJVM languages, and Java in particular, offer both a compile-time classpath and a runtime classpath. It allows modularity and reusability, but opens the door to issues regarding classpath resolution. On the other hand, Rust builds your crate into a single self-contained binary, whether a library or an executable.

To go further:

  • Maven – Introduction to the Dependency Mechanism
  • Effective Rust – Item 25: Manage your dependency graph

Originally published at A Java Geek on September 14th, 2025

The post Comparing transitive dependency version resolution in Rust and Java appeared first on JVM Advent.

View Details

We all know the mantra: “Write Once, Run Anywhere.” But for most of Java’s history, that “Anywhere” really meant “anywhere, as long as there’s Intel or AMD underneath.” When most of us were starting out with Java, ARM was something you associated with phones, a Raspberry Pi, or some mysterious “embedded” device – not with a serious backend carrying production traffic in a major cloud.

That’s why Java on ARM is still pretty niche today: hardly any backend developer seriously considered it, because for years… there just wasn’t much to talk about. The journey both ARM processors and the broader JVM ecosystem had to go through to catch up with the needs of the server world has been long and bumpy, full of ugly bugs only discovered in production, hurriedly rolled-back patches, and tons of “invisible” work happening inside virtual machines, JIT compilers, and garbage collectors under the hood.

However, to understand why now Java on ARM is finally starting to make sense – and why it’s worth paying attention to – we first need to look at the evolution that the ARM architecture itself has undergone in recent years.


Although ARM has been with us for a long time and has powered phones, tablets, routers and millions of “smart” gadgets for years, it took two key events to really bring it into the halls of “serious” IT. First – Apple’s move to its own ARM chips in Macs. Overnight, a huge number of developers suddenly had ARM-based primary work machines on their desks, not just toy dev boards.

Second – the arrival of the Neoverse architecture, a family of ARM cores designed from scratch with data centers in mind, not smartphones.

To understand Neoverse, it’s worth starting with Cortex. It’s Cortex cores – in their various A, R and M variants – that sit inside most consumer electronics: from phones and tablets to Raspberry Pi boards. They’re designed under very strict constraints on power, cost and die area, so that SoC vendors can build cheap, energy-efficient chips for battery-powered devices. They’re perfect for the “a few strong cores + GPU + modem on one chip” scenario, but much less suited to servers with hundreds of watts of socket power, massive amounts of memory and full-blown enterprise requirements.

Neoverse is ARM’s answer to the question: “what should a server ARM look like if we stop thinking like phone designers?”. It’s a separate IP line, built from the ground up for infrastructure: high core counts (N1/N2/V2 scaling to hundreds of cores per board), large caches, mesh interconnects, a strong focus on memory bandwidth, virtualization, and RAS (Reliability, Availability, Serviceability) features. In other words – Neoverse is to data centers what Cortex is to smartphones: the basic building block partners like AWS, Ampere and others can use to build their own server CPUs without the compromises typical of mobile cores.

It’s Neoverse that really brought ARM into the cloud. The first generation of AWS Graviton was still built on the well-known Cortex-A72 cores and targeted rather “lighter” workloads. The later generations – Graviton2 on Neoverse N1, Graviton3 on Neoverse V1 and Graviton4 on Neoverse V2 – are fully-fledged, high-performance server processors that in many scenarios beat x86 in terms of price/performance and energy efficiency. In practice, this means that for a large chunk of use cases, “EC2 on ARM” is no longer a curiosity but starts becoming the default option.

The second pillar of this revolution are independent vendors such as Ampere with its Altra family of processors, also built on Neoverse N1. These are the chips that power, among others, ARM instances in Oracle Cloud and many other data centers. SoftBank’s acquisition of Ampere Computing for 6.5 billion dollars is a clear signal that ARM CPUs are no longer a niche experiment but a strategic piece of infrastructure for AI and cloud – especially given that SoftBank also controls ARM itself.

The end result is that a huge portion of new cloud-native workloads now land on ARM by default: microservices in Kubernetes, backend services, event-driven systems, application servers like Spring Boot or Quarkus. Hyperscalers are aggressively promoting ARM instances with attractive pricing, while managing to deliver 20–40% better price/performance and significant energy savings compared to classic x86.

For companies counting every watt and every dollar on their cloud bill, and at the same time building new services on top of containers and JVMs, the natural question is increasingly not “will ARM work?”, but “why are we still not using ARM instances?”.

And now it’s time to ask… why, exactly? Time to look back at the history of Java on ARM.


It’s 2011, Cambridge, UK. Andrew Haley (Red Hat) and Jon Masters (Chief ARM Architect at Red Hat) are sitting in a Thai pub called “The Wrestlers.” At some point Masters drops a bomb: 64-bit ARM (AArch64) is coming, and Red Hat wants to bring Red Hat Enterprise Linux to it. There’s just one tiny problem – there’s no Java on this platform. And without Java there is no enterprise. Haley hears an initial estimate: porting OpenJDK will take two experts about a year of work. The catch? Those experts… don’t exist yet. The team has to learn the ARM architecture on the fly, writing code against simulators before any real silicon ever shows up in a data center.

Long before we could talk about performance, we lived in the age of OpenJDK Zero. The idea was beautiful: write a JVM interpreter in pure C++, without a single line of assembly. That way Java would “run” on anything that had a GCC toolchain – from routers to exotic experimental chips. Reality was brutal: performance was awful. Zero had no JIT (Just-In-Time) compiler, so bytecode was interpreted instruction by instruction. It was like pushing a sports car up a hill – technically it moved, but no one wanted to run that on production.

The real breakthrough came with JEP 237, roughly around Java 9. That’s when engineers from Red Hat and Linaro rolled up their sleeves and aimed for a full-blown port with C1 and C2 compilers that truly “understand” ARM. The biggest challenge was changing the mental model. x86 (CISC) is brute force: complex instructions that do many things at once. ARM (RISC) is precision and simplicity, but with a different geometry of power. C2 had to learn how to use 31 general-purpose registers (x86 only has 16). That’s a massive difference – the JVM can keep most “hot” variables directly in CPU registers instead of constantly spilling them out to memory and loading them back, which dramatically changes the performance profile of the whole application.

However, the performance gain happens in intrinsics in JEP 315, where the JVM replaces selected Java methods with hand-written assembly. That’s where we saw both the biggest wins and the most painful failures. On the success side, you have cryptographic instructions (AES) from ARMv8 – suddenly GCM encryption in TLS sped up by 3.5–5x on Graviton2. Similarly, operations on strings (like String.indexOf) started using NEON vector instructions and stopped being such an obvious hotspot in many services. But there were dead ends, too: the attempt to speed up String.equals using NEON turned out to be worth it only for long strings; for the short ones that dominate typical business systems, the overhead of preparing vector registers simply killed the gain. Result: the code ended up in the “rolled back / Won’t Fix” bucket. Even more spectacular was the bug in the Math.log intrinsic (JDK-8210858): for extreme values, logarithms on ARM stopped being monotonic and produced different results than on Intel. In finance or scientific computing, that’s not a “minor discrepancy” – that’s a red alert. In the end, the intrinsic was removed – correctness beat performance.

Of course, it’s not all bells and whistles. For people close to the metal, the biggest shock was the memory model. On x86 we live in the relatively comfy world of TSO (Total Store Order): if one thread writes A and then B, other threads will see those writes in the same order. The processor “smooths over” a lot of concurrency bugs, and developers grew used to the fact that “it somehow works.” ARM plays a different game. It has a weak memory model: it can freely reorder loads and stores if it decides that this will be faster. The JVM had to take all that complexity on its shoulders, sprinkling the code with the right memory barriers – but doing it in a way that wouldn’t kill performance with heavy DMBs everywhere. Newer LSE (Large System Extensions) helped here: hardware-supported atomic instructions like CAS and LDADD, much cheaper than classic locks or thick barriers. Thanks to them, concurrent Java on ARM64 is in no way a “second-class citizen.” And this is not just theory – we literally ran into a bug caused by a missing barrier here recently: JDK-8369506 (“Bytecode rewriting causes Java heap corruption on AArch64”), a rare but nasty case where the weak memory model broke the illusion of safety until it was fixed in HotSpot.

Today, when you run Java 21 on AWS Graviton4 or Google Axion, you’re benefiting from more than a decade of those experiments and missteps. Latency-sensitive workloads on ARM64 have improved by several hundred percent compared to baseline Java 8. A modern GC helps a lot (the generational ZGC loves ARM tricks like TBI – Top Byte Ignore), as do well-tuned intrinsics and native support for SVE2 vectors. Then there’s the character of the CPUs themselves: Ampere Altra, Graviton and friends don’t have Hyper-Threading, so one Java thread is one physical core. The garbage collector doesn’t have to fight your business code for ALUs, which translates into much calmer latency tails – on ARM, tail latency is often noticeably flatter and more predictable than on x86.


The history of Java on ARM is, at its core, a story about how hardware is useless without brutally hard work on the software side. We’ve gone from a slow interpreter in pure C++, through logarithms that calculated “a bit differently,” all the way to virtual machines that, on chips like Google Axion, can squeeze out up to +150% performance in AI workloads compared to the latest Intels.

And while I know there are lies, bigger lies and benchmarks…

…if you’re sticking to x86 purely out of habit, you’re probably burning money. But before you rush to move production to ARM, do one thing: update your JDK. Java 8 on ARM works, but it’s like driving a Ferrari with the handbrake on. The real fun starts with Java 17, and ideally 21 – that’s where you finally see why someone spent all those years grinding away at the AArch64 port.

So if you look for the good business reason to migrate a new Java – I found you one . Of course, if you crave the change – not everybody’s do.

The post Beyond x86: Java on ARM in 2025 appeared first on JVM Advent.

View Details

Creating or modifying an application involves many aspects, such as following best practices and applying design patterns to solve everyday problems. After writing the code, developers usually add unit tests and rely on tools like Sonar to track metrics such as code coverage and highlight potentially untested areas.

However, high test coverage does not guarantee low risk in production. A test may execute a line of code without actually verifying the outcome. A test may instantiate an object but never check all of its significant attributes. Coverage shows what code ran—not whether the tests would catch a fundamental defect.

This raises an important question: how can we measure not only how much code is tested, but how practical those tests really are?

context of the situationImagine a team responsible for several microservices. The team is recognized as one of the best, with strong code coverage and consistent promotion of good practices, such as using Sonar to detect problems and creating integration tests to verify interactions between the application and external resources, such as databases.

One day, a new feature was deployed to production—just a slight change in a few classes. Nothing that appeared risky, considering the large number of existing tests. However, a few minutes later, a significant issue surfaced, affecting the entire platform rather than just the application involved.

Seconds after the problem appeared, someone on the team analyzed the code, detected the issue, and fixed it. During the investigation, it became clear that the tests were not reliable, as they neither failed before nor after the changes.

The following is the test that caused the problem:

``` @Test
public void should_return_a_country() {
when(countryRepository.findByCode("AR"))
.thenReturn(getCountryModel());

CountryDTO response = countryService.getCountryByCode("AR");

} ``` The test calls a method but overlooks crucial attributes, potentially causing errors in other applications.

What’s mutation testing?Mutation testing is a technique for evaluating the effectiveness of a test suite by introducing small, controlled changes into the code and verifying whether the tests detect them. Instead of measuring only which lines of code are executed, mutation testing focuses on the quality of assertions and the ability of tests to catch meaningful defects.

CORE CONCEPTSThe heart of this technique implies a set of concepts:

  • Mutants: A mutation is a minor, controlled modification made to source code. Examples include flipping a boolean condition, replacing an arithmetic operator, or removing a method call.
  • Goal: Check if the existing tests can detect these changes. If a test fails when a mutant is introduced, it indicates that the test suite is sensitive enough to detect behavioral differences, a sign of strong, practical tests.

After the creation of the mutants and executing all the tests, each mutation could stay in two states:

  • Killed: When at least one test fails after a change, it indicates that the test suite has effectively detected a behavioral difference.
  • Survived: A mutant survives when all tests pass despite the injected modification. Typically, this happens when an assertion is weak or when test cases are missing.

mUTATION SCOREMutation testing provides a percentage indicating how practical the tests are. To calculate this, it’s necessary to use the following formula:

Mutation Testing Score

The way to interpret the percentage is:

  • High scores (e.g., 80–95%) often suggest strong test coverage with meaningful assertions.
  • Medium scores highlight opportunities to improve tests or simplify logic.
  • Low scores (below ~50%) usually indicate insufficient or weak tests that may not protect against regressions.

It’s important to note that a high mutation test score does not mean the application is bug-free.

Types of MutationsMutation testing tools create various modifications, known as mutations, that simulate potential defects in the code. While the specific mutation operators can differ based on the programming language or library used, they generally fall into three main categories: Decision mutations, Statement mutations, and Value mutations. Each category focuses on different aspects of the program’s behavior, helping assess how effectively the test suite validates the code’s logic, control flow, and data integrity.

Let’s see a brief explanation about each of them:

  • Decision Mutations: Modify the conditions that control program flow. These mutations focus on expressions found in if, switch, while, for, and boolean-returning operations.
  • Statement Mutations: Work by adding, removing, or altering entire statements. They test whether the test suite can detect situations where part of the logic disappears or behaves differently.
  • Value Mutations: Value mutations target constants, literals, return values, and field assignments. They simulate defects caused by incorrect data produced or used by the program.

How to IMPLEMENT IT ON AN APPLICATION?To implement mutation testing in a JVM ecosystem, several libraries are available, such as Pitest, Major, and MuJava. The first option is the best because it is actively maintained, highly performant, integrates seamlessly with Maven, Gradle, JUnit, and TestNG, supports incremental analysis with extensive configuration options, generates clear HTML reports of killed and surviving mutants, and even provides plugins for SonarQube and other tools.

This article uses a source from a GitHub repository; feel free to clone it and use it to learn about mutation testing.

To use this library, you first need to add the dependency to your application. The following block represents how to do it on a Maven project:

```

org.pitest  
pitest-maven  
${pitest-maven.version}



        HTML  
        XML


        com.twa.flights.api.catalog.*


        com.twa.flights.api.catalog.*




        org.pitest  
        pitest-junit5-plugin  
        ${pitest-junit5-plugin.version}

``` As a recommendation, check the latest version of this library on the official webpage or a repository like this regularly.

Pitest allows users to export execution results in multiple formats, including HTML, CSV, and XML. The relevance of each format depends on the report’s purpose. For example, the HTML format is ideal for those who want a simple view of execution results, including the number of mutations used. In contrast, the XML format helps integrate this information with other tools, such as Sonar, which can display mutation execution results.

On this tool, it’s possible to indicate in the same way that appears on the previous code block, which packages or test classes will be mutated, and it’s possible to indicate the same about which package of the source code will suffer modifications.

Executing mutation testing implies just running a command like the following:

``` $ mvn clean package org.pitest:pitest-maven:mutationCoverage
[INFO] --- pitest:1.7.6:mutationCoverage (default-cli) @ api-catalog ---
[INFO] Root dir is : /home/asacco/Code/testing-your-test/api-catalog
[INFO] Found plugin : Default csv report plugin
[INFO] Found plugin : Default xml report plugin
[INFO] Found plugin : Default html report plugin
......
[INFO] Found shared classpath plugin : Default mutation engine
[INFO] Found shared classpath plugin : JUnit 5 test framework support
[INFO] Found shared classpath plugin : JUnit plugin
[INFO] Available mutators : EXPERIMENTAL_ARGUMENT_PROPAGATION,FALSE_RETURNS,TRUE_RETURNS,CONDITIONALS_BOUNDARY,CONSTRUCTOR_CALLS,EMPTY_RETURNS,INCREMENTS,INLINE_CONSTS,INVERT_NEGS,MATH,NEGATE_CONDITIONALS,NON_VOID_METHOD_CALLS,NULL_RETURNS,PRIMITIVE_RETURNS,REMOVE_CONDITIONALS_EQUAL_IF,REMOVE_CONDITIONALS_EQUAL_ELSE,REMOVE_CONDITIONALS_ORDER_IF,REMOVE_CONDITIONALS_ORDER_ELSE,RETURN_VALS,VOID_METHOD_CALLS,EXPERIMENTAL_BIG_DECIMAL,EXPERIMENTAL_BIG_INTEGER,EXPERIMENTAL_MEMBER_VARIABLE,EXPERIMENTAL_NAKED_RECEIVER,REMOVE_INCREMENTS,EXPERIMENTAL_RETURN_VALUES_MUTATOR,EXPERIMENTAL_SWITCH,EXPERIMENTAL_BIG_DECIMAL,EXPERIMENTAL_BIG_INTEGER
......
......
================================================================================
- Timings
================================================================================

pre-scan for mutations : < 1 second
scan classpath : < 1 second
coverage and dependency analysis : < 1 second
build mutation tests : < 1 second
run mutation analysis : 4 seconds


> Total : 5 seconds

================================================================================
- Statistics
================================================================================

Line Coverage: 63/195 (32%)
Generated 64 mutations Killed 7 (11%)
Mutations with no coverage 54. Test strength 70%
Ran 15 tests (0.23 tests per mutation)
Enhanced functionality available at https://www.arcmutate.com/
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9.757 s
[INFO] Finished at: 2025-11-27T10:07:37-03:00
[INFO] ------------------------------------------------------------------------ ``` Execution time may vary depending on the size of the source code and the available resources on the machine where these tests are running.

To see the HTML report graphically, open the target folder and look for the pit-reports folder. The report will look like the following image:

Mutation Testing: General Overview

To reduce execution time, you can use historical execution data to detect changes in code and tests. In concrete terms, the command implies adding only one parameter, as shown in the following block.

$ mvn clean package org.pitest:pitest-maven:mutationCoverage -DwithHistory ...... ..... [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 5.689 s [INFO] Finished at: 2025-11-27T10:21:54-03:00 [INFO] ------------------------------------------------------------------------ The execution time passes from 9,7 to 5,6 seconds in a small project with a few classes. The approach is beneficial when the applications have a lot of code and tests.

A critical aspect of mutation testing is the ability to use multiple mutation engines. An engine is responsible for modifying the source code; in some cases, changing all the logic inside a method or a class, rather than just adjusting the method’s parameters or its response. By default, Pitest uses Gregor, which introduces modifications to the different sentences of a technique, but it’s possible to use Descartes, which reduces the modifications. To use it, it’s necessary to introduce some changes, like the following:

```

org.pitest  
pitest-maven  
${pitest-maven.version}



      HTML  
      XML


      com.twa.flights.api.catalog.*


      com.twa.flights.api.catalog.*

   descartes



      org.pitest  
      pitest-junit5-plugin  
      ${pitest-junit5-plugin.version}


      eu.stamp-project  
      descartes  
      1.3.2

``` As a recommendation, check the latest version of this library, as new versions are released at regular intervals.

What are THE Challenges and costs?Introducing mutation testing into an existing application is not free of challenges, as it requires understanding its limitations and trade-offs. With this in mind, it’s crucial to set realistic expectations for this type of testing. Some of the most relevant issues are:

  • Execution time: Creating mutations and executing tests takes time because it involves generating source code variations and running tests to validate their effects. In large applications with hundreds of tests or large codebases, this could increase drastically. In some cases, this situation could be a barrier to implementing this type of testing in a CI pipeline.
  • Flaky or unstable tests: In some cases, tests pass or fail sporadically due to issues with concurrent access to external resources. These scenarios could affect mutation execution, leading to false positives, so it’s essential to either exclude these tests or find a way to mitigate the problem.
  • Resource consumption: In addition to the time required to execute the tests and create the mutation, there are other resource-related issues, such as CPU and memory usage. This can affect not only the pipeline running mutation tests but also other jobs running in the same CI environment. The big challenge here is to reduce resource consumption, limit mutations, or shorten the execution time of each pipeline.
  • Complex configuration: Most tools or libraries offer many parameters to achieve the best configuration for each application. The first attempts to use these tools could lead to unrealistic expectations about the performance and results that are achievable. Finding the right balance between performance, accuracy, and execution time often requires several iterations.

None of these issues invalidates the benefits of mutation testing, but it’s essential to develop a plan to mitigate or reduce their impact.

WHICH STRATEGIES EXIST for Adopting IT?Adopting a new technique or tool involves several considerations, especially in an existing application with many classes and tests. There is no magic formula for implementing mutation testing without pain, but there are different approaches to reduce the problems to a low level. Some of the most relevant strategies are:

  • Start small: This approach focuses on familiarizing with the tool and scanning just one package, module, or critical flow to identify potential implementation issues. It’s beneficial when there are too many unit tests on the application.
  • Focus on high-risk code first: The critical code or flows are what anyone on a team or company wants to know whether it works or not. Adding mutation testing to a few classes that represent those flows can be implemented with minimal impact. Once everything is in order, we can incrementally update the configuration to scan more packages.
  • Limiting the scope: At some point, execution time becomes a critical factor, so a possible approach is to limit the number or types of mutations that can be created. It could be a good starting point: only include the most relevant mutator during execution, and measure the impact before including all the mutations.
  • Restrict execution: These tests may take longer to deploy, so a possible approach is to restrict their use locally or in the principal pipeline, which uses them to deploy to some productive environments.

It is possible to use one of these strategies or combine them to achieve better results, but in all cases, the choice depends on the size of the application and the number of unit tests.

WHAT’S NEXT?There are many resources on unit testing and mutation testing. The following is just a short list of resources:

  • Latent Mutants: A large-scale study on the Interplay between mutation testing and software evolution by Jeongju Sohn
  • Practical Mutation Testing at Scale: A view from Google
  • Mutation Testing in Evolving Systems: Studying the Relevance of Mutants to Code Evolution by Milos Ojdanic
  • Does mutation testing improve testing practices? by Goran Petrovic

Other resources could be great for understanding some concepts related to testing in depth:

  • Testing Web APIs by Mark Winteringham
  • Unit Testing Principles, Practices, and Patterns by Vladimir Khorikov
  • Software Testing with Generative AI by Mark Winteringham

Consider this just a small list of available resources. If something is unclear, find another video or resource.

CONCLUSIONCreating tests for an application does not guarantee that nothing will go wrong, and mutation testing is not a silver bullet that can detect every possible issue. However, it provides a valuable and objective way to evaluate how practical existing tests really are.

A practical approach is to adopt it gradually: start with a small number of packages or a limited mutation scope, measure the effect on the build and pipeline, and then expand its use as appropriate.

Used pragmatically, mutation testing can significantly improve test quality and increase confidence in the application’s behavior without overwhelming the development process.

The post Test Your Test appeared first on JVM Advent.

View Details

When I first stepped into backend development, I believed programming was only about one thing:

“If the output comes, the job is done.”

I bundled everything into one giant file — controllers, logic, database access.I didn’t understand why Spring insisted on layers or why DI, IoC, and annotations were so important. The project structure looked overwhelming, and everything felt abstract.

Everything changed the day I decided to trace my first Spring Boot request and watch how a simple API call traveled through the system. Suddenly, what looked like scattered pieces turned into a well-designed, meaningful architecture. It didn’t just teach me Spring Boot — it changed the way I think about writing software.


What I Used to Think Backend Development WasIn the beginning, this was my reality:

  • All classes dumped into one package
  • Endless, congested code inside one file
  • No idea what executes, what doesn’t, or why
  • Maven structure felt unnecessary
  • Layers didn’t make sense
  • Annotations were intimidating
  • No understanding of why Service existed between Controller and Repository

It was like trying to build a house without knowing what bricks, beams, or foundations were.


The Turning Point: Tracing My First RequestOne day, I opened my console logs and started following the journey of an incoming request:

  • It first hit the Controller
  • Then passed into the Service
  • Then reached the Repository
  • Finally touched the Database
  • And then returned the response through the same layers

That visual flow — even just in the logs — changed everything for me.

I realized:

  • DI reduces tight coupling
  • IoC manages object lifecycles so you don’t
  • Layers exist to protect and organize the system
  • Spring Boot is powerful because of its structure, not despite it

Architecture became a living system, not a theory.


Controller → Service → Repository: The Flow That Made Everything ClickController — The Entry PointThe controller receives the request, interacts with the service, and returns the appropriate response or view.

@Controller@RequestMapping("/employees")public class EmployeeController { private EmployeeService employeeService; public EmployeeController(EmployeeService theEmployeeService) { employeeService = theEmployeeService; } @GetMapping("/list") public String listEmployees(Model theModel) { List<Employee> theEmployees = employeeService.findAll(); theModel.addAttribute("employees", theEmployees); return "employees/list-employees"; }} Controllers should be small and focused — no business logic inside them.


Service — The Logic LayerThis is where business decisions happen. It protects the repository from being accessed directly.

public interface EmployeeService { List<Employee> findAll(); Employee findById(int theId); void save(Employee theEmployee); void deleteById(int theId); boolean existsByEmailId(Employee theEmployee);} Implementation:

@Servicepublic class EmployeeServiceImpl implements EmployeeService { private EmployeeRepository employeeRepository; @Autowired public EmployeeServiceImpl(EmployeeRepository theEmployeeRepository) { employeeRepository = theEmployeeRepository; } @Override public List<Employee> findAll() { return employeeRepository.findAllByOrderByLastNameAsc(); }} The service layer keeps your application clean, secure, and maintainable.


Repository — Where Database Interactions HappenWith Spring Data JPA, repositories become incredibly simple:

public interface EmployeeRepository extends JpaRepository<Employee, Integer> { boolean existsByEmail(String email); List<Employee> findAllByOrderByLastNameAsc();} The framework generates most queries automatically.


Entity — Mapping Java Objects to Database TablesEntities act as a bridge between the database and your Java application.

@Entity@Table(name="employee")public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @NotBlank private String firstName; @NotBlank private String lastName; @NotBlank private String email; // constructors, getters, setters} Spring (plus Jackson) takes care of converting between JSON Java Database.


When MVC Finally Made SenseMoving into full MVC with Thymeleaf gave me clarity:

  • Controllers handle the request
  • Services handle business logic
  • Repositories handle persistence
  • Templates display the data

The project structure stopped looking like a burden and became something I respected. The flow felt natural and powerful.


A Simple CRUD Example That Made It RealThe moment everything clicked was when I saw this flow in my own CRUD app:

  • Client hits GET /employees/list
  • Controller asks Service for data
  • Service queries the Repository
  • Repository returns data from the database
  • Controller adds data to Model
  • Thymeleaf displays it on the page

Suddenly the architecture wasn’t an academic concept — it was working in front of me.

Here is the GitHub repository of the CRUD app I used in this article:

https://github.com/MAffanG/employee-crud-api


What This Taught Me About CodingUnderstanding the request flow transformed everything about my approach:

  • I no longer chase outputs — I chase clarity
  • I think in terms of architecture, not hacks
  • Debugging became easier and faster
  • My code became cleaner and more intentional
  • I finally felt like a backend developer, not someone just trying things until they work

This experience didn’t just teach me Spring Boot — it changed the way I respect software design as a whole.

The post How Understanding Request Flow in Spring Boot Changed the Way I Code appeared first on JVM Advent.

View Details

LangChain4j is a top (if not the top) library in use for integrating Java applications with various LLMs (large language models). It provides a number of features such as a unified API for LLM integration, support for vector stores, prompt templates, RAG (retrievalaugmented generation) and more. While we can use it directly in popular frameworks like Quarkus and Spring there are more specific extensions for those frameworks that provide for a simplified use of the various utilities provuded by the library.

The choice of Quarkus as a modern cloud-native framework in this article is not random: the framework already provides a huge ecosystem of extension and is being active used in more and more modern Java applicaations. And it really brings developer joy with all nice goodies like fast dev mode with hot reload, dev UI, dev services, unified configuration to name a few essential ones.

In this brief article we will discuss how the Quarkus LangChain4j extensions simplifies further use of the library and how we can use it to build an agentic application.

So why a separate extension for LangChain4j ?There are several primary reasons to have a separate extension:

  • the Quarkus architecture makes heavy use of build time optimizations including the extension mechanism so that extensions can be optimized for build time optimizations and native-image builds
  • better integration using CDI beans (Quarkus uses a deicated dependency injection mechanism called ArC as a CDI implementation) in the form of a additional @AiService annotation for autodiscovery of AI services
  • type-safe configuration for LangChain4j as supported by Quarkus using specific configuration classes for LangChain4j allowing user to define configuration such as:

quarkus.langchain4j.openai.api-key
quarkus.langchain4j.chat-model

  • integration with Quarkus dev mode (i.e. for hot reload of configuration, Dev UI extension);
  • sensible default observability configuration, including one for OpenTelemetry and tracing of calls to LLMs

All of this sounds really great, not only a simple plugin for Quarkus but a highly optimized and simplified use of the LangChain4j framework !

Now let’s just demonstrate how we can quick start using it with a practical example: a simple agentic healthcare application that given a description of medical conditions gives a prediction on which physician should be visited within a target hospital. The hospital stores an information in form of a table that maps a specific disease to a doctor that most expertise in that disease within the hospital. Let’s call it DocPredict.

DocPredict: THE High-Level ArchitectureSo straight to the point, let’s define how our simple healthcare assistant looks like from a high level perspective and build it:

Ideally if we want to build a complex frontend we would prefer to implement the DocPredict Web application using a modern favascript framework like React, Vue.js or AngularJS but in our case we will use a server-side rendering engine called Qute provided by Quarkus. Our service will interact with an OpenAI model to make the proper prediction based on a supplied definition and then try to map the respose to a proper doctor based on the existing hospital database. To not overcomplicate matters the database will be simply a CSV file with disease -> doctor pairs.

DocPredict: The codeTo quickly bootstrap our initial application we can navigate to https://code.quarkus.io/ and supply proper configuration which is as simple as providing the basic Maven configuration and adding the following extensions:

  • REST
  • REST Jackson
  • SmallRye OpenAPI (for Swagger)
  • LangChain4j OpenAI
  • Qute Web (for server-side templates)

Then download the generated application, unzip it and import it as a Maven project in your favourite IDE. In addition add the following library (we will use it to parse the CSV file with disease -> doctor records):


com.opencsv
opencsv
5.7.1

Next generate an OpenAI key from https://platform.openai.com/api-keys if you don’t have one already and add it to the application.properties file of the project as follows:

quarkus.langchain4j.chat-model.provider=openai
quarkus.langchain4j.openai.api-key=

If we have multiple providers on the classpath (i.e. OpenPI and Gemini) we need to specify explicitly in the configuration which provider we want to use. In case you don’t have an API credit with a credit for OpenAI (unless you use a trial account with small initial credit) you can use a the free DeepSeek model via the OpenRouter platform for example: https://openrouter.ai/ Generate an API key there and use it as follows (the DeepSeek API is compatible with OpenAI):

quarkus.langchain4j.openai.api-key=

quarkus.langchain4j.openai.base-url=https://openrouter.ai/api/v1

quarkus.langchain4j.openai.model=deepseek/deepseek-r1:free

In the src/main/resources folder let’s create a doctors.csv file with the following content:

diabetes,Dr. Smith,Endocrinologist

hypertension,Dr. Johnson,Cardiologist

asthma,Dr. Lee,Pulmonologist

migraine,Dr. Patel,Neurologist

arthritis,Dr. Brown,Rheumatologist

pneumonia,Dr. Davis,Internist

depression,Dr. Wilson,Psychiatrist

allergy,Dr. Clark,Allergist

kidney Stones,Dr. Lewis,Urologist

anemia,Dr. Hall,Hematologist

Now let’s add logic for our application. First we will create an AI service with a prompt template that is used to interact with the LLM:

package com.javaadvent.docpredict;

import dev.langchain4j.service.SystemMessage;

import dev.langchain4j.service.UserMessage;

import io.quarkiverse.langchain4j.RegisterAiService;

import jakarta.enterprise.context.ApplicationScoped;

@RegisterAiService

@SystemMessage(“You are a professional doctor”)

@ApplicationScoped

public interface DiseasePredictionService {

@UserMessage(“””

Try to predict disease from the following list of symptoms: {symptoms}.

Return only one predicted disease as a single word without extra symbols in lowercase.

“””)

String predictDisease(String symptoms);

}

The @RegisterAiService is the essential annotation provided by the Quarkus LangChain4j extension that defines a service that interacts with the LLM. We also provide a system message to the LLM using the @SystemMessage annotation as an additional context. We define the predictDisease method that uses a prompt template defined by the @UserMessage annotation.

Using that service now let’s create a REST resource that is able to make proper predictions:

package com.javaadvent.docpredict;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.util.HashMap;

import org.jboss.logging.Logger;

import com.opencsv.CSVReader;

import com.opencsv.exceptions.CsvValidationException;

import jakarta.inject.Inject;

import jakarta.ws.rs.POST;

import jakarta.ws.rs.Path;

import jakarta.ws.rs.Produces;

import jakarta.ws.rs.core.MediaType;

@Path(“/predict”)

public class PredictionResource {

private static final Logger LOGGER = Logger.getLogger(PredictionResource.class);

@Inject

private DiseasePredictionService predictionService;

@POST

@Produces(MediaType.APPLICATION_JSON)

public PredictionResponse predictDisease(PredictionRequest request) {

PredictionResponse response = new PredictionResponse();

String predictedDisease = predictionService.predictDisease(request.getSymptoms());

response.setDisease(predictedDisease);

response.setDoctor(determineDoctor(predictedDisease));

return response;

}

private String determineDoctor(String predictedDisease) {

InputStream is = PredictionResource.class.getResourceAsStream(“/doctors.csv”);

if (is == null) {

throw new IllegalStateException(“CSV file not found”);

}

HashMap diseaseToDoctor = new HashMap<>();

try (BufferedReader br = new BufferedReader(new InputStreamReader(is));

CSVReader csvReader = new CSVReader(br)) {

String[] row;

while ((row = csvReader.readNext()) != null) {

diseaseToDoctor.put(row[0], String.format(“%s (%s)”, row[1], row[2]));

}

} catch (IOException | CsvValidationException e) {

LOGGER.error(e.getMessage(), e);

}

String doctor = diseaseToDoctor.get(predictedDisease);

if(doctor == null) {

doctor = “Visit general practitioner”;

}

return doctor;

}

}

package com.javaadvent.docpredict;

public class PredictionRequest {

private String symptoms;

public String getSymptoms() {

return symptoms;

}

public void setSymptoms(String symptoms) {

this.symptoms = symptoms;

}

}

package com.javaadvent.docpredict;

public class PredictionResponse {

private String disease;

private String doctor;

public String getDisease() {

return disease;

}

public void setDisease(String disease) {

this.disease = disease;

}

public String getDoctor() {

return doctor;

}

public void setDoctor(String doctor) {

this.doctor = doctor;

}

}

We inject the AI service that makes the call to the model API to make the prediction and then based on the result and the CSV file with disease -> doctor mappings tries to determine which practitioner is best suited for the described symptoms. If a practitioner cannot be suggested the application recommends visiting the general practitioner.

At that point we are ready to test the endpoint using Swagger when we start the Quarkus application and navigate to http://localhost:8080/q/swagger-ui/

Finally let’s make this a bit more convenient by adding a simple UI around it using Quarkus Qute template engine.

Create the following templates under src/main/resources/templates:

index.qute.html

<!DOCTYPE html>

DocPredict

Enter your symptoms

prediction.qute.html

{doctor} !

Add the following endpoint to handle the template rendering:

package com.javaadvent.docpredict;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.util.HashMap;

import org.jboss.logging.Logger;

import com.opencsv.CSVReader;

import com.opencsv.exceptions.CsvValidationException;

import io.quarkus.qute.Template;

import io.quarkus.qute.TemplateInstance;

import jakarta.inject.Inject;

import jakarta.ws.rs.*;

import jakarta.ws.rs.core.MediaType;

@Path(“index”)

public class IndexResource {

private static final Logger LOGGER = Logger.getLogger(IndexResource.class);

@Inject

private Template index; // index.qute.html

@Inject

private Template prediction; // prediction.qute.html

@Inject

private DiseasePredictionService predictionService;

@GET

@Produces(MediaType.TEXT_HTML)

public TemplateInstance getForm() {

return index.instance();

}

@POST

@Path(“submit”)

@Consumes(MediaType.APPLICATION_FORM_URLENCODED)

@Produces(MediaType.TEXT_HTML)

public TemplateInstance submit(@FormParam(“symptoms”) String symptoms) {

String predictedDisease = predictionService.predictDisease(symptoms);

return prediction.data(“doctor”, determineDoctor(predictedDisease));

}

private String determineDoctor(String predictedDisease) {

InputStream is = PredictionResource.class.getResourceAsStream(“/doctors.csv”);

if (is == null) {

throw new IllegalStateException(“CSV file not found”);

}

HashMap diseaseToDoctor = new HashMap<>();

try (BufferedReader br = new BufferedReader(new InputStreamReader(is));

CSVReader csvReader = new CSVReader(br)) {

String[] row;

while ((row = csvReader.readNext()) != null) {

diseaseToDoctor.put(row[0], String.format(“%s (%s)”, row[1], row[2]));

}

} catch (IOException | CsvValidationException e) {

LOGGER.error(e.getMessage(), e);

}

String doctor = diseaseToDoctor.get(predictedDisease);

if(doctor == null) {

doctor = “Visit general practitioner”;

}

return doctor;

}

}

And there we go once we navigate to http://localhost:8080/indexlocalhost:8080/index:

SummaryAs you can see it is quite straight-forward to get started with Quarkus LangChain4j extension and furthermore you can start more complex capabilities in your application like RAG, context memory, tool support etc.

The post Quarkus LangChain4j extension from grounds up appeared first on JVM Advent.

View Details

When December arrives, I always feel the same mix of nostalgia and excitement. The year starts to slow down, calendars fill with end-of-year meetings, and yet this is when the Java community does something uniquely joyful. We show up every day for 24 days and share what we’ve learned, built, discovered, and struggled with. It’s one of my favorite traditions, because it feels like a collective “closing of the year” where we all learn from each other one last time before the holidays.

This year feels particularly special for me. I spent most of the past months writing and publishing Applied AI for Enterprise Java Development, a book that tries to make sense of this AI wave from a developer’s point of view. Not the hype, not the hand-waving, but the real work we do when we integrate LLMs into production systems. It’s been a year of experiments, late-night debugging, mistakes, surprises, and quite a few breakthroughs. And now, opening this Advent Calendar, I can’t help but see how far the Java ecosystem has already come.

Java developers didn’t sit back and wait for AI to “happen to them.”

We did what we always do: we tested, we validated, we measured, we built frameworks, we added structure, and we created patterns that teams can actually use in the real world. Today, Quarkus, LangChain4j, and the broader Java ecosystem make it possible to build AI-infused systems without giving up the reliability and discipline we depend on.

And that’s why I’m thrilled to start this calendar with you.

For the next 24 days, you’ll see creative ideas, deep dives, practical guides, experiments, and some truly unexpected topics. And all for them written by people who care about this craft as much as you do.

So grab your favorite hot drink, take a breath, and enjoy the first door of the 2025 Java Advent Calendar.

There’s a lot of good stuff waiting behind the others.

Models as Services. The Simplest Pattern Still Matters

The official LangChain4j tutorials start with the simplest possible shape: a chat model that you call like a remote service. That’s intentional. It reinforces the pattern Java developers already understand: the model is an external dependency with its own behavior, latency, and failure modes.

Here is the Quarkus version of the official chat example (adapted from the “Chat with LangChain4j” and “AI Services” tutorials):

import dev.langchain4j.service.AiService;@AiServicepublic interface Assistant { String chat(String message);} Quarkus wires the model for you through configuration:

```

application.propertiesquarkus.langchain4j.openai.api-key=${OPENAI_API_KEY}quarkus.langchain4j.openai.chat-model.model-name=gpt-4o-mini

``` You inject it exactly like you inject any other CDI bean:

@Path("/chat")public class ChatResource { @Inject Assistant assistant; @GET public String chat(@QueryParam("q") String q) { return assistant.chat(q); }} This example looks trivial, but it captures a foundational rule:

the model is not embedded; the model is a service.

And this is where Java already shines: retries, timeouts, circuit breakers, metrics, structured logs. All of those all apply cleanly to LLM calls.

Practical Retrieval-Augmented Generation

The official LangChain4j RAG tutorial shows the basic pattern:

  • 1.Split documents into text segments
  • 2.Embed those segments
  • 3.Store them in an embedding store
  • 4.At query time:
    • embed the question
    • retrieve relevant segments
    • combine into a prompt
    • send to the model

Here is the Quarkus version of that exact flow:

import dev.langchain4j.data.segment.TextSegment;import dev.langchain4j.data.embedding.Embedding;import dev.langchain4j.store.embedding.EmbeddingStore;import dev.langchain4j.model.embedding.EmbeddingModel;import jakarta.enterprise.context.ApplicationScoped;import jakarta.inject.Inject;@ApplicationScopedpublic class RagService { @Inject EmbeddingStore<TextSegment> store; @Inject EmbeddingModel embeddingModel; public String buildPrompt(String question) { // 1. Embed the question Embedding queryEmbedding = embeddingModel.embed(question).content(); // 2. Retrieve relevant context var matches = store.findRelevant(queryEmbedding, 3); // 3. Combine retrieved text into a simple prompt StringBuilder sb = new StringBuilder(); for (var match : matches) { sb.append(match.embedded().text()).append("\n\n"); } sb.append("Question: ").append(question); return sb.toString(); }} And the usage in a resource:

@Path("/help")public class HelpResource { @Inject RagService rag; @Inject Assistant assistant; @GET public String help(@QueryParam("q") String question) { String prompt = rag.buildPrompt(question); return assistant.chat(prompt); }} This pattern is simple retrieval, simple prompt construction, no ceremony.

And importantly: you now have a deterministic pipeline around the model. That pipeline is what you test, observe, and control.

Guardrails Safeguarding Input and Output

LangChain4j provides two clear mechanisms:

  • @InputGuardrails
  • @OutputGuardrails

Both integrate directly with Quarkus.

Here’s the Quarkus version:

Input Guardrail

import dev.langchain4j.guardrail.InputGuardrail;public class NoEmptyInputGuardrail implements InputGuardrail { @Override public void validate(String input) { if (input == null || input.isBlank()) { throw new IllegalArgumentException("Input must not be empty"); } }} Output Guardrail

import dev.langchain4j.guardrail.OutputGuardrail;public class JsonMustContainSummary implements OutputGuardrail<String> { @Override public void validate(String output) { if (!output.contains("summary")) { throw new IllegalStateException("Model output missing 'summary' field"); } }} Wiring them into the AI service

@AiServicepublic interface StructuredAssistant { @InputGuardrails(NoEmptyInputGuardrail.class) @OutputGuardrails(JsonMustContainSummary.class) String answer(String question);} And Quarkus adds configuration-driven retries:

quarkus.langchain4j.guardrails.max-retries=2 Guardrails feel like an entirely new concept for many Java developers, but the structure is familiar: It’s validation, just on the other side of the API boundary.

Testing & Evaluation

If we think about Testing in general, and merge this with requirements coming in for large language models, we need to think about:

  • testing deterministic components
  • testing guardrails
  • testing model interaction in a black-box fashion
  • using curated prompt sets
  • evaluating output structure, not exact wording

Here is a Quarkus version :

Testing Guardrails

@QuarkusTestpublic class GuardrailTest { @Inject StructuredAssistant assistant; @Test void emptyInputShouldBeRejected() { assertThrows(IllegalArgumentException.class, () -> { assistant.answer(" "); }); }} Testing RAG logic (deterministic)

@QuarkusTestpublic class RagTest { @Inject RagService rag; @Inject EmbeddingStore<TextSegment> store; @Test void retrievalShouldReturnRelevantText() { store.add(TextSegment.from("Java 21 is the current LTS release."), EmbeddingModel.miniLm().embed("Java").content()); String prompt = rag.buildPrompt("What is the current Java LTS?"); assertTrue(prompt.contains("Java 21")); }} Opaque-box testing of the assistant

@QuarkusTestpublic class AssistantIT { @Inject Assistant assistant; @Test void modelShouldProduceNonEmptyAnswer() { String response = assistant.chat("Hello!"); assertFalse(response.isBlank()); }} Don’t test phrasing; test structure and expectations!

This keeps tests stable while still verifying quality and behavior.

A Festive Opening for the 24 Days Ahead

That brings us to why this article exists: to open another year of the Java Advent Calendar.

December has a special rhythm in the Java community. The year is winding down, code freezes are happening, and teams start reflecting on what actually mattered. And into that atmosphere comes a wave of articles. 24 voices, 24 perspectives, 24 stories from across our ecosystem.

  • Some will dive deep into AI.
  • Some will explore the core of the JVM.
  • Some will share practical lessons from the year’s real-world battles.
  • Some will remind us why Java continues to thrive, evolve, and surprise us.

This opening post is just the prologue.

Over the next three weeks, you’ll see what people across the community are experimenting with, breaking, fixing, and learning. You’ll see new tools, new techniques, old wisdom rediscovered, and perhaps a few things you’ll want to try during the quieter days between the holidays.

Whatever this season means to you, whether it’s festive, reflective, or simply a much-needed breather, I hope these articles offer inspiration and maybe a spark for your next project. Java has always been about community, and the Advent Calendar remains one of the warmest reminders of that.

So let’s open the first door together.

Twenty-three more to go.

Let’s enjoy the season, and write some good Java along the way.

The post Lighting the Way: Java, AI, and a Season of New Ideas appeared first on JVM Advent.

View Details

IntroChristmas is a time of tradition, and I’m delighted to continue the one we started last year. On this very same date and blog, we unveiled the development of Chicory: Chicory: WebAssembly on the JVM.

WebAssembly continues to grow steadily and strongly, much like we’ve come to expect from web technologies (link to this year Edoardo’s blog). While it’s not perfect yet, the ecosystem is expanding. For instance, CPython 3.13 is now officially released for WASI, and SQLite has added official support for WebAssembly builds using Emscripten.

This year, I’m thrilled to share an exciting announcement: Chicory, the pure Java WebAssembly runtime, has reached its first stable release: 1.0.0!

Last year, we unpacked a toolbox. This year, we’re unwrapping a heartfelt photo album, showcasing the magical journey that brought us to this incredible milestone.

The CoverLet’s start with the cover!

Chicory now has a brand-new official website: chicory.dev. We’re continuously improving and updating it. A curious detail for the most attentive visitors: the documentation is tested continuously in CI using a clever combination of Jest + Approval Tests powered by JBang. This ensures that every code snippet displayed in the docs can be successfully compiled by users.

At the forefront of this release is the prominent 1.0.0, marking Chicory’s first stable release. But what does that really mean? Over the past 12 months, we’ve been rapidly developing the engine, and early adopters likely noticed significant changes to the public API. There were several reasons for this:

  • We learned a lot about WebAssembly along the way.
  • We improved the API, making it more intuitive for those familiar with other runtimes.
  • We introduced additional modules, ensuring they integrate seamlessly.
  • We focused on performance, eliminating bottlenecks from the public API.

What to expect?With 1.0.0, we’re committing to maintaining compatibility with this API. It’s time to dive in confidently, knowing that the code you write won’t become outdated early.

Beyond the public API, we’re proud to say Chicory is now stable and useful to start providing active support for it.

The Family PictureWhen you open the cover, the first thing you see is a traditional, unmissable family photo.

Here are the key members of our Chicory family:

  • The basic building block: wasmThis is the core module, responsible for handling the nitty-gritty details of the WebAssembly specification. It provides idiomatic Java APIs for working with arbitrary binary Wasm modules.
  • The main character: runtimeAt its core, this is the interpreter we introduced last year. Now, it supports the full V1 WASM specification (except simd) along with some additional proposals. While it’s slow compared to other options, it’s extremely reliable and portable. We take pride in its readability, making it a pleasure to hack on!
  • The system interface layer: wasiThis module provides an implementation of Wasi Preview 1, still a widely supported compilation target for languages like Go, Rust, and C++. In our experience, it enables Chicory to run various real-world Wasm modules effectively. While there are limitations, you’re encouraged to roll your own implementation when the provided one doesn’t suffice.
  • The mandatory: logThis module allows (when necessary) decoupling from the Java Platform Logging (JEP 264), which isn’t available on Android devices.
  • The cool kid: wabtExternal tools like wat2wasm (for converting WebAssembly Text format .wat files to binary) and wast2json (for parsing .wast files and emitting .wasm files and .json assertion descriptions) are essential for building Chicory. These incredibly valuable tools officially ship as Wasm modules. We bundle a pure Java executable version (through Chicory, of course) to make them easily consumable as Jar artifacts.
  • Finally, the: bomThis module keeps all the others aligned and smiling for the camera.

The NewbornsTurning the first page reveals a collection of little creatures, freshly born and still in need of nurturing before they fully join the family. These modules are released with the -experimental suffix and placed in the relevant experimental namespace to indicate they’re not yet fully mature.

We encourage you to try them out and explore their potential, but keep in mind that they may not yet offer full stability as they grow towards maturity.

  • The one eager to start walking: aot-experimentalAoT stands for “Ahead Of Time.” This module translates Wasm to Java Bytecode (at the right distance, they look surprisingly similar!), generating pure Java artifacts from Wasm modules. Compiling and running modules dynamically requires some reflection, and this module depends on ASM. While it’s incredibly fast, there are still some rough edges to refine.
  • The lightweight companion to the first one: aot-maven-plugin-experimentalThis lightweight Maven plugin makes it easy to use the AoT translator at compile time. The resulting artifacts are persisted to disk, eliminating the need for external dependencies or runtime reflection. However, this approach sacrifices dynamic module loading.
  • The quiet one that flies under the radar: cli-experimentalThis CLI allows you to quickly evaluate Chicory directly in the comfort of your terminal.
  • The twins: host-module-annotations-experimental & host-module-processor-experimentalThese consist of annotations and the corresponding annotation processor, making it easier to integrate Chicory through a higher-level, fully Java-idiomatic, generated API.

The Travel MemoriesTurning another page takes us through a colorful spread of memories, showcasing the countless places where Wasm has made its mark across the software world. These moments are as diverse as they are inspiring, reminding us that WebAssembly is truly everywhere! Here’s a glimpse, in no particular order:

  • Through a refracted spectrum:With our great friends at JRuby, we got Prism running as a pure Java artifact, enabling the bootstrapping of JRuby without any native dependencies.
  • From the deserts:A Wasm Camel found its way into a low-level integration.
  • Large highways:Kafka Connect and Apache Pulsar opened up the road to polyglot data transformations built on message queues.
  • A river capturing every ripple:Debezium uses Wasm plugins to track Change Data Capture events as they flow through your data streams.
  • The wrought iron gate:Keycloak stands tall, demonstrating pluggable Policy Providers in languages beyond Java.
  • From the shallow waters of a river:A Kafka Proxy (Kroxy) opens its jaws, providing unprecedented extensibility.
  • A friendly agent:OPA (Open Policy Agent) policy issues fees on our virtual dashboard. Thankfully, we avoided the speed trap by removing the network!
  • Comfortably eating data on the Java train:SQLite offers a tasty and safer option for data consumption.
  • In the middle of a gray sky of clouds:A Spark lights up Big Data processing in new ecosystems.
  • Timeless snapshots:Rusty gems like Ocrs, Photon, and others are the must-have selfies of this journey.

The PartiesExploring the software world is always a thrill, but the most valuable part of the journey is meeting, chatting, and spending time with others who share your passions.

We’ve been fortunate to be accepted at various events, proudly taking the stage to showcase our juggling skills with compilers, runtimes, and integrations.

If you’d like to experience our journey firsthand, here are a few highlights you can watch:

  • Wasm I/O 2024:The ultimate destination for anyone building with WebAssembly. A small but top-notch gathering of experts pushing the boundaries of this technology. Check out our talk: Chicory: Creating a Language-Native Wasm Runtime by Benjamin Eckel / Andrea Peruffo.
  • Dylibso Insiders:A comprehensive and approachable retelling of Chicory’s journey. Dive into the full story with: Chicory, a JVM Native WebAssembly Runtime by Benjamin Eckel.
  • Devoxx BE 2024:The largest Java conference in Europe and a pivotal moment for Chicory. We connected with new and old friends, paving the way for the future. Watch our talk: Meet Chicory, exploit the power of WebAssembly on the server side! by Andrea Peruffo.

The Race DayThis year, GraalWASM by Oracle was announced as “production ready,” and we’ve had the pleasure of chatting with the incredible team behind the project.

Picture an adrenaline-filled drag race: vehicles lined up at the starting line, tires burning, and engines roaring. Naturally, you’re curious about how they perform when the rubber meets the road.

These benchmarks are preliminary, and more work is needed before drawing any definitive conclusions. However, we’d like to share the results of running this suite on a dedicated machine, encouraging you to take a closer look and explore further.

Removing the outliers of the interpreters, a close-up look at the Photon benchmark:

The results highlight the distinct design choices of each project:

  • Chicory’s pure interpreter:Its focus on simplicity and portability comes with a trade-off: slower performance.
  • GraalWASM:GraalWasm delivers great performance using Graal JIT, when available, to compile WASM directly to machine code.
  • Chicory’s AoT translator:Produces pure bytecode artifacts that achieve competitive performance on any standard JVM.

The Road AheadWe’ve already bought our tickets for the next leg of this journey, and the adventure has only just begun. We’re eager to explore wilder and uncharted territories, confident that exciting times lie ahead.

Here’s what we’re looking forward to:

  • Integrations and usage of ChicoryExpanding its availability in major frameworks and products.
  • Spec Proposals complianceAchieving full compliance with WebAssembly standards, including Exception Handling, Garbage Collection, and full support for SIMD.
  • Maturity and stability of -experimental modulesRefining these modules by removing limitations, fixing bugs, and ironing out their APIs.
  • SpeedWe have not spent much time on performance, and it’s time to take a serious stab at it.

We’re excited to connect with the community along the way, whether at local JUGs, conferences, or meetups. If you’re curious about WebAssembly and its growing impact on development, don’t hesitate to reach out—we’d love to hear from you!

The Back CoverNone of this would have been possible without the help of incredible contributors. On the back cover, you’ll find the names of some of the key authors who made this journey possible:

  • David Philips:Outstanding work on Wasi and the AoT translator.
  • Edoardo Vacchi:His mark is visible in the Store and public API as he joined the brawl.
  • Daniel Perano:Instrumental in contributing the first steps of the AoT compiler.
  • Ben Eckel:Shaped the initial foundation of the project.
  • The Team at Dylibso:For their continuous support and collaboration throughout this journey.

As we close this chapter, we hope these snapshots have entertained and inspired you during the festivities. The journey doesn’t end here—come along and join us!

The post The Chicory Photo Album: Celebrating 1.0.0 and a Year of Wasm appeared first on JVM Advent.

View Details

Eclipse Collections is an open source Java Collections framework. In this blog I am going to demonstrate four lesser known features of the framework. I have published similar blogs in Java Advent Calendars of 2018, 2019, 2020, 2021, 2022, and 2023. Please refer to the resources at the end of the blog for more information about the framework.

  1. primitiveStream(): Eclipse Collections offers seamless integration with Java 8+ streams. We take it a step further and provide an easier integration with Primitive Streams. The PrimitiveStream interfaces (IntStream, LongStream, and DoubleStream) provide efficient ways to process primitive data without boxing. ``` @Test
    public void primitiveStreams() {
    IntList intList = IntLists.mutable.with(1, 2, 3);
    Assertions.assertEquals(
    6,
    intList.primitiveStream().sum());

    LongList longList = LongLists.mutable.with(1L, 2L, 3L);
    Assertions.assertEquals(
    6L,
    longList.primitiveStream().sum());

    DoubleList doubleList = DoubleLists.mutable.with(
    1.0, 2.0, 3.0);
    Assertions.assertEquals(
    6.0,
    doubleList.primitiveStream().sum());
    } 2. **Immutable Primitive Collections:** Eclipse Collections offers primitive collections for all seven primitives: `int`, `long`, `float`, `double`, `byte`, `char`, and `boolean`. In addition to mutable collections, we offer *immutable* collections for all the primitives. The immutable collections play an especially important role during parallel processing and improve performance. @Test
    public void immutablePrimitives() {
    ImmutableIntList immutableIntList = IntLists
    .immutable.with(1, 2, 3);

    MutableIntList mutableIntList = IntLists
    .mutable.with(1, 2, 3);

    Assertions.assertEquals(
    mutableIntList,
    immutableIntList);

    // No mutating APIs; below line throws a compilation error
    immutableIntList.add(4);
    } 3. **`Collectors2`:** Eclipse Collections provides an extension for `java.util.stream.Collectors` to provide Eclipse Collections functionalities as inter-op with Java streams by extending the `Collector` interface. Refer to the Javadocs here for the comprehensive set of features available in `Collectors2`. The example below illustrates one of the hidden treasures covered in 2018: `partition` . As you can see, the inter-op is helpful to use Eclipse Collections iteration patterns with Java streams. @Test
    public void collectors2() {
    List integers = List.of(
    1, 2, 3, 4, 5, 6, 7, 8, 9);

    PartitionMutableList evenOddPartition = integers
    .stream()
    .collect(
    Collectors2.partition(
    each -> each % 2 == 0,
    PartitionFastList::new));

    Assertions.assertEquals(
    Lists.mutable.with(2, 4, 6, 8),
    evenOddPartition.getSelected());

    Assertions.assertEquals(
    Lists.mutable.with(1, 3, 5, 7, 9),
    evenOddPartition.getRejected());
    } 4. `symmetricDifference`: The `symmetricDifference` API returns the set of all elements that are present in exactly one of the two sets. In other words, the elements that are ***only*** in ***one*** set, but *not* in *both* are returned. The example below illustrates few scenarios: @Test
    public void symmetricDifference() {
    MutableSet set1 = Sets.mutable.with(1, 2, 3);
    MutableSet set2 = Sets.mutable.with(3, 4, 5);

    Assertions.assertEquals(
    Sets.mutable.with(1, 2, 4, 5),
    set1.symmetricDifference(set2),
    "3 is common element, hence not present");

    Assertions.assertEquals(
    set1.symmetricDifference(set2),
    set2.symmetricDifference(set1),
    "Symmetric Difference is commutative");

    MutableSet set3 = Sets.mutable.with(6, 7, 8);

    Assertions.assertEquals(
    Sets.mutable.with(1, 2, 3, 6, 7, 8),
    set1.symmetricDifference(set3),
    "There are no common elements");

    MutableSet set4 = Sets.mutable.with(9);

    Assertions.assertEquals(
    Sets.mutable.with(1, 2, 3, 9),
    set1.symmetricDifference(set4),
    "Sizes are different, no common elements");

    MutableSet set5 = Sets.mutable.with(1);
    Assertions.assertEquals(
    Sets.mutable.with(2, 3),
    set1.symmetricDifference(set5),
    "Sizes are different, has common elements");

    Assertions.assertEquals(
    Sets.mutable.empty(),
    set1.symmetricDifference(set1),
    "symmetricDifference with itself is empty");

} ```

Summary:

In this blog I explained a few lesser known features of Eclipse Collections primitiveStream(), Immutable Primitive Collections, Collectors2(), and symmetricDifference(). I hope you found the post informative. If you have not used Eclipse Collections before, give it a try. There are few resources below. Make sure you show us your support and put a star on our GitHub Repository

Eclipse Collections ResourcesEclipse Collections comes with it’s own implementations of List, Set and Map. It also has additional data structures like Multimap, Bag and an entire Primitive Collections hierarchy. Each of our collections have a fluent and rich API for commonly required iteration patterns.

  • Website
  • Source code on GitHub (Make sure to star the Repository)
  • Contribution Guide
  • Reference Guide

The post Hidden Treasures of Eclipse Collections 2024 Edition appeared first on JVM Advent.

View Details

The Java Community Process (JCP) program evolves over time, with every update maintaining the value of Java technology and community collaboration. December 2023 marked the twenty five year anniversary milestone of the JCP and we continued celebrating throughout 2024. If you are not familiar the the JCP, it provides the process through which the international Java community standardizes and ratifies the specifications for Java technologies.

Java has become one of the most used and trusted programming languages used by millions of developers worldwide. Leadership of a community at this scale can be complex. We strive to find a balance of stability and innovation, providing a predictable platform for business users, as consumers of the technology, and creating new features for technical users and enabling engagement through our membership, the Java Community. Along with myself as the Chairperson, we have Executive Committee Members, representing a cross-section of major stakeholders and members of the Java community, who are responsible for approving the passage of specifications through stages and for reconciling discrepancies between specifications and their associated test suites. The membership includes corporations, non-profit organizations, Java User Groups, and individuals. JCP Members can serve on JSRs and vote in the annual JCP EC Elections.

Some of our members throughout 2024 held celebrations around the world to celebrate the anniversary Over thirty Java User Groups joined us to celebrate around the world, in Africa, Asia, Europe, North America and South America! We could not be more thankful. A list of JUGs that celebrated with us, a video highlighting many of the celebrations and picture images are available in the JCP article on JCP.org.

JCP Program Member and Community Engagement

Since the last revision of the JCP, we have seen even greater collaboration and contributions within the Java developer community. The faster release cadence has helped Java meet the needs of developers, with new software features being incorporated into the release as they become ready every six months. This has also helped to increase innovations and contributions to the platform. In 2024 there were two releases of the Java SE Platform, Java SE 22 in March and Java SE 23 in September. Java SE 24 is in the final stages as JSR 399 with a Public Review scheduled for January 2025 and Final Release planned in March 2025. Work on Java SE 25, JSR 400, is already underway.

In the JCP EC we have discussed how can EC Members, as well as JCP Members and Java Community members, engage, participate and contribute. In October 2024, at the JCP EC face to face meeting hosted by Amazon in Seattle, we discussed some of the OpenJDK projects that are being developed to evolve Java to meet future application development needs. The work of the JCP EC is public on JCP.org. Some suggestions for participation and contributions are:

  • Review & comment on Specifications -substantial/staff experts
  • Contribute to projects (OpenJDK JEPs or other projects)
  • Test Early Access builds regularly and provide timely bug reports/comments on discussions
  • Discuss the value of adopting new versions of Java and promote discussions
  • Share news on Java versions of Java & standalone JSRs
  • Promote awareness of new early access builds and testing
  • Share experiences and migration best practices
  • Engage in Java in Education and Java Ecosystem working groups

The pipeline for new features is rich and deep, and we believe this will lead to accelerated application development. This also ensures that Java continues to attract younger developers. Java migration projects between versions will shift from major development projects, with 100 or more new features, to smaller and more incremental updates, happening more frequently. This helps to increase the amount of feedback on the early access releases as developers are evaluating their migration plans on an ongoing and continuous basis and provides businesses with the stability and predictability they require to run their teams and companies.

To that end, we have initiatives within the JCP for deeper discussions and collaboration with the community. Currently there the two groups: the Java in Education Initiative and the Java Ecosystem Working Group.

Java Ecosystem Working Group – Easier migration to New Versions of Java

In the JCP Program we oversee the evolution of Java technology in addition to balancing the needs of the overall user community and our community members. Bringing the feedback of the community into the innovation process and incorporating their experience with the technology is crucial when creating a stable and secure platform that is used by millions of developers, customers and vendors all over the world. The JCP provides the mechanisms that enable this, providing new features and innovations for developers and stability and predictability for businesses. The governance of a technology community and ecosystem of this size is an effort that requires listening to differing audiences and voices, enabling multiple compatible implementations and a flourishing ecosystem of third-party tools and libraries from the open-source developer community.

Following on the success of the faster release cadence for the Java platform, and how the community has evolved and adapted to the model over time, the JCP EC has discussed how we can collectively work with the ecosystem to influence and help them to embrace the modern delivery cadence of the Java platform making it easier for developer to migrate their applications to newer versions of Java. Following the work to update the JCP processes and enable the Java platform to release a new version every six months, there is now a potential to enable the ecosystems of tools and libraries to also adapt to transition to new versions of Java more quickly. We are looking to build on existing programs such as the OpenJDK Quality Outreach initiative to help the smaller projects that are more difficult to keep up to date. Java has a wide range of libraries and not all of them are up to date. We are looking at how we can influence them to support just the latest versions of Java. If libraries adopt the same or similar model (moving from an express model to a tip and tail model), the Java platform would be even more stable, secure, and predictable. We recognize that this is a cultural change, but it is also an opportunity for maintainers. What is necessary to make this happen? Some suggestions we discussed with maintainers are adopting a similar development model, not back porting as aggressively or back porting as little as possible – customers want stability. The main issues for maintainers are funding and time. We have formed a working group to discuss how we can listen, enable and influence efforts in the community so that with each new version Java, we have the ecosystem ready and supporting the latest releases. We believe the world is ready for the ecosystem of Java libraries, frameworks, and tools to embrace a delivery model like that of the JDK – tip and tail development, with LTS offerings. By making this shift, library maintainers can realize the same kind of benefits that has been achieved for the Java platform itself. This will further strength and extend the viability of Java overall now and in the decades to come. In the EC we discussed how we can collectively work with the ecosystem to influence and help them to also embrace the modern delivery cadence of the Java platform. The JCP EC efforts to update the JCP processes and expansion of projects such as the Quality Outreach initiative in OpenJDK has helped support the migration of many larger projects. The smaller projects are more difficult to keep up to date. Java has a wide range of libraries and not all of them are up to date on the newer versions of Java. We formed a Working Group to discuss in more detail with the community.

The goal of the Java Ecosystem JCP Working Group is to educate and increase awareness around the Java ecosystem third-party tools and libraries to increase the adoption of modern release processes by third-party tool and library maintainers. This working group was formed following the discussions in the JCP Executive Committee in 2023. Within this group, we discuss how can we influence project maintainers to support just the latest version, instead of backporting new features into many older versions. When the six-month release cadence with Long Term Support (LTS) offerings was introduced in 2018 (JDK 10), that was a major shift in how Java was delivered and how developers migrate between version of Java. The community has evolved and adapted to the model over time and we have gained a lot with the shift in delivery model. On the wiki page you can see some updates and discussion topics such as Gradle, Jenkins, Junit, Eclipse Collections, ItelliJ and more are planned for 2025 such as Apache Maven, Log4j, Spring and JoCoCo.

Java in Education Initiative – Bring up the Next Generation of Java Developers

In 2020, some of our discussions began to focus on the topic of Java in Education. This prompted a working group to think about what we can do around Java in Education. JCP EC Members and Java community leaders are in a unique position to inspire their local communities of junior developers and students to learn and use Java technology. The purpose and focus is this working group is to help bridge the gap between the educational environment and industry. Together we can provide opportunities for students, teachers and educational institutions in the form of networking, mentoring, knowledge and professional internships, open-source assignments and projects. We can also educate developers around the myths about the capabilities of modern Java technology. This effort is global, led with the Java Community and supported by the JCP program – we support Java User Groups to partner with their local educational institutions and communities to bring Java technology to the next generation of Java developers. Together we created materials and resources to help bridge the gap between the educational environment and the industry.

We have prepared presentations and videos that highlight the capabilities of Modern Java, the benefits of learning Java including some of the enhancements that the Java language has delivered in recent years. These enhancements help dispel some of the myths surrounding the language. From JShell in JDK9, the Instance Main Methods as a preview feature since JDK 21 and Implicitly Declared Classes, these are just a couple of the features that are helping Java to evolve so that students and new developers can write their first lines of code without the need to understand the concepts that apply for large programs.

As part of the materials that the Java in Education working group has designed, you’ll find some presentations related to “Why you should teach Java”, ‘What is Java and why you should learn it’, and ‘Day in the Life of a Developer’ examples, “ML & AI Workshop for Java Developers”, featuring the amazing work of JSR 381, Visual Recognition API. JSR 381 was developed through the JCP as a stand-alone optional JSR that simplifies and standardizes a set of APIs familiar to Java developers for classifying and recognizing objects in images using machine learning. In addition to classes specific to visual recognition tasks, it provides general abstractions for machine learning tasks like classification, regression, data set, and reusable design which can be applied to machine learning systems in other domains. At the current stage, it provides basic hello world examples for supported machine learning tasks (classification and regression) and image classification.

This material is available for anyone that wants to spread the knowledge on this topic. The target audience for these presentations ranges from people with no CS background to professors who want to show how wonderful Java can be in the learning experience of a new developer.

Since the JCP EC started meeting face to face again in 2023 (no face-to-face meetings 2000-2022), we have visited universities and JUGs in those locations – Singapore (hosted by Alibaba), Montreal, New York (hosted by BNY), Munich (hosted by MicroDoc) and Seattle (hosted by Amazon). In 2025, we will meet in the Bay Area (hosted by Microsoft) and Cambridge (hosted by Arm). We will also hold gatherings at FOSDEM, JavaOne and Devoxx, among others in 2025. One of the most important take aways from our visits: to engage the next generation of Java developers, you need to meet the students where they are (at the universities) – at least in the early stages. There have been several user groups who have adopted these principles as early adopters to grow and include students in the communities; below are a few examples.

The Java User Group Philippines (JUG PH) is a revitalized Java User Group focusing on Java, Cloud and AI Technologies. Since 2023 they have continuously been doing meetups, partnering with large tech communities for conferences and their meetups and doing bootcamps. Once example is the University Partner Summit by ING Hubs Philippines. To connect with various universities last Nov 2024, JUG PH provided a guest speaker and connected with various university representatives to engage and talk about the use of Java. This was an opportunity to expose the initiatives of JUG PH and it was presented in the presentation slides of ING and Philippine Software Industry Association (PSIA).

JOZI-JUG in South Africa has hosted many coding workshops (series of 6 weeks) for kids with their Devoxx4Kids South Africa initiative, where various kids from primary and high school attended for weeks. They also held Java coding days where the joiners learned to code from scratch, and, at the end of these workshops the participants had the basic understanding of programming to write a simple program on their own.

The Garden State Java User Group GSJUG) in the New Jersey area, one of the oldest jugs (founded in 2001), is also bringing students into the fold. With two Drew faculty members on the group’s leadership team, GSJUG has strong ties with the university. Since they mostly meet on campus, Drew’s students have easy access to each of their meetings. Professors in Computer Science and Cybersecurity courses encourage all students to attend. Drew University has many alums who have found jobs doing Java development. They also outreach to local high school students. A computer science teacher at Madison High School is on the JUG’s board of advisors. Members of GSJUG’s leadership team visit the high school to give talks on Java and other topics of interest to students in the school’s programming club and programming courses. They keep these presentations lively and interactive with a few slides but they spend most of the time answering the student’s questions, and actively seeking feedback from the students at the meeting.

The Dominican Republic JUG (Java Dominicano) has also contributed to this initiative, with some of their participants as faculty members of a local university they had been very close in the collaboration with college students, and prepared talks and workshops for their local community in the local language (see translated materials in Spanish). In July of 2023 they made a Workshop around the Topic of Machine Learning in Java. The workshop was part of their JUG annual conference called JConfDominicana and went from – what is a JSR, why Java is adding these kinds of APIs to the platform, to developing some real examples of Machine Learning Models and a Convolutional Neural Network for Visual Recognition – all in Java. The Dominican Republic JUG has also given talks to high school students who don’t know which language to learn or if their career will be successful in Computer Science. They presented at a local high school the presentation about “Java 4 young devs”, ideal for the previously described audience.

The Java in Education initiative has support from many JUGS around the globe, including recent contributions from the Japan Java User Group community, with the inclusion of materials translated into Japanese. These are now available, in addition to the materials translated into Spanish by the Dominican Java User Group community in 2023. The Dominican Republic JUG and Jozi JUG were also recognized in the 2023 JCP Annual Awards, in the Java in Education Community Award category, with Jozi JUG selected as the winner by the JCP Executive Committee.

Get involved today to bring Java to the next generation of developers in your local community. Engage with your local educational community to continue to grow Java in Education.

I am looking forward to 2025, including the return of the JavaOne Conference in March and many other conferences where I have the opportunity to engage and connect with the Java Community around the globe. We are so fortunate to be a part of such a wonderful community of people in the Java Community – let;s engage and connect with each other in 2025.

The post Community Engagement through the JCP Program appeared first on JVM Advent.

View Details

A talk I’ve given this year has often resulted in being asked to provide more details. This article is part of that effort. Keep an eye out for more from me on this subject.

There are many guides on achieving the right architectural balance and multiple thoughts about the ‘right’ way. In this article, I want to distil some of this thinking, apply it to Java API design, and use modern Java features. Historical patterns like AWT or RMI often influence older Java API design thinking, and it’s time to remind readers that modern Java features offer new, more powerful, and frequently safer ways to design APIs.

History We’ve seen several approaches to API design over the years, and at their heart is the desire to create an architecture that balances simplicity over adaptability and future enhancement. As a Java developer, you’ll often hear references to some standard terms:

Separation of Concerns: This fundamental design principle advocates separating code into distinct sections, each addressing a separate concern.

Strategy Pattern: In design pattern terminology, particularly from the “Gang of Four” book, the Strategy pattern encapsulates different strategies in separate classes and allows them to be swapped easily without affecting the context

Inversion of Control (IoC): While typically associated with dependency management, IoC is also about decoupling the execution of a task. It creates a hard barrier between code, what the application does, and what the framework provides.

Command Pattern: This pattern separates the action’s requester from the object that executes the action. Command objects encapsulate a request as an object, letting you parameterise clients with different requests, queue or log requests, and support undoable operations.

Delegation Pattern: This pattern involves two objects where one object handles a request by delegating to a second object (the delegate).

There are many other patterns. In fact, this is a rich vein of discussion, and there are many opinions.

Lines in the SandAll the great concepts above have tremendous value, but where those architectural lines begin to melt away is when we talk about ‘participation’. Any design requires thinking about how the user interacts with the API. How do we draw hard lines and create firm boundaries when code can be malleable, and developers are inventive? Often, our design requires or invites participation in unexpected ways. Ways we didn’t intend or expect.

In this article, I want to extract an underlying principle or two and look at what we have available to create robust designs. Designs that allow just the right sort of participation without too much opportunity to directly or inadvertently compromise our API.

Classes and InterfacesWhatever design pattern you follow or the framework you use, you still write code.

The Java API design toolkit we use essentially consists of Classes and Interfaces. We might use Annotations to signal requirements to containing systems, but, as the name suggests, they are annotations and do not have to be honoured.

As an API designer, you will consider how someone consumes the API, what constraints you must apply, and where. There are many functional and nonfunctional elements to consider, such as performance, security, observability, etc., and you have to decide how the consumer of the API will participate in all of these factors.

Participation – it’s not a spectator sport.Participation means assessing how much the API user is a consumer rather than a provider. Historically, the way we use Java classes and interfaces has remained unchanged.

Common approaches include

  • Using the provider pattern to allow objects to be ‘magically’ instantiated. i.e. via IoC or a Java Service Provider mechanism.
  • Providing an interface and Javadoc to explain expected behaviour for each method
  • Providing an abstract class for others to subclass and fill-in-the-blanks.
  • Providing a concrete class that someone can subclass to override behaviour
  • Providing a concrete class that has a constructor with parameters
  • Providing a concrete class and a selection of setter methods – the JavaBean pattern
  • We might occasionally use the Builder pattern to allow the instantiation of a complex object.

Force Fitting We assume and occasionally try to force how the end user will participate in all these standard techniques.

IoC tries to force the user to be only a consumer. Builder patterns or final classes also attempt to force consumption only, albeit with some flexibility in configuration. Meanwhile, abstract classes and plain interfaces invite the user to participate in the API’s internal behaviour as both a producer and a consumer.

The JavaBean pattern is a dreadful mix of consumer and producer but highlights the other aspect of participation: Understanding your place in the process. Ideally, once an object is instantiated, it’s fully configured, and any other method calls are to retrieve data or transform the object’s state. What the JavaBean model does is blur configuration with use. Any setter method can be called at any time so the implementation has to deal with the chance of its configuration being modified during use. That’s a challenge and a ready source of bugs.

The API design invites a particular form of user participation. JavaBeans are bad practice, and so is using subclassing as a consumption principle. Whether an abstract or concrete class, the requirement that the user use subclassing to participate must be revised.

This approach requires the user to know much more about the API’s work; the user code is now part of the API. The user code must understand its responsibilities and its place in the process. It needs to understand what state its parent is in, when it will get called, and what it’s allowed to do when accessing the parent state.

Overriding or implementing methods as an API design choice forces the user to become both producer and consumer, encouraging them to override other methods or dive into the parent class’s internals. The tie between user and API is now strongly coupled and personal.

Let’s talk dishwashing machines. Examining one, we can see that simplistically we have a few buttons and knobs that form the control panel, and we interact with the machine by loading salt, coupling up power, water and drainage and then regularly loading the machine with the dishwasher tablets and, of course, dishes.

We can see three sections by distilling this physical design into software principles.

Configuration: Where the machine is plumbed in and connected to a power supply.

Policy: The control panel allows the user to describe what they want the machine to do.

Process: Loading, policy execution, washing the dishes, Unloading etc.

Note how the dishwasher users’ participation is controlled and anticipated. Of course, they must remember to load dishes or tablets and not overload the dishwasher. Many of the common mistakes are discoverable by the machine. Open the door, and the machine stops. No tablets or softener, etc., so a light goes on. Depending on how clever your machine is, it is more likely to defend against common mistakes.

At no point in these activities is there any expectation or ability for the user to step outside their part of the process. There is a clear separation between what the machine does and what the user does.

The user doesn’t get involved in the actual act of dishwashing. They support the machine in fulfilling its function. The user selects a policy to tell the machine what sort of washing to do, provides dishes to wash and then extracts the clean dishes.

Configuration, Policy and ProcessThis model is great for API designers to follow. Thanks to recent additions to the Java language, we now have the tools to deliver robust, secure APIs and control user participation.

Configuration is where the environment is discovered, and elements that affect all usage are gathered. This might be via a pre-instantiated builder class, loaded by IoC, etc., or the parameters passed into a constructor. Think of them as ‘rules of physics’ rules that are fixed for the API during its lifetime in the JVM.

Policy is like the washing machine control panel, the rules that this particular instance of the API will follow. Policy declares what you want the API to do, not how it does or what data it will process. A policy is a reusable but immutable object. Think of it akin to something like a declarative contract.

Process is the well, process of executing the policy on a particular data set. The process uses information from the policy and configuration to do its job. In Java terms, the policy might contain selection criteria for data as a predicate. The process uses this predicate internally but cannot interact with any other part of the process. It’s isolated and contained.

Clean DesignThe separation of policy from process is an essential concept that promotes cleaner, more modular, and maintainable code. This principle contrasts with some of the older design paradigms in Java, where the lines between policy and process were often blurred, leading to complicated codebases that needed to be revised to maintain and extend.

Older Java designs, such as those found in the Abstract Window Toolkit (AWT), encouraged a blending of these two aspects. Developers often had to subclass abstract classes or override methods to integrate with an API. This design forces developers to engage deeply with the internal workings of the classes they are extending, leading to tightly coupled code that is difficult to manage and prone to errors.

Practical Implementation in Java – expanding the designer’s toolkitI hinted a little about the use of predicates above, but that’s only a part of what is available to us in modern Java. Today, Java gives us classes, interfaces, lambdas, streams, enums and records. Add in inner classes, sealed classes and even sealed jars, and we have an extensive palette of tools that allows us to deliver better and cleaner APIs.

There’s too much to unpack in this article so let’s look at a working example.

ScenarioThe scenario is that of an API that can visit a tree structure and stream the contents. There is an implicit visitor pattern here, but it’s hidden away. The consumer’s experience is more straightforward.

PolicyLet’s define the policy for navigating and traversing a tree structure. We need to know how to interpret a node in the tree as a parent and decide which nodes to visit. Beyond that, there might be particular types of trees we’re going to visit that will have other policy requirements. If the tree is a view over a website., where we’re streaming the URLs we discover, we’ll want to add rate limiting and possibly a depth limit. If we’re streaming a file system, we might want to keep the depth limit, but rate limiting is probably not useful.

Builder Pattern – Part of the APISince this scenario is complex, our policy will be created via a builder pattern. Unless the policy is simple, using a builder gives us more flexibility to provide an interface that is easier to police for misconfiguration and easier for the consumer to understand.

import static NavigatorPolicyBuilder.builder;NavigatorPolicy shortPolicy = builder() .rateLimit(100, Duration.ofMinutes(1)) // play nice .defaultReader(new HTMLRefNavigator()) .maxDepth(2) .build(); In this example, we’re setting the policy on how to navigate a website. We’ve created a policy that we can reuse. In this example, the builder pattern (using fluent style ) is easy to understand and validate, resulting in a reusable but immutable policy object. The builder is part of the API but is not a process object.

Secret SauceIn this scenario, NavigatorPolicy is a Java Interface. Until recently, that would allow anyone to implement it and subvert any restrictions we might put in place via the builder. With the advent of Java 17 and the arrival of the ‘sealed class’ feature, we now have control over who can implement the interface.

This is important because we want the NavigatorPolicy object to be read-only, so we want to remove any public ways of changing its data. Therefore, we must separate the mutator methods from the interface and hide them away.

Sealed classes let us define which can implement an interface and which can subclass a class. Only those in the permitted list can be implementors or subclasses.

The code for the Navigator Policy interface looks like this.

public sealed interface NavigatorPolicy permits AbstractNavigatorPolicy { LinkReader handler(Connection.Response r);} This code indicates that only the AbstractNavigatorPolicy class can implement it. AbstractNavigatorPolicy definition includes

public abstract sealed class AbstractNavigatorPolicy implements NavigatorPolicy permits NavigatorPolicyBuilder.MyUriPolicy { It implements the interface and is itself sealed, allowing only an inner class of the builder to subclass.

MyUriPolicy looks like this. Thats it. Note the final setting and private constructor.

public static final class MyUriPolicy extends AbstractNavigatorPolicy { private MyUriPolicy() { }} Class RelationshipsThis code pattern of a sealed interface + sealed abstract class + inner class concrete (hollow) class provides good code separation and strong controls on how they interact and can be used.

The API consumer has a NavigationPolicy that can be interrogated but not overridden or replaced. Without using reflection, there is no way for the consumer to discover policy internals.

From the API designer’s POV, the builder is part of the API, which is evident in its intention, yet the implementation details are hidden away and can not be changed. The builder might return MyUriPolicy this time, but maybe a change in the future means that the next time build() is called, it returns YourUriPolicy.

The consumer is not affected, and the API is rigorous yet evolvable.

Process Time – STREAMS and thingsHere’s a related code set for using the API to do work.

‘base’ is the URI the API is going to visit. The policy object comes from above.

URITreeSteamVisitorBuilder.newInstance(base) .policy(shortPolicy) .select() .on(Link.class) .consume(this::handleLink) .visit(); The first thing to note is that we have a reasonable DSL-like interface using a fluent style with judicious inner classes. Next is to see that the consumer’s involvement with the process is controlled. The consumer is effectively on the end of a stream of data.

In this case, the API allows some inline filtering for architecture and performance reasons. Still, it could have been written to return a stream and have the filtering and processing to be done entirely by the consumer.

We did not need to ask the consumer to provide a custom class other than the callback for received data.

Let’s expand our use of this builder to see how we can do so much with this style of API design.

URITreeSteamVisitorBuilder.newInstance(base) .policy(shortPolicy) .select() .on(Link.class) .consume(this::handleLink) .on(Meta.class) .consume(this::handleMeta) .otherwise() .consume(o -> {System.out.println("other:"+o);}) .visit(); Now, we have an additional on() clause and an otherwise() method. Hopefully, you can see how this mirrors a select expression. Again, the API provides ways for the consumer to participate in the process, but as an actual consumer, The internals are kept hidden away. The API is not just a simple class or interface. The API here is a fluent style, DSL-like mechanism that is flexible and extensible while allowing the API designer control over behaviour and interaction.

A bonus is that this style works well with IDEs; the same code seen in the IDE looks like this. Note all the additional info about the intermediate classes of this DSL-like construction. This really helps with understanding the available options as the DSL is used.

DSL-Like is your new APIFrom an API design POV using sealed classes to add extra controls on who can do what with your code, it is compelling. You could stop there, having already reduced the chances of your API being deliberately or accidentally compromised. Including the builder approach (with the same sort of DSL-like structure) takes the API design into new territory and transforms it from a static Java interface or class into something more dynamic. Now, your API can be a sophisticated builder that can be extended in ways that a simple Java interface or class would struggle to match.

ConclusionsI hope I’ve given you just a taste of what can be achieved by using the features of Java.

Using Inner classes, Sealed Classes, Lambdas, Streams, Enums, etc., and by thinking again about how we separate policy from the process, it’s possible to create APIs that provide clear, easy-to-use yet enforceable designs that are hard to compromise but can be extended and enhanced.

The post Policy and Process: Thinking differently about Modern Java API design appeared first on JVM Advent.

View Details

Enterprise solutions are designed to address the multifaceted requirements of businesses and organisations. Compared to solutions designed for consumer use, enterprise solutions typically have a longer development lifecycle and require a higher level of investment, and it is not only the development cycle, enterprise software tends to have a longer lifespan than consumer-facing solutions. The lifespan of enterprise software can vary considerably, dependent on several factors, including the complexity of the solution, the specific requirements of the organisation in question, and the pace of technological advancement. This is partly due to the higher level of investment required, the greater degree of integration with existing systems, and the need for a longer return on investment. Probably a significant amount of development teams will be involved in the software development life cycle (SDLC) of such systems. It can be reasonably assumed that a considerable number of these systems will undergo phases of reengineering, refactoring, or redesign during this period. The process is influenced by the fact that technology is an ever-evolving field, and the tools and solutions that are currently in use may become obsolete in the near future. The notion that a design solution will remain effective indefinitely is no longer a valid assumption. Consequently, the objective is to develop solutions that can be repeatedly deployed to address evolving issues, becoming progressively more sophisticated, elegant, and intelligent over time. Choosing the right set of tools or platform is a critical decision that will have a profound impact on every level of an organisation. The JVM is one of the most robust, dependable, well-maintained, secure and stable platforms for enterprise solutions globally.

A number of scientific disciplines have illuminated the complex mechanisms underlying the evolution, adaptation, and healing of biological systems, enabling organisms to not only survive but also to flourish, with life spans that greatly exceed those of humans. These solutions from a distant past demonstrate the value of celebrating, understanding, and appreciating the past as a foundation for future progress. It is a challenging task to advance rapidly and not to be tempted by the hubris of complete disruption to move forward. It is essential to strive for strategic improvement and change, with a cautious understanding of potential consequences and a commitment to building something better in their place.
It is not enough for us, as professionals engaged in the development of systems and structures, to merely create solutions that enhance the quality of human life. Rather, we should focus on designing for the well-being of the whole environment. Resources are finite! In this era of rapid change, it’s vital that our solutions are constantly up to date with the significant changes taking place in our world.

We have a duty and a responsibility to deliver and maintain applications with higher levels of usability and sustainability. We should ask big questions such as: what are the long-term implications of our decisions?
Designing long-lasting systems requires a thoughtful approach that incorporates golden principles and powerful tools.
The Java development team has adopted a strategy that integrates proven principles, thereby facilitating the evolution of the Java platform and the creation of innovative features. Furthermore, the team has adapted and delivered new features that enable developers to address present challenges, establish a robust foundation for the future, and provide a long-lasting platform that allows critical applications with a long lifespan to be in production today and in the future.
Maintaining and evolving the JVM and the Java language often requires the implementation of breakthrough features that significantly improve performance or security. The key is to communicate these changes effectively and ensure that users understand the trade-offs involved in adopting new versions. By doing so, the Java team can foster a culture that embraces change rather than resists it, encouraging organizations to try new features and take advantage of the improvements in performance, security, or footprint.
The Java programming language has undergone significant changes before September 2017, but the introduction of a regular release cadence encourages developers to adopt new practices and tools. By releasing a version every six months, developers have access to new features and improvements more frequently. By committing to a predictable schedule, Java has mitigated the risks associated with long periods of stagnation, enabling teams to innovate continuously.
In my opinion the most remarkable features present on each LTS version released since Java 8 are as follows:

Java 8: The Functional Push

  • Lambda expressions, method references, and the Stream API brought a functional programming paradigm to Java, making code more concise, expressive, and often faster. Parallel processing with streams opened doors to efficient data manipulation, especially for large datasets. Stream API, which allows for efficient processing of sequences of elements, enabling parallel processing and improved performance. [Performance Enhancements & Modern Language Features] JEP 126: Lambda Expressions; JEP 335: Method References; JEP 107: Enhanced for Loop
  • The new Date and Time API provided a standardized, reliable, and well-designed approach to date and time handling, crucial for building consistent and predictable systems. The Optional class tackled the age-old problem of null pointers, fostering cleaner code and reducing potential errors. JEP 150: Date & Time API
  • Improved security features, including updates to the Java Cryptography Architecture (JCA) and enhancements to the Java Secure Socket Extension (JSSE). [Enhanced Security] JEP 114: TLS Server Name Indication (SNI) Extension; JEP 115: AEAD CipherSuites; JEP 121: Stronger Algorithms for Password-Based Encryption ;JEP 123: Configurable Secure Random-Number Generation; JEP 124: Enhance the Certificate Revocation-Checking API;JEP 131: PKCS#11 Crypto Provider for 64-bit Windows; JEP 166: Overhaul JKS-JCEKS-PKCS12 Keystores

Java 11: Modernization and Efficiency

  • The HTTP client API provided a streamlined and modern way to interact with Web services. JEP 321: HTTP Client API
  • The removal of obsolete modules like Java EE and CORBA streamlined the JDK, making it more efficient and less prone to compatibility issues. JEP 320: Remove the Java EE and CORBA Modules
  • The Java Flight Recorder (JFR) captures a wide range of events from the JVM, including memory usage, CPU activity, thread states, and garbage collection events. This comprehensive data collection helps developers diagnose issues and optimize application performance. JEP 328: Flight Recorder
  • Garbage Collector (GC) improvements were added, including the introduction of the Z Garbage Collector, which optimizes memory management and reduces pause times. ZGC is designed to handle large heaps (up to several terabytes) while maintaining low pause times, typically under 10 milliseconds. This makes it suitable for applications requiring high responsiveness. [Performance Enhancements] JEP 333: ZGC: A Scalable Low-Latency Garbage Collector
  • Added var keyword for local variable type inference, this feature enhances code readability, especially in cases where the type is obvious from the context, such as with collections or complex types. [Modern Language Features] JEP 286: Local-Variable Type Inference

Java 17: Enhancing Security and Performance

  • Sealed classes provided a powerful tool for controlling inheritance and controlled class hierarchies. This improves code clarity and reduces the potential for errors. JEP 409: Sealed Classes
  • Pattern matching, facilitates the streamlining of type checking and casting operations, thereby enhancing the clarity and readability of the code in question. This feature enables the user to ascertain whether an object is an instance of a particular class and, if so, to cast it to that class in a single operation. This obviates the necessity for a distinct cast, thereby diminishing the amount of superfluous code. [Modern Language Features] JEP 406: Pattern Matching for switch
  • The new MacOS rendering pipeline improves the performance of Java applications on a popular platform. JEP 382: New macOS Rendering Pipeline
  • Foreign Function & Memory API was introduced as part of the ongoing effort to improve Java’s interoperability with native code and memory. This API allows Java programs to safely and efficiently access foreign memory outside of the Java heap, enabling better integration with native libraries and systems. JEP 412: Foreign Function & Memory API (Incubator) and JEP 454: Foreign Function & Memory API (final)
  • Continued performance improvements with enhancements to the JIT compiler and optimisations in the Java Virtual Machine (JVM), resulting in faster execution times. [Performance Enhancements] JEP 317: Experimental Java-Based JIT Compiler. Introduced in Java 9; JEP 295: Ahead-of-Time Compilation. Introduced in Java 9; JEP 410: Remove the Experimental AOT and JIT Compiler. Introduced in Java 17; JEP 421: Deprecate the Applet API. Introduced in Java 9; JEP 418: Vector API (Incubator). Introduced in Java 16.
  • Enhanced security with features such as Deprecate the Security Manager for Removal which provides better control over the use of native code, and improvements to the TLS protocol. [Enhanced Security]. JEP 411: Deprecate the Security Manager for Removal. Introduced in Java 17; JEP 844: TLS 1.3 Support. Introduced in Java 11; JEP 825: TLS 1.3 in the Java SE Platform. Introduced in Java 17.

Java 21: The current Java LTS

  • String templates introduced a modern way to create strings with embedded expressions, making code more readable and maintainable. Virtual threads (Preview) promised to simplify concurrent programming by providing lightweight threads, potentially leading to significant performance improvements. JEP 430: String Templates (Preview) Introduced in JDK 21; JEP 459: String Templates (Second Preview) Introduced in JDK 22; JEP 425: Virtual Threads (Preview) Introduced in Java 19; JEP 436: Virtual Threads (Second Preview). Introduced in Java 20. JEP 444: Virtual Threads. Introduced in Java 21
  • Sequenced collections offered new data structures that preserved element order, providing more options for building well-behaved and predictable systems. Pattern matching for switch (Preview) and scoped values (Preview) further enhanced code clarity and data management, paving the way for cleaner and more robust applications. JEP 431: Sequenced Collections. Introduced in Java 21; JEP 406: Initial introduction of pattern matching for switch. Introduced in Java 17; JEP 420: Pattern Matching for switch Second preview . Introduced in Java 18; JEP 427: Third preview. Introduced in Java 19; JEP 433: Fourth preview. Introduced in Java 20; JEP 441: Finalized in JDK 21; JEP 429: Scoped Values (Incubator) Introduced in Java 20; JEP 446: Scoped Values (Preview) Introduced in Java 21; JEP 464: Scoped Values (Second Preview) Introduced in Java 22; JEP 481: Scoped Values (Third Preview) Introduced in Java 23; JEP 487: Scoped Values (Fourth Preview) Introduced in Java 24;
  • Virtual Threads. This feature represents a further enhancement of the support for virtual threads, which facilitates concurrent programming by enabling developers to write code that can handle multiple tasks concurrently, without the inherent complexity of traditional thread management. JEP 425: Virtual Threads ( Preview). Introduced in Java 19; JEP 436: Virtual Threads (Second Preview). Introduced in Java 20; JEP 444: Virtual Threads. Introduced in Java 21;

Java 23 ( No LTS)

  • Primitive Pattern Matching – JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview)
    This feature facilitates enhanced pattern matching by enabling developers to utilise primitive types in all pattern contexts. This results in a more straightforward and secure code when working with patterns.
  • The incorporation of Markdown support within JavaDoc – JEP 467: Markdown Documentation Comments. Java 23 introduces support for Markdown in JavaDoc comments, thereby facilitating the production of more readable and easier-to-write documentation. This enables developers to utilise Markdown syntax for formatting, thereby enhancing the overall documentation experience.
  • The Class-File API – JEP 466: Class-File API (Second Preview). The new Class-File API standardises access to Java class files, thereby facilitating the reading, modification and creation of class files at the programmatic level. This is particularly beneficial for tools and libraries that require the manipulation of bytecode.
  • Scoped Values – JEP 481: Scoped Values (Third Preview). Building on the scoped values introduced in earlier versions, this JEP refines the application programming interface (API) for sharing immutable data across methods and threads, thereby promoting more effective data management practices in concurrent applications

In this article, I’m excited to give you an overview of the enterprise software landscape and showcase why the evolution of the Java platform makes the JVM such a powerful and useful technology. It looks at the key considerations and advancements that have shaped the development and longevity of enterprise-level solutions. There are five main things to take away from this article:

  • Enterprise solutions have a longer development lifecycle, higher investment, and longer lifespan compared to consumer solutions. This often requires reengineering and adaptation to evolving technology, which is a fantastic opportunity for developers to really add value and make a real long-term impact.
  • The Java Virtual Machine (JVM) is a robust and secure platform for enterprise solutions. It’s a great foundation to build powerful, reliable software on.
  • Java has undergone significant improvements with each Long-Term Support (LTS) release, introducing features like functional programming, security enhancements, performance optimisations, and modern language features;
  • The Java team has adopted an impressive strategy of integrating proven principles and delivering new features to address current challenges and establish a robust foundation for the future;
  • The regular release cadence encourages developers to continuously adopt new practices and tools, mitigating the risks of long periods of stagnation.

Ixchel RuizKarakun AG – Basel, Switzerland

Ixchel has developed software application & tools since 2000. Her research interests include Java, dynamic languages, client-side technologies and testing. Java Champion, Oracle ACE pro, Testcontainers Community Champion, CDF Ambassador, Hackergarten enthusiast, Open Source advocate, public speaker and mentor.

The post Java: The Brave Companion in the World of Enterprise Software Solutions! appeared first on JVM Advent.

View Details

Some years ago, events appeared in the IT world with the idea of decoupling many applications or microservices, improving performance, reducing complexity, and allowing change flows. Some companies adopted this new paradigm instead of the classic synchronic world where the client needed to wait until the provider answered; initially, everything looked fine because it solved many problems and provided the flexibility to create or change flow without any problem.

The developers adopted this new paradigm quickly but introduced a new problem that is directly related to the type of communication; in the past, most tests executed an HTTP request and waited until the response validated the results, but now this approach is not possible because things happen in an async way. Some questions could appear in your mind:

  • What happens if the events need to modify some information for a database?
  • How can you check if the modifications are okay or not?
  • What happens if the application sends a message to a specific topic or queue?

Depending on your application, many other questions could arise. However, this reveals that changing one communication approach to another involves more than adding dependencies and creating topics.

In this article, you will learn different approaches to tackle these problems of testing asynchronous communication in an agnostic way, such as which framework or library you use on your application.

context of the situationImagine that you work for a travel agency selling flights. This would represent a scenario where different situations could happen on the same microservice. Your team is responsible for developing and maintaining one microservice, which manages all the information about different reservations. The microservice is simple because it offers a CRUD for all the standard operations. Still, there is one particular consideration: the reservations need to listen to all the payment events to confirm or cancel the record.

The following figure represents the most relevant flows on the microservice and the things you need to consider to validate them.

Different flows could happen in the system.

Considering these different scenarios, you have other minor problems that combine into huge problems; the idea is to tackle them in parts to keep them simple and reusable.

NOTE: You can access this GitHub repository to find the complete solution.

PROBLEM #1 – HOW TO CREATE THE TESTS?The first problem is which library helps to test the application in an agnostic way. There are tons of different testing libraries, but one of the most recognized is Karate, which uses Gherkin to write tests like Cucumber behind the scenes. This library has some advantages, like the simple syntax used to write tests and the possibility of integrating with other tools like Playwright or Gatling to cover different types of tests.

To use this library, you first need to add the dependency on your application. The following block represents how to do it on a Maven project:

```

com.intuit.karate  
karate-junit5  
${karate.version}   
test

``` As a recommendation, constantly check which version of this library is the latest on the official webpage or a repository like this.

The step is to define the request you will use to create a reservation. A good practice is to define an external file containing the request’s body so you can reuse it for multiple scenarios or keep the tests simple. Let’s create a file called create_reservation_request.json in the test’s resource folder with the following information:

{ "passengers" : [ { "firstName" : "Andres", "lastName" : "Sacco", "documentNumber" : "31434284", "documentType" : "DNI", "birthday" : "1985-01-01" } ], "itinerary" : { "segment" : [ { "origin" : "BUE", "destination" : "MIA", "departure" : "2023-12-31", "arrival" : "2024-01-01", "carrier" : "AA" } ], "price" : { "totalPrice" : 30.0, "totalTax" : 20.0, "basePrice" : 10.0 } } } The request is not the only file you must create; a good practice is validating the entire response. To do this, let’s make a file called create_reservation_response.json, which contains the following information:

{ "id" : "#notnull", "passengers" : [ { "firstName" : "Andres", "lastName" : "Sacco", "documentNumber" : "31434284", "documentType" : "DNI", "birthday" : "1985-01-01" } ], "itinerary" : { "segment" : [ { "origin" : "BUE", "destination" : "MIA", "departure" : "2023-12-31", "arrival" : "2024-01-01", "carrier" : "AA" } ], "price" : { "totalPrice" : 30.0, "totalTax" : 20.0, "basePrice" : 10.0 } }, "creationDate" : "#notnull", "status" : "CREATED" } As you can see, the response looks like a JSON file but with some strange little things like #notnull. The idea declares these fields on the document as wildcards, so Karate will only do simple validation, ignoring the inside values. There are many other wildcards for other types of validation, so check the official documentation to obtain more information.

This problem’s core is creating a simple test that makes a POST request to some specific endpoint using Karate. Create a file called create_reservation.feature, which contains the following information:

`` Feature: Create a new reservation Background: * def api\_URL =http://localhost:8080/api/`
* def response_ok = read('./response/create_reservation_response.json')
* def request_ok = read('./request/create_reservation_request.json')

Scenario: Persist the information
Given url api_URL + 'reservation'
And request request_ok
And header Accept = '/'
And header Content-Type = 'application/json'
When method POST
Then status 201
And match response == response_ok ``` In the background section of the previous block of code, some variables use the read function, native to Karate, to load the information of one file. The syntax is simple, but let’s summarize the idea of each part in the following table:

| Keyword | Description | | Feature | This keyword contains a high-level description of the idea of the different scenarios. | | Background | It usually indicates something that needs to be done before executing one or multiple tests. It’s a good idea to put all the variables you will use across all the scenarios inside. | | Scenario | This keyword is used to represent a particular test or case that you want to validate. | | Given | Describes the initial context or preconditions for the scenario. It is the starting point of the test. | | When | Specifies the action or event that triggers the behavior in the scenario. | | Then | Describes the expected outcome or result of the action performed in the When step. | | And | Used to concatenate multiple Given, When, or Then steps in a scenario. | | def | Use this word when you declare a variable | | match | It’s responsible for validating something in the response. You could validate all the responses or just one attribute. |

The last part of this problem is to create a class responsible for executing all the karate files; the class always needs to use the annotation @Karate.Test to indicate that this test is not a Junit or another type of test.

``` import com.intuit.karate.junit5.Karate;

class APITest {

@Karate.Test  
Karate runAllTests() {  
    return Karate.run("flow/create\_reservation.feature").tags("~@ignore").relativeTo(getClass());  
}

} ``` As you can see in the class, the route and the file name contain the tests, but if you prefer, you can not indicate the name, so Karate will scan all the directories and find files with the extension .feature.

SIMULATING external communicationsOne particularity of this testing scenario is the endpoint, which creates a reservation to interact with another microservice. Thus, you have two options: interact with the real microservices in a nonproductive environment or use a tool to mock or simulate their behavior.

The best option is always to simulate the behavior of the external communications; to do this, you can use tools like Microcks, Hoverfly, or Wiremock. There are many reasons for choosing one instead of another, but consider the simplicity of using the best option, Wiremock.

The first thing to do is modify or create a docker-compose file with the image of Wiremock exposing the same port as the actual application, reducing the number of changes you need to introduce.

``` version: "3.1"
services:

api-catalog:
image: wiremock/wiremock:2.32.0
ports:
- 6070:8080
volumes:
- ./wiremock:/home/wiremock
restart: always ``` The next step is to declare the different stubs or mocks that Wiremock needs to return depending on the parameters and the URL the application invokes. To do this, let’s create a file called operation-success.json, which will contain the following information:

{ "mappings": [ { "request": { "method": "GET", "urlPath": "/api/flights/catalog/city/BUE", "headers": { "Content-Type": { "equalTo": "application/json" } } }, "response": { "status": 200, "headers": { "Content-Type": "application/json" }, "bodyFileName": "api-catalog/response/response-BUE.json" } }, { "request": { "method": "GET", "urlPath": "/api/flights/catalog/city/MIA", "headers": { "Content-Type": { "equalTo": "application/json" } } }, "response": { "status": 200, "headers": { "Content-Type": "application/json" }, "bodyFileName": "api-catalog/response/response-MIA.json" } } ] } They contain all the stubs with a part related to the request that the tool needs to receive to match and return something as a response. As you can see, you need to indicate the URL, HTTP method, and some headers on the request to have a granular response that only works with this request.

In the response section, you could indicate the entire response or create files containing the response. To do this, let’s make a file called response-BUE.json with the following information:

{ "name": "Buenos Aires", "code": "BUE", "timeZone": "America/Argentina/Buenos\_Aires" } As you can see, the file format is just a simple JSON file with nothing strange so you can copy and paste a natural response to the application.

There are two different requests in this scenario, so you need to create two files or one for each mapping, considering that you don’t always want to return the same response.

Let’s create a file called response-MIA.json with the following information to complete the scenario:

{ "name": "Miami", "code": "MIA", "timeZone": "America/New\_York" } If you want more information about the parameters you could indicate on the request, look at the official documentation, particularly this link. At the same time, if you need more information about the format of the response on the mock, you can read this article.

PROBLEM #2 – How to simulate events?Most applications use events to communicate things to one another, and in many cases, they use tools like Kafka. Still, in another case where most of the infrastructure is based on AWS (Amazon Web Services), the companies decide to use that cloud provider’s tools, such as SQS/SNS. The problem with the second approach is that it is impossible to use the actual infrastructure without affecting something on the environment, so you need to find a way to reduce the impact on all the applications by running a simple integration test.

In this case, an excellent approach to solving the problem is using a tool like Localstack. This tool provides most of the services that virtually exist in AWS so that you can use more or less the exact behavior of the fundamental infrastructure but with the same limitations.

There are many ways to use Localstack, but a possible approach is to create a Dockerfile that configures everything for us instead of doing it on each docker-compose file. So let’s make it with the following information:

``` FROM localstack/localstack:0.14.5

ENV SERVICES=sns,sqs DEBUG=1 DEFAULT_REGION=us-east-1 HOSTNAME_EXTERNAL=localhost DOCKER_HOST=unix:///var/run/docker.sock

VOLUME /docker-entrypoint-initaws.d/
VOLUME /var/run/docker.sock

EXPOSE 4566 ``` The problem when you use Localstack is to run some specific commands like creating queues, topics, or anything else you need to do using the command of the AWS inside the container, which represents a problem because it’s possible that each time that you want to execute the tests someone run the commands. A solution to this problem is to create a script that runs at the container’s start and makes everything you need, like some topics and queues. Let’s create a file called init.sh with the following information:

```

!/usr/bin/env bash

set -euo pipefail

enable debug

set -x

aws configure set aws_access_key_id "test"
aws configure set aws_secret_access_key "test"

echo "configuring sns/sqs"
echo "==================="

https://gugsrs.com/localstack-sqs-sns/

LOCALSTACK_HOST=localhost
AWS_REGION=us-east-1
LOCALSTACK_DUMMY_ID=000000000000

get_all_queues() {
awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sqs list-queues
}

create_queue() {
local QUEUE_NAME_TO_CREATE=$1
awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sqs create-queue --queue-name ${QUEUE_NAME_TO_CREATE} --attributes FifoQueue=true,ContentBasedDeduplication=true
}

get_all_topics() {
awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sns list-topics
}

create_topic() {
local TOPIC_NAME_TO_CREATE=$1
awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sns create-topic --name ${TOPIC_NAME_TO_CREATE} --attributes FifoTopic=true,ContentBasedDeduplication=true
}

link_queue_and_topic() {
local TOPIC_ARN_TO_LINK=$1
local QUEUE_ARN_TO_LINK=$2
awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sns subscribe --topic-arn ${TOPIC_ARN_TO_LINK} --protocol sqs --notification-endpoint ${QUEUE_ARN_TO_LINK} --attributes RawMessageDelivery=true
}

guess_queue_arn_from_name() {
local QUEUE_NAME=$1
echo "arn:aws:sqs:${AWS_REGION}:${LOCALSTACK_DUMMY_ID}:$QUEUE_NAME"
}

guess_topic_arn_from_name() {
local TOPIC_NAME=$1
echo "arn:aws:sns:${AWS_REGION}:${LOCALSTACK_DUMMY_ID}:$TOPIC_NAME"
}

PAYMENTS_IN_PROCESS_QUEUE_NAME="payments_in_process.fifo"
PAYMENTS_CONFIRMED_QUEUE_NAME="payments_confirmed.fifo"
RESERVATION_CONFIRMED_TOPIC_NAME="reservation_confirmed.fifo"
ASSERTIONS_QUEUE_NAME="reservation_confirmed-assertions.fifo"

echo "creating queue: $PAYMENTS_IN_PROCESS_QUEUE_NAME"
QUEUE_ARN=$(create_queue ${PAYMENTS_IN_PROCESS_QUEUE_NAME})
echo "created queue: $QUEUE_ARN"

echo "creating queue: $PAYMENTS_CONFIRMED_QUEUE_NAME"
QUEUE_ARN=$(create_queue ${PAYMENTS_CONFIRMED_QUEUE_NAME})
echo "created queue: $QUEUE_ARN"

echo "creating topic: $RESERVATION_CONFIRMED_TOPIC_NAME"
TOPIC_ARN=$(create_topic ${RESERVATION_CONFIRMED_TOPIC_NAME})
echo "created topic: $TOPIC_ARN"

echo "creating queue: $ASSERTIONS_QUEUE_NAME"
QUEUE_ARN=$(create_queue ${ASSERTIONS_QUEUE_NAME})
echo "created queue: $QUEUE_ARN"

echo "linking topic $RESERVATION_CONFIRMED_TOPIC_NAME to queue $ASSERTIONS_QUEUE_NAME"
LINKING_RESULT=$(link_queue_and_topic $(guess_topic_arn_from_name $RESERVATION_CONFIRMED_TOPIC_NAME) $(guess_queue_arn_from_name $ASSERTIONS_QUEUE_NAME))
echo "linking done:"
echo "$LINKING_RESULT"

echo "all topics are:"
echo "$(get_all_topics)"

echo "all queues are:"
echo "$(get_all_queues)" ``` After configuring the Localstack with the topics and queues, it’s time to create a docker-compose file to run the tests so that you can run them many times without depending on the database or the network. The docker-compose file will contain the Localstack image and the database that appears on the following block:

``` version: "3.1"
services:
localstack:
build: localstack/
ports:
- 4566:4566
volumes:
- ./localstack/:/docker-entrypoint-initaws.d/
- /var/run/docker.sock:/var/run/docker.sock

api-reservation-db:
image: mongo:5
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: muppet
MONGO_INITDB_DATABASE: flights_reservation
ports:
- 27017:27017

api-catalog:
image: wiremock/wiremock:2.32.0
ports:
- 6070:8080
volumes:
- ./wiremock:/home/wiremock
restart: always ``` One possible way to execute all the containers on the application when you run the test is using Testcontainers, which have support on many databases or brokers but also can manage docker-compose files. This library has support not just for Java; you can use it for other languages like .NET, Rust, or Go.

To use TestContainers, you must first add the dependency to your POM file. The latest version is available at this link or in the official documentation.

```

org.testcontainers  
junit-jupiter  
${testcontainers.version}  
test

``` The next step is to create a base class in which all the tests on the application could be used. Consider that this class will contain the annotations related to TestContainers to indicate that there are docker containers to manage inside.

Let’s create a class called BaseTest with the following content:

``` import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.io.File;

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class BaseTest {

static DockerComposeContainer dockerComposeContainer = new DockerComposeContainer(  
        new File("src/test/resources/docker/docker-compose.yml"))  
                .waitingFor("localstack", Wait.forLogMessage(".*all queues are.*\\n", 1))  
                .waitingFor("api-reservation-db",  
                        Wait.forLogMessage(".*MongoDB init process complete; ready for start up.*\\n", 1))  
                .withLocalCompose(true);

@BeforeAll  
static void setUp() {  
    dockerComposeContainer.start();  
}

@AfterAll  
static void tearDown() {  
    dockerComposeContainer.stop();  
}

} ``` In the previous block of code, some remarkable things appear, like waiting for the containers’ declaration; the idea is not to run anything until all the containers are ready, like the database and the queues. Consider that you need to modify the class APITest to extend from BaseTest to use all the benefits of the containers.

There is one slight modification after writing any test to check the behavior of the events; let’s modify the application.yml to connect with Localstack instead of the actual infrastructure. To do this, you only need to change the endpoint of AWS and the location of the queues, like appears on the following block

spring: main: allow-bean-definition-overriding: true data: mongodb: uri: "mongodb://root:muppet@localhost/flights\_reservation?authSource=admin" cloud: aws: endpoint: http://localhost:4566 region: static: us-east-1 credentials: access-key: test secret-key: test events: queues: payments-in-process: http://localhost:4566/000000000000/payments\_in\_process.fifo payments-confirmed: http://localhost:4566/000000000000/payments\_confirmed.fifo topics: reservation-confirmed: arn:aws:sns:us-east-1:000000000000:reservation\_confirmed.fifo The first step after all the modifications is to create the scenario to send a message to a queue, so let’s create a file with the name payment_confirmed_query.txt, which contains a message like the following:

Action=SendMessage&MessageBody=${reservation\_id}&QueueUrl=http%3A%2F%2Flocalhost%3A4566%2F000000000000%2Fpayments\_confirmed.fifo&MessageGroupId=group-id The next step is to create a file called reservation_confirmed_query.txt, which represents the message that the application will send after doing some task. The file content looks like this:

Action=ReceiveMessage&VisibilityTimeout=10&MaxNumberOfMessages=1 The last step is to create a file called payment_confirmed.feature, which contains everything related to tests. The idea of this scenario is to create a reservation on the application; after that, send a message that the application will listen to and process, and at the end, check if a new message will appear on another topic.

`` Feature: Check the process of confirm the payments Background: * def api\_URL =http://localhost:8080/api/* def localstack\_URL =http://localhost:4566/000000000000/payments_confirmed.fifo* def localstack\_assertions\_URL =http://localhost:4566/000000000000/reservation_confirmed-assertions.fifo`

Scenario: Check the confirmation of the payments
# Create reservation
* def response_ok = read('./response/create_reservation_response.json')
* def request_ok = read('./request/create_reservation_request.json')

Given url api\_URL + 'reservation'  
And request request\_ok  
And header Accept = '*/*'  
And header Content-Type = 'application/json'  
When method POST  
Then status 201  
* def reservationId = response.id  
And match response == response\_ok

# Send message to the queue  
* def payment\_event = read('./events/payment\_confirmed\_query.txt')  
* replace payment\_event.${reservation\_id} = reservationId

Given url localstack\_URL  
And header Content-Type = 'application/x-www-form-urlencoded'  
And request payment\_event  
When method POST  
Then status 200

# Check if the message exists  
* def assertion\_event = read('./events/reservation\_confirmed\_query.txt')  
* replace assertion\_event.${reservation\_id} = reservationId

* print localstack\_assertions\_URL + `?` + assertion\_event

Given url localstack\_assertions\_URL + `?` + assertion\_event  
And retry until karate.match("response/ReceiveMessageResponse/ReceiveMessageResult/Message/Body == '#present'").pass == true  
When method Get  
Then status 200  
And match response/ReceiveMessageResponse/ReceiveMessageResult/Message/Body == '#present'

``` As you can see, the first part of the test looks similar to the scenario on Problem #1. Still, it appears to have some differences, like reading a file with the message of an event to replace one variable in the message and doing a POST to a URL, which exposes Localstack for some services like SQS/SNS.

The last part of the test checks if the execution result is okay or not. To do this, verify if a new message exists in a specific topic. The test retries until the message appears, considering that this is not a synchronic scenario, so the message could take the same seconds to appear on the topic.

Consider that if you don’t check the message on the queues, create some mechanism to remove it because it could affect the execution of another test.

PROBLEM #3 – How to check the database?The last part of this scenario, and one of the most relevant, checks what happens in a database after some flow is executed. There are multiple approaches to solving this problem, like using an existing endpoint or creating a new one just for testing processes. Still, as you can imagine, this approach is impossible and not a good idea in all cases. Consider this situation: The best alternative is to access the database to obtain the information and check if everything is okay.

Not all databases have a tool that provides a REST API that solves all the problems related to accessing the database, like the query language and the way to obtain the data. However, in the case of MongoDB, one tool called Restheart connects with any database and exposes a simple REST interface to access the different collections or documents.

The first step is to add to the docker-compose.yml the image of Restheart connected to the database, which, in this case, is part of the same docker-compose file.

api-reservation-db-rest: image: softinstigate/restheart:6.3.3 ports: - 8082:8080 volumes: - ./restheart:/opt/restheart/etc depends\_on: - api-reservation-db After that, you must create two unique files containing all the tool configurations. Changing many things in both files is unnecessary, but let’s start with the file default.properties, which you can download from here.

The second file is restheart.yml, which you can download from here. It contains most of the tool’s default configuration with just one minor modification: the URI, as appears in the following block.

mongo-uri: mongodb://root:muppet@api-reservation-db/flights\_reservation?authSource=admin With these modifications, if you run the docker-compose file and try to access the localhost:8082, you will see all the collections in the database appear on the following image:

Restheart output

The last step after all the modifications is to create the scenario to check the database, so let’s create a file with the name payment_in_process_query.txt, which contains a message like the following:

Action=SendMessage&MessageBody=${reservation\_id}&QueueUrl=http%3A%2F%2Flocalhost%3A4566%2F000000000000%2Fpayments\_in\_process.fifo&MessageGroupId=group-id The next step is to define into a file called in_process_reservation_database.json all the content of the JSON, which represents a document in the database

{ "\_id": { "$oid": #notnull }, "passengers": [ { "first\_name": "Andres", "last\_name": "Sacco", "document\_number": "31434284", "document\_type": "DNI", "birthday": { "$date": 473396400000 } } ], "itinerary": { "segment": [ { "origin": "BUE", "destination": "MIA", "departure": "2023-12-31", "arrival": "2024-01-01", "carrier": "AA" } ], "price": { "total\_price": "30.0", "total\_tax": "20.0", "base\_price": "10.0" } }, "\_class": "com.twa.reservations.model.Reservation", "creation\_date": { "$date": #notnull }, "status": "IN\_PROCESS" } The last part of creating the scenario is developing the test that uses all these files. Let’s make a file with the name payment_in_process.feature, which contains the reservation creation, and send the event to modify the information in the database, as you can see in the following block of code.

`` Feature: Check the process the payment Background: * def api\_URL =http://localhost:8080/api/* def restheart\_URL =http://localhost:8082/flights_reservation/reservation/* def localstack\_URL =http://localhost:4566/000000000000/payments_in_process.fifo`

Scenario: Check the creation and process of payments events
# Create reservation
* def response_ok = read('./response/create_reservation_response.json')
* def request_ok = read('./request/create_reservation_request.json')

Given url api\_URL + 'reservation'  
And request request\_ok  
And header Accept = '*/*'  
And header Content-Type = 'application/json'  
When method POST  
Then status 201  
* def reservationId = response.id  
And match response == response\_ok

# Send message to the queue  
* def payment\_event = read('./events/payment\_in\_process\_query.txt')  
* replace payment\_event.${reservation\_id} = reservationId

Given url localstack\_URL  
And header Content-Type = 'application/x-www-form-urlencoded'  
And request payment\_event  
When method POST  
Then status 200

# Check the modification into the database  
* configure retry = { count: 10, interval: 3000 }  
* def change\_database = read('./database/in\_process\_reservation\_database.json')

Given url restheart\_URL + reservationId  
And retry until karate.match("response contains change\_database").pass == true  
When method GET  
Then status 200  
And match response == change\_database

``` As you can see, the last step of the test is to check the data in the database with a series of retries until the result is correct.

After all these changes related to the different problems in the article, the last step is to run the tests to see what happens. To do this, you only need to run the command mvn test in the same way that appears on the following block:

~$ mvn test .................. 10:59:13.794 [main] INFO tc.docker-compose - Docker Compose has finished running [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 22.16 s -- in com.twa.reservations.APITest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] --------------------------------------------------------- [INFO] BUILD SUCCESS [INFO] --------------------------------------------------------- [INFO] Total time: 29.020 s [INFO] Finished at: 2024-11-21T10:59:17-03:00 [INFO] --------------------------------------------------------- The execution result shows that all the tests pass without problems or require some fundamental infrastructure. If you want to see the results in another way, remember that Karate generates a report during each execution to simplify the validation process for each test step.

The result in a graphical way of the previous execution looks like the following image

Results of Karate Execution

If you click on any of these rows, you will obtain all the information about one particular test, including all the steps, as shown in the following image.

Details of a Karate Test

Remember both options: CLI or UI have the same information but are represented differently.

WHAT’S NEXT?There are many resources about different topics connected with other types of testing, but a few tackle the problems associated with creating applications that use events and databases. The following is just a short list of resources:

  • Testing Web APIs by Mark Winteringham
  • Mastering Testcontainers by Oleg Šelajev
  • Speed up writing tests with Wiremock Spring Boot by Pieter-Jan Drouillon
  • Develop and test your AWS-powered Spring Boot application locally by Anca Ghenade

Other resources could be great for understanding some concepts related to events or asynchronous communication in depth:

  • Building Event-Driven Microservices: Leveraging Organizational Data at Scale by Adam Bellemare
  • Grokking Streaming Systems: Real-time event processing by Josh Fischer and Ning Wang
  • Not Just Events: Developing Asynchronous Microservices by Chris Richardson
  • Docker in Practice, Second Edition by Ian Miell and Aidan Hobson Sayers

Consider that this is just a small list of all the available resources about event-driven or asynchronous communication. If something is unclear, find another video or resource.

CONCLUSIONLeveraging events on a platform can be highly beneficial, offering advantages such as enabling parallel collaboration with other teams and allowing flexibility to adapt workflows. However, it would be best if you found a way to validate all the possible scenarios related to this new paradigm before deploying something new on production because the risk that something wrong happens is too high compared with traditional approaches.

There are tons of different databases, and not in all the cases have a tool that offers to expose the information like a simple REST API; if this is the case, try to think of an alternative, like creating an endpoint that exposes the information and replaces the request to RESTHeart to a request on the API.

One last thing related to the selection of technologies: There is no unique way to solve the issue of using events. Still, all your decisions must be documented and discussed with other partners or colleagues. Hence, I suggest using ADR (Architecture Decision Record), an excellent way to track architectural choices.

The post Events: Love Triangle in Integration Testing appeared first on JVM Advent.

View Details

ContextIn today’s world, we develop systems that interact with applications developed by other teams or people. This is even more the case in the world of Microservices and Serveless setup. Systems that we develop might be in some cases consumers of other services, while in others can be providers that are consumed by other people services.

No matter if we are building providers (systems that provide and share data) or consumers (systems that fetch data from some other system and use it), making everything work perfectly when we hit production is the constant difficulty that we have. This is enhanced even more, by the fact, that teams can develop and push applications to production at different frequencies.

What we do today to mitigate this challengeThis challenge isn’t new and is not unique to the microservices era.

As soon as we have two systems that need to communicate, there is a question of how to keep both implementations in sync. If both systems are developed by the same team, it is easier, however problem still exists. If systems are developed by different teams, sync of some sort needs to exist between developers.

Common approach and limitations of itThe most logical solution for this problem is to have a clear specification of interactions and to cover it with tests by both teams.

In the majority of cases we are dealing with APIs, so usage of some sort of API specification is expected, for example OpenAPI spec.

Teams might go with my favorite approach in development, specification first.
In this case, once both teams agree on specifications, each team can go their own way and start developing according to the agreed specifications.
They can also use API spec to create mocks that can be used during development, this helps with the team moving at a different speed, and the ability to use different programming languages to create providers and consumers.

Since quality is important, teams would write Unit tests and Integration tests, sometimes also called End-to-end tests to validate if all is good before reaching production.

Things of course still can go wrong.

Things break during the testing integration phaseThe first problem that can happen is that things break during the integration phase, in pre-prod or acceptance env (different names can be used).
This might be a strange problem to have since we want to catch problems before they hit production correctly. The problem isn’t the fact that things broke, the problem might be to figure out which team made a mistake and where. Team building API or team consuming API.

Looking at errors and agreed specifications for API, should give us the answer in most cases to which team need to go back to the development phase.
The limitation of this is that in most cases this needs to be done manually by someone taking a look at errors and specifications. Also, what is needed for this to be as painless as possible, is for people to be pedantic on making sure specifications reflect the real state of things, that it was communicated to all parties, and that all parties are aware of the latest spec to be used.

In past, I encountered teams that were flowless on this, and at the same time, I saw teams who wrote specifications a few years ago and never bothered to update them.

So this can be anywhere from trivial to huge issues.
In the end, the more teams invest in this part being correct, it will lead to fewer issues down the road.

Things break in productionThe reality is that things can still break in production.

This usually leads to the revelation that some use case wasn’t covered in the testing phase by either team. Insufficient quality and lack of testing.
Again resolution of this problem might be from trivial to very complex, depending on the use case and problem at hand.

What is a bigger problem is the fact that error landed in production, and got exposed to end consumers.

As systems grow in complexity and interactions between them, the possibility of having this issue grows exponentially.
In the end, we as developers need to think of and write all the tests to cover all these use cases, and since there will be a lot of variations, overlooking some isn’t so unlikely.

Changes need to be made to the API specificationWhenever there is a need to modify or enhance existing APIs, it can be from trivial to almost impossible task.
If you are adding new endpoints and ways for people to interact with API, usually it shouldn’t be the problem. However, in cases where you want to modify an existing endpoint or to remove some old endpoint, there is also doubt if you identified all consumers, and have shared with them that that endpoint is changing or is being removed.
Unless you have good monitoring, there also might be the question of usage of those endpoints in the first place. Since it might be the case that no one is actually using this endpoint, removing it won’t really cause problems.

How Pact and Contract testing is trying to solve this problemNow that we have a better understanding of what we want to solve, let us look at PACT and Contract testing and how it can help.

In a nutshell, there should be a contract that is agreed upon between the provider and consumer, that defines the contract for their interaction.

The difference between specification and this contract is subtle, while very important. Specification tries to document all things that providers expose to the world, how a request should look like and how a response will look like in this case. As already discussed, the fact that for example, some API has a certain endpoint doesn’t guarantee that anyone will call it.

In the case of PACT, all start with the consumer.

How it worksConsumer create unit tests using PACT DSL and map its side of interaction with the provider. It can be API base interaction or interaction defined on messages.
Once it is done, the consumer can run the Unit test, and PACT implementation will create a mock provider that will reply to calls to it from the consumer according to what was defined in PACT DSL. This ensures that things are automated and that it is easily identified if the consumer makes mistakes.

The good thing about PACT is that it is a language agnostic, and there are implementations in multiple languages. So, consumers and providers can be implemented in different technologies.

In the case of Java, we can look in more detail how one example looks like in JUnit5 https://docs.pact.io/implementation_guides/jvm/consumer/junit5, or if you use Junit4 let us look at an example here https://docs.pact.io/implementation_guides/jvm/consumer/junit

Once the Unit tests are run, and executed successfully, a PACT file will be created. This file is written in PACT DSL and then can be sent over to the Provider.
Consumer have created contracts on how they will interact with provider and inform them of their intentions.

The provider then uses the PACT framework, to run all Pact files in unit tests, and validate that all will be good. The Pact framework plays the role of mocking consumers, sending appropriate requests to providers and validating if the response meets the expectations written in the Pact file.

An example of Provider testing can be seen here https://docs.pact.io/implementation_guides/jvm/provider/junit5

All of this is done in an automated way by using Mocks and Unit tests which allow for all of it to be run on the developer machine or as part of the pipeline.

ConclusionAs we saw Pact and Contract testing isn’t a replacement for things that we have in our toolbox, instead it is a very useful addition that can help us solve some tricky problems that we face in day-to-day life, like easily identifying potential integration problems without of need of having full environments, since all is done in unit testing. Also make sure that in case we make changes on the provider side, we don’t break anything by accident on the side of consumers.

The Pact has implementations in multiple programming languages, that increase the value of having it in our toolbox.

Resources – Additional read* http://www.itshark.xyz/posts/2022/12/20/How_to_Tackle_the_Pyramid_of_Quality_in_the_Real_World * https://docs.pact.io/ * https://docs.pact.io/implementation_guides/jvm/consumer/junit5 * https://docs.pact.io/implementation_guides/jvm/provider/junit5 * https://swagger.io/specification/

The post Contract Testing and PACT in Java appeared first on JVM Advent.

View Details

You have probably already heard of serverless functions. If you’ve played around with them, it might have been on a platform like AWS Lambda, Azure Functions, Google Cloud Functions or similar. The vendors behind these platforms offer solutions specifically for Java as well. The libraries and deployment methodologies are typically proprietary though, making it hard to move functions from one platform to another. This leaves you locked-in and at the mercy of the vendor’s price hikes or potential outages or other issues.

Serverless FunctionsThe idea behind serverless functions is really cool though. You provide your code, and the cloud platform’s provided tools packages and deploys it for you. On top of that, they will automatically scale the function to the demand of the moment as well. For most offerings, you get billed only when your function is actually used. This makes it really cheap and easy to start with serverless functions. If your experiment isn’t used, well, then you don’t really pay anything.

Of course, there are some downsides to serverless functions as well. The costs sometimes go up spectacularly when your function does get used a lot. Billing is typically based on a combination of how often a function gets called, how many resources it uses, and other factors such as storage size. In addition, you’ll likely tie in other services from the cloud provider (e.g. gateways, eventing/messaging systems, etc) – and those have a cost as well.

Another downside is that most cloud providers, even though they support multiple languages including Java, require you to write, build and package your application in a specific way. Oftentimes, you’ll need to even include proprietary libraries in your application’s code. This lock-in approach is perhaps not a problem if you’re happy to stay with one provider for the entire life of your application. However if you want to have the freedom of switching to another provider, move your functions on-prem, or use multiple providers at the same time, then you’re out of luck.

Fortunately for Java developers, there are solutions to this problem. You can either use a framework that externalizes the dependencies into ‘helper’ extensions, such as Quarkus and its Funqy extensions, or you can run serverless functions on a cloud provider agnostic platform, such as Kubernetes.

Portable Java Functions with Quarkus FunqyTo use AWS Lambda without Funqy, you typically implement the lambda requestHandler and override the handleRequest method as in the following example.

package dev.kevindubois;import java.util.Map;import com.amazonaws.services.lambda.runtime.Context;import com.amazonaws.services.lambda.runtime.RequestHandler;public class MyRequestHandler implements RequestHandler<Map<String, String>, String> { @Override public String handleRequest(Map<String, String> input, Context context) { String name = input.get("name"); String message = input.get("message"); if (name == null || message == null) { return "Please input your name and a message."; } else { return String.format("Welcome to AWS Lambda, %s! %s", name, message); } }} Then you package the application in a prescribed way which again varies from cloud provider to cloud provider. If you want to use streaming, the libraries to include and dependencies on the cloud provider increase even more.

With Quarkus Funqy on the other hand the necessary libraries and prescribed implementation are abstracted away in the extension’s functionality (in this case, funqy-amazon-lambda). Instead, simply annotate the function with @Funq and the function is transformed to the targeted cloud provider’s implementation during build time. Here’s an example of a Funqy Function:

package dev.kevindubois;import io.quarkus.funqy.Funq;public class Function { @Funq public String myFunction(Map<String, String> input) { String name = input.get("name"); String message = input.get("message"); if (name == null || message == null) { return "Please input your name and a message."; } else { return String.format("Welcome to AWS Lambda, %s! %s", name, message); } }} As you can see, it’s the same functionality, but notice how there are no more import statements mentioning amazonaws libraries. In addition, we don’t need to follow the prescribed method overriding of an amazonaws provided class. This means that now if we want to target a different provider, say, Azure Functions, or Google Cloud Functions, we don’t have to do any refactoring of the code. We just swap the Funqy extension for the funqy-azure-http in our dependencies and with that we build and deploy the function to Azure Functions instead.

To be fair, Quarkus Funqy is still a bit experimental at the moment. For example, there’s a bit of inconsistency in terms of what build and deploy command to use between the different funqy extensions. Overall though it is a nice approach to creating portable functions that you can deploy to any cloud provider with minimal interventions.

Cloud Agnostic Functions with KnativeThere is another even more ‘cloud agnostic’ and open approach to working with serverless functions, and that’s to use a solution that transcends the cloud provider’s proprietary function platforms. You have likely heard of Kubernetes and maybe even used it. Kubernetes is an open source project. It allows you to run containers in a cloud environment (even on-prem). Knative is a complementary open source project that allows you to deploy serverless functions on a Kubernetes platform. It uses the same concepts of helping you to package, build and deploy workloads and autoscale them. It also potentially scales your functions to 0 when not in use.

If you have a Kubernetes cluster available, you can install Knative on top of it using these instructions. Alternatively Red Hat Developer offers it on top of their Openshift Sandbox which is free (though needs to be renewed every 30 days).

One way to use Knative is via the kn CLI tool . E.g. to create Quarkus based function:

kn func create myfunction -l quarkus Or, if you’d rather use Spring Boot:

kn func create myfunction -l springboot This command scaffolds a sample function and a func.yaml file that contains the build instructions, such as whether you want to do a Native Build, the Java version you want to use, where to find the build artifacts etc.

An example func.yaml file for Quarkus looks like this:

specVersion: 0.36.0name: myfunctionruntime: quarkuscreated: 2024-11-26T11:14:38.471661474+01:00build: buildEnvs: - name: BP\_NATIVE\_IMAGE value: "false" - name: BP\_JVM\_VERSION value: "21" - name: MAVEN\_S2I\_ARTIFACT\_DIRS value: target/quarkus-app - name: S2I\_SOURCE\_DEPLOYMENTS\_FILTER value: lib quarkus-run.jar app quarkus When you create a function with Knative and target Quarkus, you will actually create a Quarkus Funqy function. You are able to easily deploy this function to one of the proprietary FaaS providers as well. The function is in fact structured the exact same as the Function class shown above in the Quarkus Funqy section.

Here’s the simple command to deploy the function to Kubernetes with the Knative CLI:

kn deploy Furthermore, it’s also possible to generate plain old Kubernetes yaml. This allows you to automate your functions lifecycle using GitOps. You are also able to use any other methodologies you use with a Kubernetes-based platform. Your function then also easily integrates with any other service running on or around Kubernetes. Examples are observability stacks, messaging and eventing systems, etc. Knative also provides native support for the open source Cloud Events specification out of the box. It does so with its own Knative Eventing features. This opens serverless functions to a wealth of true open source, portable and enterprise ready use cases.

This article tried to give you a taste of what’s possible for serverless Java functions. It’s worth to look beyond the proprietary prescribed ways of functions-as-a-service providers. As you could see, there are ways to still leverage their offerings with Quarkus Funqy, especially if you’re interested in billing based on the number of invocations of a function. At the same time, you now have some ideas to break free from the lock-in to specific providers. You will still be able to enjoy the capabilities of serverless functions, and perhaps even go beyond proprietary offerings by using solutions like Knative.

The post Portable Serverless Functions with Java (and Quarkus) appeared first on JVM Advent.

View Details

In today’s post, let’s talk about modernization, a world so fully loaded and spread out in almost every executive presentation. Well, maybe that’s an over-exaggeration on my part, but it indeed feels like that. Like most of my peers, I have taken on the challenge of demystifying this word in the context of Java applications. Transforming, upgrading, and updating a Java application to a future state is what we will talk about today, and mostly refactoring. For example, I won’t talk about moving an application from VMs to Kubernetes or a cloud platform, which could be termed re-platforming. There are typically 4 considerations that aid a modernization journey decision.

Technical debt: An inability to change the stack and frameworks leaves organizations sticky and vulnerable to upgrade and update.

Flexibility: The Inability to make changes to the application in a timely manner. As a result, organizations are challenged to bring in new features at their own pace and will.

Security risks: In some cases, older frameworks and applications threaten business and application and IT operations, e.g., data leaks etc.

Costs: Maintaining legacy applications is expensive; vendor support costs for outdated technology are usually higher. Furthermore, skills are costly and hard to find.

Brief history of KonveyorMigrating legacy Java applications to modern frameworks like Spring Boot, Quarkus, or Micronaut can be daunting. It consumes time, is costly, and entails a business risk that needs validations and a migration process to rule it all. Konveyor, an opensource project part of the CNCF sandbox program, aims at making migrations easier and cost-effective. It provides a suite of tools that help with the migration process. e.g., migrating apps from Virtual machines to containers, Static code analysis of Java, .Net, Go applications, etc., and most recently, the introduction of Konveyor AI, a tool leveraging Gen-AI.

Code migrations and static code analysisAnalyzing the code base is one of the first steps for any migration. This can be done manually by a person going over every code segment. However, this person, often referred to as a ‘time traveler’ in the tech industry, needs to have a deep understanding of the current state of code and frameworks in use, as well as the future state. They are called ‘time travelers‘ because they have knowledge of a past distance away and a future from the current point in time, much like a time traveler in science fiction.

Enter static code analysis; it gives us insights into the current code base—the as-is state. Using Konveyor static code analysis, it is possible also to add a target framework, so a list of incidents would be reported by Konveyor if, for example, a code block needs to be migrated from Java 8 to 17, Javax to JakartaEE, EJB to REST, JMS to Reactive, etc. Konveyor uses the Language server protocol (LSP) for static code analysis.

Konveyor rules, are written in YAML. It consists of metadata, conditions, and actions. It instructs the analyzer to take specified actions when given conditions match. For example, the following rule checks for javax.ejb.Stateful annotation.

when: or: - java.referenced: location: ANNOTATION pattern: javax.ejb.Stateful More complex rules can form a ruleset. This way, frameworks, technologies, or certain domain areas can be grouped together. Following is another example of checking localhost use in code and files within a source base.

when: builtin.filecontent: filePattern: .*\.(java|properties|jsp|jspf|tag|xml|txt|yaml) pattern: http(s)?://((localhost)|(127\.0\.0\.1))+(:[0-9]+)?(/.*)? Analysis can be executed on a CLI using the Kantra binary.

konveyor-analyzer --rules rules-file.yaml ... It is also reported as an HTML and YAML file. The latter also used in the VSCode extension that reports incidents on code blocks.

Konveyor community has built about 2400 rules that are classified by different types of migrations.

Introducing LLMs to Static code analysisEarlier this year, we embarked on a journey to use the strength of Static code analysis and combine it with Large Language Models (LLM). Most of the LLM use cases have been focusing on code generation, for example, with a defined Chat UI. This is great for sparring, and perhaps with some trial and error, one learns faster, but also while making mistakes. The inherent way of Chat UI is a transactional conversation focused on solving a certain issue, where a user continuously tries to explain its context or boundaries to an LLM, so the generation is helpful. The process can be painstaking.

What if we could provide context using the static code analysis and give enough to an LLM so that it generates a more predictable code for our use case and application context?

This is precisely the journey the Konveyor community embarked on. Let’s take a look at how this is done. The tool is called KonveyorAI (Kai).

Static Code Analysis: The tool starts by running analyzer-lsp on the codebase, identifying specific migration issues based on predefined or custom rulesets.

Generate Code Suggestions: Using the static analysis data and the relevant solved examples, the LLM creates suggestions for resolving migration issues. and finally display the suggestions in IDE.

Example JavaEE to QuarkusTo build this experience, we created a demo that takes a simple JavaEE application and migrates it to Quarkus. In this demo, we go through a standard migration scenario where a coolstore app (an e-store with cool swag) that is written in JavaEE and deployed on a platform like Wildfly is migrated to Quarkus. Let’s take a look at what’s going on.

  • The first step is static code analysis with a target for JakaratEE and Quarkus.
  • Next we are moving simple javax namespaces to jakartaee. A simple use case that could likely also be done without employing an LLM in most cases.
  • Next, we will convert JMS beans to Quarkus reactive messaging. This is a big leap from the previous namespace changes. The static code analysis is able to detect that we need to make JMS changes when moving to Quarkus and the LLM integration provides a comprehensive reasoning and git patch for the changes to reactive messaging.
  • We also do the same for EJB to REST
  • Note that the application.properites also changes as the changes are made to the codebase.
  • Finally, the static code analysis also identifies which files we won’t require further.

Okay, so that’s the process that we just went through. Looks great, doesn’t it!?

Let’s dissect this flow of changes further for our understanding.

Large Language Models (LLM) have limited context size. Only a certain amount of tokens can be processed with a given request. Using static code analysis, Kai can reduce and narrow down the problem to specific code areas and generate meaningful results. Furthermore, Since Kai can re-use the static code analysis from multiple applications, the few-shot prompting technique provides the LLM additional context to generate relevant code, even when dealing with unfamiliar frameworks.

Another challenge that we face is repeatable code changes. For example, a simple pattern to change logging across the application’s entire codebase doesn’t need to be called by the user every time. What if this could be done reactively and perhaps at one time? Using inspiration from Microsoft’s CodePlan research should allow Kai to automatically propagate changes to multiple related files.

Finally, Kai also includes an agent that iteratively refines code suggestions by checking the validity of the initial output and providing feedback to the LLM. This ensures the quality of the final code solution and a way to interact with the code base reactively.

RecapKonveyor AI (Kai) simplifies modernizing legacy Java applications by integrating static code analysis with Large Language Models (LLMs). Designed to assist in complex migrations, Kai analyzes codebases using Konveyor’s Language Server Protocol (LSP)-based static analysis, identifying migration issues based on YAML-defined rulesets. These rules detect tasks such as migrating namespaces (e.g., javax to jakartaee), converting EJB to REST, or transitioning JMS to Quarkus reactive messaging.

Work on the Kai project is underway, and you are all welcome to try it out. The community is producing evaluation builds as Kai takes more shape toward a comprehensive user experience.

The post Java, migrations argh #@! and now Large Language Models appeared first on JVM Advent.

View Details

And here we are again. For the third time in a row, we are back to the Java Advent, eager to discover what’s new with WebAssembly from a Java developer perspective.

Incidentally, since, as you know, I have a favorite topic (after programming languages and compilers, of course), it is also the third time in a row I have worked at a different company.

The first time I was at Red Hat, last year I was at Tetrate, and this year I joined Dylibso. Maybe Dylibso does jingle a bell (see what I just did there? Come on, it’s Java Advent), or maybe it does not. But if you have followed this award-winning series of blog posts, you might remember Dylibso for the Extism framework. Extism is an easy-to-use cross-platform framework to write plugins for several languages using the same API across languages.

To that end, we use and contribute to quite a few runtimes; for instance, we use wazero (the runtime I have been contributing to for the last year and a half at Tetrate) for our Go SDK. Dylibso started and have been sponsoring the development of the pure-Java Chicory runtime which has recently hit its first 1.0 milestone release. We’ll talk more about that later!

Finally, we have also just released a cool new product called XTP that builds and extends Extism with type-safe bindings, codegen and a plug-in delivery system! More on that later too!

Is This About Front-End?Well, yes, and no. Have you been living under a rock or is this your first Java Advent? Just kidding.

Dude, seriously?

Sure, Wasm was originally created to bring a safe, sandboxed execution environment to the browser, leveraging the existing JS runtime. But the Wasm spec is relatively smaller than the bulk of a full JS implementation, and, as such, a constellation of pure-Wasm runtimes was born: these smaller, lighter-weight runtimes can be used as stand-alone language VMs (similarly to a JVM), or as embededded language hosts, as you would usually do for a scripting language, except that they are language-agnostic.

In fact, Wasm is a language-agnostic compilation target and any compiler is free to generate it: that makes it a great binary format to distribute cross-platform software extensions (i.e. plug-ins) that can be hosted in any “host” application.

In the last few years, we have seen the CNCF cloud-native landscape extend, and promote this new technology, to the point that Wasm itself has now its own section in the CNCF software landscape. Wasm is now for all intents and purposes considered one of the CNCF official technologies for “Cloud-Native development”: this extends well-beyond the browser!

But how does this affect you, as a Java developer? Similarly to the other years, in this blog post, I will explore two main topics:

  • front-end development, compiling a JVM language into Wasm
  • back-end development running Wasm on top of a JVM

The Front-EndWasm was always meant to make the Web platform more efficient. That’s one reason why browsers and state-of-the-art JavaScript runtimes such as V8, JavaScriptCore and Spidermonkey, will probably always be at the forefront, when it comes to implementing practical, but bleeding-edge features. After all, for better or worse, that’s how Web development goes.

This is also why often these runtimes adopt experimental features of the Wasm spec, way before other, smaller runtimes. This is expected, considering also the workforce that is often beyond major web browsers.

Last year we explained how Wasm behaves more like a native target, than a JVM. This is, to some extent, still true: most WebAssembly runtimes do not support much beyond the so-called “linear memory”, threading support is still hit and miss, and the exception handling spec was revised a few times, finally hitting the last stage at the end of last year.

However, today all major browsers now support garbage-collected references, exception handling, and, through some shims, even threaded execution. All of these features are particularly important when it comes to supporting higher-level, managed, garbage-collected languages such as those hosted on the Java platform.

It is indeed feasible to support these features even in a native-like compilation target: after the GraalVM Native Image builder does just that. And Go is a higher-level, managed, garbage-collected, multi-threaded language and both TinyGo and “Big” Go can now be compiled to Wasm that runs perfectly in self-contained Wasm runtimes such as Wasmtime, wazero or chicory.

However, this comes with the cost of inflating the resulting binary, with baggage such as a garbage collector, emulating exception handlers, emulating multi-threading.

I believe this situation will continue for a while; that is, smaller runtimes, especially if they want to keep their footprint small, and especially if their development team is small, will generally be very conservative when it comes to implementing new, experimental part of the specs.

But it’s good that browsers are the front-runners: they have better resourcing, and hence can afford to support these experiments earlier. And besides, it makes sense for the Web to try to keep binary sizes down. For other use cases, we will see, this is less of a concern.

In the following I will show 3 compiler toolchains that generate Wasm, but still mainly target the browser.

Feeling fatigued already? We’re just getting started!

KotlinPic related: A Kotlin jar

Kotlin was one of the first JVM languages that targeted the browser with their JavaScript backend. Over the years, they gained a native compiler, and also, starting off as a flavor of their native compiler, a Wasm compiler.

The Kotlin team has recently switched their Wasm compiler backend to WasmGC and the Exception Handling proposal, resulting in smaller file sizes. However, this required a major overhaul of the compiler architecture, because a Wasm binary that targets the linear memory behaves more like a native target, while targeting WasmGC is a bit closer to how JVM class files are represented.

Luckily, with their familiarity with targeting both the JVM and JavaScript, even though it was no small feat, we now have a functional Kotlin compiler for Wasm that works for on all runtimes supporting the GC and Exception Handling specs.

To get started, you can head over to this Kotlin Wasm template.

I believe that the Kotlin toolchain is currently the most impressive and advanced when it comes to practical use cases for JVM language: the teams at Jetbrains have already ported Compose Multiplatform to it. This that, you can already today port complex applications that will run both on mobile platforms and in the browser, with the convenience of using one modern, familiar programming language!

Kudos to the team!

ScalaScala was a pioneer in the JVM language ecosystem for many reasons; it helped revive the attention to the Java platform with a new, concise and practical programming language, in a time where the Java language was still very conservative.

It is also one of the first JVM languages to gain support for a JavaScript compiler.

End users have been asking for a Wasm backend for a while. But the JavaScript backend was pretty practical and battle-tested; so the team deferred this sizable effort until there was reasonable support for some key features, such as, for instance (again!) garbage collected objects.

In the meantime, the alternative Scala Native backend initially experimented with it. Now, the Scala.js team has decided to finally take on this effort, as the feature gap in browsers is closing more and more.

As documented on the Scala.js website the Wasm backend is still in its infancy, and it comes with some experimental requirements (including garbage collected references, and exception handling support). It is also primarily targeting browsers, and as such, it still emits some chunks of JavaScript.

For some examples, check out keynmol/scalajs-wasm-game-of-life and https://github.com/sjrd/funlabyrinthe-scala.

JavaI mentioned TeaVM multiple times in the past. TeaVM has recently switched their backend to WasmGC.

The J2CL project is the successor to GWT. This project is extensively used at Google, to the point that you are probably already using it even if you don’t realize it. For instance, Google Sheets is using Wasm. Check out WebAssembly at Google by Thomas Steiner & Thomas Nattestadt. And since you are there, you might want to check out Thomas Steiner’s podcast, WasmAssembly there is also an episode interviewing our very own Steve, talking about all things Extism, including Chicory!

Read more on how to use J2CL with Wasm here.

GraalVM Native ImageAll the languages we have mentioned earlier, have something in common: they all generate Wasm code starting from source code. This means that the Kotlin compiler compiles Kotlin code, the Scala compiler compiles Scala code, and the TeaVM and J2CL compilers compile Java code. “Well, duh”, I hear you say.

The brand new GraalVM Mascot

On the other hand, GraalVM native image builds a binary starting from class files, making it possible to generate binaries for any language that typically runs on a JVM. So, considering the history of Wasm compilers so far, it would be really cool if they also started to build upon the experience they made with the Native Image builder, and implement a new backend for WebAssembly.

Oh boy, do I have news for you! It turns out that the GraalVM team is in fact working on a Wasm backend that will target browsers and state-of-the-art JavaScript runtimes such as V8 (at least at the beginning).

The work is being actively tracked on GitHub. I had the chance to take a look at an early demo, where a fully functional javac ran in the browser. The compiler is targeting WASI too, so, you should also be able to run it with Node, and, when more will catch up, with all the other self-contained Wasm runtimes.

There is some work to do but the direction is extremely exciting!

Honorable MentionsI am always fascinated with the excellent work at LeaningTech, so, even though it’s kind of a different stack, I want to mention the herculean work that this team is doing in porting code to the browser. Besides their very recent relaunch of their x86 in-browser VM, this time including a GUI, they also provide commercial support to compiling C++ and Flash to Wasm.

I believe one of the crown jewels is their Java runtime stack called CheerpJ, which they also recently relaunched. It is a sophisticated port of a proper OpenJDK to Wasm that JIT-compiles bytecode into Wasm for optimal performance, and contained code size. They can also run Java Applets and JNLP without a Java runtime installed on the desktop. That’s pretty neat!

BackendWasm in the browsers is definitely here to stay, but the thing about this ecosystem that interests me the most is Wasm workloads in the backend. Wasm is on the ThoughtWorks 2024 Tech Radar

This year a lot has happened there too. But how does it impact JVM developers?

Last year, a surprising Christmas gift for all Java developers was the first global official announcement of the Chicory interpreter. While at the time the project was in an early development stage, it was already quite capable! Last October we announced our first milestone release, including, next to the interpreter, a new experimental bytecode translator (the “AoT compiler”), that turns Wasm bytecode into JVM Wasm for improved performance. Check it out and let us all know!

The GraalVM team also recently announced that their Wasm support can be now considered stable and ready for production, with the benefit of all their past and present experience with developing languages for the JVM.

This means that it’s a great time to get started with Wasm support on the JVM. But what are the best use cases you can cover with a Wasm runtime that runs on a JVM? Isn’t this redundant? Why would you want to load a foreign bytecode format (Wasm) on a bytecode language runtime (i.e., your beloved JVM)?

There are many use cases, but I want to start from possibly the most niche and counterintuitive, because it’s cursed, and it feels wrong, and yet sometimes it makes so much sense.

Native Libraries Without JNIDid you know that “JNI” is supposed to be read as “Genie”? No? That’s because I just made that up (image by Natasha G from Pixabay)

“LOL look at this one,”JNI”: we’ve got Panama, now, you boomer!” Right. Of course, Panama will finally bring a saner developer experience to interfacing with a native library.

However, you still have to deal with all of the limitations of interfacing with native code; essentially,

  • the garbage collector and the threads (virtual and native!) may not play along well with it;
  • plus, a memory corruption bug in your native code could tear down your entire JVM.
  • finally, your build is now tied to the specific CPU architectures that your native library provides support for (this may not depend on you!)

Now, it would be most excellent if you could just bring a native library to JVM land, and be done with it. Some projects attempted this before:

  • NestedVM was a wonderfully cursed project that translated binaries for the MIPS architecture to Java bytecode. Unfortunately it hasn’t released a new version since 2009.
  • GraalVM’s Sulong is a high-performance LLVM bitcode runtime; i.e. it evaluates LLVM’s intermediate binary representation. One downside is that LLVM bitcode is known to be unstable.

The key difference is that, this time

  1. 1a lot of compilers are including first-party support for Wasm, so it’s easier to cross-compile to this target and check whether it’s working.
  2. the Web itself is huge, and a lot of developers are interested in porting well-written, performance-conscious libraries to use in their Web application

As a consequence, it’s becoming easier to find a port of a traditionally “native” library to Wasm, and if that port exists, it is (in my limited experience with ancient code) safe to assume that the binary will run.

So how would this help you? Granted, this is a niche use case, but there are situations where the one library you need is written in C or Rust, and you just want to be able to use that bit of functionality; maybe you just want to bootstrap your project (then in the future you will rewrite it), or maybe it’s just enough for your use case.

One example is JRuby’s team port of the Prism Ruby parser, where they successfully used Chicory as a fallback on platforms where the native binary may not be available.

There are many other examples in the Go space, because the wazero project is much more mature than Chicory, and the Go ecosystem has already employed it successfully in many cases: in fact the Go runtime has a surprising number of similarities when it comes to limitations with interfacing with native code. My favorite must be Xe Iaso’s “Carcinization of Go Programs”, because everyone should run a Rust library from a Go program to parse Mastodon’s toots. But there is also the large collection of wasilibs, native tools re-built on top of wazero (including things like several plugins for protoc).

Another great example is Nuno Cruces’ Go SQLite Wasm port, built on top of wazero, is competitive in many ways with other alternatives. Indeed, there is also an experimental SQLite port of SQLite to Chicory called sqlite-zero.

The GraalVM team has also shared on their brand-new GraalWasm landing page an example of embedding C code running as a Wasm binary.

And, of course, you can run Doom in Swing using both GraalWasm and Chicory.

Bringing Computations In-CoreWasm has caught the attention of many as a compact alternative to containers.

Indeed, serverless workloads are an excellent candidate for Wasm: you get a cross-platform, efficient, executable binary representation that can be compiled into fast native code. And, of course, the grumpy Java developer in you might mumble something about application servers, but hopefully we have already addressed that Wasm is not just “exactly the same” as Java Bytecode. Companies such as Fermyon and Cosmonic are building software platforms of this kind. CDN companies such as Fastly or Cloudflare are putting Wasm-computations at the edge.

But besides serverless containers, there is another space where cloud deployments could be replaced by Wasm “functions”. This is when a service plays the role of a logic “extension” to another service.

In this sense, such a service, be it a sidecar, be it a Webhook (or whatever fancy words you kids call them these days) can be often seen as a glorified “plug-in”.

Shopify with their “functions” were among the first mainstream platforms to adopt Wasm to that end.

There are a few reasons why Wasm is a good candidate for plugin embedding. First of all, it was explicitly designed to work in concert with JavaScript, which, nowadays, is the “extension language” for excellence! It was designed with the necessities of the web platform in mind: namely, security and sandboxing; in fact, it cannot access any feature of the host language without being explicitly given access to them (through what are called “host functions”). And obviously being cross-platform, efficient and compact.

Dylibso believes that all software should be “squishy”: that is, malleable, adaptable, and extensible; so they developed Extism, an open-source framework to build your own “function-as-a-service” layer inside your application. Or, to put it more simply, to easily embed your own plug-in system. At the time of writing Extism supports 16 “host” runtime platforms and over 10 “guest” languages to write your plugin (and counting!). Notably, for the Go host, Extism uses wazero, and for the Java platform, we are finalizing our Chicory SDK (Java is already supported through a native extension though).

Dylibso has also recently released an integrated platform for plugin delivery and management called XTP, building on top of Extism and OpenAPI. Essentially, if you have an API manifest for your service, you should feel at home bringing it to Extism. The codegen and testing toolset is all available free of charge. For instance, we have an example of how to port the API for the Twenty CRM and scaffold your own plug-in system and another example of how to write a programmable Discord bot.

What makes Wasm interesting as a JDK extension language is that it supports multiple languages, and the guest can be preemptively terminated, preventing it from hogging the CPU. Moreover, user-provided Wasm code can be safely loaded and unloaded in a controlled environment, making it an excellent choice for hosting user-defined functions in a database.

The GraalVM team has also released a set of compelling use cases for GraalWasm, including integrations with Micronaut and Spring Boot!

More Complex Use CasesI have recently written a few use cases for Extism, Chicory and XTP. Both the following examples support compilation into a native binary, while retaining dynamic code loading capabilities.

Kafka Data TransformsI have recently written an article on how to plug Chicory, Extism and XTP inside a native Quarkus application to build a Kafka data transform service. In this article I am giving a detailed explanation of how to use Extism, the XTP codegen tooling, and the XTP service to manage a data transform pipeline. Sounds intriguing? Check it out

There is also a follow-up in the pipeline, where I’ll explore extending the Kafka broker to the same effect. Stay tuned!

Quarkus Wasm Extension DemoIn the Quarkus Wasm Extension demo, I have implemented a middleware-style HTTP filter that you would usually run in a proxy or an API gateway like Envoy or NGINX using some API like Proxy-Wasm. Through the Quarkus extension and Chicory, you can compile the executable to a native image and load and reload extensions dynamically.

Essentially, a proxy such as Envoy implements a chain of HTTP request filters, it intercepts your requests, and then passes them through the filter. However, this requires you to deploy a fleet of proxies as sidecars alongside your application. On the one hand, makes it easier to manage policies from a central control plane; on the other hand, it might raise operational costs, because now you have even more services to deal with! Instead, you can turn some of these policies into interceptors inside your Quarkus application!

By cutting on the middleman, you are turning a complex multi-container deployment into a single application deployment! Pair that with XTP, and then you also get the missing control plane for your plugin policies!

Native DYNAMIC Software ExtensionsDid I just mention dynamic code loading with Native Image? Indeed, I just did. One of the original limitations for an executable built into a native image, was the infamous “closed world assumption”, essentially, you have to instruct the native image builder about all the classes you expect to need at run-time, especially if you load them dynamically through run-time code reflection.

Limitations with reflections are however nowadays largely solved, because a lot more tooling is available to automatically inspect your record and report it to the compiler; there are embedded JSON manifests as well as automatic detection mechanisms; besides frameworks like Quarkus provide also sophisticated code-generation mechanisms that extend GraalVM’s built-in routines.

There have been also ways to dynamically load code: you can link against native libraries or you can use the Espresso Truffle runtime.

Espresso is particularly interesting because it’s another example of a VM-in-a-VM: it’s literally an implementation of the Java VM Specification that runs on top of another JVM. While, at a very first glance, this could feel like an intellectual experiment (you could probably run an Espresso instance on top of another Espresso instance, and then… it’s Espresso all the way down), it makes a lot of sense if you build one inside a native image! Then you can have your fast-to-boot, tiny, efficient, self-contained executable, and yet be able to load Java code in a sandboxed environment! You can literally have your cake and eat it too.

Now, why would you want to use Wasm instead? Well, because, why limit yourself to JVM languages? A Wasm runtime will allow you to dynamically load any Wasm code from your users!

If you are interested in this use case, read the details in the Extism+XTP Kafka demo, and the Quarkus Wasm Extension Demo.

ConclusionsIt’s been a wonderful year for Wasm in the Java space. I hope this article sparked your interest too!

If you want to learn more about Extism and XTP, you can join us on our Discord, where we host weekly office hours on Wednesdays: ask all your questions about Wasm, Extism, XTP.

If you are especially interested in Chicory, you can also join our Zulip; and if you are the odd Gopher reading a Java blog, you might want to join the #wazero channel too.

Finally, if Extism and XTP sparked your interest and you’d like to have some fun with extending Claude.ai (and other LLMs supporting the Model Context Protocol) you might want to check out our brand-new project: mcp.run.

The post Wasm 4 the Java Geek 3: Electric Boogaloo appeared first on JVM Advent.

View Details

There are many cool apps you can build with Java, and GraalVM can make them even better — faster, smaller, more secure. In the recent years GraalVM and Native Image have gained significant traction in the Java ecosystem, so now building applications with it is easier than ever.

There are applications for which GraalVM is well known and widespread, such as microservices, cloud deployments, CLI, but you can do so much more. Read this article to find out what exactly!

What is GraalVM and why GraalVM?GraalVM is many things; here is your 1-minute refresher about GraalVM, just so we are on the same page.

GraalVM is a JDK, like the other JDKs you might know, but with unique features and capabilities. One of such unique capabilities is ahead-of-time compilation of applications with Native Image.

Native Image can compile your application into a native executable with the following advantages:

  • Fast startup and instant peak performance, as native executables don’t need to warm up
  • Low memory footprint, as no memory is used to profile and compile code at runtime
  • Throughput on par with the JVM
  • Compact packaging by including only reachable code
  • Additional security, by eliminating unused code and reducing the attack surface

Now, let’s see how we can build applications with it!

A blazing fast web server If there’s one thing you’ve heard about GraalVM, it’s probably that it gives Java applications instant startup. Native Image lets you move all the overhead work of loading, analyzing, profiling, and compiling your code to build time so your applications start instantaneously.

As an example, let’s look at a base Micronaut web app. By compiling the app with mvn package -Dpackaging=native-image, we get the following:

➜ native-micronaut-web ./target/webserver \_\_ \_\_ \_ \_ | \/ (\_) \_\_\_ \_ \_\_ \_\_\_ \_ \_\_ \_\_ \_ \_ \_| |\_ | |\/| | |/ \_\_| '\_\_/ \_ \| '\_ \ / \_` | | | | \_\_|| | | | | (\_\_| | | (\_) | | | | (\_| | |\_| | |\_ |\_| |\_|\_|\\_\_\_|\_| \\_\_\_/|\_| |\_|\\_\_,\_|\\_\_,\_|\\_\_|10:42:38.556 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 33ms. Server Running: http://localhost:8080 On my rather average Linux instance that I use for demo purposes, the application starts in 33 ms. On a more powerful machine, this time can go even below 20 ms! Not only this is a functional Java web server, it’s framework-based, meaning that you can easily extend it with various Micronaut modules and dependencies (they have an excellent Testocontainers implementation)!

Not only our application is fast, it’s also very efficient in terms of memory usage. Even under a load of executing 500000 requests via hey, max RSS stays at around 150 MB with no tuning and profiling, and can be cut down further by limiting Xmx.

CLI app What qualities would you like to see in CLI apps? Small, fast, responsive. Sounds familiar, right? Those are exactly the benefits that Native Image can give you. So it’s not surprising that in Thomas Vitale’s recent poll about CLI tools GraalVM was one of the favorites

Thanks everyone for sharing your experience building CLI applications with #Java keep sharing Picocli, Spring Shell, or Jcommander for building CLIs @graalvm.org for native executables Nobody mentioned it yet, but I find @jreleaser.org key to manage releases of your CLI tools

— Thomas Vitale (@thomasvitale.com) 2024-11-28T15:11:43.413Z

When talking about CLI apps and GraalVM, I want to start with an honorable mention of picocli. picocli is one of community favorites for building powerful, user-friendly, and GraalVM-enabled command line apps. It’s a great tool with many rich features — give it a try!

Additionally, many frameworks offer their own CLI modules. As an example, let’s look at Spring Shell. It’s intended to help Spring users easily build their custom CLI tools, with several default built-in commands.

Let’s take Spring Shell for a spin by implementing a cute version of ls. Our application is rather straightforward, most of its logic is implemented in the ls method, that goes through the current directory listing files & directories in different formats. Here it is running as a native executable:

➜ native-spring-shell git:(main) ✗ ./target/native-spring-shell ⢠⣤⣀⠀⠀⠀⠀⠀⠀⠀⢰⢦⡀⠀⢰⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣎⠙⠲⢤⣀⠀⠀⠀⡌⠈⣗⣄⡌⠘⣷⣀⣠⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢦⡀⠀⠈⠙⠲⢄⣣⡖⣯⡷⣧⠀⢸⢿⣦⡀⠈⠳⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠳⡄⠀⠀⠀⠈⣿⣼⣯⣄⣿⣦⣸⢸⣏⠻⣤⣀⣈⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⢤⠤⠴⠻⣯⠈⠋⠙⠃⠀⠈⣿⠻⣾⡿⠛⠁⠉⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠋⠀⠀⠀⠀⣠⣴⣄⠀⠀⠀⠈⠷⣤⡛⠛⢿⣿⡟⢹⡄⠀⠀⠀⠀⠀⠀⢀⡜⠁⠀⠀⠀⠀⠀⢻⠿⠿⠀⠀⠀⠀⠀⠈⢻⡙⢹⡏⠿⣆⣹⠀⠀⠀⠀⠀⢀⡞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠾⢷⡞⠁⣀⠙⢿⠂⠀⠀⠀⢀⢾⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢷⡀⣿⠀⢸⡄⠀⠀⠀⡞⠘⢧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡾⣷⣾⠇⠀⠀⢸⠈⠿⠀⠙⠲⣄⠀⠀⢀⣀⣀⣀⣀⣠⠀⠀⠀⠀⠀⠀⠀⠀⣸⠇⢀⣼⠀⠀⠀⠀⠳⣄⠀⠀⠀⣘⠷⠊⠉⠀⠀⠀⠀⠀⢃⠀⠀⠀⠀⠀⢴⣶⠿⠖⠛⣿⡄⠀⠀⠀⠀⠀⠉⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠘⣄⠀⢀⣠⠖⠋⠀⠀⠀⠀⠀⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀2024-12-04T15:52:29.523Z INFO 44103 --- [native-spring-shell] [ main] c.e.demo.NativeSpringShellApplication : Starting AOT-processed NativeSpringShellApplication using Java 23 with PID 44103 (/home/opc/demo-central/native-spring-shell/target/native-spring-shell started by opc in /home/opc/demo-central/native-spring-shell)2024-12-04T15:52:29.523Z INFO 44103 --- [native-spring-shell] [ main] c.e.demo.NativeSpringShellApplication : No active profile set, falling back to 1 default profile: "default"2024-12-04T15:52:29.557Z INFO 44103 --- [native-spring-shell] [ main] c.e.demo.NativeSpringShellApplication : Started NativeSpringShellApplication in 0.042 seconds (process running for 0.045)shell:>ls.git/.gitignore.mvn/LICENSEREADME.mdmvnwmvnw.cmdpom.xmlsrc/target/shell:>helpAVAILABLE COMMANDSBuilt-In Commandshelp: Display help about available commandsstacktrace: Display the full stacktrace of the last error.clear: Clear the shell screen.quit, exit: Exit the shell.history: Display or save the history of previously run commandsversion: Show version infoscript: Read and execute commands from a file.Cute LSls: Lists files in the specified directory, or in the current directory as default.shell:>history[ls, help, history]shell:> help reveals available commands — ls implemented by us, and several convenient commands courtesy of Spring Shell.

A look at our pom.xml reveals another interesting detail — the -Os configuration flag that stands for “optimize for size”. It’s a new flag introduced in GraalVM for JDK 23, that optimizes native executables for size, that can be particularly interesting for CLIs and other apps meant for distribution. It quite impactful — it can help you decrease the image size by about 30%.

By the way did you know you that in Spring Boot you can have your own custom banners printed upon startup just buy placing them as a txt under main/resources?

LLM Inference Engine You can build a complete LLM inference engine in Java and GraalVM! No C, no Python, no dependencies, no calls to cloud-based LLMs — a complete inference engine, which along with a model gives you a full-blown local LLM assistant. How cool is that!

This is one of my favorite projects this year, built by my brilliant colleague Alfonso² Peterssen — llama3.java. It contains all the inference components, such as tokenizer, GGFUF file parser, sampler, and more, in a one fairly short (2K LOC) Java file. There are no external dependencies — all the inference logic is implemented in Java, leveraging the latest APIs, such as the Foreign Function and Memory API for interoperability between Java code and native code, and the Vector API for fast vector computations. I believe this project is also a great showcase of how powerful and versatile the Java platform is!

Now what’s cool, GraalVM makes this project even better! Even though our Vector API implementation is a work in progress, it’s already blazing fast — on average, applications using Vector API are ~10% faster on Oraсle GraalVM JIT! Better yet, our LLM inference application is fully Native Image-compatible out of the box!

You can give a try yourself following the link above. Here’s what I get upon running a compiled application:

➜ llama3.java git:(main) ✗ ./llama3 --model Llama-3.2-1B-Instruct-Q8\_0.gguf --chatParse Llama-3.2-1B-Instruct-Q8\_0.gguf: 1179 millisLoad LlaMa model: 1397 millis> helloHello! How can I assist you today?34.49 tokens/s (21)> what can you do?I can do a wide range of things! Here are some examples:**Conversations*** Answer questions on various topics, from science and history to entertainment and culture* Provide definitions and explanations for words and phrases* Engage in simple conversations and chat about your day* Discuss current events and news**Writing and Language*** Generate text on a given topic or topic ideas* Help with writing tasks, such as proofreading and editing* Provide grammar and spelling suggestions The speed of the engine is expressed in tokens/s, and what we see here, 34.49 tokens/s on again rather mid-range machine with no GPU, is really impressive. For reference, this is way above normal human reading speed, and on par or even slightly faster than llama.cpp

Now here comes a truly fascinating part. Native Image offers a unique combination of Java’s programming model and AOT optimizations of native programs. Therefore we can preload the model’s metadata at the build time and avoid parsing the model metadata at runtime, completely eliminating any startup overhead — and this kind of optimization is possible only in Java and Native Image:

➜ llama3.java git:(main) ✗ ./llama3 --model Llama-3.2-1B-Instruct-Q8\_0.gguf --chatLoad tensors from pre-loaded model: 0 millis> helloHello! How can I assist you today?37.41 tokens/s (21)> give me a nice Christmas greeting as a poemHere's a Christmas poem for you:'Tis the season of joy and cheer,A time for love and laughter clear.The tree is lit, the stockings too,A festive atmosphere, for me and you.The fireplace crackles, warm and bright,As snowflakes fall gently through the night.The room is filled with scents so sweet,Of pine and cinnamon, a treat to greet. The startup went down to 0ms!

Give llama3.java a try (also watch our Devoxx talk!), I truly believe that this is the most convenient (and fun!) way to have a local LLM-assistant implemented in Java.

Extra-secure application We see more and more attention to Native Image because of its additional security layer. Why are natively compiled applications more secure?

  • Reduced attack surface area due to dead code removal — unused classes, methods, and fields not included in a native executable.
  • Not vulnerable to deserialization attacks via class loading — executables include only required and specified classes.
  • Not vulnerable to JIT compiler attacks, as all code is AOT compiled.

Additionally, Native Image includes support for software bill of materials (SBOM). The SBOM file can be either embedded in the executable, or made available as a classpath resource, and can be used to analyze the components of your application and scan for vulnerabilities. It’s also integrated with several tools and frameworks. For example, starting with Spring Boot 3.4 all you need to do is to build your application with the --enable-sbom=classpath flag, and Spring Boot will automatically pick it up and expose in Actuator:

SBOM support in GraalVM Native Image

See the complete example here: github.com/alina-yur/native-spring-boot-sbom.

Green application Another aspect of Native Image where we see increasing interest is resource savings thanks to AOT compilation and optimizations. This applies to memory and CPU, but also to energy consumption . As an example, we measured energy consumption of Spring PetClinic running on JIT and on Native Image in several scenarios with increasing load:

  • Scenario A: 1 curl request, 1s after curl response
  • Scenario B: 1 curl request, 10s total runtime
  • Scenario C: 4000 requests/s, 20 seconds
  • Scenario D: 4000 requests/s, 100 seconds

Energy consumption of Spring PetClinic, JIT vs AOT

As we see, the natively compiled version of the application consistently consumes less energy, even under constant load. Our findings are also aligned with a community study performed by Ionut Balosin.

As Josh Long says, save the planet and the turtles, use GraalVM!

Bonus applications This article is getting out of hand, but there are a few more fun applications that you can build with GraalVM that also deserve honorable mentions. So here’s a lightning round:

  • Compact containers: for the smallest possible container images, you can statically link your application and deploy it on a scratch container. You can achieve a size of only 13.8 MB for the JDK’s Simple Web Server, and 1.3MB for a compressed helloworld application! See how.
  • Embedded Python: You probably know that with GraalPy you can embed Python in Java, but did you know that you can also compile such application with Native Image? As an example, you can use a Python library Pygal to produce SVG charts from your Java application, and better yet, it works with Java frameworks, such as Spring Boot and Micronaut!
  • Monitoring: Native Image supports several Java monitoring tools and protocols, such as jfr, jvmstat, jmx, and others, including more specific vendor tools. I do want to also mention Micrometer, that enables passing observability data to multiple platforms & backends, and works like a charm with Native Image:

Observability of native applications via Micrometer

ConclusionI hope this article was useful for you, and inspired you to create something similar (or completely different!) with GraalVM. With all the ecosystem love and support, and the optimizations and features brought by our team, there has never been a better time to build applications with GraalVM.

Let us know what you build, and have great holidays!

The post 5 cool applications you can build with Java and GraalVM appeared first on JVM Advent.

View Details

Have you ever stumbled upon a concept that completely changed how you think about building software? For me, that moment came when our architect introduced the Event Sourcing pattern a few years ago. Like many junior developers, I started my career working on traditional CRUD applications. They were simple, efficient, and worked well—until we really understood our domain.

As system grew more complex due to complex business flows, we found struggling with basic questions:

  • How do we trace the root cause of a bug months after it occurred and after tens of versions were deployed?
  • How can we rebuild a user’s journey when all we have are their current state in the database and a lot of logs?
  • What data should be collected for any future use cases ? Is our architecture flexible enough for the unknown?

The main purpose of my article is to present the foundational concepts with pure Java code examples.

If there’s one thing I’ve learned over the years, it’s that this pattern is highly subjective in its implementation, and there’s no perfect way to do things—any cake recipe is good as long as you and your family enjoy eating it.

PS: I’ll do my best to show you that the following metaphor shouldn’t hold true: “Software ages like milk, not wine“.

To be honest, in the first place, this pattern may require additional related concepts that complement each other very well, especially for complex or large systems. Domain Driven Design (DDD) and Command Query Responsibility Segregation (CQRS) provide significant benefits when used together with Event Sourcing (ES), but they are not mandatory . Each one has their proven benefits, but the complexity added to the system, pff … can easily become a nightmare, so please be aware and think more carefully before applying any.

“If the only tool you have is a hammer , you tend to see every problem as a nail.” (Abraham Maslow)

DEFINITIONEvent sourcing is an architectural pattern where state transitions of a system are stored into an immutable append-only storage as a sequence of immutable events into an immutable append-only storage, instead of just saving the latest state.

These events represent facts that have occurred in the past, and by replaying them in order, you can reconstruct the system’s state at any point in time.

Events timeline

Event StormingGiven the fact that the only single source of truth is in the events stream we need to do a small session of brainstorming but with events, so called Event Storming.

The first step in running an event storming workshop is to invite the right stakeholders and bring them together (I prefer a physical room for high concentration, not a virtual one). It’s a place where technicalities are left at the door—no Java, no Spring Boot, just business. I get it; you might think it’s just another boring meeting, but trust me, it’s the safest playground before writing any piece of code. Try to ask as many questions as possible (even the dumbest ones, because you don’t fully understand the domain yet), you’ll thank yourself later.

The rules of the game are simple, we need coloured sticky notes:

  • Orange – for domain events. Things that happen in the system written in the past tense (e.g. Application Created)
  • Blue – for commands. Actions or requests initiated by a user or system that trigger domain events. They represent the intent to change a state (e.g. Create Application)
  • Pink – for actors. The users or systems that interact with the domain, initiating commands or reacting to events. (e.g. Student)
  • Purple – for policies. Business rules that automatically trigger commands in response to events. (e.g. Send Confirmation Email)
  • Green – for projections (read models). Business oriented and optimised query model. (e.g. List of Draft Applications).
  • Yellow – for aggregates. Core business entities that manage the consistency of the domain model. They are responsible for handling commands and emitting domain events (e.g. Application)

For our remote setup we’ll use one great tool for this called Miro. We’ve been using it since the beginning of our journey, and it has never let us down, so I highly recommend it.

Event Storming terminology

Firstly, each participant must add events to the board in any order. Then, we will remove duplicates and arrange them chronologically on the timeline — unfortunately, I don’t have sufficient space to replicate the timeline in a horizontal format.

Secondly, participants must enhance the events with corresponding commands, actors, policies and views. After all puzzle pieces are in place we must align them accordingly to consistency requirements and aggregates will be evident enough.

In DDD, an aggregate is the core lifecycle pattern treated as a single unit which enforces invariance and transactional boundaries to maintain data consistency within.

In ES, the transactional boundary is redefined within the stream of events – Sara Pellegrini wrote in her gold series about this (https://sara.event-thinking.io/2023/04/kill-aggregate-chapter-1-I-am-here-to-kill-the-aggregate.html).


Our journey begins with a real-life scenario that deviates from the usual examples (e.g., bank accounts or eCommerce shopping carts), drawing inspiration from the Spring demo project, PetClinic.

Let’s begin by defining some fundamental events.

Events

Due to space and time constraints, I will not cover here all use cases but will focus solely on the business flows related to the Pet.**

Pet domain

To sum up the board results, the command side is composed by 5 actions, all performed by the same actor:

  • Register pet – Vet creates a new pet in the clinic.
  • Update details pet – Vet updates pet details.
  • Add medical entry – Vet adds new medical entry to pet’s medical log.
  • Transfer pet – Vet changes the pet’s owner.
  • Remove pet – Vet remove the pet from the clinic because the owner changed the responsible vet or the pet is dead .

A single policy exists in our scenario for sending a notification to the new owner to inform about the ownership change.

For the read side, we currently have only 2 views:

  • Pets – Displays all pets in the clinic along with their information. This view is useful for tables, pet detail pages and similar use cases.
  • Sick pets – Shows all active pets with ongoing medical entries, which is required for the home page dashboard.

The key advantage of this approach is its flexibility—additional views can be constructed even from the existing events, allowing product owners to be highly creative in designing new features.

I’ve tried to simplify as much as I could the story and I hope you got the point.

Earlier this year, I came across the Event Modeling approach proposed by Adam Dymitruk, which feels like a natural evolution of Event Storming. While I haven’t had hands-on experience with this method yet, I’m eager to dive in, explore, and put it into practice.

Event sTOREWhat is it ? It’s a specialised database designed to store and retrieve events which are immutable records of changes that have occurred in the system.

Another database ?

Wait, wait, I can try to explain!

Why do we need it? Because we must store the source of truth in a highly available and performant storage with several essential features:

  • Append-only storage – events are written once and never modified or deleted.
  • Event streams – supports logical groupings of events into streams, preserving their order (e.g. a partition in a Kafka topic).
  • Read stream events – allows reading stream events in the same order they were written.
  • Consistency – ensures that two events with the same sequence number cannot exist in the same stream (this represents the C from ACID).

As you dive deeper into the topic you’ll discover additional specific requirements, such as appending multiple events at once, reading only committed events, reading streams from a specific offset, streaming data via subscriptions, building snapshots, and much more.

Currently, Axon Server is the leading choice in the Java ecosystem, while EventStoreDB dominates in the .NET ecosystem.

In 2024 the SQL winner among developers is PostgreSQL, so why not use it for event sourcing as well? You might think a single table partitioned by stream identifier would be sufficient.

Yes, you can do that, but I wouldn’t recommend it. I’ve tried it by myself (sadly, still here ), and it works—until you realise you’re essentially re-creating the same features that dedicated event store providers offer by default. Many frameworks support SQL databases for event storage, but that doesn’t mean it’s the right choice in production.


Enough theory, let’s turn on the JVM!

SHOW ME THE CODETo better understand event sourcing, I decided to build a basic custom infrastructure without using frameworks or libraries. This hands-on approach allowed me to focus on the core concepts.

The foundation of event sourcing lies in events, making their design the natural starting point. I initially implemented a basic interface but found it lacked sufficient compiler support for enforcing validations and raising errors if an event was not properly handled. To address this, I transitioned to a more robust solution using sealed interfaces, introduced in Java 17.

Immutability is a key requirement for events, making records the natural choice.

Applying the theory to our example, each PetEvent implements the sealed interface PetEvent, which enforces a restricted set of allowed events. Adding a new event involves two steps:

  1. Define the event as a record with the necessary business fields (e.g., PetRegistered)
  2. Include it in the sealed interface’s list of permitted implementations.

public sealed interface PetEvent permits PetEvent.PetRegistered, PetEvent.PetDetailsUpdated, PetEvent.PetOwnerTransferred, PetEvent.PetDeactivated, PetEvent.PetMedicalEntryAdded { record PetRegistered( UUID id, String name, Instant birthDate, PetType type, Owner owner ) implements PetEvent {} ....} Events can be defined as static nested classes within the interface or as separate classes. Given the small number of events in this example, I preferred them in the same interface for simplicity.

Next, we need to establish the API for the event store. Two core functionalities are critical:

  • appending events to a stream
  • reading from a stream

Additionally, a subscription mechanism is essential to notify listeners about changes.

public interface EventStore { List<Object> readStream(String streamId); void appendStream(String streamId, int expectedVersion, Object[] events); void subscribe(EventListener listener);} The read operation is straightforward, as you only need to provide the stream identifier, and a list of events is returned in the order they were written.

The append operation is a bit more complex because it must handle optimistic locking and the creation of a non-existing stream. The key element here is the provided expectedVersion.

The subscribe operation follows the basic Observer pattern.

public interface EventListener { void on(Object event);}


Let’s explore in more detail how an InMemoryEventStore might be implemented.

First, it must partition all streams by identifiers, maintaining a sequential list of events for each. A Map would be the ideal structure for this. Second, it should manage the list of subscribers. The subscribe and readStream methods are self-explanatory.

public class InMemoryEventStore implements EventStore { static final Map<String, LinkedList<EventEnvelope>> STORE = new ConcurrentHashMap<>(); static final List<EventListener> EVENTS\_SUBSCRIBERS = new LinkedList<>(); public void subscribe(EventListener listener) { EVENTS\_SUBSCRIBERS.add(listener); } public List<Object> readStream(String streamId) { return STORE.get(streamId) } .... } Instead of storing a simple event object, we use an EventEnvelope. But why is this necessary?

It splits the stored data into two parts: metadata and event payload.

  • Metadata is added by the event store and includes details like the event’s timestamp, version, and other important context.
  • Event payload contains the core business data representing the actual event.

This separation makes it easier to track events, adds flexibility for future changes, and improves scalability.

public record EventEnvelope( EventMetadata metadata, EventPayload data) {}public record EventMetadata( int version, Instant timestamp) {}public record EventPayload( String name, String data) {} Roll up your sleeves, it’s time for stream appending:

a) The stream doesn’t exists yet in the store

For safety, we check that the provided expectedVersion aligns with the actual state of an uninitialized stream in storage. I get it—sometimes the term “version” can be misleading since it also refers to the position. In our implementation the version is not zero based, so we start with 1.

Next, we loop through the list of events and map them as described above. The serialize method uses a simple Jackson setup for converting the domain event into JSON format.

if (!STORE.containsKey(streamId)) { if (expectedVersion != -1) { throw new IllegalStateException("Version mismatch for uninitialized stream"); } int lastVersion = 1; var newEvents = new LinkedList<EventEnvelope>(); for (var event : events) { newEvents.add( new EventEnvelope( new EventMetadata( lastVersion++, Instant.now()), serialize(event) ) ); } STORE.put(streamId, newEvents);} b) The stream exists already in the store

Again, for safety, we ensure the stream not only exists but also contains at least one event. Then, the powerful but simple enough optimistic mechanism is used. We verify that the last event version from stream is equal to the one we read from stream when we begin the workflow.

Finally, we append the new events, properly packaged, to the stream.

else { var currentEvents = STORE.get(streamId); if (currentEvents == null) { throw new IllegalStateException("Stream not initialized"); } var lastVersion = currentEvents.getLast().metadata().version(); if (lastVersion != expectedVersion) { throw new IllegalStateException("Optimistic locking failure"); } int newVersion = lastVersion + 1; for (var eventPayload : eventPayloads) { currentEvents.addLast(new EventEnvelope(new EventMetadata(newVersion++, Instant.now()), eventPayload)); }} In the end, we’ll notify the subscribers. Note that this operation should be performed asynchronously to ensure good performance.

for (var event : events) { for (var eventSubscriber : EVENTS\_SUBSCRIBERS) { eventSubscriber.on(event); }}


Let’s move on to the next chapter: event handling and command handling. Since I’ve chosen to combine DDD concepts, we’ll handle it within the Aggregate.

public interface Aggregrate<ID> { ID id(); int version(); Object[] uncommitedEvents();} Good enough, but the ID and version fields make it look like a classic entity. That’s true, and this is one of the limitations of this design. If you fail while identifying the aggregates you’ll have hard times refactoring (been there, done that). Another issue is that now that the stream is coupled with the aggregate, so the unit of work becomes limited, and cross-aggregate updates are possible only through eventual consistency. The alternative is the Sara Pellegrini approach, which I really must try!

But we have one more important piece: uncomittedEvents. This is the core of the entire event-sourced aggregate lifecycle. The workflow is managed through these events, which are basically stored in a Queue, not in a random collection.

The interface, though useful, is not sufficient on its own. That’s why we need another layer of abstraction, called AbstractAggregate, which implements the basic features:

  • Apply an event – only updates the aggregate state.
  • Enqueue an event – pushes the event into queue and applies it.
  • Fetch uncommitted events – pops all events from the queue for storing.
  • Replay events – rebuilds the state of the aggregate from the stream.

public abstract class AbstractAggregate<I, E> implements Aggregrate<I> { protected I id; protected int version; private final Queue<E> uncommittedEvents = new LinkedList<>(); @Override public I id() { return id; } @Override public int version() { return version; } @Override public Object[] uncommitedEvents() { Object[] events = uncommittedEvents.toArray(); uncommittedEvents.clear(); return events; } public void replay(List<E> events) { events.forEach(e -> { apply(e); version++; }); } protected abstract void apply(E event); protected void enqueue(E event) { uncommittedEvents.add(event); apply(event); }} One tricky part is the versioning of the aggregate. While applying an event, the version doesn’t increment because it is used at the end of the transaction for optimistic locking in the event store in order to check that the read version hasn’t changed in the meantime. However, during replaying, the version must increment because those events are being validated.


Puzzle pieces are in place, let’s build our Pet aggregate.

The structure is pretty straightforward, I won’t spend time explaining it. Just one note, soft delete was preferred through active flags. The argument is that maybe the Pet is coming back to our clinic – I advise you to check the Udi Dahan article about deletion.

public class Pet extends AbstractAggregate<UUID, PetEvent> { private String name; private Instant birthDate; private PetType type; private Owner owner; private boolean active; private final List<MedicalEntry> medicalEntries = new LinkedList<>(); ...} The first question is how a new Pet is created (Java way) ? Constructor. But a constructor with the first RegisterPet command as an argument.

public Pet(RegisterPet command) { enqueue( new PetRegistered( UUID.randomUUID(), command.name(), command.birthDate(), command.type(), command.owner() ) );} Simple, right? Let’s move forward. How is the event handled ? Pattern matching for switch.

After the event is enqueued to store by being pushed into uncommitted events, the apply method is triggered and the aggregate’s state is updated.

@Overrideprotected void apply(PetEvent event) { switch (event) { case PetRegistered registered -> { this.id = registered.id(); this.name = registered.name(); this.birthDate = registered.birthDate(); this.owner = registered.owner(); this.type = registered.type(); this.active = true; } ... }} The story continues the same for each command of an event, but in a more complex way. We can handle conditions in the command handling methods. Keep in mind that conditions shouldn’t be used in the event handling because that’s out of their scope.

For example, we can’t update pet details if it’s inactive.

public void handle(UpdatePetDetails command) { if (!active) { throw new IllegalStateException("Pet is not active"); } enqueue(new PetDetailsUpdated(this.id, command.name(), command.birthDate(), command.type()));} Nice, we successfully created a Pet, processed a command and applied an event. Putting all together, you should see the following in the application service:

Pet dog = new Pet( new PetCommand.RegisterPet( "My dog", LocalDateTime.of(2020, 1, 1, 0, 0) .toInstant(ZoneOffset.UTC), PetType.DOG, new Owner("Alex", "alex@gmail.com", "Street 1") ));eventStore.appendStream( dog.id().toString(), -1, dog.uncommitedEvents()); Nothing fancy, just a Pet stored. Let’s now update its details. We load the aggregate from events, emit a new command and store the new version.

Pet storedPet = new Pet( eventStore.readStream(dog.id().toString()));storedPet.handle( new PetCommand.UpdatePetDetails( "My dog is a cat", LocalDateTime.of(2020, 1, 1, 0, 0) .toInstant(ZoneOffset.UTC), PetType.CAT ));eventStore.appendStream( dog.id().toString(), storedPet.version(), storedPet.uncommitedEvents()); Expected, right?

Until now we’ve discussed the C (command) from the CQRS. Let’s move forward to the Q (query) side.

In my code example I’ve treated the query synchronously through event listeners but the desire is to be asynchronous. Policies and views (projections) are the most suited for this part.

You remember the notification policy ? We will firstly store the notifications then a worker will push them into the remote service. The policy only reacts to required events, builds the message and stores it for delivery.

public class NotificationPolicy implements EventListener { private final Repository<Notification, UUID> repository; public NotificationPolicy( Repository<Notification, UUID> repository ) { this.repository = repository; } @Override public void on(Object event) { switch (event) { case PetEvent.PetOwnerTransferred petOwnerTransferred -> { repository.save( new Notification( petOwnerTransferred.newOwner().email(), "Your new pet", "Congratulations for owning!" ) ); } default -> System.out.println( "Event " + event.getClass().getName() + " not handled" ); } }} We needed 2 views: Pets and Sick pets, right ? In a classic SQL (or ORM) environment you would expect to have different queries applied to the same tables in order to produce the expected result set. Then, in production you will face some performance bottlenecks and performance strategies are going to appear (e.g. indexes, partitioning, sharding). But why not precompute the data in the desired format ?

The projection classes act as event listeners for each desired view. They use a specialized storage solution (SQL/NoSQL – doesn’t matter for us) to save the data according to the business requirements. Fields are added and managed only if they are needed, not because they exist.

The all pets projection is complex because it must keep up to date with all the pet data.

public class PetsProjection implements EventListener { private final Repository<PetDetails, UUID> repository; public PetsProjection(Repository<PetDetails, UUID> repository) { this.repository = repository; } @Override public void on(Object event) { switch (event) { case PetEvent.PetRegistered registered -> addPet(registered); case PetEvent.PetDetailsUpdated detailsUpdated -> updatePetDetails(detailsUpdated); case PetEvent.PetMedicalEntryAdded medicalEntryAdded -> addMedicalEntry(medicalEntryAdded); case PetEvent.PetOwnerTransferred ownerTransferred -> changeOwner(ownerTransferred); case PetEvent.PetDeactivated petDeactivated -> deactivatePet(petDeactivated); default -> System.out.println("Event " + event.getClass().getName() + " not handled"); } } private void addPet(PetEvent.PetRegistered registered) { repository.save( new PetDetails( registered.id(), registered.name(), registered.birthDate(), registered.type(), registered.owner(), true, new ArrayList<>() ) ); } private void updatePetDetails(PetEvent.PetDetailsUpdated detailsUpdated) { var petDetails = repository.findById(detailsUpdated.petId()); petDetails.ifPresent(pet -> { pet.updateDetails(detailsUpdated.name(), detailsUpdated.birthDate(), detailsUpdated.type()); repository.save(pet); }); } .....} The sick pets projection is simpler because it’s interested only if a Pet has a new medical record or if it’s removed. The remaining events are not handled because they are useless for the clinic’s graphs and statistics.

public class MostSickPetsProjection implements EventListener { private final Repository<SickPetDetails, UUID> repository; public MostSickPetsProjection(Repository<SickPetDetails, UUID> repository) { this.repository = repository; } @Override public void on(Object event) { switch (event) { case PetEvent.PetMedicalEntryAdded medicalEntryAdded -> addMedicalEntry(medicalEntryAdded); case PetEvent.PetDeactivated petDeactivated -> removePet(petDeactivated); default -> System.out.println("Event " + event.getClass().getName() + " not handled"); } } private void addMedicalEntry( PetEvent.PetMedicalEntryAdded medicalEntryAdded) { var petDetails = repository.findById(medicalEntryAdded.petId()); petDetails.ifPresentOrElse(pet -> { pet.addMedicalEntry(new MedicalEntry(medicalEntryAdded.entryDescription(), medicalEntryAdded.entryDate())); repository.save(pet); }, () -> { repository.save(new SickPetDetails(medicalEntryAdded.petId(), new MedicalEntry(medicalEntryAdded.entryDescription(), medicalEntryAdded.entryDate()))); }); } private void removePet(PetEvent.PetDeactivated petDeactivated) { repository.delete(petDeactivated.petId()); }} We see how to build a view, but how to effectively query?

If we need to filter or paginate the results, it would be recommended to create query classes (or records) and pass them directly to the corresponding repository.

public record GetPetsQuery( int offset, int size, Optional<String> name) {}

public class PetsRepository extends Repository<PetDetails, UUID> { public List<PetDetails> findPets(GetPetsQuery query) { var pets = findAll(); if (query.name().isPresent()) { pets = pets.stream() .filter(pet -> pet.name().toLowerCase() .contains(query.name().get().toLowerCase())) .toList(); } return pets.subList( query.offset(), Math.min(query.size(), pets.size()) ); } } The second approach is to directly call the repository if no parameters needs to be provided

public class SickPetsRepository extends Repository<SickPetDetails, UUID> { public List<SickPetDetails> findSickPetsOrderDesc() { return findAll().stream() .sorted( Comparator.comparing(p -> p.medicalEntries().size())) .toList(); }}


CONCLUSIONEvent Sourcing is more than a trendy pattern—it’s a time-tested approach that has proven invaluable for many businesses. It preserves the entire history of changes, provides built-in audit capabilities, and allows for easy time-based reversion when needed.

Events first , view later .

If you’re just starting out, focus on building a simple proof of concept (PoC) before exploring more advanced frameworks. Study how they work, but always critically assess their approach to ensure it aligns with your needs.

Most importantly, keep your business logic decoupled from the underlying technology. Industries like banking, e-commerce, and e-government have maintained stable core systems for decades, with only incremental feature additions. Unlike frameworks, which evolve rapidly, the core business remains relatively unchanged.

Treat your domain as the most valuable part — it’s the heart of your business.


If you’ve read this far, thank you so much for your time! I’ve done my best to simplify these concepts, though event sourcing is such an immersive and vast topic that one article can hardly do it justice. Hopefully, we’ll cross paths again next year to explore even more!

In the end, I definitely need to share some links to excellent resources and the top experts I follow to learn more about this concept:

  • Axon Framework Documentation and team (e.g. Allard Buijze, Steven Beelen)
  • EventModelling by Adam Dymitruk (coincidence or not)
  • Understanding Eventsourcing by Martin Dilger
  • Yves Goeleven and his blog
  • Event-Driven blog by Oskar Dudycz
  • A list with useful resources – https://github.com/leandrocp/awesome-cqrs-event-sourcing

And of course, the article’s GitHub repository.

The post Introduction to Event Sourcing appeared first on JVM Advent.

View Details

However long you might have worked in a single industry, there will always be something new to learn, whether that’s fresh technologies, different sectors, specific customer requirements, and so on. As a Developer Advocate, I’m experiencing that first hand, a few months into a new role, and I thought it might be interesting to share some of my learnings with you as I take my first steps into the world of e-commerce.

I’ve used Java (and spoken about it at length) for most of my career, but I’ve never really worked in the e-commerce sphere before. With the popularity of online shopping ever on the rise though, this part of our industry is growing fast, and will continue to do so.

My new company, Loqate, is one of the world’s biggest providers of address (and phone, email and bank account) verification technology. If you’ve ever typed the start of your address into a website and seen it provide suggestions as you’re typing, there’s a good chance this is us working in the background. We serve many different industries and use cases but as you can imagine, e-commerce is one of our biggest.

The DevRel RoleMy goal as a Developer Advocate is to help build a community around Loqate tech; to gather feedback from our users, improve our technical content assets and build brand awareness in the developer community.

As with starting any new role, the first few months have been taken up in large part by starting to understand what the landscape is like:

  • What platforms are we using?
  • What are the most common languages and tools used in this space?
  • What do our competitors look like?
  • What problems are our users trying to solve with our technologies?

While we don’t use Java much for front-end stuff here at Loqate, it’s heavily used in the back-end of many websites so I remain hopeful that I can continue to use Java combined with some front-end languages to build some samples for our documentation.

Interestingly though, that lead me to my next question:

What open-source solutions exist for e-commerce and are there any Java projects I could use?

To answer that question, let’s start by looking at the general landscape of e-commerce today.

The e-commerce LandscapeCompared with 10 years, ago, the e-commerce landscape now is really quite different. Back then, most businesses created their own websites from nothing, with only the bigger retailers and tech companies having the resources to create feature-rich sites offering user-friendly experiences.

That is still true today for big retailers and tech companies, and many of our larger customers here at Loqate use our APIs to access our services. Many small and medium sized enterprises, however, are opting to use e-commerce platforms instead of building their own sites. Crucially, these platforms take away the need for a whole development team to run a feature rich e-commerce site.

Coming from the open-source Java world, I was pleased to find that some of the biggest players in the e-commerce platform game are open-source: such as Adobe Commerce (formally Magento) and WooCommerce, with nearly 2 million customers between them.

There are, however, loads of proprietary alternatives too, as small to medium sized businesses aren’t generally focused on things being open-source, and may not have development skills in house to take advantage of their strengths. This is where platforms like Shopify, Jimdo and Oracle Commerce come into play, with impressive numbers of customers taking advantage of their ease of use (this article on 6sense.com gives a great overview of the market share of these platforms).

IntegrationsMost modern technology platforms include pre-built integrations, and this is certainly true for e-commerce platforms. These provide seamless connectivity between platforms and technologies, for example with payments, address verification, map data for store locations and so on, allowing organisations to integrate something like our Shopify plugin without needing any coding skills whatsoever.

Back in the early 2000s, integrating an online payment system for example would have required considerable development work, the associated cost of which have deterred small businesses from trading online. Nowadays if you look on the Shopify app store, about every type of payment system you can imagine is available as an integration, and most are very easy to integrate.

I recently attended a meetup for Shopify partners and most people in attendance had businesses that exclusively traded via the Shopify marketplace so these integrations can become big businesses on their own. As the popularity of e-commerce platforms keeps growing with businesses, we’re continuing to work on developing powerful, easy to use integrations!

Most-used Languages for e-commerceIt is hard to tell what programming languages most e-commerce sites use. In my experience, larger companies normally use a combination of different languages known as polyglot. It makes sense to use different languages together when you have varied website requirements, as each language will have their own strengths. A combination of HTML, JavaScript and PHP will often be the front-end languages of choice, while back-end systems might be a combination of Java, Python, C#, .Net etc.

Java and e-commerceWhile Java is used heavily as a back-end language for many web-based systems, you don’t see much of it in open-source e-commerce projects. Many of these projects seem to favour using the same language throughout, and given that Java perhaps isn’t the best front-end language this is probably why it’s not used that much.

I did find one open-source Java project: Shopizer. I managed to get this working, however the documentation could do with updating, and likewise the Node & Angular libraries (it uses Node & Angular in the front-end for their sample store – although you could use whatever language you want). The Java back-end is built with Maven and uses the Spring framework that most Java devs are familiar with. While I’ve used this to integrate Loqate’s address, email and phone verification products (with the intention of integrating other products like Store Finder in the future), one of my goals for this year is to contribute to this Java project. Hopefully I can make some useful updates to help make it a little quicker and easier to get up and running while being more secure. The reason for this is to use it as a sample to aid in our documentation and give developers a real-life sample of how to implement our technology.

My thoughtsFor small businesses with little in-house development resources, pre-built platforms clearly make the most sense. They’re highly customizable, with loads of integrations to other technologies that make starting your online selling journey easy.

If your company has reached a certain size however, with multiple requirements for your web presence, having in-house developers and owning your own site starts to make more sense. You’ll want to be able to add new functionality without having to wait for a platform to do it; open-source projects are a good way in if you want to begin owning your site, as you don’t have to start from nothing and they generally will have a community of developer willing to help.

The e-commerce world favours languages such as JavaScript and PHP, with a few Ruby and Python projects kicking around. While Java isn’t heavily used in these open-source e-commerce projects, I do know that it’s used as a back-end language for many of the world’s biggest tech companies. Web servers like Open Liberty and Quarkus will make sure it continues to be used for many years to come – they have made Java application severs very performant in this new cloud world, with sub one-second startup times for microservices allowing scaling to help save money on our cloud bill.

My goal is to update you all in 6 months on anything new I have learned since writing this and if anyone has anything to add that could be of interest then please get in contact with myself and I will consider adding it into my next article.

I’ve only scratched the surface of the e-commerce space here, but this should hopefully help anyone who – like me – is new to this realm get started quickly in understanding the landscape and technologies used in this industry.

The post A Java Developer’s View of the World of e-Commerce  appeared first on JVM Advent.

View Details

In recent years, the software development landscape has witnessed a revolutionary advancement with the emergence of Generative AI (GenAI). As Java developers, we are well versed in handling distributed systems and event-driven architectures. We’ll explore how those familiar concepts can be applied to create sophisticated AI workflows by combining event streaming with a multi-agent approach.

Understanding Generative AI: A Primer for Java DevelopersBefore diving into the architecture, let’s establish some foundational concepts. Think of GenAI as a sophisticated pattern recognition and generation system. Just as Java’s Stream API processes data in a pipeline, GenAI models process tokens (pieces of text, images, or other data) to generate new content based on patterns learned during training.

Key concepts:

  • Large Language Models (LLMs): These are the engines behind GenAI, similar to how the JVM is the runtime engine for Java applications.
  • Tokens: The basic unit of processing in LLMs, analogous to elements in a Java Stream
  • Prompt Engineering: The art of instructing AI models, similar to how we write method specifications in Java

Event Streaming in GenAI WorkflowsThe Traditional ApproachTraditionally, interactions with AI models follow a request-response pattern:

public class SimpleAIClient { private final AIService aiService; public String generateContent(String prompt) { return aiService.complete(prompt); }} This approach works for simple use cases but falls short when dealing with complex workflows requiring multiple AI agents working together.

Enter Event StreamingBy incorporating event streaming, we can create more sophisticated workflows:

@Servicepublic class AIEventStreamingService { private final KafkaTemplate<String, AIEvent> kafkaTemplate; private final Map<String, CompletableFuture<AIResponse>> pendingRequests; public CompletableFuture<AIResponse> processAIRequest(AIRequest request) { String correlationId = UUID.randomUUID().toString(); CompletableFuture<AIResponse> future = new CompletableFuture<>(); pendingRequests.put(correlationId, future); AIEvent event = new AIEvent(correlationId, request); kafkaTemplate.send("ai-requests", event); return future; }} Why Agents Matter in GenAI WorkflowsThink of an AI agent as a specialized microservice with cognitive capabilities. While a traditional microservice might transform data or perform business logic, an AI agent can understand context, make decisions, and generate creative outputs. Here’s why agents are crucial for modern GenAI applications:

  1. Specialization and Expertise
    • Different agents can be optimized for specific tasks (code review, documentation, testing)
    • Agents can use different models or configurations based on their specialty
    • Reduced prompt complexity as each agent focuses on its domain
  2. Complex Problem Decomposition
    • Large tasks can be broken down into smaller, manageable pieces
    • Each agent handles a specific aspect of the problem
    • Results are aggregated into a coherent solution

public interface AIAgent { CompletableFuture<AgentResponse> process(AgentTask task); boolean canHandle(TaskType type); AgentCapabilities getCapabilities();}@Componentpublic class CodeReviewAgent implements AIAgent { private final LLMService llmService; @Override public CompletableFuture<AgentResponse> process(AgentTask task) { // Specialized prompting for code review String prompt = promptTemplate.format( task.getCode(), task.getReviewCriteria() ); return llmService.generateResponse(prompt) .thenApply(this::formatReviewComments); }} Multi-Agentic ArchitectureThe real power of event streaming in GenAI workflows emerges when we implement a multi-agent architecture. Each agent specializes in a specific task, similar to how we design microservices.

Agent Types and Responsibilities1. Orchestrator Agent

@Componentpublic class OrchestratorAgent { @KafkaListener(topics = "ai-requests") public void handleRequest(AIEvent event) { // Analyze request and route to appropriate specialist agents List<AgentTask> tasks = taskPlanner.decompose(event.getRequest()); tasks.forEach(task -> kafkaTemplate.send(task.getTargetTopic(), new AITaskEvent(event.getCorrelationId(), task))); }} 2. Specialist Agents

@Componentpublic class CodeAnalysisAgent { @KafkaListener(topics = "code-analysis-tasks") public void analyzeCode(AITaskEvent event) { // Perform code analysis using appropriate AI model CodeAnalysisResult result = aiService.analyzeCode(event.getTask()); kafkaTemplate.send("analysis-results", new AIResultEvent(event.getCorrelationId(), result)); }} Benefits of This Architecture1. Scalability: Each agent can be scaled independently based on workload 2. Flexibility: New agents can be added without modifying existing ones 3. Resilience: Event streaming provides natural retry mechanisms and fault tolerance 4. Observability: Easier to monitor and track the entire workflow

Multi-Agent Collaboration in GenAIMulti-agent collaboration in GenAI is fundamentally different from traditional distributed systems. Here’s what makes it unique:

  1. Cognitive CollaborationUnlike traditional services that simply pass data, AI agents can:

  2. Interpret and understand each other’s outputs

  3. Provide feedback and suggestions to other agents
  4. Engage in iterative refinement of solutions

public class AgentCollaborationManager { private final Map<String, AIAgent> agents; public CompletableFuture<Solution> collaborativeSolve(Problem problem) { return CompletableFuture.supplyAsync(() -> { // Initial solution by primary agent Solution solution = primaryAgent.solve(problem); // Iterative refinement by specialist agents for (AIAgent reviewer : reviewerAgents) { Feedback feedback = reviewer.review(solution); if (feedback.requiresRevision()) { solution = primaryAgent.refine(solution, feedback); } } return solution; }); }} 2. Dynamic Task AllocationAgents can:

  • Self-organize based on task requirements
  • Delegate subtasks to more specialized agents
  • Adapt their behavior based on other agents’ capabilities

@Componentpublic class DynamicTaskAllocator { private final List<AIAgent> availableAgents; public AgentTaskPlan createTaskPlan(ComplexTask task) { return task.getSubtasks().stream() .map(subtask -> findBestAgent(subtask) .map(agent -> new TaskAssignment(subtask, agent))) .collect(Collectors.groupingBy( TaskAssignment::getPhase, Collectors.toList())); } private Optional<AIAgent> findBestAgent(Subtask subtask) { return availableAgents.stream() .filter(agent -> agent.canHandle(subtask.getType())) .max(Comparator.comparing(agent -> calculateAgentSuitability(agent, subtask))); }} 3. Context Sharing and MemoryAgents need to maintain and share context:

public class SharedContext { private final Map<String, Object> globalContext; private final Map<String, Map<String, Object>> agentSpecificContext; public void updateContext(String agentId, String key, Object value) { // Update both global and agent-specific context if (isGloballyRelevant(key)) { globalContext.put(key, value); } agentSpecificContext .computeIfAbsent(agentId, k -> new HashMap<>()) .put(key, value); }} Event Streaming for Agent CommunicationEvent streaming provides the backbone for agent communication:

@Componentpublic class AgentEventBus { private final KafkaTemplate<String, AgentEvent> kafkaTemplate; public void publishEvent(AgentEvent event) { String topic = determineEventTopic(event); kafkaTemplate.send(topic, event.getKey(), event); } @KafkaListener(topics = "agent-communications") public void handleAgentEvent(AgentEvent event) { switch (event.getType()) { case TASK\_COMPLETED: notifyDependentAgents(event); break; case ASSISTANCE\_REQUIRED: routeToCapableAgents(event); break; case CONTEXT\_UPDATE: broadcastContextUpdate(event); break; } }} Implementing Agent Collaboration Patterns1. Chain of Responsibility public class AgentChain { private final List<AIAgent> chainedAgents; public CompletableFuture<Result> process(Task task) { return chainedAgents.stream() .reduce( CompletableFuture.completedFuture(task), (future, agent) -> future.thenCompose(agent::process), (f1, f2) -> f1.thenCombine(f2, Result::merge) ); }} 2. Feedback Loops public class AgentFeedbackLoop { private final AIAgent producer; private final List<AIAgent> reviewers; public CompletableFuture<Output> generateWithFeedback(Input input) { return CompletableFuture.supplyAsync(() -> { Output output = producer.generate(input); int iterations = 0; while (iterations++ < MAX\_ITERATIONS) { List<Feedback> feedbacks = collectFeedback(output); if (feedbacks.stream().allMatch(Feedback::isPositive)) { break; } output = producer.refine(output, feedbacks); } return output; }); }} Implementation Considerations1. Event Schema Design public record AIEvent( String correlationId, String requestType, Map<String, Object> payload, Instant timestamp) {} 2. Error Handling and Recovery @Componentpublic class AIErrorHandler { @KafkaListener(topics = "ai-errors") public void handleError(AIErrorEvent error) { if (error.isRetryable()) { kafkaTemplate.send(error.getOriginalTopic(), error.getOriginalEvent().withRetryCount( error.getRetryCount() + 1)); } else { notifyFailure(error); } }} 3. Monitoring and Metrics @Componentpublic class AIMetricsCollector { private final MeterRegistry registry; @KafkaListener(topics = "ai-events") public void collectMetrics(AIEvent event) { registry.timer("ai.processing.time", "agent", event.getAgentType()) .record(Duration.between( event.getStartTime(), Instant.now())); }} Best Practices1. Prompt Templates: Standardize prompts across agents using template engines 2. Rate Limiting: Implement token bucket algorithms for AI API calls 3. Versioning: Version your events and maintain backward compatibility 4. Security: Implement proper authentication and authorization for AI agents

ConclusionBy combining event streaming with a multi-agentic approach, we can create sophisticated, scalable, and maintainable GenAI workflows. This architecture leverages Java developers’ existing knowledge of distributed systems while providing a robust foundation for AI-powered applications.

The key is to think of agents not just as API endpoints for AI models, but as cognitive entities that can collaborate, learn from each other, and collectively solve complex problems.

Next steps to consider:

  • Evaluate different event streaming platforms (Kafka, RabbitMQ, etc.)
  • Experiment with different AI models and their capabilities
  • Start small with a simple two-agent system and gradually expand
  • Design clear protocols for agent communication
  • Implement proper monitoring and observability from day one
  • Plan for failure recovery and graceful degradation

The field of GenAI is rapidly evolving, but the principles of good software design remain constant. By applying our expertise in event-driven systems to this new domain, we can build the next generation of intelligent applications.

The post Exploring Event Streaming and Multi Agentic Approach for Generative AI Workflows appeared first on JVM Advent.

View Details

Working on web projects often means dealing with two separate environments—one for the backend and another for the frontend—each with its own ecosystem. Developers frequently run into issues when dealing with the ecosystem of their counterpart, creating a needless wall between front-end and back-end developers. You have to use unfamiliar tools, follow API changes, create mocks, and have a hard time creating e2e tests. Overall, this makes development unnecessarily frustrating and a time-consuming process.

That’s where Quarkus enters. Although Quarkus is mostly known as the Kubernetes-native Java framework for optimizing Java applications for cloud-native settings, it offers way more than that. Enhancing the Developer Experience is one of Quarkus’s main priorities. For instance, it will automatically start a container for your database and Kafka if it uses either of these. Additionally, it has hot reload, which lets you code without ever having to restart your application. These are just a few examples of what Quarkus provides.

Quarkus has a wide range of extensions, including frontend-focused ones. It provides what the Quarkus team calls “the rainbow” of front-end technologies. The “rainbow” includes both client-side and server-side rendering. We’ll concentrate on the second here.

Quinoa: Develop, build, and serve your npm-compatible web applicationsWhy Choose Quinoa?Let’s start by discussing Quinoa. Quinoa is perhaps the most straightforward choice if you already have a front-end that works flawlessly with any JavaScript framework (React, Angular, Vue.js, …). It’s extremely simple to use.

Setting Up QuinoaFirst you need a Quarkus project. If you don’t already have one you can create one with the right extension either by going to this url or you can install the Quarkus CLI and then execute quarkus create app code-full-stack -x=quarkus-quinoa -x=quarkus-rest-jackson -x=quarkus-rest

But if you already have on you can simply add the Quinoa extension to your Quarkus project by either using the Quarkus CLI
quarkus ext add io.quarkiverse.quinoa:quarkus-quinoa
or if you’re using maven:
./mvnw quarkus:add-extension -Dextensions="io.quarkiverse.quinoa:quarkus-quinoa

Quarkus will install npm for you. Once the extension is installed you can place your NPM-based frontend in the src/main/web-ui directory. Of course, you have the option of creating a new front-end or using an existing one. If you do not wish to change your front-end code, for example if it’s a different team, you may still specify another directory (doesn’t need to be in your project) by setting the property quarkus.quinoa.ui-dir. Quarkus identifies specific Web Frameworks and pre-configures them with appropriate settings. Of course, you can configure Quinoa to use your framework if it is not recognized as supported or if you have a specific configuration. Here are some important elements to consider if you’re framework is not auto-detected or if you have very specific configuration.

  • the package.json scripts: you should have script named build, optionally start/dev and test. Quarkus will base itself on those script to build and instantiate your front-end
  • the directory where the web files (index.html, scripts, …​) are generated
  • the port used by the dev-server of the web framework you’re using (ie: React: 3000, Angular: 4200, …)

Those should be translated in your application properties as this:

quarkus.quinoa.dev-server.port=3000 quarkus.quinoa.build-dir=dist quarkus.quinoa.enable-spa-routing=true

Full-stack developer workflowAfter that, you may run “quarkus dev” and begin developing without needing to restart after changing the backend or frontend.

You don’t have to worry about starting the right process; simply launch your Quarkus app. You can now simply feel pure developer bliss. It allows front-end developers to work with the most recent iterations of the back end without having to wait for a deployment, and it makes it simple for back-end developers to ensure that their most recent modification does not cause any issues with the front-end. As with Java code, you can configure it to run your front-end tests automatically anytime your code changes. In addition, you may use the open-api extension and Orval on your frontend to immediately reflect any changes to your rest API.

Wait to find out more about the web-bundler, though. What if your project didn’t require Node.js and NPM?

Web bundler: Zero config bundling for your web-appThe challenges of traditional frontend toolingOne big struggle for Java developer when it comes to front-end is to handle all the new tooling such as Node.js and NPM even before learning a new programming language. But if we look on what exists we have a solution that already exists for more than a decade They’re known as webjars.

Webjar and MvnpmPutting the standard JavaScript/front-end library into a Java archive is the concept behind webjar. In a JVM-based environment, it enables you to use a front-end library. This allows you to use the standard Java build tools. It uses Maven Central and manages transitive dependencies. It looks fantastic, right?

However, webjars have drawbacks. If a particular library version is unavailable and you require it, you will have to request it and wait for it to become available. Because Webjar doesn’t actually filter the versions, users may unknowingly use an experimental version. Lastly, if you look at the webjar content, you will see that it contains complete source code that you, as a user, don’t really need. You usually only care about the minified version.

That’s where mvnpm comes in. Mvnpm is an improvement on webjars. It proxies though and façade NPM registry meaning you don’t have to request for specific version—it’s done automatically. By default, it only includes the stable versions of the dependencies. Your app will be smaller because the jar files only contain the library’s minified version. The cherry on top is that it cleans up the dependency name to make it less node-like and more Java-friendly.

Using Quarkus Web BundlerThe ability to import React, Bootstrap, or even the classic JQuery into your project may seem fantastic, but how does it benefit your JavaScript front-end? The Web Bundler extension can help with that.

Like for Quinoa, if you don’t already have a Quarkus project, you can create one with the right extension either by going to this url or you can install the Quarkus CLI and then execute quarkus create app code-full-stack -x=quarkus-web-bundler -x=quarkus-rest-jackson -x=quarkus-rest

If you already have a Quarkus project you can just Install the web bundler extension using: io.quarkiverse.web-bundler:quarkus-web-bundler quarkus ext add.

The next step is to take look at your classical Java. You have 3 main parts:

  • your dependency management (pom.xml, builgradle.kts…)
  • your Java backend (src/main/java)
  • your resources (src/main/resources).

When you’re using the web bundler you will add all your Node.js dependencies as Java dependencies using nvmpn. That way all your dependencies are in one and only one place.

You don’t need to change anything to your backend code.

Finally, your front-end will be placed in your resources in a specific directory (web/app) and you will add an index.html that will look like this:

If you look closely at the index.html, you can spot a strange element the {#bundle}. At build time, Quarkus will replace the bundle with your own front-end and all of your application’s front-end dependencies. The web-bundler will invoke esbuild while your application is being built with all your front-end files (js, jsx, ts, tsx, css, etc.), Esbuild will perform the standard tree shaking, minification, and source mapping tasks for you. Then the ouput will be included in the index.html file.

After that, you essentially have a full-stack application that functions without ever interacting with NPM or Node.js. The web-bundler can be used to create micro-frontend applications as well as full stack apps. Your front-end must be completely compatible with esbuild itself, which is the sole restriction. This means that frameworks such as Angular are currently not supported. If you are using Angular, I recommend continuing with Quinoa.

Wrap upQuarkus transforms full-stack development with Quinoa and the web bundler in a JVM based environment. Quarkus greatly increases developer productivity by handling the difficulties of bundling and optimization and smoothly integrating your current frontend projects. It helps developers concentrate on creating outstanding applications thanks to this simplified approach.

Useful links* Quarkus Quinoa extension * Quarkus Web bundler extension * Orval * Mvnpm

The post Bridging the Gap: Full-Stack Development Without the Headaches appeared first on JVM Advent.

View Details

When you think about code optimisation, you might imagine complex analysis over entire programs, sophisticated JIT compilers, or elaborate data-flow analyses. But some of the most effective optimisations come from a much simpler technique: peephole optimisation. This approach can yield surprisingly good results with relatively little complexity.

What are peephole optimisations?Imagine looking at a piece of code through a small window – a peephole – that only shows you a few instructions at a time. As you slide this window over your code, you look for patterns that can be replaced with more efficient alternatives. Consider this simple Java code:

int y = foo + 0; which might compile into the following Java bytecode:

iload\_0iconst\_0iadd Clearly, the addition of zero to some other value is redundant and through our peephole, we can spot this pattern and remove it.

Applying the optimisation would result in just a single instruction instead of the original three:

iload\_0 This optimisation is local – we only needed to look at a small sequence of instructions to identify and apply it. We didn’t need to analyse the entire program or understand complex control flow. This locality is what makes peephole optimisation both powerful and simple.

Why build a peephole optimiser?With modern JVMs performing sophisticated runtime optimisations, including JIT compilation, you might wonder if there’s still value in bytecode optimisation.

Every unnecessary instruction in your class files takes up space, and this space adds up across a large application. Peephole optimisations, especially when combined with other techniques, can lead to significant savings in application size and often wins in performance. Mobile developers especially know that every kilobyte counts when users are downloading apps over mobile networks and performance matters for users with low-powered devices. This is why tools like ProGuard and R8 are standard in the Android toolchain – they use optimisation techniques including peephole optimisation, alongside tree-shaking, to reduce application size.

Building a peephole optimiser is also a great learning opportunity to understand how optimisers work, to get an understanding of Java bytecode and the patterns of instructions that can be optimised. Peephole optimisation is not just useful in the Java world either: it’s a common technique used by compilers and optimisers, for example the InstCombine pass in LLVM applies peephole optimisations to LLVM IR.

In this post, we’ll build a working peephole optimiser using Java’s new Class-File API (a preview feature in Java 23 and targeted as final for Java 24). This API is a modern approach to Java bytecode manipulation, replacing the visitor pattern used by older libraries with modern Java features and idioms.

Java Class-File APIThe Class-File API is a new Java API, targeted for Java 24, that aims to provide a standard API for parsing, generating, and transforming Java class files. It is currently in its third iteration as JEP 484, and previously appeared as previews in JEP 457 and JEP 466.

Unlike libraries such as ProGuardCORE, ASM or ByteBuddy its scope is smaller: only parsing, generating, & transforming are in-scope while code analysis features are explicitly out of scope. Since peephole optimisation does not require any complicated code analyses, building such an optimiser on top of the API is relatively straight-forward.

Older libraries like ASM and ProGuardCORE (both 20+ years old) make heavy use of the visitor pattern (which made a lot of sense at the time) whereas the new API uses more recent Java idioms and features that weren’t present when the older libraries were designed. Generating a class which prints HelloWorld requires just a handful of lines of Java code:

ClassFile.of().buildTo(Path.of("HelloWorld.class"), ClassDesc.of("HelloWorld"), classBuilder -> classBuilder .withMethodBody("main", MethodTypeDesc.ofDescriptor("([Ljava/lang/String;)V"), ACC\_PUBLIC | ACC\_STATIC, codeBuilder -> codeBuilder .getstatic(ClassDesc.of("java.lang.System"), "out", ClassDesc.of("java.io.PrintStream")) .ldc("Hello World") .invokevirtual(ClassDesc.of("java.io.PrintStream"), "println", MethodTypeDesc.ofDescriptor("(Ljava/lang/Object;)V")) .return\_())); In JDK 23, the Class-File API is currently a preview feature, so the --enable-preview flags need to be provided to both the compiler and the java commands:

$ javac --enable-preview GenerateHelloWorld.java$ java --enable-preview GenerateHelloWorld$ java --enable-preview HelloWorld Now let’s dive in and start building our optimiser!

Reading and writing classesThe overall structure of our optimiser will look like this:

  1. Read a jar file
  2. Read each class file from the jar file
  3. Optimise the bytecode of each class
  4. Write the result to a new jar

The optimised jar will be semantically equivalent to the original jar but (hopefully) smaller in size.

The first thing we’ll need to do is set up a framework for reading and writing jar files; for these we’ll use the tools available in the java.util.jar package. I’ll leave out some error checking to keep things simple but the full code for the optimiser can be found here.

public class Optimizer { public static void main(String[] args) { var input = new File(args[0]); var output = new File(args[1]); optimizeJar(input, output); } private static void optimizeJar(File input, File output) { try ( var jarFile = new JarFile(input); var outputStream = new JarOutputStream( new BufferedOutputStream( new FileOutputStream(output))) ) { var entries = jarFile.entries(); while (entries.hasMoreElements()) { var entry = entries.nextElement(); try (var inputStream = jarFile.getInputStream(entry)) { var newEntry = new JarEntry(entry); outputStream.putNextEntry(newEntry); if (entry.getName().endsWith(".class")) { var originalBytes = inputStream.readAllBytes(); try { // TODO: optimise the class file. var optimizedBytes = originalBytes; outputStream.write(optimizedBytes); } catch (Exception e) { // If there's an error during optimisation, // copy over the original bytes instead. System.err.println( "Error optimising " + entry.getName() + ": " + e.getMessage() ); outputStream.write(originalBytes); } } else { // Copy other files across unchanged. inputStream.transferTo(outputStream); } outputStream.closeEntry(); } } } catch (IOException e) { System.err.println("Error: " + e.getMessage()); } }} For now, we just copy class files from the input to the output and the TODO in the snippet shows the location where we’ll need to apply optimisations to the class files.

Creating an input for testingBefore we continue, let’s create a test input jar so that we can exactly control the input bytecode sequences for testing purposes. Of course, we can use the Class-File API for this!

We’ll create a jar that contains a single class named Test, which is equivalent to the following Java code:

public class Test { public static void main(String[] args) { StringBuilder sb = new StringBuilder(); sb.append(“The length“); sb.append(“ of the”); sb.append(“ arguments array is ”); sb.append(args.length + 0); System.out.println(sb.toString()); }} The Java bytecode can be generated using the ClassFile API as follows:

public class TestJarGenerator { public static void main(String[] args) throws IOException { if (args.length != 1) { System.err.println("Usage: java --enable-preview TestJarGenerator.java <output.jar>"); System.exit(1); } byte[] classBytes = ClassFile.of() .build(ClassDesc.of("Test"), cb -> cb .withMethodBody("main", MethodTypeDesc.ofDescriptor("([Ljava/lang/String;)V"), ACC\_PUBLIC | ACC\_STATIC, codeBuilder -> codeBuilder .new\_(ClassDesc.of("java.lang.StringBuilder")) .dup() .invokespecial(ClassDesc.of("java.lang.StringBuilder"), "<init>", MethodTypeDesc.of(CD\_void)) .ldc("The length") .invokevirtual(ClassDesc.of("java.lang.StringBuilder"), "append", MethodTypeDesc.of(ClassDesc.of("java.lang.StringBuilder"), ClassDesc.of("java.lang.String"))) .ldc(" of the") .invokevirtual(ClassDesc.of("java.lang.StringBuilder"), "append", MethodTypeDesc.of(ClassDesc.of("java.lang.StringBuilder"), ClassDesc.of("java.lang.String"))) .ldc(" arguments array is ") .invokevirtual(ClassDesc.of("java.lang.StringBuilder"), "append", MethodTypeDesc.of(ClassDesc.of("java.lang.StringBuilder"), ClassDesc.of("java.lang.String"))) .aload(0) .arraylength() .iconst\_0() .iadd() .invokevirtual(ClassDesc.of("java.lang.StringBuilder"), "append", MethodTypeDesc.of(ClassDesc.of("java.lang.StringBuilder"), CD\_int)) .invokevirtual(ClassDesc.of("java.lang.StringBuilder"), "toString", MethodTypeDesc.of(ClassDesc.of("java.lang.String"))) .getstatic(ClassDesc.of("java.lang.System"), "out", ClassDesc.of("java.io.PrintStream")) .swap() .invokevirtual(ClassDesc.of("java.io.PrintStream"), "println", MethodTypeDesc.of(CD\_void, ClassDesc.of("java.lang.String"))) .return\_() )); var manifest = new Manifest(); var attr = manifest.getMainAttributes(); attr.put(MANIFEST\_VERSION, "1.0"); attr.put(MAIN\_CLASS, "Test"); try (var jos = new JarOutputStream(Files.newOutputStream(Path.of(args[0])), manifest)) { var entry = new JarEntry("Test.class"); jos.putNextEntry(entry); jos.write(classBytes); jos.closeEntry(); } }} This bytecode gives us the opportunity to apply a couple of optimisations: removing redundant zero addition and merging string constants to remove unnecessary StringBuilder.append calls.

If you run the optimiser on the jar file now, the output should be the same as the input:

$ java --enable-preview optimiser.java input.jar output.jar$ java -jar output.jar foo barThe length of the arguments array is 2 You can check the bytecode with the javap tool which will be useful to see the results of the optimisations later:

$ javap -c -v -p -cp output.jar Test…23: arraylength24: iconst\_025: iadd… Now that we can read and write jar files, let’s see how we can transform classes with the Class-File API.

Transforming classesTo optimise the class files in the jar we’ll need to:

  • Parse the bytes into a ClassModel
  • Transform the code attributes in the ClassModel
  • Write the resulting bytes of the transformed ClassModel to the output jar

We’ll create a helper method named optimizeClass to implement the parsing and ClassModel transform. The TODO in optimizeJar can be replaced with a call to the new method:

var optimizedBytes = optimizeClass(originalBytes);outputStream.write(optimizedBytes); The method uses the Class-File API to parse the original bytes into a ClassModel and then uses the ClassFile.transform method to apply a transform:

private static byte[] optimizeClass(byte[] bytes) { // Parse the class bytes into a class model. // Drop line numbers and debug info, to simplify the peephole pattern matching. var classModel = ClassFile .of(DROP\_LINE\_NUMBERS, DROP\_DEBUG) .parse(bytes); // When transforming the class, use a new constant pool instead of adding new // entries to the existing one. return ClassFile .of(NEW\_POOL) .transform(classModel, transformingMethods( (methodBuilder, methodElement) -> { if (methodElement instanceof CodeAttribute codeAttribute) { methodBuilder.withCode(codeBuilder -> { // TODO: optimize code methodBuilder.accept(codeAttribute); }); } else { methodBuilder.accept(methodElement); } } ));} Some things to pay attention to in the code snippet:

  • We drop line numbers and debugging information: this makes the peephole pattern matching easier, since this information introduces extra pseudo-instructions in the code in between actual instructions.
  • We use the NEW_POOL option when creating the transformed class file: by default the original constant pool is used as it is more efficient but it means that the constant pool can grow in size if we add new elements; since we’re interested in making classes smaller, it’s better to use a new constant pool.
  • We’re using a static helper method ClassFile.transformingMethods to reduce some of the necessary boilerplate.
  • The ClassFile.transform method returns a byte array: these are the new, optimised bytes that we’ll write out to the output jar.

If you run the optimiser on the jar file again, the output should still be the same as the input, since we have not yet applied any transformations to the code attributes. We have a TODO in the location we’re we’ll apply the peephole optimisations to the code attributes.

Class hierarchy resolverWhen transforming bytecode we need to be careful about stack map frames. These frames, required since Java 7, help the JVM verify the bytecode’s type safety. They describe the types of values on the operand stack and in local variables at certain points in the code.

By default, stackmap frames are automatically generated by the Class-File API when required. However, to generate stackmap frames correctly, the API needs to understand the class hierarchy. For example, when branches merge, it needs to find the common supertype of values coming from different paths.

If you think of a method that returns a List, and in one branch we return a LinkedList and another branch ArrayList; then to verify that both types satisfy the method return type we would need to look up the hierarchy to check that both LinkedList and ArrayList share List as a common supertype.

We don’t have any branches in our simple test case but real applications will have branches, so this is optional for now but you should add it if you want to try the optimiser on real applications.

As we’re reading classes from a jar file we can provide an implementation of ClassHierarchyResolver that can read classes from the jar file that we’re optimising and delegates to a resource parsing resolver.

public static class JarClassHierarchyResolver implements ClassHierarchyResolver { private final ClassHierarchyResolver resourceClassHierarchyResolver; public JarClassHierarchyResolver(JarFile jarFile) { this.resourceClassHierarchyResolver = ClassHierarchyResolver .ofResourceParsing( classDesc -> { var desc = classDesc.descriptorString(); // Remove the L and ; from the descriptor // e.g. Ljava/lang/Object -> java/lang/Object var internalName = desc .substring(1, desc.length() - 1); var jarEntry = jarFile .getJarEntry(internalName + ".class"); // Class not found if (jarEntry == null) return null; try { return jarFile.getInputStream(jarEntry); } catch (IOException e) { // Error reading class return null; } }); } @Override public ClassHierarchyInfo getClassInfo(ClassDesc classDesc) { return resourceClassHierarchyResolver.getClassInfo(classDesc); }} We’ll need to create an instance of the resolver in the optimizeJar method and pass it to the optimizeClass method:

private void optimizeJar(File input, File output) {... var resolver = ClassHierarchyResolver .defaultResolver() .orElse(new JarClassHierarchyResolver(jarFile)) .cached();... And we use it in optimizeClass by passing it as an option to the ClassFile.of method:

return ClassFile .of(NEW\_POOL, ClassHierarchyResolverOption.of(resolver)) .transform(classModel, transformingMethods(... Creating the peepholeOur optimiser is a peephole optimiser which means that we need to create a peephole window of some size that slides through the code elements. We’ll look for patterns in the window and decide if we want to replace them, remove them, or keep them.

We’ll create a window by iterating through the code elements and creating a fixed size array containing the current element and the next 4 elements. This gives us a window size of 5 which can be adjusted according to the length of the patterns that are to be matched but usually the window size for peephole optimisations should be small.

The following method implements the sliding window but does not yet apply any optimisations:

private static void optimizeCodeAttribute(CodeAttribute codeAttribute, CodeBuilder codeBuilder) { var elements = codeAttribute.elementList(); var windowSize = 5; var currentIndex = 0; while (currentIndex < elements.size()) { // Create a fixed size window with up to // windowSize elements and the remainder nulls. var window = new CodeElement[windowSize]; for (int i = 0; i < windowSize && currentIndex + i < elements.size(); i++) { window[i] = elements.get(currentIndex + i); } // TODO: apply optimisations on the window here // No optimisations, so continue to the next element. codeBuilder.accept(elements.get(currentIndex++)); }} Don’t forget to update the optimizeClass method to call the optimizeCodeAttribute method:

...methodBuilder.withCode(codeBuilder -> { optimizeCodeAttribute(codeAttribute, codeBuilder);});... If you run the optimiser now, you’ll again see that no transformations occurred: the output will be the same as the input since we simply call codeBuilder.accept with all the original code elements.

Let’s finally implement our first optimisation!

Your first peephole optimisationAt the beginning we introduced the following sequence of instructions where the addition of integer zero is redundant:

iload\_0iconst\_0iadd There are also other ways we can push the zero integer onto the stack:

  • ldc 0
  • bipush 0
  • sipush 0

In the Class-File API model, all of these instructions are implementations of the ConstantInstruction interface which means we can implement the optimisation for all of these in the same way.

To implement the peephole optimisation we need to check for two consecutive instructions:

  • A ConstantInstruction with constant value 0
  • An iadd instruction

The type of first + operand (the bytecode instruction before these two) does not matter: that’s the value that we’re adding zero to; so the optimisation can simply remove these two consecutive instructions.

The peephole window is an array and we can check the first and second elements using the instanceof, taking advantage of pattern matching to check the constant value and the opcode of the instructions:

if (window[0] instanceof ConstantInstruction c && c.constantValue().equals(0) && window[1] instanceof Instruction i && i.opcode() == IADD ) { // Skip the two matched elements // and emit no new elements. currentIndex += 2; continue;} The optimisation does not require emitting any replacement instructions; we simply skip the two matched instructions so that they are not emitted.

If you now run the optimiser on the test input, you will see that two instructions are removed:

$ java --enable-preview Optimizer.java input.jar output.jar$ java -jar output.jar foo barThe length of the arguments array is 2$ diff -w <(unzip -p input.jar Test.class > /tmp/Test1.class && javap -c /tmp/Test1.class | sed -E 's/#[0-9]+/#/g;s/^[[:space:]]*[0-9]+: //') <(unzip -p output.jar Test.class > /tmp/Test2.class && javap -c /tmp/Test2.class | sed -E 's/#[0-9]+/#/g;s/^[[:space:]]*[0-9]+: //')15,16d14< iconst\_0< iadd Congratulations, you’ve implemented your first peephole optimisation and saved 2 bytes!

A StringBuilder optimisationWe’ll implement one more peephole optimisation: when multiple StringBuilder.append invocations with constant strings appear sequentially we will merge them. For example, given the following Java code:

stringBuilder.append(“foo”).append(“bar”); We can remove the second append call by merging the two constant strings:

stringBuilder.append(“foobar”); This peephole optimisation is a bit more complicated than the previous one:

  • We need to match four instructions in the window:
    • A constant string instruction
    • A StringBuilder.append call
    • A second constant string instruction
    • A second StringBuilder.append call
  • We also need to emit a new instruction: we need to emit a replacement with the new concatenated string
  • Strings in the constant pool have a maximum size of 65535 bytes: we need to check that combining two shorter strings together does not exceed this limit

In Java bytecode, the instructions to match look like this:

ldc “foo”invokevirtual java/lang/StringBuilder#append(Ljava/lang/String;)Ljava/lang/StringBuilder;ldc “bar”invokevirtual java/lang/StringBuilder#append(Ljava/lang/String;)Ljava/lang/StringBuilder; And the replacement would simply be:

ldc “foobar”invokevirtual java/lang/StringBuilder#append(Ljava/lang/String;)Ljava/lang/StringBuilder; This replaces the four original instructions with just two instructions.

The peephole optimisation can be implemented in a similar way as the previous optimisation, using instanceof with pattern matching. Notice the use of the codeBuilder to emit new instructions for the newly created concatenated constant string and invoke instruction.

if (window[0] instanceof ConstantInstruction c1 && c1.constantValue() instanceof String s1 && window[1] instanceof InvokeInstruction i1 && i1.owner().asSymbol().equals(ClassDesc.of("java.lang.StringBuilder")) && i1.method().name().equalsString("append") && i1.typeSymbol().equals(MethodTypeDesc.of(ClassDesc.of("java.lang.StringBuilder"), ClassDesc.of("java.lang.String"))) && window[2] instanceof ConstantInstruction c2 && c2.constantValue() instanceof String s2 && window[3] instanceof InvokeInstruction i2 && i2.owner().equals(i1.owner()) && i1.method().equals(i2.method()) && i1.type().equals(i2.type())) { var concat = s1 + s2; // Emit the concatenated string constant, if it fits. if (concat.getBytes(UTF\_8).length <= 65535) { codeBuilder .ldc(concat) .invokevirtual(i1.owner().asSymbol(), i1.method().name().stringValue(), i1.typeSymbol()); // Skip the four matched instructions. currentIndex += 4; continue; }} If you now run the optimiser on the test input, you will see that five instructions are removed and one instruction is added:

$ java --enable-preview optimiser.java input.jar output.jar$ java -jar output.jar foo barThe length of the arguments array is 2$ diff -w <(unzip -p input.jar Test.class > /tmp/Test1.class && javap -c /tmp/Test1.class | sed -E 's/#[0-9]+/#/g;s/^[[:space:]]*[0-9]+: //') <(unzip -p output.jar Test.class > /tmp/Test2.class && javap -c /tmp/Test2.class | sed -E 's/#[0-9]+/#/g;s/^[[:space:]]*[0-9]+: //')7,9c7< ldc # // String The length< invokevirtual # // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;< ldc # // String of the---> ldc # // String The length of the15,16d12< iconst\_0< iadd Peephole optimisations are often applied multiple times because some optimisations enable other optimisations. For example, in the test input after one optimisation pass we end up the opportunity for another constant string append optimisation:

ldc “The length of the”invokevirtual java/lang/StringBuilder#append(Ljava/lang/String;)Ljava/lang/StringBuilder;ldc “ arguments array is”invokevirtual java/lang/StringBuilder#append(Ljava/lang/String;)Ljava/lang/StringBuilder; You can make a simple change in the optimizeJar method to apply the optimisations multiple times to a class, by calling optimizeClass again with the optimised bytes from the previous call:

...var optimizedBytes = originalBytes;for (int pass = 0; pass < numberOfPasses; pass++) { optimizedBytes = optimizeClass(resolver, optimizedBytes);}outputStream.write(optimizedBytes);... There are many other peephole optimisations that can be applied to StringBuilder calls, arithmetic instructions and more: try adding some new optimisations yourself!

Next stepsPeephole optimisation demonstrates that sometimes the simplest approaches can yield impressive results. By focusing on local patterns, we can achieve meaningful improvements without complex whole-program analysis. While these local optimisations are valuable on their own, they become even more powerful when combined with global optimisations like method inlining. For example, when a method is inlined, it often creates new opportunities for peephole optimisation that weren’t visible before.

The new Class-File API makes implementing peephole optimisations, and code transformations generally, for Java bytecode more straightforward than ever, providing a modern interface for bytecode manipulation that is part of the Java standard library.

As a next step, try extending the optimiser with your own patterns and optimisations, for example try implementing more arithmetic optimisations where constants are involved.

You can find the full code for the optimiser over on GitHub.

The post Peering through the peephole: build a peephole optimiser using the new Java Class-File API appeared first on JVM Advent.

View Details

In my job as author and teacher, I have many repetitive tasks, such as moving files around and transforming their content in tedious ways. In my quest to automate the boring stuff, I look at a task and think “no big deal, I’ll write a shell script”. Then the inevitable happens. As more special cases arise, the script turns into a festering mess of bash code. And I wish that I had written it in a real programming language instead.

The “obvious” choice is Python, but the Python API isn’t all that wonderful, and dynamic typing means that I spend too much time debugging. So I tried Java. I know the API by heart—at least for collections, files, regex, and so on. Java is statically typed so I am saved early from my foolishness. And the development environments are terrific.

But, I hear you say, really, a separate POM file and src/main/java hierarchy for every script? Ugh.

I don’t do that. Fortunately, modern Java and tools don’t require it. Read on for the details!

Launching without CompilingConsider a simple, but not too simple, task. As an example, I have a procedure to verify that my backups actually work. I retrieve ten random files once a day, in a scheduled job. (This is a really good idea that has saved me more than once from unreliable backups.) A script randomly picks ten files from a directory tree. It’s written in Java. And it sits in a directory with quite a few utility scripts.

Of course, I could compile it. But then my utility script directory would be cluttered with class files. Or I could make a JAR file. But that’s work. When you write a script whose value may not yet be evident, who has the patience for JARs and uber JARs?

That’s why I love JEP 330 and JEP 458. Now I can put my code in a .java file and just launch it as

java RandomFiles.java 10 /home/cay/data The file gets compiled on the fly, every time that I run the script. And that’s just the way I want it during development or later tinkering. And I don’t care during regular use because it’s not that slow. The Python crowd never loses sleep over that, so why should I?

You can compile scripts into native executables with Graal for faster startup time. I have experimented with that, but don’t find it makes a meaningful difference for most of my use cases.

Why not use JShell? I love using JShell for quick experiments (most of which seem to involve debugging regular expressions ). But it’s not great for scripts. The JShell tool itself has a very rudimentary editor integration, and the JShell support in IDEs is poor.

Instance Main Methods and Implicit ClassesJEP 477 reduces the verbosity of writing small Java programs. This effort is motivated by two desires. First, to make it easier to learn Java. And to simplify “other kinds of small programs, such as scripts and command-line utilities”. Having taught Java for many years, I never ran into students who said “my head hurts when I copy/paste the public static void main thing”. But I knew plenty of professors who were bothered by it. So it’s a good thing it is going away.

And for us scripters, it’s nice not to look at clutter.

var someVariable = initialValue;String helper(int param) { ... }void main(String[] args) { ...} No pesky class, no static.

Technically, any Java file with a top-level main method becomes an implicit class whose instance variables and methods are the top-level variables and methods in the file. Note that it is perfectly ok, and even desirable, to have classes, interfaces, enumerations, or records, in an implicit class. They turn into nested types.

As an added, benefit, all of the java.base module is automatically imported. Hooray, no more

import java.util.List; (As it turns out, the class names in java.base have been carefully curated not to conflict with each other.)

As of Java 23, three methods are automatically imported from java.io.IO: println, print, readln. From a teaching perspective, that’s not ideal because it is yet another factoid to remember. But as a scripter, I’ll take it.

We get to enjoy these automatic imports only in an implicit class. But that’s ok for many scripts.

Records and EnumsPython programmers often use ad-hoc dictionaries (i.e. maps) to aggregate related information. In Java, we have records:

record Window(int id, int desktop, int x, int y, int width, int height, String title) {} They make the code easier to read, and they become natural spots for methods:

record Window(...) { int xmax() { return x + width; } int ymax() { return y + height; }} The same holds for enumerations:

enum Direction { NORTH, EAST, SOUTH, WEST }; Much nicer than the clunky Python enumerations.

Other Helpful Language FeaturesWith complex programs, I am conservative with the use of var and only use it when the type is blindingly obvious, e.g.

var builder = new StringBuilder(); But in a script, I use var liberally. It’s almost like in Python, except that you still have compile-time typing. In fact, it is better syntax than Python because you can distinguish between declaration and assignment.

I am also more aggressive with static import:

import static java.lang.Math.*;diagonal = sqrt(pow(width, 2) + pow(height, 2)); (It’s just an example, you can actually use hypot(width, height).)

Text blocks are nice to keep data with your code. They play the same role as “here documents” in scripts. I hope that interpolation will come back soon, but in the meantime I use String.formatted for variable text parts.

Helpful API FeaturesThe Java library for strings, regex, collections, and date/time is excellent and extremely well documented. I much prefer it to the equivalent in Python, JavaScript, or (ugh) Bash.

For example, reading a file into a string is simply:

var content = Files.readString(Path.of(filename)); I use a helper for running an external process:

String run(String... cmd) throws Exception { var process = new ProcessBuilder(cmd).redirectErrorStream(true).start(); process.waitFor(); return new String(process.getInputStream().readAllBytes());} Note, by the way, that since JEP 400, I can rely on UTF-8 as the default encoding.

For HTTP, there is the HTTPClient (JEP 321) and the simple web server (JEP 408).

The XML support is serviceable. The API is antiquated and cumbersome, but at least it works predictably. In Python, you get a multitude of choices, each partially broken in its own way.

There are two things that are sorely missing in the standard library: JSON and command-line processing. For a large Java program, this isn’t a big issue. Just add your favorite library, such as Jackson or PicoCLI, to the POM. But it is a roadblock when writing scripts. You don’t want to manually get all of the dependencies of Jackson downloaded, and then added to the class path.

One trick is to use really simple libraries that fit into a single file. I’ve used Essential JSON and JArgs. Just toss the file into the same directory as your script.

Checked ExceptionsDepending on your circumstances, it may well be acceptable if the script terminates with a stack trace when something went wrong. But of course, you still need to declare or catch checked exceptions. In a large program, this makes sense, but it can feel like a burden in a script.

The simplest remedy is to add throws Exception to each method that may throw a checked exception, including main.

As an aside, this could be another “ceremony reduction” for beginning students. Why not do that automatically in methods of implicit classes? But I don’t make the rules.

There is still a problem with checked exceptions in lambda expressions. Scripts do a lot of file handling, and sometimes the API provides streams of file paths. So you want to go on with something like

streamOfPaths.map(Files::readString) But you can’t since the readString method may throw an IOException.

The correct remedy is, of course, to handle the exception in some way. Return an empty string. Log the exception. Turn it into an UncheckedIOException. Only you can make the appropriate decision.

But in a script, you may not care, and just want the program to terminate. There are a number of “sneaky throw” libraries, such as Sneaky Fun to address this problem. They take advantage of a hole in the Java type system. Through a clever use of generics, one can turn a method with throws specifiers into one that doesn’t have any. The details are, well, sneaky, but you don’t need to know them to use the feature. Simply write:

streamOfPaths.map(sneaky(Files::readString)) I am pretty sure this will never be a part of the JDK, because it is arguably bad for large and serious programs. But in a quick and dirty script, why not? Just remember to take it out if your script scales to the point where it no longer quick and dirty.

IDEs and File OrganizationYou don’t want to write a script with a barebones text editor. The whole point of using Java is that it is a statically typed language where the IDE can help you out with code completion and instant display of programming errors.

I usually start with a middle-weight editor such as Visual Studio Code or Emacs with LSP mode. That gives me Java integration, but without the need to set up a separate project for every script. Just open the Java file and start editing.

As I already mentioned, I find it demotivating to start a new src/main/java directory structure whenever an idea for a script occurs to me. So, I get going with my favorite editor. Eventually the script grows to the point where I no longer want to debug with print statements. You can debug a Java program inside VS Code, but I don’t find it particularly convenient. At that point, I would like the comfort of an actual IDE. But without src/main/java.

It is actually possible to coax your heavy-weight IDE into using the project base directory as the source directory. In Eclipse, that’s straightforward in the project setup. In IntelliJ, you need to go to Menu → Project structure… → Modules, remove the “content root”, and add the project base directory as a new “content root” that is marked as “Sources”. It sounds weird but it works.

JBangThe biggest pain point with Java scripting is the use of third party libraries. Why is it that the single-file java launcher can’t import stuff from Maven? Well, for starters, Java has no idea that Maven exists. There is nothing in the Java language standard that says anything about the Maven ecosystem. This is where Java shows its age. More modern programming languages have a unified mechanism for third party libraries. But I don’t think that this is something that Oracle can or wants to fix. So, you need some tooling to integrate with the Maven ecosystem, and it won’t be a part of the JDK.

As a quick remedy (adapted from this hack), I sometimes make a trivial Gradle script with Maven coordinates to get the files fetched, and to print a class path. But that’s only when I am not allowed to use JBang. (See this JavaAdvent article for an introduction to JBang.)

The killer feature of JBang is that you can add Maven dependencies right into the source file:

//DEPS org.eclipse.angus:jakarta.mail:2.0.3 Then you can run

jbang MailMerge.java In Linux and Mac OS, you can also turn the file into an executable script with a “shebang” line:

///usr/bin/env jbang "$0" "$@" ; exit $? Note that the // hide the shebang from Java, and the exit $? masks the rest of the Java file from the shell. (Three slashes are used for an arcane Posix compliance reason.)

The rest of JBang is just gravy. You can launch JShell with your file and its dependencies loaded. You can launch an IDE with symlinks to your source inside a temporary src/main/java. There are many more thoughtful features, but not too many. If you are serious about scripting in Java, and are able to use third-party tools, get JBang.

NotebooksSo far I focused on scripts—short programs that one runs regularly. Another aspect of programming in the small is exploratory programming: writing code once or a few times, to get some result out of a data set. Data scientists favor notebooks for this work. A notebook consists of code and text cells. The result of each code cell is displayed as text, a table, an image, or even as an audio or video clip. The code cells invite a trial-and-error approach. Once the desired result is obtained, the computation can be annotated with the text cells.

Why is this better than JShell? It is much easier to tinker with the cells than with lines of code in JShell. You can see tabular data and graphs. It is easy to save and share notebooks.

The most common notebook in Python is called “Jupyter”. You can run it locally, usually with a web interface, or it can be hosted. A popular hosted service is Google Colab.

Actually, the core Jupyter technology is language independent. One can install different kernels for various programming languages. The kernel installation process can be fussy, but this JavaAdvent article describes Jupyter Java Anywhere, a simple mechanism (using JBang) for installing a Java kernel.

Confusingly, there are a number of different Java kernels (including IJava, JJava, Ganymede, and Rapaio). Each kernel has its own way for installing Maven dependencies, displaying non-text results, and so on. Juypter Java Anywhere installs the classic IJava kernel, which has some open issues around dependency resolution. It really would be desirable for Oracle or another major vendor to step up, curate a kernel, and even—dare we hope—provide a Colab-like Java notebook service. Something more useful than the Java playground.

Python notebook coders are blessed with a couple of libraries for number crunching, in particular NumPy and Matplotlib. I have not found either of them to be God’s gift in terms of API design, but they are ubiquitous, and therefore StackOverflow and your favorite chatbot will offer suggestions, many of them useful, for tweaking computations and graphs.

Exploratory coding in Java is not (yet) common, and there isn’t a deep bench of support libraries. I think tablesaw could be a reasonable NumPy equivalent. It has a wrapper for the well-regarded Plot.ly JavaScript drawing package.

Sven Reimers is developing the JTaccuino notebook to offer a better experience. This is a JavaFX implementation with a friendlier user interface than the web-based Jupyter notebook. It uses JShell under the hood. The project is still in its early stages but worth watching.

For Kotlin, there is the Kotlin Notebook IntelliJ plugin.

While Java notebooks may not be ready for prime time, there is hope for the future.

ConclusionWith the right tooling, Java is a surprisingly effective choice for small programs. For simple scripts that use only the Java API, you can simply launch a Java source file. JBang makes it very easy to launch programs with third-party libraries. You benefit from compile-time typing and an upgrade path for when your programs get more complex, as they often do.

For the same reasons, Java can become an attractive choice for exploratory programming, but the tooling is not yet where it could be.

The post Java in the Small appeared first on JVM Advent.

View Details

Alright, hold on tight, because in today’s edition of the advent calendar, we’re going to talk about AI! Cause you know, we have 2024 and we do not need any other reason.


Let’s start by discussing what this article will cover. Each time the topic of AI arises, everyone tends to think of something slightly different.

There’s a joke about what makes a good AI conference: paradoxically, it’s one where nobody actually uses the term “AI.” So, as a kind of establishing shot, let’s clarify what this article will really be about. We won’t be talking about training models or broadly about Data Engineering. We’ll also skip over LLMs (sorry, Langchain4j). Instead, we’ll focus on model inference—and more specifically, all the peculiar things that need to happen inside a virtual machine and the JDK to handle this topic effectively.

I promise – it will!

But first, what is an inference?Imagine you have a magical gizmo capable of predicting “things”—with the precision of the Oracle of Delphi—but instead of a crystal ball, it uses math. In the world of artificial intelligence, this gizmo is a trained ML model. The model doesn’t pull predictions out of thin air; its “knowledge” comes from the data it was trained on. Inference is the process of applying this “knowledge” to new data to generate predictions.

For example:

  • When you ask a voice assistant about the weather, an ML model analyzes your query and predicts which weather data you need.
  • When you upload a photo to an image recognition app, the model identifies what’s in the picture—whether it’s a cat, a dog, or something more exotic

To understand the challenges of inference, let’s break down an ML model. Think of it as a series of filter layers through which data flows. Each layer analyzes the data in its own way, with the output of one layer becoming the input for the next. These layers contain numerous learned weights—also called parameters—which are essentially mathematical variables adjusted during training. For models like GPT-4, the number of parameters reaches hundreds of billions.

At every stage, the input data is processed through various calculations, such as matrix multiplications and additions. These operations are the essence of a model’s function—like sifting data through a series of increasingly fine filters to extract the most relevant information.

All of this would be straightforward if the data could be processed layer by layer with unlimited time. But in reality, inference needs to be extremely fast:

  • A chatbot with a multi-second delay? Unacceptable.
  • An image recognition app taking 30 seconds? The user has already closed the app.

So, ML models are like packing increasingly larger luggage into a shrinking car. Each new model demands more memory and processing power. But where do these challenges originate?

Memory Bandwidth
Even the fastest machines have limits when transferring data between memory and the processor. Imagine trying to push water through a narrow faucet—no matter how large your water tank is, the flow is restricted. Similarly, in inference, models demand more data than the memory can deliver in real-time.

Computation Time
Every layer of the model requires processing enormous amounts of data. For instance, a language model like GPT-4 must compute the probability of the next word (or token) based on billions of parameters. Imagine assembling a puzzle where every piece needs to be checked against every other piece—and you have to do this in fractions of a second.

Model Scaling
Model sizes keep growing. Just a few years ago, a model with 100 million parameters was considered massive. Today, GPT-4 has hundreds of billions of parameters. It’s like comparing carry-on luggage to moving an entire house. Larger models mean:

  • More operations to perform.
  • More data to store and process.
  • Higher hardware requirements, which aren’t easy to meet.

In summary, inference is a balancing act between model accuracy and computational efficiency. The larger the model, the more precise its predictions can be – but the computational costs also increase. This is why optimization – both at the level of model architecture and hardware infrastructure – is critical for practical AI applications, particularly in the Java ecosystem.

Let’s address the elephant (or snake) in the room.

Reusing Python Solutions with GraalPythonPython has long been the undisputed leader in the field of artificial intelligence. Its syntax is simple, clear, and the availability of powerful libraries like TensorFlow, PyTorch, and scikit-learn makes it the first choice for researchers and engineers working on machine learning. In research labs and during rapid prototyping, Python reigns supreme.

However, behind this dominance lies a paradox: Python itself is not particularly efficient. Its success is driven by libraries written in more performant languages, such as C or C++, which handle the bulk of computational workloads.

In this ecosystem, Python acts more as an abstraction layer, enabling developers to easily tap into these capabilities. The challenge arises when transitioning from experimental phases to production environments, where scalability and performance are paramount. In such cases, Python’s limitations, such as the Global Interpreter Lock (GIL), become increasingly evident.

Enter GraalPython—a technology that aims to bridge the flexibility and versatility of Python with the performance and stability of the JVM. GraalPython is an implementation of Python running on the GraalVM platform, itself built on Truffle, an advanced framework for creating programming language interpreters. With Truffle, GraalPython leverages just-in-time (JIT) compilation, enabling dynamic code optimizations that improve runtime speed. Moreover, its integration with the JVM allows Python to become part of larger, scalable systems historically dominated by languages like Java.

GraalPython offers something unique: the ability to use Python libraries in JVM applications without complex technological bridging. Python code can execute seamlessly in the same environment as Java, Kotlin, or Scala, simplifying integration processes and reducing technical barriers. Take a look at this example of using NumPy:

try (Context context = Context.newBuilder("python") .allowAllAccess(true).build()) { String pythonCode = """ import numpy as np # Creating a 3x3 matrix matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) result = matrix.T result.tolist() """; Value result = context.eval("python", pythonCode);System.out.println("Transposed matrix: " + result); } catch (Exception e) { e.printStackTrace(); } }​​ While GraalPython opens doors to new possibilities, it is not without its limitations. Its compatibility with CPython—the standard implementation of Python—is restricted. This means that some advanced features or extensions may require adaptation or may not work at all. A particularly challenging area is native extensions like NumPy or TensorFlow, which rely on libraries written in C. Supporting such extensions requires additional mechanisms, such as LLVM, which can pose technical challenges (though it must be said that the GraalVM team is making progress with every release).

Another challenge is the project’s relative youth. GraalPython is still maturing, and its community and technical support are not as robust as CPython’s. This can be a barrier for large-scale production projects that demand full reliability and broad compatibility.

GraalPython will not replace Python in its traditional role as a tool for research and prototyping. Nor does it aspire to become a new standard. Instead, it offers an alternative path—especially for teams that need seamless Python-JVM integration and want to leverage the strengths of both ecosystems. It represents a step toward combining Python’s flexibility with Java’s robustness, which is an impressive achievement in itself. I believe this will become a highly viable approach – perhaps we’ll need to wait for another two GraalVM releases (which isn’t that long, realistically) before I can confidently recommend it for production use to less adventurous teams.

So let’s back to the JDK and JVM, cause now it is time to take a look

Float 16 – Precision in Computation is not always a good thingPrecision is fundamental in computing – we want our programs to be precise – but do we always need it in abundance? Maybe we can make some shortcuts sometimes. Now it is time to understand the nuances of precision, especially the role of the mantissa.

Let’s introduce some math theory!

Floating-point numbers represent real numbers in computer memory, enabling operations on extremely large and small values. These numbers consist of three parts: the sign, indicating whether the number is positive or negative; the exponent, which determines the scale of the number; and the mantissa, which defines its precision.

For instance, the number 6.756.756.75 in floating-point format can be represented as:

6.75=1.6875×226.75 = 1.6875 * 2^26.75=1.6875×22

  • Mantissa: 1.68751.68751.6875, storing the number’s fractional details (here: 1+0.68751 + 0.68751+0.6875).
  • Exponent: 222, indicating how far the decimal point is “shifted.”

In the float32 format, the mantissa is 23 bits long, allowing for highly precise representations, such as 1.68751.68751.6875, expressed as 1.1011…1.1011…1.1011… in binary. By comparison, float16 reduces the mantissa to 10 bits, capturing fewer details. This results in rounding and less precise values. For example:

  • float32: 6.756.756.75 remains exactly 6.756.756.75.
  • float16: 6.756.756.75 might be approximated as 6.74218756.74218756.7421875, reflecting mantissa rounding.

In AI applications, such minor differences are negligible since models are trained on data with inherent noise. Would the difference between 6.756.756.75 and 6.74218756.74218756.7421875 alter an image recognition or translation model’s performance? Usually, the answer is no.

Reducing precision brings immediate benefits. Each number takes up less memory – critical for models with hundreds of billions of parameters, like GPT-4, where switching from float32 to float16 allows storing twice as much data in the same space. Computations are also faster since processors and GPUs can handle more numbers simultaneously when each is smaller. Finally, reduced data size lowers energy consumption and operational costs, making processing both economically and environmentally sustainable.

Until now, the JVM lacked native support for float16, forcing developers to rely on float32 even when unnecessary. Project Valhalla addresses this gap, introducing float16 to the Java Virtual Machine (JVM) ecosystem. This enables Java applications to fully leverage modern hardware, such as NVIDIA Tensor Cores, optimized for float16 operations. By embracing float16, the JVM becomes more competitive in AI computations and unlocks pathways to scalable, efficient solutions.

In summary:However, the size of the memory is just a part of the story. There is yet another more crucial component that we mentioned in the introduction – Memory Bandwidth.

Project Panama and JExtract – modern replacement for JNIOff-heap memory plays a important role in systems designed for large model inference, where performance and precise resource management are crucial. In Java’s traditional memory model, data is allocated on the heap, and the garbage collector automatically manages its lifecycle. This approach works well in many applications, greatly simplifying memory management (as evidenced by the fact that many of you, readers, have likely never managed memory manually). However, in inference processes requiring operations on large datasets and precise resource utilization, this approach can lead to challenges. Heap memory limitations, particularly in handling large objects, become especially apparent in environments where models need to operate in real-time or on massive input matrices.

Using off-heap memory helps overcome these limitations. It provides full control over memory allocation and deallocation, eliminating the risk of unpredictable pauses in system operation, such as during critical inference tasks. This is especially useful for operations on large data blocks, such as weight matrices or tensor buffers, which in traditional heap memory can lead to fragmentation or exceed permissible object size limits. It’s worth noting that popular computation accelerators, such as GPUs with CUDA technology or other hardware accelerators, provide their libraries exclusively in native languages like C or C++. Utilizing off-heap memory enables direct integration with these libraries, eliminating the need for data copying between the JVM and native environments, significantly speeding up the inference process – a topic we will explore further.

Traditional approaches using Java Native Interface, while effective, are complex and error-prone (which knows anybody who ever compiled C headers for usage with Java). This is where Project Panama, particularly the jextract tool, comes into play. Jextract automates the generation of bindings to native libraries from C/C++ header files. This allows developers to seamlessly integrate native libraries with Java code without manually writing JNI, reducing the risk of errors and shortening implementation time.

Jextract also supports new mechanisms such as the Foreign Function & Memory API, which enable safe and efficient off-heap memory management. In the context of large model inference, these tools open new possibilities for resource optimization, overcoming the limitations of traditional methods such as memory fragmentation or object size constraints on the heap. As a result, these solutions enhance the efficiency of inference systems, providing integration with modern computation acceleration technologies. Internet is full of great Project Panama tutorials, however the very details, including design docs, can be found on the official project repo on GitHub.


Alchemical Marriage of Valhalla and Panama – Vector APIIt’s time to discuss a project that bridges key elements of both Valhalla and Panama while unlocking new possibilities for high-performance computing. The Vector API, incubating in Java for several releases, merges Valhalla’s philosophy—enhancing performance through new primitive data types—with Panama’s approach of simplifying access to low-level hardware capabilities. This tool takes the language to a new level, offering explicit support for vector operations on data, which is critical in applications like machine learning inference, graphics processing, and numerical simulations.

Autovectorization, a feature of traditional Just-In-Time (JIT) compilers, has long been used to optimize Java by transforming certain loops into vectorized operations. However, its capabilities have been limited to simple, easily recognizable patterns in the code, making it unpredictable for developers. The Vector API, developed within the scope of the Panama and Valhalla projects, elevates vectorization to a new level by providing Java developers with an explicit and precise tool for designing efficient SIMD (Single Instruction, Multiple Data) operations.

In data processing, SIMD fundamentally differs from the traditional SISD (Single Instruction, Single Data) model, where each operation processes a single data element at a time. In SISD, for instance, adding two arrays involves the processor iterating through each element and performing the operation sequentially. SIMD, on the other hand, allows the same operation to be executed on multiple data elements simultaneously. This means processors can handle entire blocks of data in parallel, leading to significant time savings when working with large datasets—a common scenario in ML model inference.

Traditional autovectorization in JIT worked behind the scenes and was constrained by what the JVM could infer from the code. The Vector API changes this paradigm. Now, developers can explicitly define vector operations, signaling that data should be processed as groups rather than individual elements. The JVM, using the Vector API, automatically maps these operations to the best available SIMD instructions, such as AVX-512 on modern processors, or older ones if the hardware has other constraints.


In practice, instead of relying on hidden autovectorization mechanisms, the Vector API gives developers full control over the process. This allows for designing operations that are optimized for modern hardware right from the start, without the need for writing manual C or assembly code.

The Vector API, although still in incubation, is being actively developed, with its full implementation tied to the completion of Valhalla. Valhalla introduces new primitive data types and optimized handling of data structures, enabling the Vector API to reach its full potential within the Java ecosystem. Even now, several projects like Llama3.Java, JLama, and JVector are leveraging the Vector API, showcasing its immense potential.

  • Llama3.Java implements language models like LLaMA in the JVM ecosystem, using the Vector API to accelerate inference.
  • JLama is a modern inference engine for large language models (LLMs) written entirely in Java. It utilizes the Vector API and Project Panama to speed up inference processes, enabling Java developers to efficiently leverage models available on platforms like Hugging Face.
  • JVector is a fully Java-based vector search engine, employing modern graph algorithms inspired by DiskANN. Used by DataStax Astra DB and planned for integration with Apache Cassandra, JVector uses Project Panama to accelerate index building and querying, making it a cutting-edge tool for vector search in the Java ecosystem.

With the Vector API, Java is becoming a serious contender in domains previously dominated by C++.

However…


Step up – AcceleratorsAlthough vector operations on CPUs, supported by technologies like the Vector API, significantly accelerate computations, the true revolution in computational performance came with the development of GPUs. Graphics cards, originally designed for rendering 3D graphics, have evolved into powerful computational machines capable of taking over tasks where CPUs, even with advanced SIMD instructions, couldn’t match their performance.

The origins of GPUs (Graphics Processing Units) trace back to the 1990s, when graphics cards began incorporating dedicated processors for advanced graphical functions like geometric transformations and lighting. A major breakthrough came in 1999 with the release of the NVIDIA GeForce 256—the first chip marketed as a “GPU.” Equipped with a built-in T&L (Transform andLighting) engine, this card could handle complex graphical computations, relieving the CPU of these tasks.

Over the following years, the introduction of programmable shaders marked a transformation of GPUs from purely graphics-focused chips to more versatile computational platforms. This allowed developers to leverage GPUs for tasks beyond graphics—such as physics simulations, data analytics, and image processing.

A pivotal moment in this evolution was the launch of NVIDIA’s CUDA (Compute Unified Device Architecture) platform in 2006. CUDA opened GPUs to a wide range of general-purpose computing applications (GPGPU—General-Purpose Computing on Graphics Processing Units). GPUs began accelerating machine learning, numerical computations, and scientific simulations, offering an advantage through thousands of parallel cores capable of processing large datasets simultaneously.

Around the same time, open standards like OpenCL emerged, enabling the use of GPUs from various manufacturers, including AMD and Intel. As GPU applications grew, their architecture was adapted to meet computational demands, with modern NVIDIA GPUs featuring Tensor Cores specifically designed for matrix computations in deep learning.

The fundamental difference between a CPU and a GPU lies in their architecture. CPUs are optimized for tasks requiring high logical complexity and sequential processing, which is why they feature a few powerful cores. GPUs, on the other hand, are designed for massive parallel processing, making them ideal for operations on large data matrices—crucial in machine learning and graphics.

CPU vector operations, supported by SIMD and technologies like AVX-512, accelerate data processing but have their limitations. GPUs, with thousands of cores, can process thousands of threads simultaneously, offering a significant advantage for tasks like AI model training, scientific computations, or physical simulations. The advent of libraries such as TensorFlow and PyTorch, with built-in GPU support, has made graphics cards a standard in AI processing.

Java has traditionally been a CPU-focused language. While the Vector API and SIMD operations on CPUs offer considerable speed-ups, GPUs remain irreplaceable for the most demanding computational tasks. However, our language is evolving (as you might have guessed from this lengthy introduction) towards effectively leveraging both technologies, creating a versatile ecosystem for building advanced applications.


GPUs in Java – Initial approachUsing GPUs in Java relies on efficient parallel processing, which requires structures like kernels and ndgrid. A kernel is a small piece of code that runs at the same time across hundreds or thousands of GPU threads, speeding up tasks like matrix calculations, simulations, or scientific computations. Ndgrid organizes these threads into blocks and grids, helping to manage hardware resources and coordinate data sharing. Without these structures, using GPUs effectively would be very difficult, as programmers would need to handle the complexity of parallel processing manually. JCuda provides tools in Java to define kernels in CUDA and manage their setup, but this requires a deep understanding of GPU architecture and can be error-prone.

Aparapi and Rootbeer tried to make GPU programming easier by letting developers write kernels directly in Java. Aparapi translated parts of Java code into OpenCL, handling GPU configuration automatically but limiting it to operations that could be converted into OpenCL. Rootbeer went further by allowing code written entirely in Java to be analyzed and turned into parallel GPU tasks. While these approaches made it easier to use GPUs, they also restricted advanced control over GPU resources, which could be a problem for complex applications.

Project Sumatra, created by Oracle, aimed to integrate GPUs into Java’s Stream API, allowing GPU execution without the need for developers to understand kernels or ndgrid. It promised to make GPU processing simple and accessible for many Java users. However, the project was stopped because of challenges like the lack of GPU standardization (CUDA vs. OpenCL) and difficulties managing communication between user software and hardware. These issues, combined with the rise of popular tools like TensorFlow and PyTorch, led Oracle to end the project.

TornadoVM – Engine for Heterogenous ProgrammingIt’s finally time to dive into “modern” projects. TornadoVM is a tool that allows Java developers to speed up their applications by offloading them to hardware like GPUs, FPGAs, or multi-core CPUs, without the need to learn complex low-level code. It can be compared to a “translator” that takes Java code and converts it into instructions that such devices can understand. With TornadoVM, developers remain in the familiar Java ecosystem, while the tool ensures that their applications run faster by leveraging modern hardware.

The Task-Graph in TornadoVM acts as an abstract computation flow model, enabling developers to define tasks and their interdependencies. This structure allows parts of an application to be identified for parallel processing, while also specifying the input and output data for each task. As a result, the Task-Graph becomes the foundation for mapping computational logic onto heterogeneous hardware platforms like GPUs or FPGAs, while maintaining application flexibility and scalability.

int size = 512; Float2D a = new Float2D(size, size); // Matrix A Float2D b = new Float2D(size, size); // Matrix B Float2D c = new Float2D(size, size); // Resulting Matrix CFloat2D d = new Float2D(size, size); // Resulting Matrix D for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { a.set(i, j, i + j); b.set(i, j, i - j); } } TaskGraph taskGraph = new TaskGraph() .task("matrixAddition", MultiTaskGraph::add, a, b, c) .task("matrixScaling", MultiTaskGraph::scale, c, d, 2.0f); TornadoRuntime.getTornadoRuntime().submit(taskGraph); System.out.println(c.get(0, 0)); This is a crucial tool for simplifying the management of complex data flows, allowing developers to focus on application logic instead of low-level implementation details.

Execution Plans form a control layer over the Task-Graph, ensuring optimization and precise management of task execution. They enable dynamic task assignment to appropriate computing devices like GPUs or CPUs, depending on available resources and application requirements. This structure also allows for profiling and debugging mechanisms, making the entire process transparent to the developer. Execution Plans not only improve performance but also allow applications to adapt in real-time to changing environmental conditions, such as system load or hardware availability. The concept is somewhat analogous to how database engines optimize SQL queries.

With these mechanisms, TornadoVM introduces a high-level parallel programming model that harmoniously combines the simplicity of the Java ecosystem with the capabilities of modern hardware accelerators. If you want to learn more (and with the proper level of detail), please check The TornadoVM Programming Model Explained. It is currently one of the most exciting projects for GPU (and other “accelerator”) programming on the JVM. One of the most exciting, because there’s still another cherry on our cake.


La Grand Finale – HAT: Heterogeneous Accelerator ToolkitThe Heterogeneous Accelerator Toolkit (HAT) is the official JDK project announced on JVM Language Summit 2023. Its goal is to create a unified ecosystem enabling seamless collaboration between diverse hardware technologies, such as mentioned CPUs, GPUs, and FPGAs.

A key feature of HAT is the automation of translating Java code into optimized forms for various hardware accelerators. Through advanced mechanisms like code reflection and integration with Project Babylon, applications can be analyzed in real-time and adjusted to the specific hardware they are running on. This means that even complex operations, such as matrix calculations or machine learning models, can be executed with maximum efficiency regardless of the platform.

Project Babylon is an advanced extension of Java’s traditional reflection, enabling deep runtime analysis and transformation of code logic to optimize performance for specific hardware. Unlike standard reflection, which focuses on metadata like class names or method signatures, Babylon introduces code reflection, allowing the JVM to inspect and modify the actual computational structure of code, such as loops and operations, in real time. This enables dynamic translation of high-level Java code into hardware-specific implementations.

HAT operates on the concept of Code Models, which allow for the abstract representation of program logic, enabling the JVM to dynamically map that logic onto different platforms. For example, a Java method performing matrix operations can automatically be transformed into CUDA code optimized for GPUs. If a GPU is unavailable, HAT switches the application to a high-performance version running on the CPU, leveraging SIMD instructions like AVX. All of this happens without developer intervention, eliminating the complexity of manually tailoring code to specific hardware requirements.

One unique aspect of HAT is its seamless integration with heterogeneous computational environments. Developers write Java code focusing solely on application logic, while the JVM handles translation and optimization automatically. This enables performance comparable to low-level programming languages such as C++ or CUDA, without sacrificing the convenience and safety typical of Java.

HAT’s potential extends far beyond traditional applications, unlocking new possibilities in fields like AI, High-Performance Computing (HPC), and large-scale data processing. Its automated memory management, dynamic code adaptation, and integration with modern accelerators position HAT as a main player in Java’s future.

Summary: Where are we now, and where are we heading?Java may not yet be ready to replace Python in AI, but it’s making tremendous strides. Thanks to projects like GraalPy, Valhalla, TornadoVM, and HAT, the JVM could become a viable alternative for production environments looking to combine AI’s power with Java’s reliability.

However, a drop of bitterness spoils the picture—the entire ecosystem has already bet on slightly different horses. As we know, an alternative must be significantly better if it wants to replace the leading solution. Otherwise, we’ll spend the rest of our lives calling FastAPI through, ironically, an API.

So, while Java may not yet be ready to fully replace Python in AI, projects like HAT demonstrate that in the future the JVM can be a viable alternative for production environments seeking flexibility, reliability, and performance in a modern, heterogeneous computing landscape. We are just running against the clock.

PS: I know it was long… but I hope you all had as much fun on this journey as I did!

The post JVM in the Age of AI: A Bird’s-Eye View for the Mechanical Sympathizers appeared first on JVM Advent.

View Details

It’s been almost 3 years since the inception of github-workflows-kt library, a tool that lets you write GitHub Actions workflows in Kotlin instead of YAML (featured in Java Advent in 2022 here). One of its flagship features, and an interesting subproblem, is providing type-safe Kotlin bindings for as many GitHub actions as possible. We started by hand-crafting and unit-testing each individual binding, but it clearly doesn’t scale well as we want to support as many actions as possible. Curious about the challenges we faced (and the ones we still face!), and what approaches we tried on our way? Want to learn how we implemented a Maven-compatible server as a part of the current solution? Does generating and compiling Kotlin code on the fly sound interesting? Read on!

What’s the problem again?I assume that you know what GitHub Actions are, and you had a chance to look up what github-workflows-kt is. This is crucial to go further.

To get us started with something, here’s a simple workflow (but complex enough to see some interesting parts):

View the code on Gist.Today we’re going to focus on the pieces that start with uses:. Let’s analyze them:

View the code on Gist.actions/cache is used to restore the contents of build and .cache directories, from the key build-cache-dirs. Notice that the first parameter is a multi-line string, enumerating directories to cache.

View the code on Gist.The most popular action in the world: actions/checkout, used to clone repos. Your attention is probably brought to the peculiar value of fetch-depth: are we cloning 0 commits? Nope, it means cloning the whole history for all branches and tags.

View the code on Gist.No parameters this time, but this action is special because its coordinates don’t point to the repository root. See gradle/actions, there’s a setup-gradle directory there, meaning we run an action from a subtree of the GitHub repo. v4 still points to the git ref (branch/tag).

Now that we’ve got most of the interesting cases in front of us, our goal is to represent these so that the users of github-workflows-kt can conveniently consume them from within the Kotlin script. We’re obviously skipping the stringly-typed approach, like:

View the code on Gist.although something like this is also possible, as a last resort, for whatever reason. We need more type-safety!

The journeyIt turns out it’s one of these challenges where it’s just impossible to sit down for a few evenings and code it. It’s like walking in the mountains: once you’re done with the first hill, there’s a peak waiting for you, and the next one, and the next one. Somewhere behind the bushes, you hear a melody of the perfect solution, the sweet spot – but it’s not on your map, you have to find it by trial and error. But wait, is the source of the melody moving? Is it even a single source?

Let’s see what it’s been like to tackle this problem so far. Step by step.

Step #0 – hand-craft the bindingsWe started really small. The first iteration on a binding for actions/checkout looked similar to this:

View the code on Gist.Just this single parameter. You can see that we got excited with the way the fetchDepth parameter can be modeled, especially the misleading 0 that actually means “check out everything”. Now we had a way of saying it explicitly, with the designated object Infinite.

After some time, we noticed some arguments are missing, so here they are, along with proper types:

View the code on Gist.In the above snippet, you can also see how the parameters get converted to a map that is then easy to serialize to YAML. Pretty much boilerplate.

Wait, where are the unit tests? Yes sir!

View the code on Gist.After an hour or so, it was ready. Tedious translation, from YAML to Kotlin, for which one would probably use an LLM today (and then double-check it manually). Anyway, this baby was hand-written. And so were the bindings for 12 other actions.

Even though it was time-consuming and contains a massive amount of boilerplate and repetition, this phase was necessary to get a feeling of what it’s like to create such bindings, learn what are the most common input types, and figure out what Kotlin primitives are best to model these. That’s how I’d start today as well, as a necessary step on the evolution path.

The “13” (actions) was our lucky number that motivated us to make the bindings easier to create and maintain, thanks to…

Step #1 – use code generationAfter hand-coding a dozen of action bindings, some patterns emerged. The inner lazy developers in us started screaming “automate!”.

The naive idea was to build the Kotlin code using string templates, just glueing strings together. This, however, would mean that the string would let us make any error in the generated code, and we’d learn about it as late as upon compiling it. That’s why we thought it would be best to generate Kotlin… from Kotlin. Please welcome our guest, square/KotlinPoet.

To give you a sense of what it’s like to use KotlinPoet, here’s a snippet:

View the code on Gist.I realize it may be moderately readable at first glance. At the second one, it does read in a pretty straightforward way – you just add certain members of the class, set its various properties. Most of the chained function calls above come from KotlinPoet, and some are our domain-specific extension functions.

This was cool, but what was more important and ground-breaking wasn’t actually the way we generate the code, but the whole infra for it, so also how we get the data to generate it. We started calling GitHub to fetch action’s manifest – the action.y(a)ml file where info about the inputs and the outputs is stored by action owners. There was also a way to store information about the typings. In the early stage, we stored it in Kotlin like this (inputs of type “string” were initially omitted):

View the code on Gist.We ended up with a module that we know today as the Action Binding Generator. It accepts action coordinates (owner, name, version), the typings, and gives you a piece of Kotlin code with the action binding.

Did we also generate unit tests for each binding? Not really. It was enough to unit-test a synthetic binding with all possible kinds of inputs, so it’s just a single test that changes rarely. We also thoroughly tested a piece of logic that converts action and input names to camel case suitable for Kotlin.

What’s also worth noting is that for a long time, we kept the generated code in the repository. While it may seem like a smell, it was crucial for us to track how each change in the generation logic and the action manifest affects the produced code. This way we could iron out any remaining issues, including bugs (not always on our side) when putting inputs’ descriptions as KDoc comments, and ultimately got high enough confidence that the module works fine 99.9% of the time.

To make it easier for library users to find out which actions have their bindings, and to find their source code, a special page in the docs was generated and automatically updated alongside the bindings (they were called wrappers back then):

We lived with this state of affairs for almost a year, and it let us add support for 81 actions, 98 if you count each version separately.

Step #2 – let action owners host the typingsSubsequent library versions were released, the customers were happy, everything was fine. Well, not really. Some releases looked like this (an extreme case):

When adding support for new actions, we had to add typings for it. If someone wanted to contribute it, they had to learn how our little code generator works, and how to describe the typings.

There were also the “update” kind of changes. Each one usually meant that the action owner changed something in their action, even fixed a typo in some input’s description, so we had to regenerate the Kotlin bindings. Sometimes they added or modified the inputs, so we had to check with the action’s manifest or the docs what the type was.

It was super-boring. Our release cadence was two weeks, as a compromise between providing people with updated bindings fairly frequently, and not having to think about these updates too often. Sure we had some automation that created PRs for us or detected a change in inputs, but it still didn’t feel like the long-term solution.

We decided to start a journey to make this process more sustainable. A new tool, typesafegithub/github-actions-typing, was born. It’s a way to describe types of action’s inputs and outputs in a machine-readable format, so that our binding generator could easily parse it. It’s language-agnostic, isn’t aware of github-workflows-kt or Kotlin, and any other code generator could use it as well. The theory was to talk to the action owners, and ask them to store an extra file called action-types.yml, also for the benefit of their action’s users, as a standardized format of action’s API docs. If they agreed, we would remove the typings from the library’s repo, and some part of the problem of keeping the bindings up-to-date for a given action would be solved.

In practice, it didn’t always work, and still doesn’t work. For some actions, the issues/PRs related to adding the typings were never addressed, as if the action wasn’t maintained. If we did get in touch with the maintainers, some of them just didn’t like the idea, and responded that e.g. if GitHub adds such a feature, they will consider using it. Well, without getting too deep into such approach, let me say I respect it. The GitHub folks weren’t very enthusiastic about the idea so far (see this PR).

But it wasn’t all that negative. We’ve got some adoption! Some action owners did like the idea; one of the most prominent examples are microsoft/setup-msbuild (yes, Microsoft!), or benchmark-action/github-action-benchmark and ReactiveCircus/android-emulator-runner, both with ~1000 stars. Actions that hosted their own typings got a fancy marker on our “Supported actions” page in the docs:

So far (November 2024), excluding forks, this protocol of providing the typings was adopted by 24 actions. I’d say it’s fairly good for a bottom-up initiative! The more actions onboard this solution, the higher chances are for it to become a de facto standard. I really hope GitHub will revisit the idea of making GitHub Actions more type-safe.

To make things coherent, we ditched the Kotlin-based typing definitions stored in the library’s repo:

View the code on Gist.in favor of YAML-based typings, stored in a directory structure with well-defined conventions (e.g. actions/checkout/v2/action-types.yml):

View the code on Gist.After some time, we thought it would be a good idea to extract all typings to a separate repo, like what DefinitelyTyped is to TypeScript libraries, typeshed is to Python ecosystem, or SchemaStore is to JSON. It would address all these cases where we couldn’t get the typings hosted with the action in its repo. That’s how typesafegithub/github-actions-typing-catalog was created.

Looks like we managed to start a little pro-type-safety movement in the GitHub Actions community. It felt good!

Step #3 – allow client-side binding generationAll right, we did partially delegate maintenance of typings for some actions to their owners and the community, but we did not get rid of the need to maintain and vend the bindings. The generated code was still sitting in the library’s repo. The clients had to wait for at most two weeks for updated bindings, and for bindings for new major versions. There had to be a better way.

The first idea was to use a conventional technique of code generation (or alike) usable with standard Kotlin: KSP or a compiler plugin. Unfortunately, none of them is supported with Kotlin Scripting (see e.g. [KT-47384] Add ability to use compiler plugins in .main.kts (Kotlin Script) files). It means that the entry point of code generation had to be provided in a different way.

What’s the most obvious and explicit way of generate the code? Ask the user to do it! We’d ship the action binding generator as a stand-alone library, along with some convenience functions so that the amount of boilerplate is minimal. We’d also need a way of listing which actions in which versions should get their bindings.

As a result, as an experimental approach, we introduced client-side binding generation. The user only (ironically, of course) had to add an new Kotlin script like:

View the code on Gist.enable the feature via a flag in the workflow (generateActionBindings = true) which resulted in an extra step in the workflow YAML:

View the code on Gist.and add one more file

View the code on Gist.which repurposed the GitHub workflow’s YAML to drive the client-side binding generation. The nice thing about this was that dependency updating bots like Renovate or Dependabot could bump versions here, and such PRs would be auto-merged.

Upon generating the bindings, they landed in a .github/workflows/generated directory, and could be imported from the workflow scripts with e.g. @file:Import("generated/actions/checkout.kt").

It worked! We were proud we moved the needle by just a bit, and gave more freedom to the users in which actions they could use, and they weren’t tied to the library’s release cadence anymore.

However, it turned out that it’s just too much ceremony. I was blinded by the good parts of this approach, and didn’t see its bad ergonomics. The early adopters had to remember to regenerate the bindings every now and then. It was also impossible to get proper IDE support for multi-file Kotlin scripts, it’s been a long-standing issue (see e.g. [KT-42101] Scripts: @file:Import() in kotlin-main-kts uses a stale cache or [KTIJ-14580] Imported script are not supported for scripts outside of a source root).

That’s why we removed this experimental feature, and got back to the design board. We did collect some important findings, though.

Step #4 – create a Maven-compatible binding serverAfter the last experiment, we were back to the bundled bindings, without perspectives for a change.

One day, it hit me. My chain of thoughts was similar to this:

Action bindings are just another kind of dependency, but not on Maven libraries. We could publish them to Maven Central, but publishing over 100 artifacts with each library release sounds like it could fail a lot (recalling random issues with uploading to Central). Besides, we’d be tied to the release cadence, and it doesn’t really solve any problem.

I wish publishing to Maven Central was more flexible, perhaps done on demand when a user wants some action, but it’s not possible.

Can we create our own Maven server?

Yes, we can! And it’s not that difficult. Let’s ignore all the voices in our heads telling us to not create a service if it’s not absolutely needed, and go step by step to see how hard it is to get it working.

The following problems needed to be solved:

  • the API: finding an elegant way to map action coordinates to Maven artifact coordinates
  • creating a JAR file with the binding class, which can be split into:
    • generating the binding class’ source code
    • compiling the code
    • putting the compiler’s output into a ZIP (JAR’s underlying format)
  • finding a hosting solution for the service

Starting with the API, Kotlin Scripting supports declaring dependencies on Maven artifacts, like this:

View the code on Gist.If we look at a typical coordinates of a GitHub action, they match pretty well. For example, for actions/checkout@v4:

  • action’s owner (“actions”) corresponds to the GitHub’s user name or org name, and can be mapped to Maven’s group ID
  • action’s name (“checkout”) corresponds to the GitHub’s repo name, and can be mapped to Maven’s artifact ID
  • action’s version (“v4”) corresponds to a git ref (branch or tag), and can be mapped to Maven’s version

It means that a URL to a JAR that contains the example action’s Kotlin binding could look like this: https://some-custom-maven-repo.com/actions/checkout/v4/checkout-v4.jar. Several other auxiliary files like POM or maven-metadata.xml would need to be hosted as well.

There’s one edge case here: actions that have their manifests hosted not in their repository’s root, so e.g. gradle/actions/setup-gradle. Notice that the first separator is in fact different from the second one, and if we were to treat “actions/setup-gradle” as Maven’s actifact ID, it would create a problem. Why? Because it would map to a URL like https://some-custom-maven-repo.com/gradle/actions/setup-gradle/v4/actions/setup-gradle-v4.jar – the JAR’s name would contain the slash, plus we wouldn’t really be able to tell if “gradle” is the owner and “actions/setup-gradle” is the action name, or maybe it’s “gradle/actions” and “setup-gradle” respectively. That’s why for the purpose of the binding server, we went ahead with replacing any slashes in the path relative to the repository root with a double underscore, so adding a dependency on such action looks like this: @file:DependsOn("gradle:actions__setup-gradle:v4"). The double underscore is rare enough to not expect it in owner or action names.

Creating a JAR turned out to be much simpler than I thought. Since generating the source code of a binding class is already solved with the previously described Action Binding Generator module, and putting files into a ZIP is fairly simple, the only true challenge was to run the Kotlin compiler. Luckily, the Kotlin compiler is available as a stand-alone Kotlin library! Its usage resembles how we’d use it through the CLI (kotlinc), along with some extra config. This function depicts how easy it is:

View the code on Gist.It’s time to expose the JAR (et.al.) generation logic via a REST API compatible with Maven, to allow providing the bindings on demand, on the fly. The lightweight ktor was used to expose a server, and the required routing can be depicted with a short code snippet:

View the code on Gist.where the artifacts from the route with package version are described as follows:

View the code on Gist.Providing the checksums isn’t a result of me being a purist, they’re needed to make Kotlin Scripting happy. Otherwise we get a nasty warning.

Regarding performance, generating a JAR of a single binding takes from 1 to 5 seconds, depending on how many HTTP requests the logic of fetching action metadata and typings has to make (“.yml” or “.yaml” extension is possible, and the typing may live in the action or the typing catalog). A simple in-memory caching mechanism was put it place (using cache4k), to address a case where a flood of requests coming from compiling Kotlin scripts from a single repo arrives to the server:

View the code on Gist.After asking around who would be able to host the service, Leo Colman from Brazil agreed to host it on his private VPS, making this project truly inter-continental.

That’s it! The service has been alive for several months now, and in github-workflows-kt starting from v3.0.0 this is the only way of providing bindings. I’m free from releasing the library every month with updated bindings, it’s all driven by the users and the community. The server supports any action, within seconds, and anyone can contribute typings for any action.

Current challengesIs it the end of the story? No, of course not. Despite the service and the hosting proved to be stable and cope with the current load well, several challenges appeared.

Let’s start with the most customer-facing problem: dependency updating bots cannot handle all cases. They work fine when it comes to bumping versions of actions stored at the top level of their repo, so e.g. actions/checkout. In this case, e.g. Renovate creates a single PR that correctly updates both the Kotlin script and the YAML, and what’s important, it can be auto-merged without user’s intervention. The problematic case is for actions stored in a subdirectory, so e.g. gradle/actions/setup-gradle. Renovate creates two PRs in such case: the first one that updates the Kotlin script whenever we have @file:DependsOn("gradle:actions__setup-gradle:v4"), and the other one to bump all occurrences in YAML that refer to the “gradle/actions” repo. It’s because the bindings for such sub-actions are modeled by the bindings server as a separate artifact for each sub-action; for Maven, e.g. gradle:actions__setup-gradle and gradle:actions__wrapper-validation have merely the same group ID, but are disjoint libraries.

The ideal solution would be to mimic what’s done on the YAML level, so perhaps have a single Maven artifact to gather all actions in a given repo. However, it would be problematic because I can imagine actions with dozen sub-actions, and code generation for it would take significantly longer, so it’s about the scalability. Another approach is making the dependency updating bots aware of such cases, so that they create a single PR. So far, this problem hasn’t been too painful, so we’re staying with the current approach, and waiting for more data on how painful it is for the users.

The second problem is about backward-incompatible changes in the bindings provided by the server. Despite there have been none released so far intentionally, just to let people adapt to the new approach, we have a couple of improvements in the queue that would break at least some users. These are:

  • type-safe outputs for jobs and steps, so far there are just strings. See the PR
  • consistent visibility of the .copy(...) method for data classes. We’d like to stop exposing it by making the bindings regular classes instead of data classes. More on this in the issue

It’s generally possible to expose a “v2” of the bindings server (already implemented by a faithful contributor here), but it is extra hassle to keep the library in sync with the server, especially that the library exposes a RegularAction class that the bindings provided by the service inherit from. It will certainly require adding some validation to ensure that the users use mutually compatible library and server versions.

The third problem is that the library isn’t just a library with the bundled bindings anymore, so a single JAR you could security-review, and ensure the bindings’ code does what the user expects. If the server gets compromised, one can potentially inject some harmful logic into the bindings, causing e.g. data leak or impacting performance, depending on the context your GitHub workflows run. It’s been a blocker for at least one of the library’s users. While I think the shared, first-party server (https://bindings.krzeminski.it/) is fine for most open-source projects, I definitely hear the concern.

The ideal solution would be to follow a similar practice that is used to harden YAML-based workflows, so pinning to specific revisions for both the action logic (by commit hash) and the JAR (by checksum):

View the code on Gist.This, however, isn’t supported by Kotlin Scripting as of today, and if one uses Maven Central, this feature is not really needed because it’s guaranteed the artifacts are immutable. There would be also other problems with this approach, i.e. the dependency updating bots would have to be made aware of the JAR’s checksum.

What can be done about it right now? There are several possibilities:

  • The server’s image is available in Docker Hub: https://hub.docker.com/r/krzema12/github-workflows-kt-jit-binding-server. One can host a private instance of the service, invest into security-reviewing a single image revision that will be referred to in the service config by the image digest
  • Build the server from source, this is the corresponding Gradle module: typesafegithub/github-workflows-kt/(…)/jit-binding-server. Security review is easier on source code level, and updating to newer revisions is a matter of reviewing Kotlin diff
  • Use the Action Binding Generator module in an automatic/semi-automatic/manual way, the corresponding Gradle module lives here: typesafegithub/github-workflows-kt/(…)/action-binding-generator. I can imagine hooking the generation into some workflow where you could even keep the bindings’ source code version-controlled

We’ve got too little feedback yet to officially support any of the above approaches, so please let us know if you need help!

SummaryLooking back, it’s been a fascinating and fun journey of evolving the solution, trying out various approaches, automating whatever makes sense, and listening to the users.

I hope that this article showed that code generation and in-process Kotlin compilation isn’t that hard, thinking outside the box can bring surprisingly good results, and scaling a solution requires creativity at each step.

I’d like to thank all the contributors and the users who provided valuable feedback and improvements. In particular (alphabetically):

  • Jean-Michel Fayard () – for implementig most of the original binding generation logic, adding support for tens of popular actions, and more
  • Björn Kautler () – for valuable code contributions and discussions, being an early adopter of the library, and also helping me provide support for the project on Kotlin’s Slack (#github-workflows-kt)
  • Leonardo Colman Lopes () – for owning the hosting for the binding server (including hard work to make monitoring work with Jaeger and Prometheus!), and being an early adopter of the library

I feel like we’ll have yet another revolution when it comes to providing the bindings if [KT-47384] Add ability to use compiler plugins in .main.kts (Kotlin Script) files ever gets implemented…

The post The journey of providing Kotlin bindings for GitHub Actions appeared first on JVM Advent.

View Details

The development of powerful yet maintainable software solutions remains at the heart of modern software development. The Command Query Responsibility Segregation (CQRS) pattern offers an efficient method for this by creating a clear separation between executing commands and querying data, which simplifies the system architecture and improves performance. At the same time, the Data-Oriented Programming (DOP) approach brings a strong focus on the efficient handling of data. This article shows how the integration of CQRS and DOP leads to more robust, scalable, and easier-to-maintain systems.

Introduction to CQRSCommand Query Responsibility Segregation (CQRS) is a pattern first described by Greg Young [1]. It is based on the principle of separation of responsibilities and goes back to the Command-Query Separation (CQS) principle originally introduced by Bertrand Meyer in his book “Object-Oriented Software Construction”. While CQS states that methods should either be commands that change the state of an object but do not return a value, or queries that return a value but do not change the state, CQRS extends this principle to the architectural level of software applications.

CQRS separates the responsibility for processing commands that change the state of a system from the responsibility for querying information about this state. This separation enables an optimized design of both areas of responsibility, which can lead to better structuring. By modeling commands and queries separately, developers can also implement more complex business logic more clearly and easily.

However, introducing CQRS into a system can increase its complexity because two separate models have to be managed. As we will see in the course of the article, however, this apparent disadvantage can also be a great advantage in terms of data queries and performance. It can also lead to easier maintainability because changes to the query functionality can be made independently of the command logic, and vice versa. In addition, the separation enables optimized scaling because read and write operations place different demands on system resources and can therefore be scaled independently of each other. It is important to note here that this is not relevant for many business applications because the number of users and user behavior is known in advance.

Data-Oriented ProgrammingData-oriented programming (DOP) is a paradigm that offers an alternative approach to traditional object-oriented programming (OOP) by focusing on the data and its structures rather than the objects and their behavior. The key concepts of DOP are:

  1. Immutability
    Data structures are immutable, meaning that they cannot be changed once they are created.
  2. Separation of identity and state
    In DOP, the identity of a data item is separated from its state. This means that the state of an object at a given point in time is simply a snapshot of its data, making its history and changes over time easier to understand.
  3. Data modeling as a central design element
    In contrast to OOP, where the focus is on the behavior and methods of objects, DOP focuses on the design of the data models. This leads to a clear structuring of the data and makes data manipulation and querying easier.

Brian Goetz discusses the implementation of DOP in Java in his article [2] and describes how the combination of the new Java features, records, sealed classes, and pattern matching supports the DOP principles and leads to more precise, readable, and reliable programs. In particular, the commands from CQRS are modeled according to Brian Goetz’s ideas.

Modern Java FeaturesJava has introduced several important new language features in recent versions that significantly affect the way developers write and structure code. The new features that are important for this article are records, sealed classes, and pattern matching which are described below.

Records were introduced in Java 16 as a preview feature and have been an integral part of the language since Java 17. Records are a special type of class that is used to model simple data structures, so-called data carriers, with minimal code. A record automatically creates all fields as final and generates getters, but without a get prefix, for these fields, as well as appropriate implementations of equals(), hashCode(), and toString(). Records are therefore ideal for modeling immutable data objects and significantly reduce the amount of code to be written.

Sealed classes were officially introduced in Java 17 and offered a way to restrict inheritance. By sealing a class or interface, a developer can explicitly control which other classes or interfaces can inherit from this type. This is achieved by the sealed keyword together with the permitted keywords, which specify the exact types that are allowed to inherit from the sealed class. Sealed classes promote more precise control over inheritance and allow developers to define and secure hierarchical-type systems more precisely, which is particularly useful in domain modeling.

Pattern matching for the instanceof operator was introduced in Java 16 as a preview feature and has been further developed since then. It enables a more compact and readable way of performing type queries and subsequent type conversions. With pattern matching, you can not only check in an if and now also with a switch statement or expression whether an object belongs to a certain type, but if there is a match, convert it directly to a local variable of the corresponding type. This simplifies the code by removing the need to perform an explicit type conversion in a separate step and thus reduces the error rate when handling type conversions. Finally, Java 21 also adds record patterns, which allow direct access to individual components of a record.

CQRS Commands with modern JavaTo illustrate the integration of CQRS with modern Java and the new features mentioned above, we will focus on the commands. An example is a web shop that offers standard functions such as “Create order”, “Add item” and “Change quantity”. In a traditional approach, REST interfaces would be created for this, with which the order can be created and changed. Listing 1 shows an implementation that I often encounter in practice. Basically, the entire order is always transferred, regardless of which data has changed. The PurchaseOrderDTO is also a one-to-one copy of the PurchaseOrder entity and can therefore be easily mapped using a mapper such as ModelMapper or MapStruct.

@ResponseStatus(HttpStatus.CREATED)@PostMappingvoid post(@RequestBody PurchaseOrderDTO purchaseOrderDTO) { var purchaseOrder = modelMapper.map(purchaseOrderDTO, PurchaseOrder.class); customerRepository.findById(purchaseOrder.getCustomer().getId()) .ifPresent(purchaseOrder::setCustomer); purchaseOrderRepository.save(purchaseOrder);}@PutMapping("{id}")void put(@PathVariable Long id, @RequestBody PurchaseOrderDTO purchaseOrderDTO) { if (id.equals(purchaseOrderDTO.getId())) { throw new IllegalArgumentException(); } var purchaseOrder = modelMapper.map(purchaseOrderDTO, PurchaseOrder.class); purchaseOrderRepository.save(purchaseOrder);} This design presents several problems. In the put() method, it is not clear which data is changed by the interface. In addition, too much data is transferred because the entire order is always sent from the client to the server, which is completely unnecessary in most cases. On the client side, it is unclear which data in the interface object can be changed.

CQRS helps us solve these problems by sending commands from the client to the server instead of objects. The commands can be derived from the requirements at the beginning of the section: «Create order», «Add item» and «Change quantity». Focusing on commands has a positive effect on understanding the application because it adds semantics to the code.

sealed interface OrderCommand permits CreateOrder, AddOrderItem, UpdateQuantity { record CreateOrder(long customerId) implements OrderCommand {} record AddOrderItem(long orderId, long productId, int quantity) implements OrderCommand {} record UpdateQuantity(long orderItemId, int quantity) implements OrderCommand {}} Since commands are immutable, it is a good idea to model them as Java records (Listing 2). To group them and further improve comprehensibility, the example uses a sealed interface that is implemented by all commands. Thanks to the sealed interface, we can use the exhaustiveness of the switch expression to implement the handling of the commands. Exhaustiveness means that the compiler checks whether all values ​​have been handled. Firstly, this is a big advantage compared to an if/else if/else construct, and secondly, when you add a new command during further development, you are informed if it is not processed.

switch (orderCommand) { case OrderCommand.CreateOrder(long customerId) -> { var purchaseOrder = orderService.createOrder(customerId); return created(...).buildAndExpand(...).toUri()).build(); } case OrderCommand.AddOrderItem(long orderId, long productId, int quantity) -> { var orderItemRecord = orderService.addItem(orderId, productId, quantity); return created(...).buildAndExpand(...).toUri()).build(); } case OrderCommand.UpdateQuantity(long orderItemId, int quantity) -> { orderService.updateQuantity(orderItemId, quantity); return ok().build(); }} In Listing 3, in addition to the use of the switch expression, you can also see a use case for pattern matching with record patterns. The commands CreateOrder, AddOrderItem, and UpdateQuantity are deconstructed and the individual fields are passed directly to the OrderService. In this example, this has the advantage that the OrderService has no knowledge of the commands and thus remains independent. The entire source code of the examples can be found at [3].

ConclusionJava has been constantly evolving to keep pace with changes in the technology world. In recent versions, Java has introduced several modern language features such as records, pattern matching, and sealed classes. These extensions not only improve the readability and writeability of the code but also enable more functional programming approaches and improved data modeling that help Java developers write more efficient and expressive programs.
The article has shown that the use of CQRS with the separation of responsibilities makes it easier to understand and maintain and benefits greatly from the new Java language features, especially on the command side of implementation.

Links[1] Greg Young (2010): CQRS Documents by Greg Young
https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
[2] Brian Goetz (2022): Data Oriented Programming in Java, InfoQ https://www.infoq.com/articles/data-oriented-programming-java/
[3] https://github.com/simasch/cqrs-meets-modern-java

The post CQRS meets modern Java appeared first on JVM Advent.

View Details

The necessity for static analysis of source code …Most Java (and not only) developers have used at minimum some sort of a static analysis tool to perform a task such as (to name a few):* deriving source code metrics such as line of code or cyclomatic complexity; * discovering bugs, vulnerabilities or code smells such as unused variables (what popular IDEs typically do); * performing automated refactoring or code completion; * enforcing code and quality standards.

To perform static code analysis we typically need a proper representation of source code, suitable for analysis. A programming language can be described by a formal grammar. Furthermore a parser can be created or generated following the rules of a formal grammar to create proper representation (typically a parse tree) from source code. Based on the type of language we want to represent we can use different types of formal grammars:* regular grammars (i.e. regular expressions): they are available in most programming languages but are used typically for more basic parsing tasks as in many cases it is not suitable (or possible) to parse a modern programming langugage, it is slow and it is hard to maintain the grammar (i.e. the regular expression); * context-free grammars (i.e. BNF or eBNF): one of the most well-known formats is BNF (and its variants), a parser can also be generated by the grammar rules; * other formal grammars (i.e. PEG).

It is not uncommon that in the early days different tools for static code analysis required writing a parser manually which is not a trivial task ….Parser generators to the rescue …Tools can be created to generate parsers based on a target context-free grammar rules. This is, for example, the case with tools like LEX and YACC written in C and generating code in C. At a high level parser generation is illustrated by the following diagram:In the early days of Java Sun Microsystems has developed a parser generator called Jack which was then later renamed to JavaCC (which stands for Java compiler-compiler). Another popular alternative for generating a parser for a Java grammar is ANTLR (ANother Tool for Language Recognition). Both of these parser generators are well supported and written in Java. JavaCC (similar to YACC for C) can combine grammar rules with Java code that is included in the generated parser, however JavaCC provides code generation only for Java while ANTLR is general purpose, has a large number of grammars for a number of programming languages and provides the possibility to generate parsers in different languages. Both of these tools work with formal grammars in eBNF form and considering for example the above general diagram here’s how the process of parser generation looks like in Antlr with a simple example of an expression parser generator:The parse tree that is generated by the parser itself provides more effort in terms of code analysis so that is why typically parser generators provide the possibility to generate a more concise representation which eliminates extra symbols and provides additional symbol resolution capabilities: the AST (abstract syntax tree). The process of using ANTLR or JavaCC in a standard Maven/Gradle project is very similar.For ANTLR:* create grammar files under src/main/antlr4 (in g4 format, Java 20 grammar files available here) * add Antlr4 dependency and plugin in Maven/Gradle build file

org.antlrantlr4-runtime4.7.1….org.antlrantlr4-maven-plugin4.7.1antlr4

  • generate lexer and parser using Maven/Gradle build

For JavaCC:* create grammar files under src/main/javacc (in jj format, Java 1.8 grammar file available here) * add JavaCC dependency and plugin in Maven/Gradle build file

net.java.dev.javaccjavacc7.0.13org.codehaus.mojojavacc-maven-plugin3.0.1javaccjavacc

  • generate lexer and parser using Maven/Gradle build

Once this is in place the generated parser can be used to generate a parse tree that can have i.e. a listener attach for specific executions during the parsing process. Example using Antlr-generated parser: String content = "public class Example { public void func(int x){ return x + 10; } }";Java20Lexer lexer = new Java20Lexer(CharStreams.fromString(content));CommonTokenStream tokens = new CommonTokenStream(lexer);Java20Parser parser = new Java20Parser(tokens);ParseTree tree = parser.compilationUnit();ParseTreeWalker walker = new ParseTreeWalker();ExprListener listener = new ExprListener();walker.walk(listener, tree); An alternative way to create a parser is by a parsing expression grammar (PEG). A library that implements this approach (the grammar rules are written in Java code directly as part of the application) is Parboiled.Java libraries to the rescue …Parser generators and PEG parsers are quite generic. They also may not be up to date with the desired Java version. As an alternative a specializing parsing library can be used such as JavaParser or Eclipse JDT.JavaParserIt is based on JavaCC, it is well maintained and provides support for JDK 21. It provides enhanced symbol resolution and generates and AST from the source code. In addition it provides capabilities to query the AST via a DSL provided by the library, generate code from the AST or modify it. It is really simple to get started using the library, the following example counts the number of methods in a class: public static int countMethods(File file) throws FileNotFoundException { CompilationUnit cu = StaticJavaParser.parse(file); int count = 0; for (Node node : cu.findAll(MethodDeclaration.class)) { count++; } return count; } To get started using JavaParser it is sufficient to include the following dependency: com.github.javaparser javaparser-symbol-solver-core 3.26.3 Eclipse JDTEclipse JDT (Java Developer Tools) is the main fuel behind the Java editor in Eclipse IDE that provides advanced capabilities like partial compilation, code completion etc. In earlier days of Eclipse it was not straighforward to use JDT outside of the Eclipse IDE primarily because these requred a number of extra dependencies to be dragged as well. Now Eclipse JDT is available as standalone library via the following dependency:

org.eclipse.jdtorg.eclipse.jdt.core3.36.0

The following example implements a method to count the number of methods in a Java class: public static int countMethods(File file) throws IOException, MalformedTreeException, BadLocationException { String source = FileUtils.readFileToString(file, Charset.defaultCharset()); Document document = new Document(source); ASTParser parser = ASTParser.newParser(AST.JLS21); parser.setSource(document.get().toCharArray()); CompilationUnit unit = (CompilationUnit) parser.createAST(null); int count = 0; List<AbstractTypeDeclaration> types = unit.types(); for (AbstractTypeDeclaration type : types) { if (type.getNodeType() == ASTNode.TYPE\_DECLARATION) { List<BodyDeclaration> bodies = type.bodyDeclarations(); for (BodyDeclaration body : bodies) { if (body.getNodeType() == ASTNode.METHOD\_DECLARATION) { count++; } } }}return count;} As you can see there are multiple options you can choose from in order to start writing a tool on your own for static analysis of Java code.The post The art of static code analysis appeared first on JVM Advent.

View Details

My demo of OpenTelemetry Tracing features two Spring Boot components. One uses the Java agent, and I noticed a different behavior when I recently upgraded it from v1.x to v2.x. In the other one, I’m using Micrometer Tracing because I compile to GraalVM native, and it can’t process Java agents.

I want to compare these three different ways in this post: Java agent v1, Java agent v2, and Micrometer Tracing.

The base application and its infrastructureI’ll use the same base application: a simple Spring Boot application, coded in Kotlin. It offers a single endpoint.

  • The function beyond the endpoint is named entry()
  • It calls another function named intermediate()
  • The latter uses a WebClient instance, the replacement of RestTemplate, to make a call to the above endpoint
  • To avoid infinite looping, I pass a custom request header: if the entry() function finds it, it doesn’t proceed furtherIt translates into the following code:

@SpringBootApplicationclass Agent1xApplication@RestControllerclass MicrometerController { private val logger = LoggerFactory.getLogger(MicrometerController::class.java) @GetMapping("/{message}") fun entry(@PathVariable message: String, @RequestHeader("X-done") done: String?) { logger.info("entry: $message") if (done == null) intermediate() } fun intermediate() { logger.info("intermediate") RestClient.builder() .baseUrl("http://localhost:8080/done") .build() .get() .header("X-done", "true") .retrieve() .toBodilessEntity() }} For every setup, I’ll check two stages: the primary stage, with OpenTelemetry enabled, and a customization stage to create additional internal spans.

Micrometer TracingMicrometer Tracing stems from Micrometer, a “vendor-neutral application observability facade”.

Micrometer Tracing provides a simple facade for the most popular tracer libraries, letting you instrument your JVM-based application code without vendor lock-in. It is designed to add little to no overhead to your tracing collection activity while maximizing the portability of your tracing effort.

— Micrometer Tracing site

To start with Micrometer Tracing, one needs to add a few dependencies:

  • Spring Boot Actuator, org.springframework.boot:spring-boot-starter-actuator
  • Micrometer Tracing itself, io.micrometer:micrometer-tracing
  • A “bridge” to the target tracing backend API. In my case, it’s OpenTelemetry, hence io.micrometer:micrometer-tracing-bridge-otel
  • A concrete exporter to the backend, io.opentelemetry:opentelemetry-exporter-otlp

We don’t need a BOM because versions are already defined in the Spring Boot parent.

Yet, we need two runtime configuration parameters: where should the traces be sent, and what is the component’s name. They are governed by the MANAGEMENT_OTLP_TRACING_ENDPOINT and SPRING_APPLICATION_NAME variables.

services: jaeger: image: jaegertracing/all-in-one:1.55 environment: - COLLECTOR\_OTLP\_ENABLED=true #1 ports: - "16686:16686" micrometer-tracing: build: dockerfile: Dockerfile-micrometer environment: MANAGEMENT\_OTLP\_TRACING\_ENDPOINT: http://jaeger:4318/v1/traces #2 SPRING\_APPLICATION\_NAME: micrometer-tracing #3 1. Enable the OpenTelemetry collector for Jaeger 2. Full URL to the Jaeger OpenTelemetry gRPC endpoint 3. Set the OpenTelemetry’s service name

Here’s the result:

Without any customization, Micrometer creates spans when receiving and sending HTTP requests.

The framework needs to inject magic into the RestClient for sending. We must let the former instantiate the latter for that:

class MicrometerTracingApplication { @Bean fun restClient(builder: RestClient.Builder) = builder.baseUrl("http://localhost:8080/done").build()} We can create manual spans in several ways, one via the OpenTelemetry API itself. However, the setup requires a lot of boilerplate code. The most straightforward way is the Micrometer’s Observation API. Its main benefit is to use a single API that manages both metrics and traces.

Here’s the updated code:

class MicrometerController( private val restClient: RestClient, private val registry: ObservationRegistry) { @GetMapping("/{message}") fun entry(@PathVariable message: String, @RequestHeader("X-done") done: String?) { logger.info("entry: $message") val observation = Observation.start("entry", registry) if (done == null) intermediate(observation) observation.stop() } fun intermediate(parent: Observation) { logger.info("intermediate") val observation = Observation.createNotStarted("intermediate", registry) .parentObservation(parent) .start() restClient.get() .header("X-done", "true") .retrieve() .toBodilessEntity() observation.stop() }} The added observation calls reflect upon the generated traces:

OpenTelemetry Agent v1An alternative to Micrometer Tracing is the generic OpenTelemetry Java Agent. Its main benefit is that it impacts neither the code nor the developers; the agent is a pure runtime-scoped concern.

java -javaagent:opentelemetry-javaagent.jar agent-one-1.0-SNAPSHOT.jar The agent abides by OpenTelemetry’s configuration with environment variables:

services: agent-1x: build: dockerfile: Dockerfile-agent1 environment: OTEL\_EXPORTER\_OTLP\_ENDPOINT: http://jaeger:4317 #1 OTEL\_RESOURCE\_ATTRIBUTES: service.name=agent-1x #2 OTEL\_METRICS\_EXPORTER: none #3 OTEL\_LOGS\_EXPORTER: none #4 ports: - "8081:8080" 1. Set the protocol, the domain, and the port. The library appends /v1/traces 2. Set the OpenTelemetry’s service name 3. Export neither the metrics nor the logs

With no more configuration, we get the following traces:

The agent automatically tracks requests, both received and sent, as well as functions marked with Spring-related annotations. Traces are correctly nested inside each other, according to the call stack. To trace additional functions, we need to add a dependency to our codebase, io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations. We can now annotate previously untraced functions with the @WithSpan annotation.

The value() part governs the trace’s label, while the kind translates as a span.kind attribute. If the value is set to an empty string, which is the default, it outputs the function’s name. For my purposes, default values are good enough.

@WithSpanfun intermediate() { logger.info("intermediate") RestClient.builder() .baseUrl("http://localhost:8080/done") .build() .get() .header("X-done", "true") .retrieve() .toBodilessEntity()} It yields the expected new intermediate() trace:

OpenTelemetry Agent v2OpenTelemetry released a new major version of the agent in January of this year. I updated my demo with it; traces are now only created when the app receives and sends requests.

As for the previous version, we can add traces with the @WithSpan annotation. The only difference is that we must also annotate the entry() function. It’s not traced by default.

DiscussionSpring became successful for two reasons: it simplified complex solutions, i.e., EJBs 2, and provided an abstraction layer over competing libraries. Micrometer Tracing started as an abstraction layer over Zipkin and Jaeger, and it made total sense. This argument becomes moot with OpenTelemetry being supported by most libraries across programming languages and trace collectors. The Observation API is still a considerable benefit of Micrometer Tracing, as it uses a single API over Metrics and Traces.

On the Java Agent side, OpenTelemetry configuration is similar across all tech stacks and libraries – environment variables. I was a bit disappointed when I upgraded from v1 to v2, as the new agent is not Spring-aware: Spring-annotated functions are not traced by default. In the end, it’s a wise decision. It’s much better to be explicit about the spans you want than remove some you don’t want to see.

The complete source code for this post can be found on GitHub.

To go further:

  • Demo of OpenTelemetry Tracing
  • Micrometer Tracing
  • OpenTelemetry Traces
  • OpenTelemetry Java integration
  • OpenTelemetry Java examples
  • Distributed Tracing with Spring Boot 3 — Micrometer vs OpenTelemetry
  • Observability With Spring Boot 3

Originally published at A Java Geek on August 3rd, 2024

The post OpenTelemetry Tracing on Spring Boot, Java Agent vs. Micrometer Tracing appeared first on JVM Advent.

View Details

When a nerdy dad and 14-year-old music-playing son join forces and start experimenting with music and code, some nice things can happen. Did you ever present your music piece in a business dashboard with charts? Did you know that the FXGL game library can be used to generate a piano with fireworks? And can Virtual Threads play back MIDI events with just a few lines of code and thousands of threads?

About MelodyMatrixMy son wants to become rich and has multiple ideas daily to achieve that. Almost all those ideas are unrealistic, already exist, or very hard to realize. However, occasionally, when such an idea needs a website or some application, I tend to follow his idea and try to create something. In such a case, I use my existing knowledge and add one thing I want to learn.

That’s how we created 4DRUMS, a website to share drum videos. I used Spring Boot, Vaadin, and PostgreSQL to build the complete system during a few evenings and I learned how to use the APIs of YouTube and Vimeo so we didn’t need to render and host the video files ourselves. It’s a nice project, but as it goes with such websites, the programming was the easy part. Attracting users and making money out of it will probably never happen…

My son also loves to create YouTube movies with piano music, so he came up with another idea: “Can we create an application to visualize music in different ways?” As a JavaFX lover, this immediately triggered me, and I started experimenting with MIDI and different ways to visualize the data that can be received from a musical instrument. That’s how MelodyMatrix was born! Using the same tools used for 4DRUMS, I created the website melodymatrix.rocks, where you can find more information about the app and where you can download it.

The website’s “Thanks to…” page lists all the libraries and tools used for the application and website.

About MIDIThe Musical Instrument Digital Interface (MIDI) standard has existed for a long time and defines how musical instruments and controllers (PCs) can interact with each other and share data. The main thing you should know is the data format. With each press or release of a key on, e.g., a piano, a message is sent with three bytes of data:

  1. Status (4 bits) + Channel (4 bits)
  2. Data 1
  3. Data 2

The data values are used differently depending on the status. A good article with much more detail can be found on songstuff.com.

OpenJDK includes code to interact with MIDI devices and handle MIDI files. You can find the sources in the OpenJDK GitHub repository, where you can see that this code is quite old. The stability of the MIDI standard and its implementation in Java result in the fact that there haven’t been any changes in this part of OpenJDK. A more modern implementation of MIDI on the JMV is provided by ktmidi by Atsushi Eno, a Kotlin Multiplatform library for MIDI 1.0 and MIDI 2.0.

In Foojay podcast #54: Music and MIDI with Java and Kotlin, I talked with Atsushi and Geert Bevin about using MIDI with Java. Geert is a Belgian Java Champion who moved to the US and now works for Moog Music, which creates synthesizers and other musical instruments. You may also know him as the creator of RIFE2 and bld, two other amazing Java projects.

Building BlocksI once learned that focusing on one thing at a time is vital to mastering something new in a project. So I decided to build MelodyMatrix with Java, JavaFX, with Kotlin as “the new thing”. This is inspired by the fact that the ktmidi library is also written in Kotlin, and it can be mixed with the many existing and wonderful Java libraries created by the community, like the Charts library by Gerrit Grunwald and the FXGL game library by Almas Baim. The complete list of libraries used in the app and website is listed here.

The desktop app installer is created with jDeploy and runs on GitHub Actions to fully automate the distribution of new versions and update the already installed ones.

While looking for a way to sell licenses for the app, I learned about “Merchants of Record”. This is a company that handles the sales of digital products, makes invoices, takes care of taxes, etc. I use Polar, which claims to be “the fastest way to add SaaS & digital products to your stack”. They are a young company, share much of what they do as open-source, and are very responsive if you need help. So, although we haven’t sold licenses yet, I’m pleased with their service!

More Readable Code With KotlinThe most important advantage of using Kotlin in this project is the .apply {} approach that leads to more readable code. When using JavaFX, you end up with a lot of code that initializes a UI component and then applies a set of options. The following code generates the same UI but is written more cleanly.

The code in Java:

var borderPane = new BorderPane();var buttons = new VBox();buttons.setSpacing(10);buttons.getChildren().addAll( new Button("Button 1"), new Button("Button 2"), new Button("Button 3"));borderPane.setLeft(buttons); The same in Kotlin, using .apply {} to remove the repetition of the object names:

var borderPane = BorderPane().apply { left = VBox().apply { spacing = 10.0 children.addAll( Button("Button 1"), Button("Button 2"), Button("Button 3") ) }} In my opinion, the second code is cleaner. I shared a more extended example in a blog post and video some months ago but got some mixed reactions… 😉

Playing Music With Virtual ThreadsAs MelodyMatrix is a new project, I started with the latest Java Long Term Support (LTS) version: 21. This unexpectedly helped me solve a coding challenge very easily! As explained before, MIDI events only contain three bytes of data. In MelodyMatrix, you can make a recording, which stores these events as a record with the timestamp of the event and the data. Represented as JSON, it looks like this:

{ "name": "Test recording", "start": 1711203466078511000, "data": [ { "t": 1711203466078534000, "d": [-112, 60, 29] }, { "t": 1711203466480248000, "d": [-112, 60, 0] }, ... ]} So, a recording can be visualized as a timeline of data packets:

I needed to find a way to play back a recording, send it to an instrument, and generate the visual effects. I considered an approach with some continuous loop to go through the list of events, send them as MIDI data as soon as the timestamp has passed, and mark them as handled so they would no longer be evaluated in the loop. But a quick experiment with virtual threads revealed that a much simpler approach can be used!

What Are Virtual Threads?Virtual Threads were introduced as a preview feature in OpenJDK 19 as part of the Project Loom. They became fully integrated in OpenJDK 21. These virtual threads are also called “Lightweight Threads” as they are constructed as Java objects and take far fewer resources than traditional threads. The JVM runtime manages them, and they have no one-to-one mapping with the OS threads. As soon as their task blocks while waiting for an API response, database query, file to open,… the JVM will put them back into a “todo list”, and handle other tasks.

They are ideal for concurrency use cases where you must switch between many tasks. But you should not use them for long-running tasks that will never block, as the virtual thread system will cause overhead in such cases.

Virtual Threads in MelodyMatrixWhen you start playing a recording in MelodyMatrix, a virtual thread is constructed for all the events. So, each press or release of a music key or pedal becomes a task to be handled.

// A list to store all the threads, // so we can interrupt them if we want to stop the playback.var playThreads: MutableList<Thread> = mutableListOf()// Thread factory with a custom naming pattern.// The trailing number will increment starting from 0.val factory = Thread.ofVirtual().name("recording-player-", 0).factory()// Executor service to create a new thread for each submitted task.val executor = Executors.newThreadPerTaskExecutor(factory)// Timestamp of the first data point in the recording,// used to calculate relative timings for the playback tasks.val recordingStart = recording.data.first().timestamp// Create a task for each event with the time difference// between the first event and the current one.recording.data.forEach { d -> val task = MidiDataPlayer(d, d.timestamp - recordingStart) val thread = Thread.startVirtualThread(task) executor.submit(thread) playThreads.add(thread) } What happens in each of these tasks is basically… sleeping! 😉 The task waits till the time has passed that it has to wait to send the MIDI data to the instrument. This is a blocking action, so a perfect use-case for Virtual Threads and the JVM can easily handle thousands of these events and handle them at the right moment.

override fun run() { try { Thread.sleep(seconds, nanos.toInt()) midiHandler.play(dataLine, delayInNanos) } catch (e: InterruptedException) { // Nothing, just accept the interruption }} Is this the perfect implementation for playing back music? Maybe not, and there are probably much smarter approaches to achieve this. But it works! And as a good team leader once said, “If it works, don’t touch it…”

ConclusionJava and JavaFX are excellent combinations for creating a user interface application. By adding Kotlin, I could learn something new and create more readable and maintainable code. The application is far from finished and will probably never get finished, as most pet projects…. But it’s an ideal way to learn how to turn an idea into a sellable project and all the side activities involved, like marketing, creating videos, talking about it at conferences, etc.

If you are into music, please download the free version. Give it a try, and give us some feedback! Check this GitHub repository to see how the views are created. Maybe you can even make a pull request to improve them or add a new one?

If you want to learn more about MelodyMatrix or see it in action, take a look at this recording of the Devoxx talk we gave in Antwerp, Belgium, in October:

The post Coding for fun: An experiment with Virtual Threads, JavaFX, and Music appeared first on JVM Advent.

View Details

If you’re anything like me, you’ve reached a point where your tests start getting cluttered with configurations, and you aren’t quite sure where to put them. You think a helper class might do the trick, then you move them to a TestConfiguration that you import for every test. But even then, you find yourself reusing helpers in each test, wishing everything would just work behind the scenes, making your tests clean and elegant.

I’ve seen countless articles about JUnit 5 Extensions but never really gave them much thought—until my colleague Johnny (special thanks to you) showed me just how powerful they could be in action. I realized I’d been missing out.

So, let me share how JUnit 5 Extensions made my life so much easier, and how they helped me fall back in love with my tests.

TL;DR JUnit 4 had some limitations * JUnit 5 introduced a new Extension model * You can hook into multiple Injection Points * Creating Custom Extensions is easy * You can inject parameters into methods and leverage shared state between tests * JUnit5 Extensions have your back, and your team’s back*


Understanding JUnit 5 ExtensionsIn JUnit 4, we extended test behavior with Runners and Rules, but they had limitations. For instance, you could only use one Runner per test class, making it impossible to combine functionalities from multiple Runners. Rules were a bit more flexible, but they still involved extra boilerplate code and didn’t quite achieve the composability developers needed.

JUnit 5 overcomes these challenges with a unified extension model that emphasizes composability and separation of concerns. Extensions in JUnit 5 can be registered at various levels—field, parameter, method, or class—providing more flexibility in managing test behavior and reducing boilerplate.

Registering ExtensionsExtensions can be registered in a few different ways:

  • At the Class Level:

View the code on Gist.

  • At the Method Level:

View the code on Gist.

  • At the Field Level:

View the code on Gist.

By offering these options, JUnit 5 lets you decide where and how to apply extensions, whether it’s across the entire class or just a specific test method. It’s all about giving you the control to write cleaner, more maintainable tests without unnecessary hassle.

Popular ExtensionsJUnit 5 comes with built-in extensions, and there’s a thriving community that has contributed even more. Here are a couple of popular ones that most of us have used at least once:

  • MockitoExtension: Makes it easy to integrate Mockito with JUnit 5 for creating and injecting mock objects.

View the code on Gist.

  • SpringExtension: Integrates the Spring TestContext Framework into JUnit 5, allowing for dependency injection and transaction management.

View the code on Gist.

Injection Points of JUnit 5 ExtensionsJUnit 5 provides several injection points that let extensions hook into different stages of the test lifecycle. This flexibility allows you to customize and extend test behavior in a reusable way. Here are some of the key injection points:

  • BeforeAllCallback: Runs code before all test methods in a test class, typically used for global setup tasks.

View the code on Gist.

  • AfterAllCallback: Executes code after all test methods, often used for global cleanup.

View the code on Gist.

  • BeforeEachCallback and AfterEachCallback: Run code before or after each individual test method. This is useful for setting up or tearing down state specific to each test.
  • TestWatcher: Monitors individual test statuses—like passing or failing—which is helpful for logging and reporting purposes.
  • ParameterResolver: Allows you to inject dependencies directly into your test methods by resolving method parameters. These injection points enable you to create extensions that are both reusable and composable, helping you keep your tests clean and maintainable. They offer a level of flexibility that goes beyond what annotations alone can provide, allowing you to dynamically modify test behavior as needed.

GETTING OUR HANDS DIRTYThe best way to demonstrate the benefits of JUnit 5 extensions is to see them in action. Let’s refactor an integration test in a Spring Boot project that uses Testcontainers with PostgreSQL and Spring Boot Data JPA.

DependenciesAdd these dependencies to your pom.xml:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-testcontainers</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <!-- postgresql driver --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.34</version> <scope>provided</scope> </dependency></dependencies> These dependencies provide the testing framework and containerized database we’ll need for our examples (Tip: use https://start.spring.io/ to get these out of the box)

The Cluttered TestHere’s a sample integration test that sets up a PostgreSQL container and performs some database operations:

View the code on Gist.

This test functions correctly but is cluttered with setup and teardown code, which can make it harder to read and maintain. The additional boilerplate distracts from the core purpose of the test, making it less clean and elegant than we aim for, and a loss let reusable.

Refactoring with a PostgreSQL ExtensionTo declutter our test code, we can create a dedicated PostgreSQL extension that handles the setup and teardown logic.

Creating the PostgreSQLExtensionView the code on Gist.

Refactored Test ClassNow, we can refactor the test class to use the PostgreSQLExtension:

View the code on Gist.

By refactoring, we’ve removed the setup and teardown code from the test class. This makes the test cleaner and allows us to focus on the actual test logic, while being able to reuse the same logic without any duplication.


Introducing the SQLite ScenarioBut what if you need your tests to run faster during development? Switching to SQLite might just do the trick. For this, we’ll setup an in-memory SQLite database that our tests will use and a new SQLiteExtension responsible for all the required configuration:

View the code on Gist.

Switching Between DatabasesUsing the PostgreSQL extension looks like this:

View the code on Gist.

To switch over to SQLite, simply use:

View the code on Gist.

See how the test logic stays the same? By swapping out the extension, your tests become flexible and adaptable to different scenarios without any extra hassle.


Creating a Reporting ExtensionNow, let’s figure out how long our tests take when running against different databases. This ties in nicely with our earlier focus on writing clean, reusable code for database extensions. But before jumping straight into using extensions for this, let’s take a look at how we’d typically approach the task without them. Then, we’ll dive into how a reporting extension can make the process smoother and more consistent.

Without ExtensionsHere’s an example of how you might measure test execution time without using extensions. While effective, this approach adds boilerplate code to every test class, making the tests harder to maintain and less focused on their core purpose.

View the code on Gist.

While this works, it introduces repetitive boilerplate code in every test class. Now, let’s see how we can use a reporting extension to clean this up.

Using ExtensionsTo clean things up, let’s create a ReportingExtension that handles timing and reporting:

View the code on Gist.

Integrating the Reporting ExtensionWe can now register the reporting extension alongside our database extension:

View the code on Gist.

Resulting in such an output:

test() PASSED in 878 ms This setup shows how reporting integrates naturally with the existing extensions, also emphasizing the composability of JUnit 5 extensions.

Furthermore, we now have the ability to add the same reporting extension to the tests against SQLite database and be able to compare one approach to the other.

Going even furtherMade it until here? Good. We’ll now deliver the final strike.

We will take our ReportingExtension to the next level by incorporating a bunch of endpoint metrics into our test reporting.

Suppose we have an endpoint that returns various metrics such as averageProcessingTime, meanProcessingTime, and so on. To enrich our test report, we will include these metrics for different database types, specifically PostgreSQL and SQLite.

In this scenario, we will:

  1. Use JUnit 5’s parameter injection to extend our ReportingExtension, showcasing the parameter injection and state capabilities of JUnit 5 extensions.
  2. Maintain a shared state for the ReportingExtension that stores the metrics during testing.
  3. Use the two test classes—one for PostgreSQL and one for SQLite—to compare their metrics.

Enhancing the ReportingExtensionTo incorporate endpoint metrics into our reporting, we will need to enhance our ReportingExtension so that it:

  1. Tracks metrics for each test: We need to store the metrics for each database type.
  2. Uses a shared state: The state will be injected into each test, allowing us to add metrics during the test execution.
  3. Generates a final report: At the end of all tests, we will include the endpoint metrics in the report. Creating the ReportingStateFirst, let’s create a ReportingState class to hold our metrics.

View the code on Gist.

We’ll use this class to store metrics from each test and print the report. Along with another cool feature of JUnit 5 extensions, called ExtensionContext, we’ll implement a way to safely store state between tests (and even test suites).

STITCHING EVERYTHING TOGETHERWe’ll update our ReportingExtension to manage this state and configure parameter injection so all our tests are able to leverage this state in order add their own results:

View the code on Gist.

Here’s what we’ve added:

  • Initialization: Before all tests, we initialize the ExtensionContext with a new ReportingState allowing us to reference it between tests runs safely.
  • Parameter Injection: We enable parameter injection to pass ReportingState into test methods.
  • Final Reporting: After all tests, we output the collected metrics. Creating the Test ClassesNow, let’s create two test cases—one for PostgreSQL and one for SQLite. Each one will be calling the same endpoint that returns metrics, which we’ll add to our ReportingState.

Metrics Test ClassView the code on Gist.

What we’ve done:

  • ReportingExtension is registered at class level so it’s available for both our test cases.
  • Each test cases is also extended with the specific database extension that we need.
  • The ReportingState is injected into our test methods allowing us to populate it with results

Running the Tests and Viewing the ReportWhen we run these tests, the ReportingExtension will generate a report that includes metrics from both PostgreSQL and SQLite tests. Here’s what the output might look like:

---- Test Results ----testPostgresMetrics(ReportingState): PASSED in 33 ms testSqliteMetrics(ReportingState): PASSED in 20 mssqlite:{ "averageTime" : 0.4350518160108173, "meanTime" : 0.8010830012824306, "maxTime" : 0.2139154359220964, "minTime" : 0.7566418082724364}postgres:{ "averageTime" : 0.8143944973374195, "meanTime" : 0.22552853357243552, "maxTime" : 0.5854190992149244, "minTime" : 0.3479001940740827} This gives us a clear comparison of the metrics for each database type, highlighting how powerful JUnit 5 extensions can be for managing complex testing requirements.

By enhancing our ReportingExtension with parameter injection and state management, we’ve made our tests more insightful. Injecting shared state simplifies the test logic and allows us to generate comprehensive reports that go beyond simple pass/fail results.

Feeling inspired?It’s pretty straightforward to extend this approach to other needs in our projects. For example, one thing I’ve found handy is having access to the database instance within our tests for more granular control or data setup. Now that we’ve streamlined our database extensions, I’ll let you get your hands dirty and tweak them to inject the database object right where you need it.

Give it a try, see how it elevates your testing experience and share with the world what other cool ways of using extensions you’ve come up with.


ConclusionJUnit 5 extensions have pretty much transformed the way I design and write tests. With these amazing capabilities of encapsulating repetitive configurations and setups into reusable components, my tests get cleaner, more readable, and easier to maintain.

Remember, the goal is to focus on what matters: your test logic. Let Extensions handle the heavy lifting, making it easier to standardize testing practices and keep your codebase tidy.

Keep in mind that they also scale effortlessly across projects, making them a valuable asset for teams aiming to standardize testing practices and keep their codebases tidy.

So why not give it a go? After all, better tests lead to better software.

All examples from this article are publicly available on github.

Java love.

The post Leveraging JUnit5 Extensions for Greater Flexibility appeared first on JVM Advent.

View Details

All I want for Xmas is ChicoryINTROIn a lot of cultures, during Christmas time, Santa would distribute toys and presents to the good kids around. If you are reading Java Advent Of Code, that means that you care about Java and I’m sure you are a good one who deserves a present!

Let me be your Santa today and I’ll show you a new shiny present:

From the outside, the box looks small. We are not going to find any full-fledged framework to write Enterprise applications.

On the front, there are pictures of mythical integrations and fancy plugin systems, but a note on the back catches the attention: “requires assembly”. Gotcha, this is the kind of educational toy that requires us to read the guide to make some sense out of the pieces.

Are you ready for the unboxing?

Here you have a fancy early version of a Wasm interpreter called Chicory!

Whaaaat????

What does that even mean?

Don’t get upset, the instructions are detailed, let’s go through them together.

WebAssembly (Wasm)From Wiki we can read:

defines a portable binary-code format and a corresponding text format for executable programs as well as software interfaces for facilitating interactions between such programs and their host environment.

Ok, I get it, it’s another bytecode format to express programs, but how and why should we use it? Is the JVM bytecode not good enough?

Wasm has some different characteristics that give it an advantage over the JVM for some use cases. Because it was born on the web, Wasm has a sandboxed memory model which prevents modules from reading memory or jumping to code outside of their own scope. And by default, Wasm will not allow you to access any system resources such as files or networks. Wasm also has no concepts of objects or heap and that means it can run low-level languages very efficiently. This makes Wasm an ideal candidate for running untrusted / third party code written in a variety of languages.

Included piecesWasm has something like an assembly language that maps directly to its instructions. You can write this by hand, but like Assembly, you typically don’t. Fortunately, we do have much better and higher level languages to solve the nitty-gritty problems of the low-level for us.

Many of the most popular languages are starting to offer the possibility of TARGETING WASM, translating your program written in a high-level programming language into a series of Wasm instructions.

Often, the instructions for educational games are informative, but you don’t truly understand it until you play it yourself. Let’s connect the first pieces and compile a simple program to Wasm.

The program:

**const VOWELS: &[char] = &['a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U'];****#[no\_mangle]****pub extern fn count\_vowels() -> i32 {** **let s = String::from("Hello World!");** **let mut count: i32 = 0;** **for ch in s.chars() {** **if VOWELS.contains(&ch) {** **count += 1;** **}** **}** **count****}** Compile with:

**rustc count\_vowels.rs --target=wasm32-unknown-unknown --crate-type=cdylib -C opt-level=0 -C debuginfo=0 -o count\_vowels.wasm** For simplicity, you can use the provided Dockerfile.

Now that we have a .wasm file to try things out we need “something” to run the instructions.

Without getting too fancy and cutting nuances off we have two main options:

  • Another compiler: This should be able to take the wasm format and translate it to machine code that a computer can run (e.g. wasm2c)
  • An interpreter: This will process the content of the wasm file directly executing the instructions.

Chicory, as of today, is a Pure Java (no dependencies other than the Java standard library) interpreter and there are tradeoffs to consider here:

  • Compiler:
    • PRO: fast execution
    • CONS: needs a “developer” toolchain to be executed, more opportunity for security vulnerabilities
  • Interpreter:
    • PRO: self-contained and easily portable, less opportunity for security vulnerabilities
    • CONS: can be slow for heavy compute programs

More specifically a compiler can apply optimizations and transformations before emitting the target output, usually making the resulting binary better in terms of speed and efficiency.

An interpreter instead can directly run Wasm code without the need for additional toolchains, this makes it a natural fit for running arbitrary, user-defined functions unknown at application compile time.

Chicory is doing a direct mapping from the Wasm format to the JVM’s native primitives, which doesn’t need any advanced techniques like introspection or reflection, thus making it a great candidate for embedding in GraalVM native-image binaries.

Using our experience and test suite of the interpreter engine, we’re also working on a compiler from Wasm to JVM Bytecode. As demonstrated in the Go ecosystem with Wazero both an interpreter and a compiler are extremely useful for different use cases.

Now, let’s stop looking at the separately sold additional pieces of our present and keep reading the instructions

Usage instructionsWhen we start thinking about the possible use cases for a Wasm interpreter, the sky’s the limit. But, let’s look closer at the examples to see how we can employ such technology.

  • Enable polyglot plugin systems
    Kroxylicious is a pluggable proxy for Kafka, it enables you to write “filters” in Java to perform various kinds of operations, for example, data manipulation of the messages. In this example you can see how to plug in the Chicory interpreter and automagically offer support for plugins written in other languages. Extism is another great example of an ultra-portable plug-in system.
  • Reuse of libraries from different ecosystems
    It’s not always desirable to rewrite a library in multiple different languages, for the sake of speed, correctness and maintenance.
  • Dynamic functions execution
    GraalVM native-image makes it viable for Java programs to be compiled into static binaries. The downside is that the usage of reflection becomes an obstacle and should be configured Ahead Of Time. This takes out a good slice of “dynamicity”. In this example we are including Chicory in a statically built CLI binary that can execute user-provided code provided via the command line.

So, the pictures are nice, now let’s try to write and run an example on our own.

Now we are going to write a Quarkus Web Server from scratch that will expose two endpoints:

  • Load code dynamically
  • Execute the loaded code on the user input

You can think of it like a mini Java-powered “functions as a service” platform. And with native image, we will be compiling it to native binaries so it should be fast.

Step-by-step Install the Quarkus CLI and create a standard Quarkus application: **quarkus create** This command will automatically scaffold a full-blown server-side application in the folder code-with-quarkus , so open it with your favorite IDE/Editor and we can start to play! * Fix the dependencies in pom.xml* as shown in this diff:

    • use plain RestEasy (as opposed to the default reactive version)
    • add the dependency on Jackson to handle Json objects from your API
    • add the dependency on the Chicory runtime! ( as described in the project Readme )
  • Remove the default content in src/main/java/org/acme and scaffold your implementation with the content available here.In the code we are defining a stateful “Service” shared between 2 “Resource”s depending on it:
    • WasmResource defines one endpoint able to receive a wasm file in binary format
    • ComputeResource is the endpoint that will provide the final “functionality” executing the last wasm function uploaded against the user input
    • WasmService example API exposes two methods to perform the desired operations

**@ApplicationScoped****public class WasmService {** **private Module module;** **public void setModule(InputStream module) {** **???** **}** **public int compute(int content) {** **???** **}****}** * Fill the empty WasmService implementation to finally perform the desired actions using Chicory’s primitives:

**@ApplicationScoped****public class WasmService {** **private Module module;** **public void setModule(InputStream module) {** **this.module = Module.build(module);** **}** **public int compute(int content) {** **if (this.module == null) {** **throw new IllegalArgumentException("The WASM program have not been set, please do it!");** **}** **var instance = module.instantiate();** **var exportedFunction = instance.getExport("exported\_function");** **var result = exportedFunction.apply(Value.i32(content));** **return result[0].asInt();** **}****}** * + setModule takes an InputStream and builds a Wasm Module out of it, reading the structure and the instructions contained. The Module will be stored into an example local variable to be reused. + compute is a method that takes a simple user input (an int !) and performs the rest of the operations: - instantiate the module to be ready to run - find a declared function named “exported_function” - invoke the function passing the user content to it - return the result as an “int” * Congratulations! You have just written your first application server leveraging a Wasm interpreter! Let’s test it out by running the server in a shell:

**mvn quarkus:dev** Spin another shell and use the endpoints:

let’s first upload a Wasm module, to try things out in an easy way you can use the pre-built example (feel free to modify and recompile it!):

**curl -sL https://raw.githubusercontent.com/andreaTP/first-chicory-blog/main/dynamic-loading/example.wasm --output example.wasm** and upload it to the running web-server:

**curl -H 'Content-Type: application/octet-stream' -X POST --data-binary @example.wasm http://localhost:8080/wasm** finally, we can call the “compute” endpoint:

**curl -v 'http://localhost:8080/compute' -H 'Content-Type: application/json' --data-raw '41'** and we should receive the long-awaited answer:

**{"value":42}** as the compiled example is simply adding 1 to the user input.

  • To make sure that there is nothing that depends on the development environment we can now containerize our application. Drop a file named Dockerfile.native-scratch in the src/main/docker folder of our Quarkus application, and fill it with the following content (adapted from the official documentation):

***## Stage 1 : build*FROM quay.io/quarkus/ubi-quarkus-graalvmce-builder-image:jdk-21 AS build****USER root****RUN microdnf install make gcc****RUN mkdir /musl && \** **curl -L -o musl.tar.gz https://more.musl.cc/11.2.1/x86\_64-linux-musl/x86\_64-linux-musl-native.tgz && \** **tar -xvzf musl.tar.gz -C /musl --strip-components 1 && \** **curl -L -o zlib.tar.gz https://www.zlib.net/zlib-1.3.tar.gz && \** **mkdir zlib && tar -xvzf zlib.tar.gz -C zlib --strip-components 1 && \** **cd zlib && ./configure --static --prefix=/musl && \** **make && make install && \** **cd .. && rm -rf zlib && rm -f zlib.tar.gz && rm -f musl.tar.gz****ENV PATH="/musl/bin:${PATH}"****USER quarkus****WORKDIR /code****COPY . .****RUN ./mvnw package -Dnative -DskipTests -Dquarkus.native.additional-build-args="--static","--libc=musl"*****## Stage 2 : create the final image*****FROM scratch****COPY --from=build /code/target/*-runner /application****EXPOSE 8080****ENTRYPOINT [ "/application" ]** Build the container image:

**docker build -f src/main/docker/Dockerfile.native-scratch -t chicory/getting-started .** And run it:

**docker run -i --rm -p 8080:8080 chicory/getting-started** You can notice that the final image is built FROM scratch making sure that our application will not be accessing any system-level resource. You can exercise the built image by using the same curl demo commands provided before.

Pretty cool, right?

Now you have a toy server that can run your users’ Wasm code against the user input running as a native-image !

You can find all the code we used in this exercise here.

NextThanks for bearing with these instructions, I hope that you are reading this final comment while basking, feeling accomplished, with a working example.

We can silently leave the room, to get back, heads down, implementing the last details of the WebAssembly specification.

This year we have built a little rocket toy, but we strongly believe that this innovation brings great benefits to Java and the ecosystem and we are looking at landing on the Moon and making this project a solid building block for the Java applications of tomorrow!

What else?Demo repo:​​https://github.com/andreaTP/first-chicory-blog

Acknowledgment/Further reads:Edoardo Vacchi has written a series of 2 blog posts to track the progress of Wasm-related projects especially targeting Java developers:

  • https://www.javaadvent.com/2022/12/webassembly-for-the-java-geek.html
  • https://www.javaadvent.com/2023/12/a-return-to-webassembly-for-the-java-geek.html

Similar projects:GraalVM implementation of WebAssembly objectives are pretty much the same as Chicory’s, and this implementation is, at the time of writing, somehow more mature and complete.

Chicory’s differentiates itself for a few, but we believe compelling, reasons:

  • Zero dependencies: Chicory’s doesn’t need any additional dependency(other than itself obviously)
  • No platform lock-in: Chicory’s can, and will always, run on any compatible implementation of the JVM as it’s not based on any opinionated framework. The post A zero dependency Wasm runtime for the JVM appeared first on JVM Advent.

View Details

We live immersed in social networks. And we are deeply dependent on them. Because of that, your reputation has practical impact in your life and your career. That makes Reputation one of your most precious assets you have in life. Reputation is more important than your job, house and even money. And it directly affects how you get or maintain all those things.

Reputation is what people believe about you. Although it is not equal to trust, your reputation has a strong relationship with how people trust you and your capabilities.

Although you don’t have full control of how people see you, you nonetheless are able to strongly influence it.

The reputation formula helps you identify the things you can do to increase your reputation in your technology career. It also highlights some things that you can prevent, to reduce the chance of a building a bad reputation.

Let’s examine the formula. We can start with things you can do to increase your Reputation.

7 Things that Grow Your ReputationFocusBecause reputation is what people believe about you, a clear focus helps others understand what you do, and how you can be helpful.

That said, focus is not a technology or even one thing that you do. Focus is the perceived value you can bring to the table, or, what problems you are able to solve.

To increase your reputation, try to articulate clearly what is the problem that you solve, and who benefits from what you do. Having that clear vision will help you have a bigger reputation among those that matter to what you do.

VisibilitySince reputation is what people believe about you, people need to know about you for you to have a reputation.

That does not mean that you need to be a public figure or a rock star. You need to be visible in the social networks that are important for you.

To build visibility, articulate your message to people that your focus can help. Make sure to integrate yourself with social networks that need and value what you can do.

ActionPeople that only talk and don’t take action have a low reputation. No one can count on them to solve problems or help the social networks they are part of. Taking action, and gaining experience by doing things, is fundamental to building your reputation.

To increase your reputation, take action on your focus. Make sure you solve problems for you and for others. Become active in your social groups, and make yourself available to take the needed actions.

SkillIn the technical world, skill and competence are highly regarded, and play a strong role in how people see you.

Skill and focus go hand in hand. It is hard to associate and identify competence when it is fuzzy and dispersed.

To build your skills, first identify the most important ones related to your focus. Practice them slightly beyond your comfort zone. Force yourself to the point of making mistakes during practice. Yet, keep the mistakes at a controllable level, so you can work inside the sweet spot of skill acquisition.

Be a GiverPeople are more receptive to building relationships with those that are helpful. Reputation is associated with the social networks you are part of. Be it your family, friends, work, or social groups. That makes building strong relationships a way to boost your reputation.

Try to be helpful, and support people around you. Use your focus as a guideline on what and how you can help others. Be helpful around your focus will help you increase your skills and your visibility. It will also show your capacity to take action.

ResponsibilityTrustworthy people take responsibility for their actions and their commitments. Being responsible increases your reputation and the trust people deposit in you.

Keep on top of what you have committed to do, and make sure you put your share of the effort. Take responsibility for the biggest problem you are able to help with, don’t hide failures, and make sure to ask for help when needed. Also, share the results with others. Those are amazing reputation builders.

ConsistencyWhen people around you know and are comfortable with how you behave and know what to expect from you, they will see you as consistent and trustworthy. That can be a huge boost to your reputation.

Try to keep consistency in how you treat people, and how you respond to the highs and lows of life, especially in a professional setting. Keep your anger under control and refrain from lashing out. Those inconsistencies are very damaging for your reputation.

Doing those 7 things will show you in a good light, and will influence your reputation in a positive direction. On the other hand, there are things that their mere existence can damage your reputation. Avoid those things as much as possible.

3 Things That Destroy Your ReputationMe me mePeople don’t trust those that only care about themselves. People that only focus on their own needs, and don’t care for the good of the social networks they are part of, will have a lower reputation.

The easy way to spot that are people that talk too much about themselves, that are too much of a “me, me, me” person.

Make sure you focus on others, listen to what they say and care for them. When speaking, avoid talking too much about yourself.

LiesA person that is caught lying loses their reputation very fast, and the news spreads out through the social network.

Always speak the truth, or at least refrain from lying. Specially, drop those small lies that only serve to damage your reputation. Things like the reason why you are late for a meeting, or why you didn’t come to work yesterday. Speaking the truth, or simply apologizing without elaborated false explanation, will be better.

DifficultNo one likes to work with difficult people… Remember Sheldon, from the Big Bang Theory? Don’t be like him.

Being intransigent. Insisting you are only “speaking the truth.” Not accepting others opinions. Exploding in anger. Avoid all those things that only make people not want to work with you, and bring no value to anyone.

How does the formula work?All that said, the formula compensates things to a certain degree.

You may be just a little bit visible, yet, take huge responsibility and have amazing skills.

You may not be totally consistent, but you are a strong giver and take lots of focused action.

It is even possible to be a little difficult person to work with. Although that will decrease your reputation, you can compensate for that. For example, with a high level of skill, responsibility and giving help.

Yet… This is just to a certain extent.

If you have zero visibility inside your social groups, your high skill and focus will bring you no results, because no one will see it.

If you are such a difficult liar, that no one wants to work with you… It does not matter how much skill you have, it won’t give you the benefits of a good reputation.

Last, but not least, the Career Reputation Formula focuses on a positive reputation. It is certainly possible to build a negative reputation. If you go that route, even the good things you do will be seen as bad faith and opportunism. I don’t recommend that.

Building a good reputation in your career opens doors. It gets you invited to the best jobs, puts you working together with the top teams and solving the most interesting problems.

All this helps your reputation to grow even more, creating a positive spiral for your career.

If you want to build your career and your reputation, this year me and Heather VanCura launched our book, “Developer Career Masterplan”. You can get a FREE copy of the first chapter of the book — The Secret to Learning about Technology Quickly and Continuously.

Want to get a bit of positive visibility? How about sharing in the comments below ideas you plan to put in place to improve your reputation? I’ll try to help you speed up that!

The post The Reputation Formula – 10 steps to Turbocharge Your Technical Career appeared first on JVM Advent.

View Details

Java 21 has introduced a groundbreaking feature called String Templates. This new feature, which is part of JEP 430, changes the way Java handles strings by combining literal text with dynamic expressions and template processors. This is perfect for situations where you need to use values that are calculated at runtime or entered by the user. String Templates revolutionize the way strings are handled in Java applications. In this article, we will take a closer look at String Templates.

Delving into Template ExpressionsAt the core of String Templates is a new syntax for Java called ‘template expressions’. This syntax enables efficient and secure string interpolation, going beyond simple string concatenation to transform structured text into various object types based on predefined rules.

For instance:

String userName = "Bazlur";String greeting = STR."Welcome, \{userName}";assert greeting.equals("Welcome, Bazlur"); // evaluates to true The Role of STR Template ProcessorThe STR processor, a key component of Java’s string interpolation toolkit, skillfully replaces embedded expressions within templates, converting them into strings. For example, this capability could be particularly useful for creating structured HTML:

``` String pageTitle = "About Us";String message = "Welcome to our site";String htmlContent = STR.""" {pageTitle} {message}

                      """;

``` This example highlights the versatility of template expressions in producing dynamic and secure HTML content.

The FMT Template Processor: A Formatting PowerhouseComplementing STR is FMT, a fellow template processor that boasts enhanced formatting capabilities. FMT’s distinguishing feature lies in its ability to interpret format specifiers similar to those found in java.util.Formatter. This trait makes FMT an ideal choice for generating structured and formatted outputs.

Consider this scenario:

record Building(String name, double length, double width) { double footprint() { return length * width; }}Building[] buildings = { new Building("Tower", 20.5, 15.75), new Building("Warehouse", 40.0, 22.3), new Building("Office", 30.1, 18.6),};String buildingTable = FMT.""" Name Length Width Footprint %-15s\{buildings[0].name} %7.2f\{buildings[0].length} %7.2f\{buildings[0].width} %10.2f\{buildings[0].footprint()} %-15s\{buildings[1].name} %7.2f\{buildings[1].length} %7.2f\{buildings[1].width} %10.2f\{buildings[1].footprint()} %-15s\{buildings[2].name} %7.2f\{buildings[2].length} %7.2f\{buildings[2].width} %10.2f\{buildings[2].footprint()} """;Output: Name Length Width FootprintTower 20.50 15.75 322.88Warehouse 40.00 22.30 892.00Office 30.10 18.60 559.86 This example effectively demonstrates FMT’s proficiency in handling intricate string outputs, such as those found in tabular presentations.

Embracing Custom Template ProcessorsJava 21 extends its capabilities by introducing custom template processors. This flexibility empowers developers to customize string manipulation according to their specific needs.

Consider the following example of a custom processor:

var TEMP = StringTemplate.Processor.of((StringTemplate st) -> { StringBuilder result = new StringBuilder(); Iterator fragmentIterator = st.fragments().iterator(); for (Object value : st.values()) { result.append(fragmentIterator.next()); result.append(value); } result.append(fragmentIterator.next()); return result.toString();});double temperature = 36.5;String healthStatus = TEMP."Body temperature: \{temperature}°C";//Body temperature: 36.5°C In this example, TEMP adeptly handles the combination of text fragments and dynamic values to produce a coherent string.

Let’s explore some additional examples:

Concatenating Strings with a Delimiter (DELIM):

This processor is designed to concatenate a collection of strings, such as elements from a list, using a specified delimiter like a comma.

var DELIM = StringTemplate.Processor.of((StringTemplate st) -> { String delimiter = ", "; var values = st.values(); return values.stream() .flatMap(it -> { if (it instanceof List item) { return item.stream(); } else return Stream.empty(); }).map(String::valueOf) .collect(Collectors.joining(delimiter));});List fruitNames = List.of("Apple", "Banana", "Cherry");String fruits = DELIM."\{fruitNames}";System.out.println(fruits);//Apple, Banana, Cherry The outcome is a well-formatted, comma-separated string, ideal for presenting lists in a clear and concise manner. Although such strings can be created using various techniques, defining a dedicated String processor offers the benefit of reusability. Once established, this processor can be effortlessly incorporated into any part of the source code, aligning with the DRY (Don’t Repeat Yourself) principle. This approach not only simplifies coding but also promotes consistency and efficiency throughout the application.

Formatting Dates (DATE_FORMAT):

This processor transforms LocalDate objects into formatted string representations, utilizing DateTimeFormatter.

var DATE\_FORMAT = StringTemplate.Processor.of((StringTemplate st) -> { var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); return st.values().stream() .map(value -> ((LocalDate) value).format(formatter)) .collect(Collectors.joining(", "));});LocalDate date1 = LocalDate.of(2023, 3, 15);LocalDate date2 = LocalDate.of(2023, 4, 20);String formattedDates = DATE\_FORMAT."\{date1} \{date2}";System.out.println(formattedDates);//2023-03-15, 2023-04-20 The output demonstrates the processor’s ability to convert date objects into a standardized, human-readable date format.

Building a JSON Object (JSON_BUILDER):

Aimed at creating JSON structures dynamically, this processor pairs each fragment with a value to construct a JSONObject.

``` var JSON_BUILDER = StringTemplate.Processor.of((StringTemplate st) -> { JSONObject json = new JSONObject(); Iterator valueIterator = st.values().iterator(); for (String key : st.fragments()) { if (valueIterator.hasNext()) { json.put(key.trim(), valueIterator.next()); } } return json;});String product = "Laptop";double price = 999.99;JSONObject productJson = JSON_BUILDER."product: {product}, price: {price}";System.out.println(productJson);//{"product:":"Laptop",", price:":999.99}This example illustrates the processor’s capability to efficiently generate JSON objects, which is useful in data interchange and API interactions.

Generating XML Elements (XML_GEN): This processor is particularly adept at creating XML structures from Java objects, wrapping each value in XML tags.

var XML\_GEN = StringTemplate.Processor.of((StringTemplate st) -> { var xml = new StringBuilder(""); var values = st.values(); if (!values.isEmpty()) { if (values.getFirst() instanceof List items) { items.forEach(value -> xml.append("") .append(value) .append("")); } } xml.append(""); return xml.toString();});List book = List.of("Book", "Pen", "Notebook");String items = XML\_GEN."\{book}";System.out.println(items);//BookPenNotebook The output exemplifies the processor’s utility in generating structured XML content, which can be essential for data representation and communication in XML-based systems.

Note:

String Template is a preview feature. Here’s how you can enable and use preview features:

Compile with Preview Features: When compiling your Java code, use the --enable-preview flag with the javac command. For example:
javac –enable-preview –release 21 YourJavaFile.java

Run with Preview Features: Similarly, when running your Java application, include the --enable-preview flag with the java command. For example:
java –enable-preview YourJavaFile

IDE Support: If you’re using an Integrated Development Environment (IDE), ensure it supports the latest Java version. Most modern IDEs like IntelliJ IDEA, Eclipse, or Visual Studio Code have options to enable preview features in their project settings.

ConclusionThe introduction of String Templates marks a significant leap forward in Java’s string manipulation capabilities, providing developers with a powerful and versatile toolkit for crafting secure and efficient strings. This new feature is sure to have a transformative impact on Java programming practices.

The post Beyond the Basics: Elevating Java String Handling with Custom Template Processors appeared first on JVM Advent.

```

View Details

As per the release schedule, Mark Reinhold, Chief Architect, Java Platform Group at Oracle, formally declared that JDK 22 has entered Rampdown Phase One. This means that the main-line source repository has been forked to the JDK stabilization repository and no additional JEPs will be added for JDK 22. Therefore, the final set of 12 features, in the form of JEPs, for the GA release in March 2024 will include:

  • JEP 423: Region Pinning for G1
  • JEP 447: Statements before super(…) (Preview)
  • JEP 454: Foreign Function & Memory API
  • JEP 456: Unnamed Variables & Patterns
  • JEP 457: Class-File API (Preview)
  • JEP 458: Launch Multi-File Source-Code Programs
  • JEP 459: String Templates (Second Preview)
  • JEP 460: Vector API (Seventh Incubator)
  • JEP 461: Stream Gatherers (Preview)
  • JEP 462: Structured Concurrency (Second Preview)
  • JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview)
  • JEP 464: Scoped Values (Second Preview)

This final set of features can be separated into four categories: Core Java Library, Java Language Specification, HotSpot/GC and Java Tools. Please join me in a journey to explore these new features in their respective categories and where they fall under the auspices of the four major Java projects – Amber, Loom, Panama and Valhalla – designed to incubate a series of components for eventual inclusion in the JDK through a curated merge.

Six of these features are categorized in the Core Java Library:

JEP 464, Scoped Values (Second Preview), under the auspices of Project Loom and formerly known as Extent-Local Variables (Incubator), proposes to re-preview the API in JDK 22, without change, in order to gain additional experience and feedback from the previous round of preview, JEP 446, Scoped Values (Preview), delivered in JDK 21, and JEP 429, Scoped Values (Incubator), delivered in JDK 20. This feature enables sharing of immutable data within and across threads. This is preferred to thread-local variables, especially when using large numbers of virtual threads.

JEP 462, Structured Concurrency (Second Preview), also under the auspices of Project Loom, proposes to re-preview this API in JDK 22, without change, in order to gain more feedback from the previous round of preview: JEP 453, Structured Concurrency (Preview), delivered in JDK 21. This feature simplifies concurrent programming by introducing structured concurrency to “treat 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.”

JEP 461, Stream Gatherers (Preview), proposes to enhance 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.” More details on this JEP may be found in the original design document written by Viktor Klang, Software Architect, Java Platform Group at Oracle.

JEP 460, Vector API (Seventh Incubator), under the auspices of Project Panama, incorporates enhancements in response to feedback from the previous six rounds of incubation: JEP 448, Vector API (Sixth Incubator), delivered in JDK 21; JEP 438, Vector API (Fifth Incubator), delivered in JDK 20; JEP 426, Vector API (Fourth Incubator), delivered in JDK 19; JEP 417, Vector API (Third Incubator), delivered in JDK 18; JEP 414, Vector API (Second Incubator), delivered in JDK 17; and JEP 338, Vector API (Incubator), delivered as an incubator module in JDK 16. The most significant change from JEP 448 includes an enhancement to the JVM Compiler Interface (JVMCI) to support Vector API values.

JEP 457, Class-File API (Preview), proposes to provide an API for parsing, generating, and transforming Java class files. This will initially serve as an internal replacement for ASM, the Java bytecode manipulation and analysis framework, in the JDK with plans to have it opened as a public API. Brian Goetz, Java language architect at Oracle, characterized ASM as “an old codebase with plenty of legacy baggage” and provided background information on how this draft will evolve and ultimately replace ASM. Further details on JEP 457 may be found in this InfoQ news story.

JEP 454, Foreign Function & Memory API, also under the auspices of Project Panama, proposes to finalize this feature after two rounds of incubation and three rounds of preview: JEP 412, Foreign Function & Memory API (Incubator), delivered in JDK 17; JEP 419, Foreign Function & Memory API (Second Incubator), delivered in JDK 18; JEP 424, Foreign Function & Memory API (Preview), delivered in JDK 19; JEP 434, Foreign Function & Memory API (Second Preview), delivered in JDK 20; and JEP 442, Foreign Function & Memory API (Third Preview), to be delivered in the upcoming GA release of JDK 21. Improvements since the last release include: a new Enable-Native-Access manifest attribute that allows code in executable JARs to call restricted methods without the use of the –enable-native-access flag; allow clients to programmatically build C function descriptors, avoiding platform-specific constants; improved support for variable-length arrays in native memory; and support for multiple charsets in native strings. More details on JEP 454 may be found in this InfoQ news story.

Four of these features are categorized in the Java Language Specification and under the auspices of Project Amber:

JEP 463, Implicitly Declared Classes and Instance Main Methods (Second Preview), formerly known as Unnamed Classes and Instance Main Methods (Preview), Flexible Main Methods and Anonymous Main Classes (Preview) and Implicit Classes and Enhanced Main Methods (Preview), incorporates enhancements in response to feedback from the previous round of preview, namely JEP 445, Unnamed Classes and Instance Main Methods (Preview). This JEP proposes to “evolve the Java language so that students can write their first programs without needing to understand language features designed for large programs.” This JEP moves forward the September 2022 blog post, Paving the on-ramp, by Brian Goetz, Java language architect at Oracle. Gavin Bierman, consulting member of technical staff at Oracle, has published the first draft of the specification document for review by the Java community. Further details on JEP 445 may be found in this InfoQ news story.

JEP 459: String Templates (Second Preview) provides a second preview from the first round of preview: JEP 430, String Templates (Preview), delivered in JDK 21. This feature enhances the Java programming language with string templates, string literals containing embedded expressions, that are interpreted at runtime where the embedded expressions are evaluated and verified. More details on JEP 430 may be found in this InfoQ news story.

JEP 456, Unnamed Variables & Patterns, proposes to finalize this feature after one previous round of preview: JEP 443, Unnamed Patterns and Variables (Preview), delivered in JDK 21. This feature will “enhance the language with unnamed patterns, which match a record component without stating the component’s name or type, and unnamed variables, which can be initialized but not used.” Both of these are denoted by the underscore character as in r instanceof _(int x, int y) and r instanceof _.

JEP 447, Statements before super(…) (Preview), under the auspices of Project Amber, proposes to: allow statements that do not reference an instance being created to appear before the this() or super() calls in a constructor; and preserve existing safety and initialization guarantees for constructors. Gavin Bierman, consulting member of technical staff at Oracle, has provided an initial specification of this JEP for the Java community to review and provide feedback.

One of these features is categorized in Hotspot/GC:

JEP 423, Region Pinning for G1, proposes to reduce GC latency by implementing region pinning to the G1 garbage collector. This will extend G1 so that arbitrary regions may be pinned during both major and minor collection operations so that disabling the garbage collection process may be avoided while implementing JNI critical regions.

And finally, one of these features is categorized in Java Tools:

JEP 458, Launch Multi-File Source-Code Programs, proposes to enhance the Java Launcher to execute an application supplied as one or more files of Java source code. This allows a more gradual transition from small applications to larger ones by postponing a full-blown project setup.

ConclusionWith 15 new features delivered in JDK 21, JDK 22 will follow that up nicely with 12 new features. As shown in Figure 1, the number of features dropped by almost half after the release of JDK 17.

Figure 1: The OpenJDK Release Cadence

Two features in JDK 22: JEP 454, Foreign Function & Memory API; and JEP 456, Unnamed Variables & Patterns, are now finalized after having spent their respective times as incubating or preview features.

Thanks for taking this journey with me into JDK 22 and Happy Holidays!

The post What Developers Can Expect in JDK 22 appeared first on JVM Advent.

View Details

Java is an amazing language with lots of features, performance, and its ecosystem. This coming from a Java developer, intersect that with AI use cases, and suddenly, questions can be asked is Java suitable for AI/ML workloads? Truth be told, the short answer is YES! In this article, I will highlight some of the latest advancements and areas we, the Java developer community, can feel at ease doing some of the most interesting things out there. This may give us all some inspiration to bridge and strengthen this intersection further in 2024.

Let’s start with the most interesting bits that have taken the tech world by storm. Although still in its infancy, LLMs have come to serious attention once OpenAI released ChatGPT for the masses. It has changed how we work, and how we search and ask questions. But most importantly, it has also given new opportunities when generating content for specific use cases. One of those areas is LangChain. It is originally a Python-based library. However, now also has a Java variant. Dmytro Liubarskyi, the author of LangChain4J, made his initial commit on June 20th this year, making it one of the most interesting projects for LLM-related work in Java space.

LangChainLangChain

LangChain is a framework that enables and enhances the use of LLMs for more use cases than just simplistic prompt engineering sent as questions to an LLM. It introduces concepts such as Chains for API and datasets that can be vectorized and shared with the LLM to give context. It enables context/memory and techniques like RAG (Retrieval Augmented Generation). All in all, it brings more to the basic model and enables all of us to write applications for interesting new use cases.

Let’s take a look at a basic example.

  1. Create an embedded store. In this example, the simple InMemoryStore is used. However, there are quite a few other options possible, e.g., Chroma, Redis, PgVector, etc.

EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>(); 1. A prevalent method for handling and searching through unstructured data involves embedding this data and saving the resultant vectors. When a query is made, the unstructured query is also embedded, as in our example, the documents are split based on segment size and stored in the vector store.

EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().documentSplitter(DocumentSplitters.recursive(500, 0)).embeddingModel(embeddingModel).embeddingStore(embeddingStore).build(); 1. The following code loads a document. As per our instructions, it will split this up and load it into the vector store.

Document document = loadDocument(toPath("example-files/story-about-happy-carrot.txt"));ingestor.ingest(document); 1. the program then retrieves those embedding vectors that closely match the query’s embedding. Essentially, a vector store is responsible for maintaining these embedded data records and executing vector-based searches on your behalf.

ConversationalRetrievalChain chain = ConversationalRetrievalChain.builder().chatLanguageModel(OpenAiChatModel.withApiKey(ApiKeys.OPENAI\_API\_KEY)).retriever(EmbeddingStoreRetriever.from(embeddingStore, embeddingModel))// .chatMemory() // you can override default chat memory// .promptTemplate() // you can override default prompt template.build(); 1. And finally, sending our query to the LLM

String answer = chain.execute("Who is Charlie?"); For more in-depth examples, follow the langchain4j-examples

QuarkusClement Escoffier, in his recent post on Quarkus.io, introduced the first version of the Langchain4J extension. Relying solely on the knowledge of a Large Language Model (LLM) might not suffice. Hence, the Quarkus LangChain4j extension introduces two features to augment AI capabilities for application developers using Quarkus. The extension uses the RegisterAIService annotation, similar to how REST applications are developed in Quarkus. With this annotation, developers can introduce simple functions like Memory, Beans, etc. Furthermore, the ability to inject Embeddings, Stores, and Ingestors. Another interesting annotation introduced is Tools, This lets the LLM invoke Quarkus as required e.g. by providing Tool to a method that calls on a Panache entity to access data from the database.

@ApplicationScopedpublic class CustomerRepository implements PanacheRepository<Customer> { @Tool("get the customer name for the given customerId") public String getCustomerName(long id) { return find("id", id).firstResult().name; }} LangChain4J is not the only Gen-AI support Quarkus can offer, it also integrates well with the Semantic Kernel. The reference superheroes Quarkus app showcases the use of Semantic Kernel. Let’s delve into Semantic Kernel.

Semantic KernelSemantic Kernel

To build awesome Gen-AI apps LangChain is not the only option out there. Earlier in July this year, Microsoft announced Semantic Kernel for Java, an opensource library similar to LangChain, specifically for use cases with Azure AI and OpenAI. This functionality empowers developers to utilize a variety of prompts as distinct skills, link these prompts together, and establish shared contexts for them. For developers, it also offers a framework for managing the prompting pipeline and applying specialized design patterns. SK supports prompt templating, function chaining, vectorized memory, and intelligent planning capabilities out of the box. One of the interesting features is the ability to add Skills to a program. Semantic Kernel keeps its compatibility to Java 8 which might be great for certain types of Java applications even though we by now have more language features up to JDK 21. One of the interesting bits of this framework is the ability to add Skills so prompts are enhanced and then the obvious integration to Memory, context etc similar to LangChain.

The following code is from the Quarkus superheroes app

  1. Creating a textCompletion function using the OpenAI client.

var textCompletion = SKBuilders.chatCompletion() .withOpenAIClient(this.openAIAsyncClientInstance.get()) .build(); 1. Creating the Semantic Kernel with the text completion

var kernel = SKBuilders.kernel() .withDefaultAIService(textCompletion) .build(); 1. Registering which skills will be used. A skill refers to a domain of expertise made available to the kernel as a single function, or as a group of functions related to the skill. A Function is represented by a “skprompt.txt” and optionally a “config.json”. In this case a NarrationSkill

var skill = kernel.importSkillFromResources("skills", "NarrationSkill", "NarrateFight"); Spring AISpring framework offers an alternative to LangChain and Semantic Kernel with Spring AI. Similar concepts as Memory, Prompts, Function chaining, Transformers, Retrievers etc. Spring AI also like LangChain enables the integrations with multiple LLMs such as HuggingFace. This bring more flexibility for the Java developers. For example for a simple RAG (Retrieval Augmented Generation)

  1. Loading documents

JsonLoader jsonLoader = new JsonLoader(bikesResource, "name", "price", "shortDescription", "description");List<Document> documents = jsonLoader.load(); 1. Adding the vectorized data to the Vector store

VectorStore vectorStore = new InMemoryVectorStore(embeddingClient);vectorStore.add(documents); 1. Similarity search

List<Document> similarDocuments = vectorStore.similaritySearch(message); The detailed example can be found here

These advancements demonstrate the growing integration of LLMs in Java applications, showcasing AI-enhanced capabilities in practical applications. There is more to come. Java’s role in the enterprise and the evolution process that it has been through brings so much more to this field. Take the example of Apache Camel or Mule, bringing more integrations into this space is a strength of Java, and would be great to see more of it next year.

That’s some of the amazing things to look forward to in 2024 and get our hands dirty with.

However one might ask, can Java do more in the areas of AI/ML? Let’s take a look at some of those advancements and how they can likely turn the industry to make use of the Java language in more ways than ever before.

Vectors

A vector computation consists of a sequence of operations on vectors. A vector comprises a (usually) fixed sequence of scalar values, where the scalar values correspond to the number of hardware-defined vector lanes. A binary operation applied to two vectors with the same number of lanes would, for each lane, apply the equivalent scalar operation on the corresponding two scalar values from each vector.

– JEP 448

Vectors are important for training model data primarily because of the performance optimization it brings. Imagine running training models and going through thousands of features * data input. The complexity is high, and if the operations were done scalar, it would take ages to complete. A good example is NumPy, a popular tool in the Python ecosystem that is used for vectorized operations enabling the handling and manipulating of data when training machine learning models. The ability to take advantage of SIMD(Single Instruction, Multiple Data) instructions of modern CPUs or GPU acceleration boosts the performance of the training process.

In recent years Java has made advancements in these areas and continues to do so. The Java Vector API was first proposed in JEP 338 and integrated into JDK 16 as an incubation API. As of JDK 22 – JEP 460 it’s in the incubation. Another notable improvement for this is Project Valhalla which aims to enhance Java’s object model, e.g., Value Classes and Objects

The following code is a basic example of 2 Vectors multiplied while they are split into multiple lanes by the mask defined. Once the multiplication is done. The vector is then again, this time multiplied with one value and stored in a variable named vm.

static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES\_PREFERRED;public static void vectorComputation(float[] a, float[] b, float[] c) { for (int i = 0; i < a.length; i += SPECIES.length()) { var m = SPECIES.indexInRange(i, a.length); // FloatVector va, vb, vc; var va = FloatVector.fromArray(SPECIES, a, i, m); var vb = FloatVector.fromArray(SPECIES, b, i, m); var vc = va.mul(va) .add(vb.mul(vb)) .neg(); vc.intoArray(c, i, m); System.out.println(vc); // multiply the whole vector with a single float var vm = vc.mul(5.0f); System.out.println(vm); }} Output: java --enable-preview --add-modules jdk.incubator.vector VectorExample

java --enable-preview --add-modules jdk.incubator.vector VectorExample[-2.0, -8.0, -18.0, -32.0, -0.0, -0.0, -0.0, -0.0][-10.0, -40.0, -90.0, -160.0, -0.0, -0.0, -0.0, -0.0] A detailed video with examples

GPU SupportGPU support is crucial for AI/ML workloads because of their parallel processing capabilities, computational power and optimization of the types of calcultations commonly encountered in machine learning tasks. GPU acceleration helps speedup training, reduce inference time and ultimately maxis AI/ML applications more efficient and practical. Good examples are Real-time interfaces like autonomous vehicles, image processing etc.

TornadoVM serves as a plugin for OpenJDK and GraalVM, enabling developers to offload JVM applications onto diverse hardware platforms, including multi-core CPUs, GPUs, and FPGAs. It’s important to note that this integration has not yet become an integral part of core Java frameworks. Alternatively, leveraging GraalVM to enhance the native experience and harness the potential of native instruction sets presents another approach.

Additionally, Project Babylon is driven by the objective of expanding Java’s applicability to diverse programming paradigms, encompassing SQL, differentiable programming, machine learning models, and GPUs. This endeavor opens up exciting new possibilities, making it a development worth monitoring.

SummaryThe article highlights Java’s remarkable evolution, especially its intersection with AI/ML, showing that Java is well-suited for AI/ML workloads. Key advancements like LangChain, initially a Python library and now available in Java as LangChain4J, have made Java a strong player in LLM-related work. LangChain enhances LLM usage beyond basic prompts by introducing chains, context, and memory techniques, allowing developers to create complex applications.

Quarkus’s LangChain4j extension and Microsoft’s Semantic Kernel (SK) for Java have further bolstered Java’s AI capabilities. These tools offer advanced features like prompt templating, function chaining, and memory management. Moreover, Spring AI provides a similar framework, enabling integration with multiple LLMs and offering flexibility for Java developers.

These advancements are not just current achievements but also pave the way for Java’s future in AI/ML. With the continuous development of Java’s ecosystem, including advancements in vector computations and GPU support, Java is poised to play a more significant role in AI/ML. This progression makes Java an increasingly attractive option for AI/ML developers, promising even more exciting developments in the future.

The post Java and the AI Frontier: Leveraging Modern Tools and Techniques for Machine Learning appeared first on JVM Advent.

View Details

Part of the success of Java can be attributed to how the language is evolved and how the developer community is collaboratively involved in the evolution. The Java Community Process (JCP) Program is the process by which the international Java community standardizes and ratifies the specifications for Java technologies. In December 2023, we celebrate 25 years of community collaboration in the JCP program, since it was first introduced at the Java for Business conference in New York City, December 1998. The announcement of the JCP was made by Alan Baratz, who was the president of JavaSoft at Sun Microsystems at the time. Jim Mitchell, Sun Labs Fellow at the time, was appointed as the first Director of the JCP program.

The JCP was formed to ensure that high-quality specifications are developed using an inclusive, consensus-based approach. Specifications ratified by the JCP program must be accompanied by a Reference Implementation (to prove the Specification can be implemented), and a Technology Compatibility Kit (a suite of tests, tools, and documentation that is used to test implementations for compliance with the Specification).

While the JCP has evolved over time from that first announcement of JCP to the current update of the JCP, there are has been consistency in the values and continued community collaboration.

The JCP ensures that the promises and values of Java technology deliver to the ecosystem performance. stability, security, as well as ensuring the compatibility and maintainability of Java code. As we look to address the challenges of modern application development, we want to ensure Java continues to deliver increased performance with each new release. Working together with the community, we balance conservatism with innovation through a thoughtful evolution of Java technology.

In commemoration of the 25 Year Anniversary of the JCP program, we created a timeline graphic. We also created a photographic video montage highlighting some of our community members and activities…. because we form a community around a shared interest in Java technology but community is about the people and our shared experiences.

1998 – JCP 1.0 – Created JCP, in which Sun approved requests and members contributed to specifications.

2000 – JCP 2.0 – Established Executive Committees to review and approve JSRs.

2002 – JCP 2.5 – Equal standing to individual developers participating n the JCP.

2004 – JCP 2.6 – Embraced open source, streamlined processes, opened up early drafts, license, and TCK terms to the public.

2009 – JCP 2.7 – Required transparency for public comments and issues.

2011 – JCP 2.8 – Introduced EC Standing Rules, required transparency in JSR communication, shortened JSR deadlines.

2011 – Adopt-a-JSR – Launched to include voices from JUGs around the world on Java standards.

2012 – JCP 2.9 – Merged the two ECs into one committee that votes on all JSRs.

2016 – JCP 2.10 – Broadened membership, introduced new types of membership, removed barriers to membership, added Contributors, and established three types of EC seats.

2018/2019 JCP 2.11 – Streamlined JCP program’s processes for agility and aligned with open source software development.

2020 – Java in Education – Launched initiative to inspire and educate the next generation of Java developers and bridge the gap between academia and industry.

The JCP Executive Committee (EC)The JCP EC plays a key role in the evolution of Java technology. oversees the development and evolution of the Java technologies within the JCP. The EC.was formed in 2000, and is elected every year by the JCP program membership. The JCP EC is overseen by the JCP Chairperson. We have had several JCP Chairs over time including George Paolini from: 2000-2002; Rob: Gingell from 2002- 2004l Onno Kluyt from : 2004 – 2007; Patrick Curran from 2007 – 2017; and Heather VanCura 2017 – present. The JCP EC meets six times a year, and two times in person, after resuming face to face meetings in 2023.

As of Java SE 10, we moved to a 6-month release cadence, with a release of the Java platform coming every 6 months, rather than every few years. Over the past 5 years, we have consistently delivered a new Java SE Platform release, starting with Java 10 through the latest Java SE 21 release in September 2023. With many changes in the Java community, the continuation of the JCP program remains constant. The values and focus on performance, stability, security, compatibility, and maintainability of code remains. Work on the platform is completed with the contributions and collaboration of the Java community working together, and as the work of the Java Enhancement Proposals (JEPs) is completed in OpenJDK, they are targeted for inclusion in a platform release and as part of a JSR in the JCP program, for ratification and approval for the JCP Executive Committee. This release process allows for thoughtful evolution, trust and predictability for developers and users of the technology, but also a rich pipeline of innovations being delivered continuously, driving adoption of the latest releases.

JCP Program Membership

Anyone can apply to join and participate in the JCP program — either as a Corporation or Non-Profit (Full Member), Java User Group (Partner Member) or Individual (Associate Member). The stability of the JCP program and participation from community members ensures continued success of the Java Platform and its’ future.

Corporate and Individual Membership in the JCP program has been established for quite some time, but participation and membership of Java User Groups is one of the increasing areas of engagement in the JCP Program. Nearly 100 JUGs participating are participating in the JCP program from all around the world. As mentioned above, JUGs can join the JCP program as Partner Members: https://jcp.org/en/participation/jug

As we celebrate the 25-year anniversary of the JCP Program, we will be partnering with JUGs around the world to host their own celebrations in their local communities. We started with a special event hosted by the New York Java Special Interest Group (NYJavaSIG) and Garden State Java User Group (GSJUG) in September, hosted by the Bank of New York (BNY) Mellon in New York City. Industry experts from the EC participated in a panel discussion sharing some of their JCP memories and their favorite features from the latest release of Java. In January, we will host another event with the San Francisco and Silicon Valley Java User Groups at the Computer History Museum in Mountain View, California. We will also present the annual JCP Program Awards – 2023 nominees.

Java in Education – Inspiring the next generation of Java developersIn 2020, the JCP started an initiative around Java in Education – inspiring the next generation of Java developers. We began encouraging Java community leaders to participate in inspiring the next generation of developers to code using Java by engaging with their local educational communities.

The purpose and focus are to help bridge the gap between the educational environment and industry. Together we can provide opportunities for students, teachers and educational institutions in the form of networking, mentoring, knowledge and professional internships, open-source assignments and projects. Java is the top in demand skill from employers for technical talent and the most highly paid technical skill. Once students are working on projects in industry, it is difficult to find a project that does not include or touch some Java code.

This effort is designed to be global, JUG led, and supported by the JCP program. There are materials for the community and w wiki to share experiences. One of the recent presentations available is based on the work of the recently completed JSR 381, Visual Recognition Specification, showcasing how you can use Java for ML and AI with images. When the JCP EC met in Singapore earlier this year, hosted by Alibaba, we met with the Singapore JUG for an extended Java Heroes event and also met with students at local universities to discuss this technology.

The JCP is more open than it has ever beenHow will you participate? As in individual is okay. It is even better as a team. You can help each other and work together in your Java User group or your team at your employer to make Java better. Working together we achieve more.

Why participate

  • Acquire knowledge. Learn from experts, early access. Enable an easier transition between releases. Be faster to market. Put your Requirements into process.
  • Build your resume. Add experience and skill development. Grow as developers. Communication, collaboration, negotiation, teamwork.
  • Increase professional visibility: curriculum, articles, workshops, presentations.
  • Become famous! Grow your reputation and network, as well as the reputation of your JUG.
  • Make Java better: Specifications based on real world experience are more successful. Gain new customers based on your expertise. Create the future Java technology.

Steps to Participation & Collaboration

  1. Pick a project. There are many options. You can pick a JSR out of the active JSRs list on JCP.org: https://jcp.org/en/jsr/stage?listBy=active

For example, the Java SE Platform JSR: https://jcp.org/en/jsr/detail?id=397

You can participate in OpenJDK by downloading the Early Access Builds and joining the Adoption Group of OpenJDK. Join the mailing list and then indicate your interest and feedback.

https://wiki.openjdk.java.net/display/Adoption

As part of the Adoption Group, there is also the Quality Outreach group. You can support FOSS Java projects keep up to date with the latest release of Java. There are over 100 projects currently participating and listing of each project communication forum. Two examples of projects participating that have found new contributors via this program are Apache Maven and Eclipse Collections.

https://wiki.openjdk.java.net/display/quality

https://wiki.openjdk.org/display/quality/Quality+Outreach

You can participate in OpenJDK by downloading the Early Access Builds. You can also download early access builds of some OpenJDK projects to learn about upcoming enhancements, such as Project Loom, Leyden, Panama and Valhalla: https://jdk.java.net/

You can also become a Contributor of OpenJDK A contributor is a Participant who has signed the Oracle Contributor Agreement (OCA), or who works for an organization that has signed that agreement or its equivalent and makes contributions within the scope of that work and subject to that agreement. A Contributor may submit changes larger than a simple patch, may propose new Projects, and may take on various roles within Groups and Projects.

For more information on how to get involved with OpenJDK https://openjdk.java.net/contribute/

  • Communicate. Once you pick a project, either collectively or on your own, remember to communicate within your JUG. You also need to communicate with the Project Lead or Spec Lead and EG on public discussion and issue trackers. Communication is a two-way street.

3) Decide on actions. Once you have communicated your interest, decide and agree on the actions you will take. See below for some suggestions. These are some things that have been successful – you should not be limited by this list.

  • Share ideas and feedback, comment on list and public issue trackers.
  • Read early versions and share feedback on specifications and Javadocs.
  • Download and provide feedback on early access reference implementation.
  • Try writing sample applications using early builds of reference implementation.
  • Write or speak about the technology and encourage others to participate. Translate into your native language.
  • Evangelize – use social media, blogging or lightning talks to share your knowledge.
  • Help with documentation.

4) Follow through. It is crucial that you follow through on your agreed actions. This can include contributing to public discussions/issue trackers and providing your feedback and comments. It is important to keep in mind that the Specification Lead and the Expert Group or Project Lead has the final decision on incorporating the feedback. For specifications in Public Draft, new feature requests may not be considered for the current release. Multiple groups or JUGs can and should collaborate on projects. There is always plenty of work and going through the material multiple times can improve the quality of feedback provided.

5) Participate and organize hack days. A hack day can be virtual or in person and can be a small (3-5 developers) or large group (100s).

When you organize or participate in a hack day, you may follow suggestions to past successful hack days:

Test your applications against the early RI builds; use them to find pain points, report bugs, suggest feature enhancements.

Help triage issues; reproduce issues, erase/merge duplicates, set priorities/categories etc.

Give feedback on design; discuss issues and deliver feedback, think about how you would use as a developer.

Most of all, when you engage, have FUN – being part of the Java community is FUN! For companies, it will help you to develop new Markets, adapt and inform your technology strategies, and enable you to retain developers and grow as a developer. As an individual, being part of the Java community should be enjoyable. For companies, it will help you to develop new markets, adapt and inform your technology strategies, and enable you to retain and grow your team of developers.

Looking Forward to the Future – EASier migration to New VersionsFollowing on the success of the faster release cadence for the Java platform, and how the community has evolved and adapted to the model over time, the JCP EC has discussed how we can collectively work with the ecosystem to influence and help them to embrace the modern delivery cadence of the Java platform making it easier for developer to migrate their applicaitons to newer versions of Java. The JCP EC completed work to update the JCP processes and allow the Java platform to release a new version every six months. Now there is potential to enable the ecosystems of tools and libraries to also adapt to transition to new versions of Java more quickly. We are looking to build on existing programs such as the Quality Outreach initiative to help the smaller projects that are more difficult to keep up to date. Java has a wide range of libraries and not all of them are up to date. We are looking at how we can influence them to support just the latest versions of Java. If libraries adopt the same or similar model (moving from an express model to a tip model), the Java platform would be even more stable, secure, and predictable. We recognize that this is a cultural change, but it is also an opportunity for maintainers. We need to ask what is necessary to make this happen. Some suggestions we discussed at the last JCP EC Meeting are not back porting as aggressively or back porting as little as possible, because customers want stability. We know that the main issues for maintainers are funding and time. In 2020, the JCP EC will be forming a working group to discuss how we can enable this and influence efforts in the community, building on existing programs, so that with each new version Java, we have the ecosystem ready. For example, on the first day after a release of Java SE, like the IntelliJ IDE, supporting the latest released. The world is ready for the ecosystem of Java libraries, frameworks, and tools to embrace a delivery model like that of the JD – tip development, with LTS offerings. By making this shift, library vendors can realize the same kind of benefits that has been achieved for the Java platform itself. This will further strength and extend the viability of Java overall now and in the decades to come.

The post The JCP Celebrates 25 Years of Community Collaboration appeared first on JVM Advent.

View Details

Eclipse Collections is an open source Java Collections framework. In this blog I am going to demonstrate four lesser known features of the framework. I have published similar blogs in Java Advent Calendars of 2018, 2019, 2020, 2021, and 2022. Please refer to the resources at the end of the blog for more information about the framework.

  1. HashingStrategy: If you want to define a hashing strategy to have a custom hashCode() and equals() without overriding the default hashCode() and equals() then use HashingStrategy. This is particularly useful in conjunction with UnifiedSetWithHashingStrategy, UnifiedMapWithHashingStrategy, and HashBagWithHashingStrategy. The code examples HashingStrategy and Collections with HashingStrategy are presented together
  2. Collections with HashingStrategy: Eclipse Collections offers Set, Map, and Bag wherein the uniqueness is determined by a custom hashCode() and equals() . This is particularly useful when we cannot modify the default hashCode() and equals(). This is generally useful during data processing before inserting in the database. Hashing Strategies can be used to ensure uniqueness based on a primary key. This is especially useful while using Java Records. // Domainpublic record Person(String firstName, String lastName) {}Person person1 = new Person("Alex", "Smith");Person person2 = new Person("John", "Smith");Person person3 = new Person("John", "Brown");List<Person> people = Lists.mutable.of(person1, person2, person3);// Uniqueness is defined by Last NameHashingStrategy<Person> LAST\_NAME\_HASHING\_STRATEGY = HashingStrategies.fromFunction(Person::lastName);

@Testpublic void unifiedSetWithHashingStrategy() { MutableSet<Person> hashingStrategySet = UnifiedSetWithHashingStrategy.newSet( LAST\_NAME\_HASHING\_STRATEGY, people); Assertions.assertEquals( Sets.mutable.with(person1, person3), hashingStrategySet, "Uniqueness defined by only Last Name");}

@Testpublic void unifiedMapWithHashingStrategy() { MutableMap<Person, Integer> hashingStrategyMap = UnifiedMapWithHashingStrategy.newMapWith(LAST\_NAME\_HASHING\_STRATEGY); hashingStrategyMap.put(person1, 1); // The key is considered same due to same last name. // Hence, by Map symantics, the new value will be stored hashingStrategyMap.put(person2, 2); hashingStrategyMap.put(person3, 3); Assertions.assertEquals( Maps.mutable.with(person1, 2, person3, 3), hashingStrategyMap, "Uniqueness defined by only Last Name");}

@Testpublic void hashBagWithHashingStrategy() { MutableBag<Person> hashingStrategyBag = HashBagWithHashingStrategy.newBag( LAST\_NAME\_HASHING\_STRATEGY, people); Assertions.assertEquals( Bags.mutable.with(person1, person1, person3), hashingStrategyBag, "Uniqueness defined by only Last Name, hence, person1 comes up twice");} 3. detect: is used to find the first instance of an element that satisfies the Predicate. In case no element satisfies the Predicate then a null is returned. This method is similar to findFirst() in JDK Collections except that a null is returned when no element is found. @Testpublic void detect() { var list = Lists.mutable.with(1, 2, 3); Assertions.assertEquals( 2, list.detect(each -> each % 2 == 0)); Assertions.assertNull( list.detect(each -> each % 4 == 0));} 4. detectOptional: This is a correlated method to detect wherein instead of returning an element that satisfies the Predicate an Optional is returned. In case no element satisfies the Predicate then an empty Optional is returned. This method is equivalent to findFirst() in JDK Collections. @Testpublic void detectOptional() { var list = Lists.mutable.with(1, 2, 3); Assertions.assertEquals( Optional.of(2), list.detectOptional(each -> each % 2 == 0)); Assertions.assertEquals( list.stream() .filter(each -> each % 2 == 0) .findFirst(), list.detectOptional(each -> each % 2 == 0)); Assertions.assertEquals( Optional.empty(), list.detectOptional(each -> each % 4 == 0)); Assertions.assertEquals( list.stream() .filter(each -> each % 4 == 0) .findFirst(), list.detectOptional(each -> each % 4 == 0));}

SummaryIn this blog I explained a few lesser known features of Eclipse Collections namely HashingStrategy, Collections with HashingStrategies, detect, and detectOptional.

I hope you found the post informative. If you have not used Eclipse Collections before, give it a try. There are few resources below. Make sure you show us your support and put a star on our GitHub Repository

Eclipse Collections ResourcesEclipse Collections comes with it’s own implementations of List, Set and Map. It also has additional data structures like Multimap, Bag and an entire Primitive Collections hierarchy. Each of our collections have a fluent and rich API for commonly required iteration patterns.

  • Website
  • Source code on GitHub (Make sure to star the Repository)
  • Contribution Guide
  • Reference Guide

The post Hidden Treasures of Eclipse Collections 2023 Edition appeared first on JVM Advent.

View Details

Artificial Intelligence is all the craze lately. With the recent Quarkiverse extension you can can get to work with Large Language Models (LLM) using LangChain4j. I thought it would be nice to demystify and show the simplest way you can get to have an AI agent to help you understand and update your own code.

What is LLMLLMs, or Large Language Models, are advanced AI systems trained on vast text data that excel in understanding and generating human language. The basics of the system is a neural network known from the 20th century which with more access to data and compute power now is really good at classifying and generating content. Java developers can utilize such LLMs to enhance their applications by generating code, providing natural language interfaces, and improving language-related tasks, all through easy API integration.

Why use Java to work with LLM?The java ecosystem is huge – not only is there a massive ecosystem of reusable libraries, there are also 9 million developers using it in existing and new Java applications. With LLM rolling over the planet, it is critical to enable these application developers to use it and help shape this new approach to software.

Why use Quarkus to work with LLM?By using Quarkus we not only get a simple extendable programming mode. It enables your existing or new applications to have LLM integrated all while getting all the enterprise features, such as the following:

  • Seamless integration with the Quarkus programming model
    • CDI beans for the Langchain4j models
    • Standard configuration properties for configuring said models
  • Declarative AI Services
  • Built-in observability
    • Metrics
    • Tracing
    • Auditing
  • Build time wiring
    • Reduced footprint of the library
    • Feedback about misuse at build time
  • Leverage runtime Quarkus components
    • REST calls and JSON handling are performed using the libraries used throughout Quarkus
      • Results in reduces library footprint
      • Enables GraalVM native image compilation
  • Dev UI features
    • View table with information about AI services and tools
    • Add embeddings into the embedding store
    • Search for relevant embeddings in the embedding store

Enough with the context – lets write some code!

Demo UsecaseIn this example, we’ll make a script that gives you a simple chat interface where an AI can help answering questions about your project by reading the filesystem and do updates and deletes if needed.

To do this, we will make a prompt to the AI asking for help and give it access to a set of methods annotated with @Tool which the AI is allowed to call. It will then do those operations and ask/inform about what it does.

In this, we also add some basic security measures to protect against a possible malicious attack or error.

The source code is available on github.com/maxandersen/javaadvent-2023-quarkus-ai-scripting

Keeping it SimpleFor this holiday example, we are going to keep it simple and just make a Java script that we can run directly using JBang. If you prefer you can put this into a full-blown Maven or Gradle project.

First, we need the right setup. We will use Java 17 or higher and have -parameters enabled to have Java compiler retain parameter names metadata.

//JAVA 17+//JAVAC\_OPTIONS -parameters Now lets setup the dependencies we use: Quarkus Platform for dependency management, LangChain4j OpenAI (could be any other model) and then PicoCli for easy parsing of command line options.

//DEPS io.quarkus.platform:quarkus-bom:3.6.3@pom//DEPS io.quarkiverse.langchain4j:quarkus-langchain4j-openai:0.4.0//DEPS io.quarkus:quarkus-picocli And a little bit of Quarkus config properties. In this case, we are going to increase the OpenAI timeout as sometimes the LLM needs more than just a few seconds to process. We are also enabling use of “gpt-4” as it is better at assisting but if your account does not have that API access you can remove the line and it will use the default gpt-3.5 model.

//Q:CONFIG quarkus.langchain4j-openai.timeout=60s//Q:CONFIG quarkus.langchain4j.openai.chat-model.model-name=gpt-4 FileManager ToolTo have the AI be able to read, update, and delete files we need to make a class available that has those operations. In this example, we make a FileManager class that has methods like getFiles, getFile, createOrUpdateFile and removeFile to perform those operations.

They are straightforward standard Java code using java.io.File’s API annotated with some natural language hints for the AI to understand. Below is the getFiles to illustrate it:

@Tool("""Get the files in a directory.The list of files recursively found in this directory.Will by default return all files in the root directory. """)public List<String> getFiles(@P(""" The name of the directory relatively to the root of the project. Is a simple string. Use '/' to get the root directory. """) String directory) throws IOException { directory = handleDir(directory); info("Getting files in directory " + directory); var files = Files.list(Paths.get(directory)).map(p -> p.toString()).toList(); return files;} Securing the toolThe tool is going to be executed by Quarkus LangChain4j based on what you tell the AI. Just like if you expose an API to a human or some other system it can perform malicious or accidental problematic actions. For example ask to remove a file in the parent directory or your root folder.

To mitigate that all the methods taking a path calls out to this handleDir() method to check if a potential issue.

// Check for '..' in the path to avoid accessing files outside of the project.// Make request to / be the same as a request to . (root of current directory)private String handleDir(String directory) {if (directory.contains("..")) {throw new IllegalArgumentException("The path cannot contain '..' as it would allow to access files outside of the project.");}directory = (directory == null || directory.isBlank())? "/" : directory;return (directory.startsWith("/"))? directory.substring(1) : directory;} In addition, the script also ask the user if he is okey to perform any action that updates or remove content.

Talking to the AITo communicate with the AI we add an interface called ProjectHelper that is annotated with @RegisterAiService and some message annotations to give the AI its initial prompts.

The “tools = FileManager.class” tells the AI it can use it to perform additional operations.

@RegisterAiService(tools = FileManager.class)public interface ProjectHelper { @SystemMessage("""You are to help a developer understand his project. You can ask him questions or query the files in his project to get more info.If user asks you to modify or create a file, you can do so by calling the 'updateFile' tool. """) @UserMessage("{question}") String ask(String question);} Question and AnswersTo handle questions and answers we make a simple loop to have a conversation with the ProjectHelper that gets injected. The run() method has @ActivateRequestContext as a hint to Quarkus that this method wants to have request scope available.

@InjectProjectHelper ai;@ActivateRequestContextpublic void run() {while (true) {var scanner = new Scanner(System.in);if (question == null || question.isBlank()) {out.println("Please enter your question:");question = scanner.nextLine();if (Set.of("exit", "quit").contains(question.toLowerCase())) {break;}}out.println("Thinking...");String answer = ai.ask(question);out.println("Answer: " + answer);question = null;}} Running it allTo use OpenAI we need a token setup. Easiest is to put a .env file with the following key and put your own secret token.

QUARKUS\_LANGCHAIN4J\_OPENAI\_API\_KEY=sk-a-secret-1234</code></pre> Now you can run the script in an existing project directory. In this example we do it on the code itself using jbang devhelper.java.

The initial question is “What can you tell me about this project?”.

Remember, what is shown below is a unique run – the run(s) you make will be different.

Thinking...Answer: Your project contains the following files:- devhelper.java- .gitignore- .env- .gitThe main file in your project is `devhelper.java`. It is a Java file that uses Quarkus LangChain4j to help understand and update code. The file contains a command-line interface for asking questions and getting answers related to the project.The `devhelper.java` file includes several dependencies and configuration settings related to Quarkus and LangChain4j. It also defines an interface `ProjectHelper` with a method `ask` for asking questions.Within the `devhelper.java` file, there is a nested `FileManager` class that provides several tools to interact with files in the project. These tools include:- `getFiles`: Retrieves the files in a directory.- `getFile`: Retrieves the content of a file.- `createOrUpdateFile`: Allows creating or updating the content of a file.- `removeFile`: Removes a file.The `devhelper.java` file also includes a `run()` method that continuously prompts for user input (`question`) and calls the `ask` method to get answers. The answers are then printed to the console.To run the project in development mode, you can use the command: `jbang --fresh -Dquarkus.dev -Dquarkus.console.enabled=false devhelper.java`. This command ensures a fresh build, enables development mode, and doesn't grab the console so that you can ask questions in the terminal.Note: The code provided here is a truncated version of the `devhelper.java` file for readability purposes. If you need more detailed information, please let me know.</code></pre> It knows about the files present thus it used the FileManager to get the directory content. It also know details about the devhelper.java so it fetched the content too.

Let’s make the AI do work for us by making a readme file:

Please make a readme.md to explain how to run this using jbang.Thinking...Update file 'readme.md' with content:# Running the Project with JBangThis project can be run using [JBang](https://www.jbang.dev/), which is a lightweight scripting tool for Java.To run the project, follow these steps:1. Install JBang by following the instructions [here](https://www.jbang.dev/download).2. Clone the project repository: git clone cd 3. Open a terminal and navigate to the project folder.4. Execute the following command to run the project with JBang: jbang --fresh -Dquarkus.dev -Dquarkus.console.enabled=false devhelper.java The `--fresh` option ensures a fresh build, while `-Dquarkus.dev` and `-Dquarkus.console.enabled=false` enable development mode and disable the console in order to interact through the terminal.5. The project will start running and prompt you to enter your question. You can ask any question related to your project, and the Quarkus LangChain4j will provide an answer. Example: Please enter your question: What can you tell me about my project? Thinking... Answer: Your project contains several files including 'devhelper.java', '.gitignore', '.env', and '.git'. The main file in your project is 'devhelper.java' which uses Quarkus LangChain4j to help understand and update code. 6. You can continue asking questions and interacting with the project as needed. To exit the program, you can enter 'exit' or 'quit' when prompted for a question.Are you sure you want to update the content of readme.md? (yes/no)yesAnswer: I have created the `readme.md` file with instructions on how to run the project using JBang. You can find the file in your project repository. And just like that it made a brilliant readme.md for the project.

You can continue interacting with it and ask things like “Add a Apache license header to all the files”, “Please refactor to separate classes”, etc.

Note: it is not perfect and can fail – so don’t let it loose on your project without having a backup or be okey to loose data.

Conclusion In very few lines of code we made a runnable chatbot that interacted with our own local API and allow the AI to help us on our project.

Now imagine you added more tools to the AI like your ToDo list, Calendar and issue tracker – it could help you do all sorts of things so you can get a more relaxed holiday.

Similarly if this was in an enterprise application setting that exposed tool API could be anything from there – you rest endpoints, other Quarkus extensions, Camel routes, etc. The possibilities are endless – try it out but do ensure you take security into considerations.

Hope you liked it and do give it a try from github.com/maxandersen/javaadvent-2023-quarkus-ai-scripting

The post Scripting Quarkus AI with Large Language Models appeared first on JVM Advent.

View Details

When a new paradigm of communication between applications appears, most developers are excited to migrate and start using it without thinking about the possible problems. Using events instead of classic communication like SOAP, REST, or GraphQL is no exception because it introduces a lot of benefits like decoupling the producers from the consumers, giving you the possibility to change flows, and increasing the performance, but not all are benefits.

The theory of the use of events looks excellent without any problem. Still, when you start to work with events, many challenges appear like how to document it in a way that anyone could understand the idea and the body of each event, what you need to do if an exception appears, how to deal with modifications on the body of the events and other problems related with the coordination of the event’s flows.

In this article, you will learn more about some common problems that could appear and some possible solutions to solve or mitigate the impact.

PROBLEM #1 – orchestration vs choreographyDefining how the events and the applications interact is one of the most documented situations. Books, articles, and conference talks explain each approach’s main idea. Still, it’s difficult to see which of them is the best alternative for some specific situation because most of them explain in a general way, so when you start to implement events, you choose one alternative instead of the other after a period in the future, the problems appear.

To recap, two alternatives or possible implementations of the SAGA pattern are choreography and orchestration. The first one indicates that there is a set of applications that produce and consume events without having control of the entire flow; each doesn’t know the entire flow and the events that compensate for every kind of problem. On the other hand, you have orchestration, where one or more applications manage the part or the entire flow, which implies knowing many things related to business logic.

ORCHESTRATION VS CHOREOGRAPHY

The main problem radicates with the complexity of the business logic and the number of applications that need to interact; this is one of the problems about why it is possible to choose the wrong approach. A possible solution for using SAGA instead of choosing one strategy or another could be a mix between both, which implies that you split your platform into different domains, sections, or groups where each of them has a specific microservice or application that works as an orchestrator and the interaction when different domains/group/sections could work with the choreography approach. The main benefit of this approach is that it is not one application that knows or listens to all the events from different domains, so it reduces the complexity of the platform and simplifies the problems of changing the format of different events.

ORCHESTRATION AND CHOREOGRAPHY

Consider this approach when your platform contains a lot of microservices, and most of them interact using events. If you have a simple scenario with a couple of events or the number of microservices is just a few, it could be a good option to consider one of the possible implementations of SAGA.

PROBLEM #2 – TestingTesting an application implies many challenges, but when you introduce events, the complexity increases because, with the unit tests, you only cover some parts of the logic of creating integration tests that need to consume events from a topic that exists on AWS, for example.

There are two alternatives at that point: the first is to do a manual test on some environment, testing the entire flow, which is more like an end-to-end test than an integration test. The other alternative is to use a Docker image to simulate the infrastructure; for example, in the case of AWS, you have Localstack to simulate the SNS topic.

Some considerations related to using Localstack are creating a Docker file and adding a bash file to create all the topics and queues you need, like the following.

FROM localstack/localstack:0.14.3ENV SERVICES=sqs,sns DEBUG=1 DEFAULT\_REGION=us-east-1 HOSTNAME\_EXTERNAL=localhost DOCKER\_HOST=unix:///var/run/docker.sockVOLUME /docker-entrypoint-initaws.d/VOLUME /var/run/docker.sockEXPOSE 4566 The VOLUME /docker-entrypoint-initaws.d/ will contain a file more or less like the following:

```

!/usr/bin/env bashset -euo pipefail# enable debug# set -xecho "configuring sns/sqs"echo "==================="# https://gugsrs.com/localstack-sqs-sns/LOCALSTACK_HOST=localhostAWS_REGION=us-east-1LOCALSTACK_DUMMY_ID=000000000000get_all_queues() { awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sqs list-queues}create_queue() { local QUEUE_NAME_TO_CREATE=$1 awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sqs create-queue --queue-name ${QUEUE_NAME_TO_CREATE} --attributes FifoQueue=true,ContentBasedDeduplication=true}get_all_topics() { awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sns list-topics}create_topic() { local TOPIC_NAME_TO_CREATE=$1 awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sns create-topic --name ${TOPIC_NAME_TO_CREATE} --attributes FifoTopic=true,ContentBasedDeduplication=true}link_queue_and_topic() { local TOPIC_ARN_TO_LINK=$1 local QUEUE_ARN_TO_LINK=$2 awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sns subscribe --topic-arn ${TOPIC_ARN_TO_LINK} --protocol sqs --notification-endpoint ${QUEUE_ARN_TO_LINK} --attributes RawMessageDelivery=true}guess_queue_arn_from_name() { local QUEUE_NAME=$1 echo "arn:aws:sqs:${AWS_REGION}:${LOCALSTACK_DUMMY_ID}:$QUEUE_NAME"}guess_topic_arn_from_name() { local TOPIC_NAME=$1 echo "arn:aws:sns:${AWS_REGION}:${LOCALSTACK_DUMMY_ID}:$TOPIC_NAME"}send_message_to_queue() { local QUEUE=$1 local MESSAGE=$2 awslocal --endpoint-url=http://${LOCALSTACK_HOST}:4566 sqs send-message --queue-url $QUEUE --message-body $MESSAGE --message-group-id "saraza" --message-deduplication-id "saraza"} ORDER_QUEUE_NAME="it-api-checkout-events-orders.fifo"echo "creating queue: $ORDER_QUEUE_NAME"ORDER_QUEUE_URL=$(create_queue ${ORDER_QUEUE_NAME})echo "created queue: $ORDER_QUEUE_URL"echo "creating topic: $EVENTS_TOPIC_NAME"EVENTS_TOPIC=$(create_topic ${EVENTS_TOPIC_NAME})echo "created topic: $EVENTS_TOPIC"echo "linking topic $EVENTS_TOPIC to queue $ASSERTIONS_QUEUE_NAME"LINKING_RESULT=$(link_queue_and_topic $(guess_topic_arn_from_name $EVENTS_TOPIC_NAME) $(guess_queue_arn_from_name $ASSERTIONS_QUEUE_NAME))echo "linking done:"echo "$LINKING_RESULT"echo "all topics are:"echo "$(get_all_topics)"echo "all queues are:"echo "$(get_all_queues)"

``` A good alternative to using Localstack and creating an integration test is Karate, which is simple to implement and write the tests. I suggest that you create the event on each particular test, like appears in the following example:

Feature: consume orders Background: * url AppUrl //This is the base path of the application Scenario: test PRODUCT\_SOLD event //Insert the event into the topic * def eventQuery = read('json/xxxx/event-query-scenario-.txt') Given url `http://localhost:${localstackPort}/000000000000/it-api-checkout-order.fifo?` + eventQuery When method get Then status 200 //Check if the event was proccesed Given path 'order/1' When method get Then status 200 This is a possible approach to solve the integration problem; if you use Kafka instead of AWS SNS, the idea is more or less the same, but instead of using Localstack, you will use a Docker image of Kafka.

Last suggestion: in the case that your application sends an event to some topic, the idea is that you check if the event exists and, after that, delete it.

PROBLEM #3 – thin vs fat messagesThe size or the amount of information each message contains is essential for the different applications. Still, it introduces a problem that you need to consider the tradeoff between including a lot of attributes in the body of the message that perhaps not all the consumer needs or instead only adding the IDs of different elements that allow you to reduce the size of the message. You only request certain microservices or applications to obtain the information your microservices need.

THIN VS FAT

There is no correct strategy for all situations; choosing one instead of another depends on the context, but if you send an event that a lot of different applications consume, perhaps it could be a better option to send the ID of the different elements and each application will have the responsibility to obtain the information that needs it to work.

PROBLEM #4 – versioningThis problem is familiar or connected with the use of events because the same happens when you need to introduce some disruptive changes on the endpoints of a microservice.

There are many alternatives to tackle this problem, like using the Semantic Version to indicate which version of the event represents one message. The consumer could filter the events for another version that does not support introducing some strategy to parse into different objects depending on which version in particular is.

Another alternative is to delegate the responsibility of registering the different versions of the events and indicate if they are backward-compatible in an Event Schema Registry; for example, in the case of Kafka, you can use Schema Registry. Before sending a message, the producer obtains the schema of the event, creates the message, and publishes it on the broker; the consumer does the same to deserialize the message.

PROBLEM #5 – DocumentationDocumenting the events is one of the biggest problems if you have not started to do it at the beginning of the creation of the platform because it implies that you need to invest a lot of time to model all the events, the consumers, and the producers.

Why is it so important to do it, can you think? The main problem is what happens if some little attributes are in the message. If you don’t know the impact of the changes or the connection between different applications, it isn’t easy to understand how the platform works. To solve this particular problem, there are different strategies, some more sophisticated and others more trivial.

The trivial solution implies that you create a document on some tool like Confluence, Notion, Google Doc, or any that you prefer and put all the information about the consumers of an event, which is the message format. As you can imagine, this solution is not the best because having the entire platform map is complex, and not all developers like to write a long document with tons of information.

Conversely, some solutions are simple and close to the developers’ tools, like AsyncAPI, which is similar to OpenAPI3 to document REST applications.

This approach is great because it generates the documentation dynamically, and it’s simple to share with other developers. Still, again, you don’t have the entire picture of how the platform works, which is so important. To solve this particular problem, there is a tool called EvenCatalog, which allows the creation of documentation of the different applications and the message that goes from one application to another.

Example of the documentation using Service Catalog

One of the main benefits is that it’s simple to use. It is just an XML where you declare different things, and you can version the changes, but the main problem with this mechanism is that someone needs to keep the documentation updated with the latest changes.

One tool that combines the solution of using AsyncAPI to generate the documentation dynamically and how the different interactions between them could be Backstage, which Spotify created.

Consider that all the solutions could work for you depending on the size of the platform and the number of events. The best scenario is not to implement any solution to document your events, so analyze the tradeoff of using each tool and choose the right one for you.

PROBLEM #6 – failures processingErrors processing events are expected because the same could happen on a simple HTTP request that produces, for example, an internal server error. In most cases, the queues have some strategies of retries by default, and in other cases, you need to configure the number of attempts before sending the message to the DLQ.

When something terrible happens, you need to consider many things, like creating alerts that detect the problem and notify you; there are many tools to do it, like Signoz, NewRelic, Dynatrace, and many more. Another issue associated is what happens with the status of distributed transactions, which implies that you throw an event to do the rollback of some operation after a certain number of retries or not change anything and send the event to the DLQ and with some strategy you decide what you do with the flow.

One aspect to consider to understand the problem and reproduce the situation with a test is to log everything with the event that your application consumes. This gives a quick way to know if the issue is in the application or the information the event contains.

PROBLEM #7 – RESEND INFORMATIONNot all the events that fail during the different attempts to be processed are unrecoverable; there are some situations when one of the applications consumes an event, and another application that provides certain vital information is down. This situation produces the event after some attempts to go directly to the DLQ.

When you have messages on the DLQ, you have two alternatives: if the error or problem is unrecoverable, you can discard the message, but in this case, you can solve the problem by reprocessing the event again. A possible implementation of this solution implies that you re-send the event to the queue again, like a new message, but how can you do this? How can you do the same on multiple DLQs that your platform has?

Create an endpoint of mechanism on each application that shows the messages on the DLQ and provides the logic to move or resend the message again, introducing the problem of having the same code or solution across multiple applications, which implies that someone needs to know different URLs.

DISTRIBUTED PROCESSOR

An alternative to this approach could be to create one that consumes the information of the DLQ of all the events, and with some back office, you decide manually what happens with each event.

RE-PROCESOR SERVICE

In both cases, you need to decide what you want to do with all the events on the DLQ; the question is which approach reduces the complexity to you.

An alternative to the previous solutions is to create a cron on each application that iterates the messages on the DLQ and can decide if the events could be reprocessed depending on some information. This implies many things, like if the error was related to some deployment or if some resource on the infrastructure was unhealthy. Still, it’s a possible solution instead to do all the manually.

PROBLEM #8 – naming conventionsIn most cases, the naming conventions of the events depend on each company, like the names of the microservices or the UI components. Still, the problem with choosing a wrong or non-descriptive name for the events is the complexity when something bad happens, and you need to check the logs. Let me show an example; imagine that you work on an e-commerce website with tons of events, and for some reason, the order of one client was canceled; you check the logs and see that there was a problem processing the event on process-order and you don’t have information about which application produced the error, in this situation could be difficult to find which are the applications that are involved on the problem.

The idea of the naming conventions is that all the events follow the same pattern, which gives the necessary information to understand which part of the entire platform has a problem; following this approach, some possible naming alternatives could be:

<namespace>-<product>-<event-type>

<application>-<data-type>-<event-type>

<organization>-<application-name>-<event-type>-<event>

The previous examples are some of the most common, but you can choose the best alternative representing your company’s situation. Consider adding some restriction or validation on the broker to prevent adding new topics that do not follow the pattern, which could be automatic or manual, where someone is responsible for checking each name.

PROBLEM #9 – order of the eventsasynchronous communication if you need some mechanism to avoid it. The order of the events is not only connected with messages for the same event into a queue; in a distributed architecture, you need to know if all the events associated with a particular flow previous to you are executed correctly or not.

A question could appear in your mind: Why is a problem happening? There is no unique explanation for why this happens, but possible answers could be that some event was processed incorrectly or someone needed to understand the flow of events and produce the wrong event. It’s challenging to think of a solution based on one particular problem; you need to find a mechanism that guarantees the consistency of the information in any situation.

Let’s explain with one example: imagine that you work in e-commerce, and the microservice that processes a buy receives a message to deliver the products to the client. However, you never receive the event from the payment service that tells you if the payment was approved or not. In this example, the possible solution is to create a state machine that checks the actual status and which transitions are valid to execute before doing it.

Another solution, instead of having an attribute containing the status of the operation/order/buy, is to request the different microservices to check if the other events are finished. Here, the complexity implies making many requests to validate something.

PROBLEM #10 – no experienceThe previous problems have more or less different alternatives to solve, but if most of your team or the team in the company does not have experience with the use of events, you will have a problem if you do not tackle it as soon as possible.

Someone in the company needs to research how to implement and solve the different problems that an event-driven architecture introduces, creating some archetype or template with the basics about consuming and publishing an event. Perphaps, after reading the previous sentence, you think that is obvious. Still, some companies decide to use events without analyzing the possible problems, so at some point in the future, the platform will contain many issues with different implementations using the same framework or library.

If you are an expert using event-driven architecture, try to create a small webinar or a talk to explain the best practices but focus on the problems to show your audience which catastrophe could occur in some situations, like processing a payment for a customer.

WHAT’S NEXT?There are many resources about different topics connected with the management of events, but a few tackle some problems. The following is just a short list of resources:

  • Flow Architectures By James Urquhart
  • Building Event-Driven Microservices: Leveraging Organizational Data at Scale by Adam Bellemare
  • Grokking Streaming Systems: Real-time event processing by Josh Fischer and Ning Wang
  • Not Just Events: Developing Asynchronous Microservices by Chris Richardson
  • Kafka Topic Naming Conventions by Chris Riccomini

Other resources could be great for solving some particular problems related to the documentation.

  • Catalog your Events with AsyncAPI by David Boyne
  • Designing Event-Driven Architectures Using the AsyncAPI Specification by Fran Mendez

CONCLUSIONUsing events on a platform could be great and give you a lot of benefits, like the possibility of working in parallel with other teams and changing the flows. Still, it would be best to focus on solving the problems before they appear because it’s not simple and implies analyzing different alternatives and discussing them with other members of your team or company.

There are a lot of possible problems that could appear using events. You can’t tackle all of them, so I suggest you prioritize them to solve the issue that could produce significant pain; for example, decide which type of SAGA (choreography or orchestration) you will use that is more relevant than the naming conventions.

One last thing: Do not be frustrated if you choose one approach to solve a problem instead of another; after some time, new problems appear. There is no unique way to solve the issue of using events, but all your decisions must be documented and discussed with other partners or colleagues. Hence, I suggest using ADR (Architecture Decision Record), which is an excellent way to track architectural choices.

The post I see events everywhere; who will organize them? appeared first on JVM Advent.

View Details

After over 5 years of hands-on experience with Java and Spring/Spring Boot, I decided to take the plunge and go for the Spring Boot Certification. It’s one of those milestones many of us developers aim for, right?

Drawing from memories of my Oracle Java Certification learning process, I was ready for a long ride where theory often seems worlds apart from practice. However, as I delved deeper, I came across aspects of Spring Boot that don’t usually make it to your everyday news and tutorials.

In this article, I’m eager to share these unexpected findings. I’m happy that I’ve unraveled cool features that have genuinely shaped my understanding. Keeping things straightforward, I’ll explain these with real examples, aiming to enrich or simply refresh our collective knowledge of Spring Boot.

However, this is not a definitive guide on how to pass the Spring Professional Developer certification, and it is far from a comprehensive learning material.

This being said, let’s dive in…

TL;DR* Bean scopes are great, but when mixing them strange things start happening * @PreDestroy doesn’t get called for Prototype beans * BFFP vs BPP – one processes definitions, the other one processes instances * Autowiring – you can do it with collections as well – even with generics * AOP – it depends – JDK Dynamic proxies vs GCLIB * JDBC – cool callbacks * @MatrixVariable * antMatchers vs mvcMatchers – trailing slash * permitAll() vs web().ignoring()

Beans – scopes, lifecycle, and (some) processorsI’ve always known that the foundation of Spring is built on Beans, so understanding these would help me along the way. All the bean scopes are nice and aid you along the way to shape your application behavior; do you want a short-lived cache that spans over a single request? throw a @Bean over a method that returns a HashMap set the scope to REQUEST and let the framework do the rest; want to store a user’s preferences for the time that the user is online? simply create a UserPreferences bean and throw a SESSION scope on it. So simple, then how could this get weird?

Having fun with scopesImagine having a SINGLETON bean that depends on a PROTOTYPEbean. What does this translate to? In the bean creation and dependency injection phase, we will have one singleton bean instantiated and initialized, with a single prototype bean instance inside. However, for every request on the prototype bean, we need a new instance. How does Spring handle this? Well, there are two (main) ways in which we can dictate the behavior in this scenario:

  1. Proxy approach
    • By marking the prototype bean with @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS), Spring will inject a proxy into the singleton bean. Each time the singleton bean accesses the prototype bean, the proxy ensures that a new instance of the prototype bean is created and returned.
  2. Lookup Method Injection (the heck?) – ever heard of **@Lookup**?
    • You can provide a method that Spring will intercept and provide a new instance of the same type as the return type of the method, every time it’s called
    • There are two ways of doing this
      • The cleaner way:

View the code on Gist.

      • The uglier way:

View the code on Gist.

The key difference is that using the abstract method only works when the surrounding bean is created using component-scanning (@Component) because Spring dynamically implements the abstract method, the class itself should not be fully instantiated in the typical way (like a regular Spring bean – @Bean).

Not what you would expect from bean lifecycleScenario: you have a bean, you need to add behaviour after the bean is set up and before the bean is destroyed. Say (for the sake of the article) that you connect to a database before the bean is handed to you and when you’re done with it, the connections are to be released.

Spring conveniently offers two lifecycle annotations that you can use on your methods: @PostConstruct and @PreDestroy. Besides this, someone tells you that every time someone accesses the bean, you need to provide an isolated instance for “(almighty) security purposes”.

You go ahead and implement the logic accordingly, ship the code to production and move on to your weekend. You get a call Saturday evening that no one can access the application and the on-call guy let’s you know that the database doesn’t accept any more connections. You confidently tell him to restart the database and everything gets back to normal. Sunday evening the same thing happens, what did go wrong?

When confidently making use of the provided lifecycle annotations provided by the framework, you forgot to check how the scope of the bean works, because, who does that?

PROTOTYPE bean scope ensures a new instance of the bean is created every time it’s requested from the Spring container. Unlike Singleton, which ensures a single shared instance, Prototype creates a fresh bean for each request.

However, another key difference is that for PROTOTYPE beans, @PreDestroy is not invoked!.

The reason behind this is simple: Spring manages the full lifecycle of Singleton beans but with Prototype beans, it hands over the bean after initialization. The destruction or cleanup of prototype beans falls outside of Spring’s responsibility.

Imagine buying a coffee machine (Singleton Bean). You set it up once (@PostConstruct) and dispose of it when it’s no longer functional (@PreDestroy).

Now, think of a coffee capsule (Prototype Bean). You use a new one every time you make coffee @PostConstruct. But when you throw it away, it’s outside the machine’s responsibility (@PreDestroy doesn’t get called). Just like your database connections not getting cleaned up.

Behind the Beans: The Post Processor DiariesWhile this topic deserves it’s own article, I would like just to point out one crucial difference that helped me differentiate between BeanFactoryPostProcessor and BeanPostProcessor. That is:

  • BeanFactoryPostProcessor operates at container level before any beans are instantiated and modifies the bean definitions themselves, not bean instances
    • one example use case for this is to modify the property values in the bean definition (replace @Value annotated fields with actual property values)
  • BeanPostProcessor acts on instances of beans, so after the bean has been instantiated and dependencies injected
    • this is where AOP proxies are usually set up around beans and methods

It is very important to be able to differentiate between the exact places in the bean lifecycle where each of these intervene so you save yourself some headaches when customizing your beans.

(Auto)Wiring for SuccessCatch them allWe’re used by now to take advantage of our precious Dependency Injection mechanism and bring all the universe together into the same service. I’ve recently found out there is an easier, more straightforward way to inject all the beans you need. Have you ever Autowired a collection of beans? I know I didn’t. Spring offers a way of injecting all beans of a particular type into the same collection. Imagine you like spam and want to notify your users on all possible platforms available. You could trigger the notifications for each and one of the external notification channels you are using manually, or you could just unify them under a single interface and make use of dependency injection to simplify everything.

View the code on Gist.

And then just autowire (yes, I know, stop using @Autowired) everything in the same List of beans.

View the code on Gist.

Take a step back and think of how easy it is now to add another notification channel. SOLID much?

No matter whatNow let’s take this to another level. What if we implemented a generic notification MessageProcessor that we can specialize based on the type of the message? There’s no way we could autowire generics as well, right? Well…

In Java, due to type erasure, generic type information is lost at runtime. This should pose a challenge in dependency injection frameworks like Spring, where type matching is crucial for autowiring dependencies. However, Spring overcomes this limitation through the ResolvableType class. This class provides a way to capture and retain the full generic type information at runtime, enabling Spring to perform accurate type matching even for generic types.

Just like before, we define a generic interface MessageProcessor<T> where T represents the type of message, then implement the interface for different message types:

View the code on Gist.

While you can still autowire all MessageProcessors like we did before, another cool trick is that you can also autowire by type, even if it’s generic:

View the code on Gist.

Always Order Pizza (AOP)Code against interfaces, not implementationsWhat does this have to do with Spring and AOP? Well, little did I know that behind the famous magical toolbox of Spring’s Aspect Oriented Programming implementation stand 2 different methods for creating proxies: GCLIB and JDK Dynamic Proxy.

Why do we need two of them? Because as everything else in programming, it depends. It depends on the context.

  • GCLIB proxies work by extending the target class. They generate a subclass at runtime and override the methods of the target class. These are ideal when proxying classes rather than interfaces, because CGLIB doesn’t require the target class to implement an interface. Therefore, GCLIB cannot proxy final classes or methods, as they can’t be overridden in the subclass.
  • JDK Dynamic proxies work by implementing the interfaces of the target class at runtime. They use reflection to invoke methods and require the target class to implement one or more interfaces. This approach is less invasive and doesn’t require subclassing, which makes it simpler and more transparent.

Spring uses JDK Dynamic Proxies by default when the bean implements interfaces, and CGLIB proxies when the bean does not. This approach allows Spring to handle a wide range of scenarios while maintaining compatibility and performance. If you think you know better, you can always explicitly specify the proxying mechanism to be used.

Proxying in circlesI want to take advantage of this opportunity and emphasize one thing that we all (kind of) know, but it’s not very straightforward for someone who still thinks of Spring as magic. Proxies are a great mechanism for separating our application business behaviour from crosscutting concerns (like security, logging etc) and Spring relies heavily on these. However, there are still limitations which might affect our application performance in ways we don’t expect. Let’s consider the following example:

View the code on Gist.

At first glance, you might expect that the call to **methodB** would be cached on subsequent calls to **methodA**. It is important to understand this crucial difference.

When methodA is called from outside MyService, Spring’s AOP proxy intercepts this call. However, when methodAinternally calls methodB, this call does not go through the proxy. Instead, it’s a direct internal method call within the same object.

As a result, the @Cacheable annotation is effectively bypassed, and the caching behavior does not get applied.

Java(script) Database ConnectivityIf you’ve ever worked even a bit with javascript, you must have heard about callback hell. Well, this is exactly what popped into my mind when studying Spring’s **JdbcTemplate**.

I know, we are all running away from SQL by using cool, fancy frameworks and ORMs for our data access layer, but we need to remember to always honour our elders.

For this particular certification, there seems to be a slight emphasis on the **JdbcTemplate** result handlers (callbacks), so I thought they were worth to be mentioned. I also found them to be cool and discovered the subtle differences between them.

  1. .query() callbacks:
    • RowMapper retrieves data from the ResultSet and returns an object representing each row. Useful for mapping the result row by row (as the name already implies)
    • ResultSetExtractor retrieves data from the ResultSet and returns an object representing the entire result. It’s useful for aggregating results or mapping complex relations.
    • RowCallbackHandler processes each row of the ResultSet individually, allowing for more memory-efficient processing, especially for large datasets. Slight difference here, this handler processes the rows, which means it does not return anything.
  2. What if you need Column Names and Values:
    • **JdbcTemplate** allows easy access to the column names and values from the result set, enabling the mapping of database columns to entity attributes in your Java application. How?
      • queryForList is used to retrieve a list of rows from the database. Each row is represented as a Map<String, Object>, where the keys are the column names, and the values are the corresponding column values. So, we’ll have a **List<Map<String,Object>>**, pretty, right?
      • queryForMap is used when you expect a single row in the result. It returns a Map<String, Object>where, similar to queryForList, the keys are the column names, and the values are the corresponding column values.
  3. Query and Update Methods:
    • query method is used for fetching data; returns data and accepts a callback for further mapping or processing
    • update method is used for insert, update, and delete operations; returns an integer indicating the number of rows affected
    • execute for executing general SQL statements, especially DDL or for complex database procedures (yes, you can also use it for DML, but it’s not recommended as it does not return the affected rows).

Path of NeoGoing over the Spring MVC chapter in the preparation book, I was pretty confident I can just have a quick overview and move on to the other chapters. After all, I have been working with MVCs and REST APIs since the beginning. What could possibly surprise me here?

My surprise was getting into the world of Matrix somewhere I didn’t expect. No, I’m not insane, not yet. What do I mean by this? I found out that @RequestParam and **@PathVariable** are not the only ways of mapping the URL parameters … (drums).

The @MatrixVariable annotation in Spring MVC offers a unique and flexible way to extract data from URL path segments. This annotation can be incredibly useful in scenarios where you need to deal with complex URL structures to retrieve specific data. I think it will all make sense if we consider the following example:

  1. Searching for Specific Coffee Blends:
    • URL Example: /coffees;roast=medium;origin=ethiopia;flavor=fruity
    • In this example, the URL is used to search for medium roast coffee blends from Ethiopia with fruity flavor notes. The @MatrixVariable annotation extracts the roast type (medium), origin (ethiopia), and flavor notes (fruity) from the URL path segment.
  2. Fetching Coffee Blend Details and Reviews:
    • URL Example: **/coffees**;roast=medium;origin=ethiopia;flavor=fruity**/details**;brand=BeanBrew**/reviews**
    • Here, the URL is structured to not only search for specific coffee blends but also to fetch detailed information and customer reviews for a particular brand (BeanBrew). This showcases how @MatrixVariable can be used for multi-level information retrieval within the same URL structure.

The code to handle such a scenario would look like this:

View the code on Gist.

Pretty cool, right?

Locking things upThere’s hardly any Spring Boot-based application that does not make use of Spring Security, which is great. The developers are doing a great job of abstracting away all the complexity of security while offering great APIs to bootstrap the security you need in your application in as few lines of code as possible. It has evolved a lot in the past years, and it still does, getting easier and easier to use while offering much more.

DISCLAIMER: Please don’t implement your own security unless you’re 99.99% certain that you know what you are doing. This can also be said about configuring Spring Security. Make sure you understand what you are doing when disabling the one little configuration that we all disable (it’s popped into your mind while reading this, I know it).

Like that, there are other subtleties in the Spring Security configuration. I want to remind you just about a few of them—those that really sparked my interest.

1. antMatchers vs mvcMatchers

I realise that we live in an ideal world and all people work on the latest Spring / Spring Security version where deprecated code is instantly refactored and updated.

However, I still believe it’s worth mentioning that, while we now use .requestMatchers() , there were times when we had to decide between using **antMatchers**or **mvcMatchers**.

Did you take into consideration trailing slashes when making this decision? Or just tried out things until “they just worked”?

  • antMatchers utilizes Ant-style path patterns and does not automatically handle URL normalization for trailing slashes. This means .antMatchers("/secured") matches the exact /secured URL but not /secured/, potentially leaving endpoints accessible to unauthorized users due to slight URL variations.
  • mvcMatchers, on the other hand, aligns with Spring MVC’s URL interpretation. It is more comprehensive, matching /secured as well as /secured/, /secured.html, /secured.xyz, etc., thus handling potential configuration mistakes more effectively.

2. permitAll() vs web().ignoring()

I must confess I’m guilty of using .permitAll() any time I needed to exclude particular routes or resources from getting picked up by Spring Security. I know better now …

  • permitAll()
    • Used within the httpSecurity configuration.
    • Allows all users, whether authenticated or not, to access a specified path.
    • Security Implications:
      • Requests to paths permitted by permitAll() still pass through the entire Spring Security filter chain.
      • This means that even though access is unrestricted, these requests are still subject to various security checks, including CSRF protection.
    • Ideal for paths that should be publicly accessible but still need some level of security checks, like a login page or public API endpoints.
  • web().ignoring()
    • Applied within the WebSecurity configuration.
    • Instructs Spring Security to completely bypass the security filter chain for specified paths.
    • Security Implications:
      • Bypassing the filter chain reduces processing overhead, enhancing performance for the specified paths.
      • Potential Security Risks: The complete bypass of security checks, including CSRF protection, can pose risks if incorrectly applied.
      • Inconsistencies and Monitoring Gaps: Since these requests do not pass through the filter chain, they are not logged or monitored by Spring Security, which can create blind spots in security monitoring.
    • Best suited for static resources like CSS, JavaScript, or public images, where security checks are unnecessary and performance is a priority.

Here’s an example showing how permitAll() and web().ignoring() can be used in a Spring Security configuration (this time upgraded for Spring Security 6+):

View the code on Gist.

As previously stated, you should be aware of the mechanisms you are manipulating when configuring Spring Security, as one trailing slash could change your world.

ConclusionGetting the Spring Professional Developer certification has been a lot of fun. One thing I loved is that after lots of real experience with Spring and Spring Boot I was able to get humble(d) and learn a lot of things again.

Finally, I’ve given myself the time and motivation to understand how the framework works, and I’m so glad I did. I really think that you should go through the preparation tools, whether you’re going for the certification or not. It will make a huge difference in your progress.

No matter how much we believe we know or how much experience we have, being humble will always help us learn new things and see our work in new ways. Learn more and share more.

Wishing you a wonderful holiday season!

May you rest and come back fresh and ready to tackle a new year!

The post 5 years in Spring, yet certification taught me this appeared first on JVM Advent.

View Details

As a Java developer you probably spend a lot of time writing Java source code and executing a Java compiler to convert that human readable Java source into machine readable bytecode stored in Java class files.

If you’ve ever wondered how a compiler works or how a Java compiler creates Java class files, then keep reading! We’ll work through writing a compiler for a simple programming language that compiles to Java bytecode.

We’ll implement a compiler for the esoteric programming language Brainf*ck, which is simple enough that it doesn’t require much code to create a working compiler.


What is Brainfck?Brainfck is a programming language that consists of just 8 operators which operate on an array of memory cells. Even though there are only 8 operators, the language is Turing complete and so, in theory, you can write any program you can think of; whether you’d want to is another question!

The 8 operators use and manipulate the values in the memory, with the current memory cell pointed to by a data pointer. Implementations of the language typically have a memory size of at least 30,000 cells and the cells are typically 1 byte in size but this can vary.

These are the 8 operators:

| > | Moves the data pointer to the right | | < | Moves the data pointer to the left | | + | Increment the value in memory pointed to by the data pointer | | | Decrement the value in memory pointed to by the data pointer | | [ | Jump to the location after the corresponding ] if the current value is zero | | ] | Jump to the corresponding [ if the current value is not zero | | , | Read 1 character (1 byte) from the standard input | | . | Print 1 character to standard out |

Any character not in this list is considered a comment. At the beginning of a Brainf*ck program all the values are zero and the data pointer points to the left-most block:

[0][0][0][0][0]…[0]|DP Given the following program, +>++>++++-, the memory will end up looking like this:

[1][2][3][0][0]…[0] | DP


Compiling to Java BytecodeWe’ll write a Java program that will read Brainfck input files and produce Java class files in a jar as output: then you’ll be able to execute the jar with the java command (java -jar output.jar*).

Java class files contain Java bytecode: these are the instructions that are executed by a Java virtual machine. A Java virtual machine is a stack-based machine: many of the instructions deal with pushing and popping from the operand stack. For example, the instruction iconst_0 is used to push a constant 0 onto the stack and the pop instruction will pop the top value from the stack.

So, how do we create a class file? Technically, a class file is just a bunch of bytes so we could just start writing out a stream of bytes but that’ll get hard pretty quickly so we’d benefit from a higher-level API to help us out.

ProGuardCOREProGuardCORE is a Java bytecode manipulation & analysis library that contains the tools required to read, write and manipulate Java class files and their bytecode. It abstracts away some of the details and provides model classes, editors and builders for all things class file related. It’s not the only such library out there: ASM, Byte Buddy and the new JEP457 are other examples.

ProGuardCORE is published to Maven Central, so you can simply create a new Java project and add a dependency to start using it. For example, a Gradle build.gradle file could look like the following:

plugins { id 'java'}repositories { mavenCentral()}dependencies { implementation 'com.guardsquare:proguard-core:9.1.1'}


A Brainf*ck compilerWe’ll start with some boiler plate code for setting up the input and output arguments, creating a class with a main method and writing the class to a jar.

We can use the ClassBuilder utility to create a class, which takes as parameters the class file version (we’ll use Java 1.6), the access flags (public), the name (BF) and the super class (java/lang/Object).

Adding a method using the ClassBuilder is easy with the addMethod builder method: we first need to provide the access flags (public, static), the name (main) and the descriptor of the method (([Ljava/lang/String;)V).

The final parameter of addMethod takes a function that allows building code with a CodeBuilder. The CodeBuilder interface declares a single method compose that provides a CompactCodeAttributeComposer parameter: this is how we’ll generate the specific bytecode instructions needed to implement Brainfck logic. For now, we’ll just add a return* instruction.

Finally, the utility method writeJar can be used to write our class to a jar.

public class BfCompiler { public static void main(String[] args) throws IOException { if (args.length != 2) throw new RuntimeException("Expected input and output arguments"); var input = Files.readString(Path.of(args[0])); var output = args[1]; var bfClass = new ClassBuilder(CLASS\_VERSION\_1\_6, PUBLIC, "BF", NAME\_JAVA\_LANG\_OBJECT) .addMethod(PUBLIC | STATIC, "main", "([Ljava/lang/String;)V", 65\_535, composer -> { // TODO: Generate code here // Generate the return instruction composer.return\_(); }).getProgramClass(); IOUtil.writeJar(new ClassPool(bfClass), output, externalClassName(bfClass.getName())); }} If you run BfCompiler, it will generate a jar file containing a class called BF, which contains a main method that does nothing but return! Try it out and see nothing for yourself:

$ java -jar output.jar You can though use the javap command to look at the generated bytecode where you’ll see the main method which contains the return instruction at offset 0.

$ javap -cp output.jar -c -v -p BFpublic class BF minor version: 0 major version: 50 flags: (0x0001) ACC\_PUBLIC this\_class: #2 // BF super\_class: #4 // java/lang/Object interfaces: 0, fields: 0, methods: 1, attributes: 0Constant pool: #1 = Utf8 BF #2 = Class #1 // BF #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object #5 = Utf8 main #6 = Utf8 ([Ljava/lang/String;)V #7 = Utf8 Code{ public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC\_PUBLIC, ACC\_STATIC Code: stack=0, locals=1, args\_size=1 0: return} Next we’ll need to generate the code that implements the logic of the the input Brainf*ck program.


MemoryA Brainf*ck program operates on a block of memory with a typical implementation size of 30,000. Some implementations automatically expand the memory size if it’s exceeded but for our implementation we’ll use a fixed-size byte array to represent the memory; and to keep it simple we won’t handle out of bounds access.

So, the first code we’ll need to generate in our main method is to declare the array, which will require 3 instructions:

composer .sipush(30\_000) .newarray(arrayTypeFromInternalType(BYTE)) .astore(MEMORY); The sipush (short integer) instruction pushes the desired size of the array, 30000, onto the stack which the newarray instruction will pop from the stack. The operand for the newarray instruction is the type of array: in this case a byte array. The actual value here is 8 but we use the utility function arrayTypeFromInternalType and the BYTE constant to make the code more readable.

Then we store the array that was just pushed onto the stack into a local variable slot with an astore instruction. The astore instruction pops a reference value from the stack and stores it in the specified local variable slot.

MEMORY is a static final field int that contains the local variable slot number, which you should add to the BfCompiler class:

private static final int MEMORY = 1;


Data PointerWe’ll also need to keep track of the data pointer value: we’ll use another local variable slot for that and generate some code to initialise the value to zero. The iconst_0 instruction pushes the integer 0 onto the stack and then the istore instruction will pop the value from the stack and store it in the specified local variable slot.

composer .iconst\_0() .istore(DATA\_POINTER); DATA_POINTER is a static final int field that contains the local variable slot number, which you should add to the BfCompiler class:

private static final int DATA\_POINTER = 0; Now we’ve initialised the memory and the data pointer; next we need to generate the code for each of the Brainf*ck operators in the input.


ParsingWe don’t need a complicated or fancy parser since the language is so simple. We can simply iterate over the characters in the input string, generating the corresponding code for each operator and then continue to the next one:

input.chars().forEach(c -> { switch (c) { case '>' -> move(composer, 1); // Move right by 1 case '<' -> move(composer, -1); // Move left by 1 case '+' -> increment(composer, 1); // Increment by 1 case '-' -> increment(composer, -1); // Decrement by 1 case ',' -> printChar(composer); case '.' -> readChar(composer); case '[' -> loopBegin(composer); case ']' -> loopEnd(composer); default -> { // Ignore other characters. } }}); This will call a corresponding method to generate the code for each operator and ignore any non-operator characters.

We’ll take a look at each code generation method one by one.


The move operators < >The < and > operators move the data pointer left or right by 1: in our generated Java program this corresponds to incrementing or decrementing the data pointer by 1.

Out of all of the Brainfck operators, this is the easiest to generate code for as there is a single Java bytecode instruction that does exactly what we need: iinc*.

The iinc instruction takes as operands a local variable index and a signed byte value; and when executed the local variable will be incremented by that value.

private static void move(CompactCodeAttributeComposer composer, int amount) { composer.iinc(DATA\_POINTER, amount);}


The increment / decrement operators + –The increment and decrement operators increment or decrement the value in the current memory cell by 1. In our generated Java bytecode this corresponds to incrementing or decrementing the value in the array at the data pointer index i.e. in Java source code it would look like MEMORY[DATA_POINTER]++.

The baload instruction can be used to load a byte value from a byte array. It pops its two operands from the stack: the array reference and the array index. So to load a value from the memory array requires 3 instructions:

  • aload to push the memory array onto the stack
  • iload to push the data pointer array index onto the stack
  • baload to load the value from the memory array at the data pointer index

There is also a corresponding bastore instruction which pops from the stack the array reference, the array index and the value to store into the array.

Since we’re going to need the memory array and data pointer on the stack later for using with bastore we duplicate the top two entries on the stack, using dup2, before the baload instruction pops them:

composer .aload(MEMORY) .iload(DATA\_POINTER) .dup2() .baload() After executing these instructions the JVM stack will look something like this:

| MEMORY[DATA_POINTER] | | DATA_POINTER | | MEMORY |

We’ll then add 1 or -1 to the value on the top of the stack by pushing 1 or -1 and using the iadd instruction to add them together. The iadd instruction pops two integers from the stack, adds them together and then pushes the result back onto the stack.

composer .aload(MEMORY) .iload(DATA\_POINTER) .dup2() .baload() .iconst(amount) .iadd() The JVM stack will then look something like this:

| MEMORY[DATA_POINTER] + (-)1 | | DATA_POINTER | | MEMORY |

We now have all the operands in place to use the bastore instruction to store the incremented value back into the memory array. The bastore instruction will pop the memory array, data pointer array index and the value to store into the memory array.

The full increment code generation method looks like this:

private static void increment(CompactCodeAttributeComposer composer, int amount) { composer .aload(MEMORY) .iload(DATA\_POINTER) .dup2() .baload() .iconst(amount) .iadd() .bastore();} So far we can move the data pointer and modify the values in memory but we can’t yet observe what’s happening: next we’ll generate code for the print operator.


The print operator .If you’re a Java or Kotlin developer, you’ll know that to print something to standard out you’ll use the System.out.print method (or println if you want a newline printed). We’ll generate the code to invoke the same print method to print a character from the Brainf*ck memory.

The code to print a character from memory will need the following instructions:

  • getstatic to push the System.out reference onto the stack
  • aload / iload / baload to load the byte from the memory array
  • i2c to convert the byte to a char
  • invokevirtual to invoke the print method on System.out

The getstatic instruction takes the fully qualified class name, field name and descriptor as parameters and pushes the field value onto the stack:

composer .getstatic("java/lang/System", "out", "Ljava/io/PrintStream;") The code to load the byte from the Brainf*ck memory is familiar, since it’s the same 3 instructions as for the increment operator:

composer .getstatic("java/lang/System", "out", "Ljava/io/PrintStream;") .aload(MEMORY) .iload(DATA\_POINTER) .baload() We’ll then add the i2c instruction to convert the byte to a char:

composer .getstatic("java/lang/System", "out", "Ljava/io/PrintStream;") .aload(MEMORY) .iload(DATA\_POINTER) .baload() .i2c() After executing these instructions the JVM stack will look something like this:

| (char)MEMORY[DATA_POINTER] | | System.out |

The stack is now set up for executing an invokevirtual instruction for the print method. The object on which to execute the method is popped from the stack after the method parameters, in this case the single character.

The invokevirtual instruction requires the fully qualified class name (java/io/PrintStream), the method name (print) and the method descriptor ((C)V).

The full printChar code generation method looks like this:

private static void printChar(CompactCodeAttributeComposer composer) { composer .getstatic("java/lang/System", "out", "Ljava/io/PrintStream;") .aload(MEMORY) .iload(DATA\_POINTER) .baload() .i2c() .invokevirtual("java/io/PrintStream", "print", "(C)V");} We’ll now be able to see the results of the increment operations on the memory by printing out values. If you run the compiler with the following input and execute the resulting jar, what do you see?

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.


The read operator ,We can now print characters using System.out.println and next we’ll implement the read operator with System.in.

As with the print operator, we’ll start by pushing System.in onto the stack:

composer .getstatic("java/lang/System", "in", "Ljava/io/InputStream;") To read a byte from the input we can use the read(byte[] array, int offset, int length) method of the System.in input stream. This method takes as parameters a byte array, a destination offset within the array and the length of bytes to read from the stream which will then be stored in the array. The method returns the number of bytes that were actually read from the stream.

Our Brainfck memory is a byte array and the data pointer points to the position in the array where the read byte should be stored; so these can be directly used as parameters to the read method. The Brainfck read operator only reads a single byte at a time so the length parameter to the read method is a constant integer 1.

This means we need to push the memory array, the data pointer and constant integer 1 onto the stack before the invokevirtual instruction to invoke the read method:

composer .getstatic("java/lang/System", "in", "Ljava/io/InputStream;") .aload(MEMORY) .iload(DATA\_POINTER) .iconst\_1() .invokevirtual("java/io/InputStream", "read", "([BII)I") Remember that the read method returns the number of bytes read? The invokevirtual instruction will push that return value onto the stack. We don’t care about it so we can discard it with a pop instruction.

The full readChar code generation method looks like this:

private static void readChar(CompactCodeAttributeComposer composer) { composer .getstatic("java/lang/System", "in", "Ljava/io/InputStream;") .aload(MEMORY) .iload(DATA\_POINTER) .iconst\_1() .invokevirtual("java/io/InputStream", "read", "([BII)I") .pop();} Now try compiling and running ,+.; type a character, press enter and what do you see?

We’re almost done but we’re still missing the [ and ] operators.


The loop operators [ ]So far we’ve implemented 6 out of the 8 operators and have been able to compile and execute some simple Brainf*ck programs. But the two remaining operators are required to allow us to run more complicated programs.

Let’s remind ourselves of what the remaining two operators do:

| [ | Jump to the location after the corresponding ] if the current value is zero | | ] | Jump to the corresponding [ if the current value is not zero |

For our compiler, this means that the generated code for the [ operator will need to conditionally jump forward to a location in code which we have not yet generated for the ]; and the code for the ] operator will need to conditionally jump back to the location of the corresponding [ operator.

To implement this in Java bytecode we’ll use the ifeq and ifne instructions that jump to a specified offset if the value on the stack is 0 or not 0 respectively. When we’re parsing the [ operator to generate the ifeq we’ll need to already know the location to jump forward to even though we haven’t yet generated it.

ProGuardCORE solves this problem with labels: the CompactCodeAttributeComposer has a createLabel() method that will create a label that can be used as a parameter for the label pseudo-instruction like this:

var exampleLabel = composer.createLabel();composer .iconst\_0() .ifeq(exampleLabel) ... .label(exampleLabel) .... We’ll need to keep track of the labels that we create because we’ll create the labels when generating code for the [ and then need to use the labels later when generating the code for the corresponding ].

We’ll do this with a stack of pairs of labels: one label for the loop body and another for the loop exit. We use a stack because loops can be nested and this allows us to match the corresponding [ and ].

The following record and field should now be added to our BfCompiler class:

private record LoopInfo(Label body, Label exit) {}private static final Stack<LoopInfo> loops = new Stack<>(); For the beginLoop code generation method, we start by creating labels and pushing them onto the stack:

var loopInfo = loops.push(new LoopInfo(composer.createLabel(), composer.createLabel())); Then we need to generate the code for the [ operator with the following instructions:

  • aload / iload / baload to load the byte from the memory
  • ifeq to conditionally jump to the label loopInfo.exit
  • label to mark the position in the code with the label loopInfo.body

The full beginLoop code generation method then looks like this:

private static void loopBegin(CompactCodeAttributeComposer composer) { var loopInfo = loops.push(new LoopInfo(composer.createLabel(), composer.createLabel())); composer .aload(MEMORY) .iload(DATA\_POINTER) .baload() .ifeq(loopInfo.exit) // jump to loop exit if zero .label(loopInfo.body); // mark the start of loop body} The code generation for the ] operator works in a similar way except it pops the labels from the loops stack; then it generates the code for the ] operator with the following instructions:

  • aload / iload / baload to load the byte from the memory
  • ifne to conditionally jump to the label loopInfo.body
  • label to mark the position in the code with the label loopInfo.exit

We expect that the [ and ] operators are correctly balanced when popping from the array but in the case of a syntax error in the input this may not be the case. For example, the following input would throw an EmptyStackException if we try to pop labels from the loops stack: ++[--]].

We can provide a nicer error message by checking if the stack is empty before we try to pop the labels from the loops stack.

The full endLoop code generation method then looks like this:

private static void loopEnd(CompactCodeAttributeComposer composer) { if (loops.empty()) throw new RuntimeException("Unexpected ']'"); var loopInfo = loops.pop(); composer .aload(MEMORY) .iload(DATA\_POINTER) .baload() .ifne(loopInfo.body) .label(loopInfo.exit);} As a final improvement, we can also detect when there are too many [ operators compared with the number of ] operators and provide a nicer error message. If the loops stack is not empty at the end of our code generation we know that we’ve pushed more loop labels onto the stack than we have popped:

// Add just before composer.return\_();if (!loops.empty()) throw new RuntimeException("Too many '['"); Now our compiler is ready for anything (almost)!


Finally, a working compilerCongratulations! The compiler can now run almost any Brainf*ck program. Try the following, what does it do?

-[------->+<]>-.-[->+++++<]>++.+++++++..+++.[--->+<]>-----.+++++[->++<]>.>+[--->++<]>.---------.-[-->+<]>-----.[--->+<]>-. The full compiler code can be found over on GitHub which you can easily run via Gradle:

$ ./gradlew run --args "examples/hellojvm.bf build/output.jar"$ java -jar buid/output.jar Why Almost?There are some limitations in our implementation that prevent us from running every possible Brainfck program. For one, the memory is limited to a fixed size of 30,000 cells: some Brainfck programs may require more than this.

There are also some JVM limitations: we currently generate code in a single method. A single method in the JVM is limited to 65,535 bytes which we could exceed. In fact, if you compile this Mandelbrot Brainf*ck program we already reach 43,658 bytes.

One improvement we can make to reduce code size could be to generate helper methods for each of our operations rather than generating all the code in a single method.

But a bigger improvement would be to optimise the generation of the code for the move (< >) and add (+ -) operators.

OptimizationsConsider the following Brainf*ck program: >>>>>

Our compiler will call the move code generation method 5 times, generating 5 iinc instructions; when instead we could generate a single iinc instruction with 5 as the increment amount.

The move and increment methods are already set up to pass in increment amounts other than 1 or -1: can you modify the compiler to optimise the generation of consecutive move and increment operations?

Here’s a nice blog post that describes this and other optimizations; can you implement them in our compiler?


Next stepsWe’ve written a JVM compiler for a real, albeit esoteric language using a small amount of Java code and just 19 bytecode instructions; but there are many more bytecode instructions — take a look at the specification.

For your next steps with ProGuardCORE, take a look at the manual, examples or the source code for ProGuard which uses ProGuardCORE to shrink & optimise Java bytecode.

ProGuardCORE is also not the only library that provides the tools to generate Java bytecode: other examples are ASM and Byte Buddy; and the functionality to generate Java bytecode will even be included in the Java standard library soon with JEP 457.


The post My First Compiler appeared first on JVM Advent.

View Details

What’s an assertion?It’s a way to test an assumption in the code normally associated with an expected result, where we will compare it to the current outcome.

We all know a lot of different assertions: if it is null, equal, true and all its variations are negations: not null, not equal, false, and so on.

Adding assertions in the tests is that makes the test a test!

Assertion libraries for the win!The unit test libraries support different assertions but are limited in the way few variations exist. Taking JUnit 5 as an example, it has the class Assertion with 8 main assertion targets, excluding its variations as the negation (not and false) and parameter types:

  • array
  • exceptions
  • equals
  • instanceOf
  • iterable
  • null
  • timeout
  • true

Because of this, new libraries that provide only assertion methods emerged to solve this gap. Tools like Truth, Hamcrest, and AssertJ provide extensive ways to assert different aspects providing different features.

This article will use AssertJ given the extensive assertion methods, constant development, and ability to extend its features.

Custom Assertion with AssertJAt this point, I assume that you know about assertions and how to use them.

The caseLet’s imagine you have an entity within the following restrictions:

  • amount where the minimum acceptable is 1.000 and the maximum is 40.000
  • installments where the minimum acceptable is 2 and the maximum is 48

The Simulation class expresses these constraints:

public class Simulation { @NotNull(message = "Amount cannot be empty") @Min(value = 1000, message = "Amount must be equal or greater than $ 1.000") @Max(value = 40000, message = "Amount must be equal or less than than $ 40.000") private BigDecimal amount; @NotNull(message = "Installments cannot be empty") @Min(value = 2, message = "Installments must be equal or greater than 2") @Max(value = 48, message = "Installments must be equal or less than 48") private Integer installments;} The classical assertionWe can easily test the object data using the SoftAssertions example and the isBetween() assertion to check the amount and installments min and max constraints. PS: see how AssertJ is awesome in providing assertions like this?

There’s nothing wrong with a classic assertion like this, but when you need to validate the amount and installments, your code will have the constraint value, creating more efforts to change it later.

class SimulationTest { @Test void basicCheck() { Simulation simulation = Simulation.builder().name("Elias").cpf("123456").email("elias@elias.com") .amount(new BigDecimal(1000)).installments(48).insurance(false).build(); SoftAssertions.assertSoftly(softly -> { softly.assertThat(simulation.getName()).isEqualTo("Elias"); softly.assertThat(simulation.getCpf()).isNotEmpty(); softly.assertThat(simulation.getEmail()).isEqualTo("elias@elias.com"); softly.assertThat(simulation.getAmount()).usingComparator(BigDecimal::compareTo).isBetween(new BigDecimal(1000), new BigDecimal(40000)); softly.assertThat(simulation.getInstallments()).isBetween(2, 48); softly.assertThat(simulation.getInsurance()).isFalse(); }); }} The custom assertionIt seems hard but it’s not! AssertJ provides the AbstractAsser class to create your assertion, with all the other ones handy.

The steps are simple, as we need to:

  1. Create the custom assertion class
  2. Add the required constructor
  3. Add the assertThat method
  4. Implement the custom assertion methods

  5. Create the custom assertion classThe first point is to create the custom assertion class, which we will call SimulationAssert.
    The class must extend the AbstractAssert specifying two arguments: the class itself and to be able to chain the custom methods and the class under test, which is the Simulation class.

public class SimulationAssert extends AbstractAssert<SimulationAssert, Simulation> {} 2. Add the required constructorAn assertion always has the actual and expected results. In a custom assertion, we need to express the actual as the class under test in a constructor, calling the parent AbstractAssert constructor.

public class SimulationAssert extends AbstractAssert<SimulationAssert, Simulation> { protected SimulationAssert(Simulation actual) { super(actual, SimulationAssert.class); }} 3. Add the assertThat methodThe creation of the assertThat method is necessary and the first point to assert the class under test. The method must be static having a parameter which is the class under test, and return its new class instance

// previous code ignoredpublic static SimulationAssert assertThat(Simulation actual) { return new SimulationAssert(actual);} This is the assertThat that will make available the custom method without losing the basic ones.

  1. Implement the custom assertion methodsThe custom methods have a pattern:

  2. will return, all the time, it’s class as we do in a Fluent Builder interface

  3. [optional] have a method parameter to specify any extra information
  4. a check of the class under test isNotNull()
  5. the usage of the failWithMessage() method when the assertion doesn’t satisfy the requirements

Let’s create one custom assertion to validate the valid installments. If you remember from the Simulation entity, the allowed values are a minimum of 2 and a maximum of 48. The method will have an if statement where we will check the minimum and maximum values:

// previous code ignoredpublic SimulationAssert hasValidInstallments() { isNotNull(); if (actual.getInstallments() < 2 || actual.getInstallments() > 48) { failWithMessage("Installments must be must be equal or greater than 2 and equal or less than 48"); } return this;} * line 3 shows that the method hasValidInstallments() return the SimulationAssert class * line 4 has the isNotNull() check for the class under test * line 6 has the check for the min and max values * line 7 will fail the test where we can set a custom message using the failWithMessage() method * line 10 returns its class

You can do the same for the amount constraint in the entity class:

// previous code ignoredpublic SimulationAssert hasValidAmount() { isNotNull(); var minimum = new BigDecimal("1.000"); var maximum = new BigDecimal("40.000"); if (actual.getAmount().compareTo(minimum) < 0 || actual.getAmount().compareTo(maximum) > 0) { failWithMessage("Amount must be equal or greater than $ 1.000 or equal or less than than $ 40.000"); } return this;} The usage in the testNow comes the easiest part: the test creation!

class SimulationsCustomAssertionTest { @Test void simulationErrorAssertion() { var simulation = Simulation.builder().name("John").cpf("9582728395").email("john@gmail.com") .amount(new BigDecimal("1.500")).installments(5).insurance(false).build(); SimulationAssert.assertThat(simulation).hasValidInstallments(); SimulationAssert.assertThat(simulation).hasValidAmount(); }} * line 1 is the test class * line 4 is the test method * lines 5 and 6 have the Simulation object with some valid data

To use the custom assertion, instead of using the Assertions class from AssertJ, we need to use the custom assertion class SimulationAssert. You will have access to all the custom-created assertions plus all the methods from the AbstractAssert.

So, lines 8 and 9 use the SimulationAssert methods to assert the class under test (Simulation) complies with the custom validations.

Of course, when the value of the attributes does not meet the validation we created the failWithMessage() method will take place. Try it out!

How about the other fields to check?If you need to validate the other fields of the Simulation object you can either mix the custom assertion with the AssertJ one. In the below example add an extra assert to check if the name is the expected one, as you can see in line 8:

@Testvoid simulationValidationAssertion() { var simulation = Simulation.builder().name("John").cpf("9582728395").email("john@gmail.com") .amount(new BigDecimal("1.500")).installments(5).insurance(false).build(); SimulationAssert.assertThat(simulation).hasValidInstallments(); SimulationAssert.assertThat(simulation).hasValidAmount(); Assertions.assertThat(simulation.getName()).isEqualTo("John");} You can also create another custom assertion method like hasName(). If you would like to do so, the same pattern we learned will apply by adding the optional step: the parameter to check. This would be a possible implementation:

public SimulationAssert hasNameEqualsTo(String name) { isNotNull(); if (!Objects.equals(actual.getName(), name)) { failWithMessage("Expect the Simulation to have the name equals to %s", name); } return this;} Line 4 verifies if the name value of the Simulation actual result is equal to the expected one. If not the failWithMessage() will fail the test with the corresponding defined message.

You can now use the new custom method to check the name value without losing the class context.

@Testvoid simulationValidationAssertion() { var simulation = Simulation.builder().name("John").cpf("9582728395").email("john@gmail.com") .amount(new BigDecimal("1.500")).installments(5).insurance(false).build(); SimulationAssert.assertThat(simulation).hasValidInstallments(); SimulationAssert.assertThat(simulation).hasValidAmount(); SimulationAssert.assertThat(simulation).hasNameEqualsTo("John");} The endThat’s all folks!

You can find a fully implemented and working example in the manage-data branch of the credit-api project, where you can see the:

  • SimulationAssert class
  • Test usage in the SimulationsCustomAssertionTest class

The post Assert with Grace: Custom Assertions for Cleaner Code appeared first on JVM Advent.

View Details

Why AI orchestrationLarge Language Models (LLMs) are changing the way we develop and interact with applications and software. To back-office employees to business users to developers, natural language is the future of user interaction. This brings challenges and innovation in the way we build and code AI applications.

We need modern ways to manage these AI applications. AI orchestration is the process of managing multiple AI applications and services in a coordinated way. It helps to optimize the performance, scalability, and reliability of AI solutions. AI orchestration is solving the problem of complexity and fragmentation in the AI landscape, where different tools, platforms, and frameworks are used for different tasks and goals.

What is Semantic Kernel aka SK?Semantic Kernel (SK) is a powerful SDK that allows you to combine traditional programming languages, such as Java, C#, and Python, with the most advanced Large Language Model (LLM) AI “prompts” that support prompt templating, chaining, and planning features. This lets you create new functionalities in your apps that can boost the productivity of your users: for example, summarizing a long chat conversation, highlighting an important “next step” that’s automatically added to your to-do list, or planning a whole vacation instead of just booking a flight.

Components of SKThere are several components within SK to build AI applications. In the following section let me walk through them and explain their usage know-how.

KernelThe kernel orchestrates a user’s ask. To do so, the kernel runs a pipeline or chain of tasks that is defined. While the pipeline or chain is executing, a common context is provided by the kernel so data can be shared and passed between those underlying tasks.

First to create a Kernel we need the OpenAI endpoint and model details. In the following example we are using Azure OpenAI endpoint.

client.azureopenai.key=XXYYZZ1234client.azureopenai.endpoint=https://nljug.openai.azure.com/client.azureopenai.deploymentname=gpt-35-turbo Then to initialize Kernel, we would need to read these properties (in this example from conf.properties file)

AzureOpenAISettings settings = new AzureOpenAISettings(SettingsMap. getWithAdditional(List.of(new File("src/main/resources/conf.properties"))));OpenAIAsyncClient client = new OpenAIClientBuilder().endpoint(settings.getEndpoint()) .credential(new AzureKeyCredential(settings.getKey())).buildAsyncClient();TextCompletion textCompletion = SKBuilders.chatCompletion() .withOpenAIClient(client) .withModelId(settings().getDeploymentName()) .build();Kernel kernel = SKBuilders.kernel().withDefaultAIService(textCompletion).build(); You can select the LLM service while instantiating Kernel object, for example, all models of OpenAI, Azure OpenAI or Hugging Face.

Also, to gain visibility of what the Kernel is doing you can add telemetry and logs to this object.

PluginsPlugins are like the “body” of your AI app. They consist of prompts and native functions. You can connect your application to AI plugins, enabling interactions with the real world. By leveraging plugins, you can encapsulate various capabilities into a cohesive unit of functionality. This unified functionality can then be executed by the kernel. Plugins have the flexibility to incorporate both native code and requests to AI services through semantic functions.

A plugin consists of one or more Semantic functions. To create a Semantic function, you need to define a “skprompt.txt”, which holds the prompt you want to use with input definitions.

Below is the example of a “Translate” function.

Translate the input below into {{$language}}MAKE SURE YOU ONLY USE {{$language}}.{{$input}}Translation: To provide a semantic description of this function (and configure the AI service), you’ll need to create a “config.json” file in the same folder as the prompt. This file outlines the function’s input parameters and description.

{ "schema": 1, "type": "completion", "description": "Translate the input into a language of your choice", "completion": { "max\_tokens": 2000, "temperature": 0.7, "top\_p": 0.0, "presence\_penalty": 0.0, "frequency\_penalty": 0.0, "stop\_sequences": [ "[done]" ] }, "input": { "parameters": [ { "name": "input", "description": "Text to translate", "defaultValue": "" }, { "name": "language", "description": "language of translation", "defaultValue": "" } ] }} To use this in the Kernel object and execute the Semantic function, you will need to write the below lines of code.

ReadOnlyFunctionCollection skill = kernel. importSkillFromDirectory("TranslateSkill", "src/main/resources/Skills", "TranslateSkill");CompletionSKFunction translateFunction = skill.getFunction("Translate", CompletionSKFunction.class);SKContext translateContext = SKBuilders.context().build();translateContext.setVariable("input", "How are you doing?");translateContext.setVariable("language", "Dutch");Mono<SKContext> result = translateFunction.invokeAsync(summarizeContext); We can also create Plugins and functions as a Java class. Then we can instantiate and use it in type-safe way. There is a submodule defines some out-of-the-box Plugins you can use –> semantickernel-plugin-core. But it is also possible to implement your own, using annotations @DefineSKFunction, @SKFunctionInputAttribute, @SKFunctionParameters. This pattern is specifically used to invoke external APIs, or some execute business logic and is useful when you chain different semantic functions.

Chaining Plugins/Semantic functionsChaining plugins or semantic functions involves linking multiple functions together to create a cohesive pipeline. Each function processes input data and passes the result to the next function. This allows developers to build complex workflows, combining various AI services and native code seamlessly. Think of it as assembling a series of interconnected building blocks, where each function contributes to the overall functionality of the application.

The following code demonstrates chaining of Plugins where Summarize and Translate happens one after another for a given input text.

kernel.importSkillFromDirectory("SummarizeSkill", "src/main/resources/Skills", "SummarizeSkill");kernel.importSkillFromDirectory("TranslateSkill", "src/main/resources/Skills", "TranslateSkill");SKContext summarizeContext = SKBuilders.context().build();summarizeContext.setVariable("input", ChatTranscript);summarizeContext.setVariable("language", "dutch");Mono<SKContext> result = kernel.runAsync( summarizeContext.getVariables(), kernel.getSkill("SummarizeSkill").getFunction("Summarize"), kernel.getSkill("TranslateSkill").getFunction("Translate")); PlannersPlanner in the Semantic Kernel is a powerful feature that automatically orchestrates AI services. It takes a user’s request and generates a plan on how to achieve it. By intelligently combining registered plugins, planners create workflows for tasks like reminders or complex data mining, enhancing the flexibility and efficiency of AI-powered applications.

For a given construct above, if the “Task” is to Summarize and then Translate a given text, the Planner will pick up “Summarizer” and “Translator” functions to generate the “Result”.

Another example is, for a “Task” to Define and then Email generation for a given text, the Planner will pick up “Define” and “Email Gen.” functions to generate the “Result”.

There are 3 different planners available, Action, Sequential and Stepwise.

The following code example shows a Sequential planner which has 3 Plugins with multiple semantic functions in each of them, and depending on the task it will intelligently pick the semantic functions necessary to perform the task.

kernel.importSkillFromDirectory("WriterSkill", "src/main/resources/Skills", "WriterSkill");kernel.importSkillFromDirectory("SummarizeSkill", "src/main/resources/Skills", "SummarizeSkill");kernel.importSkillFromDirectory("DesignThinkingSkill", "src/main/resources/Skills", "DesignThinkingSkill");SequentialPlanner planner = new SequentialPlanner(kernel, new SequentialPlannerRequestSettings( <relevancyThreshold>, <maxRelevantFunctions>, Set.of(), Set.of(), Set.of(), <maxTokens> ), <SystemPrompt>);Mono<SKContext> result = planner. createPlanAsync("rewrite the following text in Yoda from Starwars style" + <TextToSummarize>) .invokeAsync(); Giving memories to SKWhat’s memory? Think of giving LLM information to support it when executing a (or a set) of Plugins. For example, you want to get a summary of your dental insurance details from a 100-page insurance document, here you might choose to send the whole document along with your query to the LLM, but every model in LLM has a limit on tokens what it can process with per request. To manage that token limitation, it is helpful if you can search the document first, get the relevant pages or texts out of it and then query the LLM with those selected pages/texts. That way LLM can efficiently summarize the content and not run out of tokens per request. The “selected pages/texts” in this example are Memories.

For this example, we will be using 2 Kernels, one with AI Services type Embedding and another one with TextCompletion.

Kernel with embedding needs a MemoryStore, this can be in-memory or any vector store. Although, right now, SK in Java supports only Azure Cognitive Search, support to other vector stores will be coming soon.

The kernel with Embedding with Azure Cognitive Search (ACS) as data or vector store is instantiated as follows:

EmbeddingGeneration<String> textEmbeddingGenerationService = SKBuilders.*textEmbeddingGeneration*() .withOpenAIClient(openAIAsyncClient) .withModelId("embedding") .build();Kernel kernel = SKBuilders.*kernel*() .withDefaultAIService(textEmbeddingGenerationService) .withMemoryStorage(new AzureCognitiveSearchMemory("<ACS\_ENDPOINT>", "<ACS\_KEY>")) .build(); If documents and/or data are already indexed in ACS, you can search on it, which gives you back the relevant sections of the memory.

Mono<List<MemoryQueryResult>> relevantMemory = kernel.getMemory() .searchAsync(<INDEX\_NAME>,<QUERY> , 2, 0.7f, true); Then we can use these search results as an extra context while invoking LLM, for example, a Summarizer function on the above search results.

List<MemoryQueryResult> relevantMems = relevantMemory.block();StringBuilder memory = new StringBuilder();relevantMems.forEach(relevantMem -> memory.append("text: ").append(relevantMem.getMetadata().getText()));Kernel kernel = kernel();ReadOnlyFunctionCollection conversationSummarySkill =kernel.importSkill(new ConversationSummarySkill(kernel),null);Mono<SKContext> summary = conversationSummarySkill.getFunction("SummarizeConversation", SKFunction.class).invokeAsync(relevantMemory); The above example uses Summarizer defined as a Java class using annotation @DefineSKFunction.

Wrapping UpThis article introduces SK, there are many more components and patterns you can implement using this SDK which I could not include here. The link to the official documentation and the GitHub repo of SK.

If you are interested in playing around with examples, please check out my personal GitHub repo.

The post AI orchestration with Semantic Kernel appeared first on JVM Advent.

View Details

Puzzler #1: Colons, Arrows, Braces, Break, YieldWhich of the following lines can occur in a switch? (Not all necessarily in the same.) And in which kind of switch?

case 5: if (Math.random() < 0.5) break; Click arrow to reveal answerSure. This is legal in a classic switch statement. Half the time, execution falls through the next case. Don’t code like this at home.

case 5 -> log("TGIF"); yield "Friday"; Click arrow to reveal answerNo. This is a switch expression without fall through. The -> must be followed by an expression, throw, or a block. It would be ok if you enclosed the code following the -> in braces: case 5 -> { log("TGIF"); yield "Friday"; }

case 5: log("TGIF"); yield "Friday"; Click arrow to reveal answerYes. This is a switch expression with fall through. This branch doesn’t fall through, actually, but yields the expression’s value. No braces needed because, colon.

case 5 -> { if (Math.random() < 0.5) break; log("TGIF"); } Click arrow to reveal answerAll good. This is a switch statement without fall through. Half the time, the call to log is skipped. Don’t code like this at home. Note that the braces are necessary.

Did you get all four right? Congratulations! You earned a partridge in a pear tree. Skip the next section and move on to puzzler #2.

Principle #1: Two AxesThe classic switch of the C language had a simple purpose: to be compiled into a “jump table” that holds the memory addresses of the code for each case. The value of the “selector”—the expression inside switch (...)—is used as table index, either as an offset or with a binary search. That is more efficient than a linear if/else if/else if/else branch sequence, particularly if the number of cases is large. In a high-level language, there is no way to code a jump table directly. Hence the switch statement.

Many programmers, when learning switch, were warned of the weirdness of “fall through”. By default, execution flows from one case to the next. Of course it does. That’s how jump tables work. They only care about the efficient jump. If you don’t want to fall through, just add a break, which is compiled into a jump to the end.

Thirty years later, many modern programming languages support pattern matching. In its simplest form, using Java syntax:

String seasonName = switch (seasonCode) { case 0 -> "Spring"; case 1 -> "Summer"; case 2 -> "Fall"; case 3 -> "Winter"; default -> "???"; }; There are two crucial differences:

  • Each case yields a value
  • The branches are disjoint; there is no fall through

Are these differences crucial enough to come up with a different syntax for pattern matching? The Java designers didn’t think so. This is what they wrote in JEP 361:

“By teasing the desired benefits (expression-ness, better control flow, saner scoping) into orthogonal features, switch expressions and switch statements could have more in common. The greater the divergence between switch expressions and switch statements, the more complex the language is to learn, and the more sharp edges there are for developers to cut themselves on.”

Not everyone agreed.

So, now we have four forms of switch:

  • The classic switch statement, unchanged from Java 1.0. With fall through.
  • Expression switch with no fall through—the crisp, clean form that you just saw, with -> value after each case.
  • A modern switch statement with no fall through.
  • For completeness, expression switch with fall through. Why would you ever want that??? You probably don’t. Except if one of the cases has a side effect, such as the logging call above. Then turn all arrows into colons, and add yield in each case. Hopefully your IDE can help you with that rewrite.

For a switch to be an expression, it must be in expression position: assigned to a variable or passed as a method argument. Also, if you see break, you know it must be a statement. And if you see yield, it must be an expression.

The colon : denotes classic fall-through. The -> indicates no fall-through. Mercifilly, you can’t mix them in the same switch.

After a colon, you can have any number of statements. As always. With a switch expression, there must be one or more yield statements.

Conversely, after an arrow, there can only be an expression, or throw, or a block. Which must have yield in a switch expression.

Caution: Some programmers think that -> signals an expression switch because it looks like a lambda expression. And because it must be followed by an expression or block. That is not so. A no-fall-through switch statement uses case ... -> { ... }.

Puzzler #2Is this legal?

Object x = ...;String result = switch (x) { case "" -> "empty"; case 0 -> "zero"; default -> "something else";}; Click arrow to reveal answerNo—a constant label of type java.lang.String and of type int is not compatible with switch selector type Object

What about

enum Size { SMALL, MEDIUM, LARGE, EXTRA\_LARGE };Object x = ...;String result = switch (x) { case Size.EXTRA\_LARGE -> "extra large"; default -> "something else";}; Click arrow to reveal answerPerfectly legal.

Why isn’t it like the preceding code snippet? The constant label has type Size, and the switch selector type is Object.

The rules are different for enum case constants. Their value must merely be assignment compatible to the selector type.

Principle #2: Selector typesThe selector types of switch have expanded over time:

  • Java 1.0: int, short, byte, char
  • Java 5: Integer, Short, Byte, Char
  • Java 5: enum
  • Java 7: String
  • Java 17: any reference type, pattern cases
  • Still to come: float, double, long, boolean

With pre-pattern matching switches, a constant case label must be a compile-time constant, and it must be assignment-compatible to the selector expression type. For example, you can have case 5 when the selector type is Integer.

With pattern-matching switches, the rules are different and complex. When the selector type is Object or some other supertype of String, Integer, Short, Byte, or Char, you can’t have constant labels. For example,

case 0 -> "zero"; won’t work when the selector type is something other than int, short, byte, char, Integer, Short, Byte, Char.

The remedy is:

case Integer i when i == 0 -> "zero"; But for enum, the rules have evolved differently. First off, the rules have changed for the case constants. Previously, you wrote

case EXTRA\_LARGE -> "extra large"; The enum type was inferred from the selector type. Since now the selector type can be a supertype, you qualified enum names:

case Size.EXTRA\_LARGE -> "extra large"; You can use them even if you don’t have to, with an enum selector type.

More importantly, you are allowed to use enum constants in case labels. This is useful for pattern matching in a sealed hierarchy where some of the implementing classes are enumerations, such as in this (incomplete) JSON primitive type hierarchy:

sealed interface JSONPrimitive permits JSONNumber, JSONString, JSONBoolean {}final record JSONNumber(double value) implements JSONPrimitive {}final record JSONString(String value) implements JSONPrimitive {}enum JSONBoolean implements JSONPrimitive { FALSE, TRUE; }JSONPrimitive p = ...;result = switch (p) { case JSONNumber(v) when v == 0 -> "zero"; case JSONString(s) where s.isEmpty() -> "empty"; case JSONBoolean.FALSE -> "false"; default -> "something else";} Finally, note that constants are not allowed inside record patterns. For example, you cannot use

case JSONNumber(0) -> "zero"; You can use a when clause, as in the preceding example. Nicer syntax may come in the future.

Puzzler #3Looking again at this (incomplete) JSON primitive type hierarchy:

sealed interface JSONPrimitive permits JSONNumber, JSONString, JSONBoolean {}final record JSONNumber(double value) implements JSONPrimitive {}final record JSONString(String value) implements JSONPrimitive {}enum JSONBoolean implements JSONPrimitive { FALSE, TRUE; } compare

if (j instanceof JSONNumber(var v)) d = "" + v;else if (j instanceof JSONString(var s)) d = s;else if (j instanceof JSONBoolean b) d = b.name(); and

switch (j) { case JSONNumber(var v): d = "" + v; break; case JSONString(var s): d = s; break; case JSONBoolean b: d = b.name(); break;}; Do they do exactly the same thing? If no, for which value of j do they differ?

Click arrow to reveal answerBy design, pattern matching for instanceof and switch have the same behavior, including the binding to the matched variable (v or b in the example).

But there is one crucial difference. For historical reasons, instanceof is null-friendly. The expression null instanceof ... is simply false. But switch is null-hostile: switch (null) { ... } throws a NullPointerException.

So, the answer is: the two statements have the same effect except when j is null.

Knowing this, let’s move on to record patterns:

record Box<T>(T contents) { }Box<String> boxed = null;String unboxed = switch (boxed) { case Box(String s) -> s;}; What happens?

Click arrow to reveal answerA NullPointerException. No surprise.

What about

Box<String> boxed = new Box(null);String unboxed = switch (boxed) { case Box(String s) -> s;}; Click arrow to reveal answerNo problem. s is bound to null, and unboxed becomes null.

What about

Box<Box<String>> doubleBoxed = new Box(null);String unboxed = switch (doubleBoxed) { case Box(Box(String s)) -> s;}; Click arrow to reveal answerAn implicit mechanism tries to match Box(null) wiith a Box(b), which is a Box(String s), and then set s = b.contents(). The match is deemed to fail, and there are no further matching cases. Therefore, a MatchException is thrown. Not a NullPointerException.

Principle #3: NullTo nobody’s surprise, null is always a cause of grief. In Java 1.0, switch was only defined for primitive types, so null wasn’t an issue. When wrappers were added, it made sense to say that null was exceptional. When enum was added in Java 5, that still made sense. Why would an enum value ever be null? And with switching on strings in Java 7, there was no reason to rock the boat either. A switch with a null selector simply throws a NullPointerException.

But with pattern matching, it was decided that it would be ugly to surround switch with checks against null, and a case null was allowed. For example:

String unboxed = switch (boxed) { case Box(String s) -> s; case null -> "empty";}; Note that the first case is not a match. That explains the doubleBoxed puzzler.

You can combine case null with default, but not with any other case:

case null, default -> "something else"; // Okcase null, 0 -> "nullish"; // ERROR Adding case null to any switch makes the switch null-friendly, but it also turns it into a “modern” switch, which has more stringent requirements than its classic cousin. See the following sections.

Puzzler #4Compare the following two uses of switch. Which one is incorrect, and why?

int x = ...;String d = switch (x) { case 0 -> "zero"; case 1, 2, 3 -> "small"; }switch (x) { case 0: d = "zero"; break; case 1, 2, 3: d = "small"; break;} Click arrow to reveal answerThe first switch—an expression—won’t compile. It is not exhaustive. If x is something other than 0, 1, 2, 3, it can’t produce a value.

The second switch—a classic statement—doesn’t have to be exhaustive. If x is something other than 0, 1, 2, 3, nothing happens.

Ok, now what about

Integer x = ...;String d = "";switch (x) { case 0: d = "zero"; break; case 1, 2, 3: d = "small"; break; case null: d = "null"; break;} Click arrow to reveal answerThis switch statement doesn’t compile. It is not exhaustive.

Wait…since when do switch statements have to be exhaustive? If you are surprised, read on.

Principle #4: ExhaustivenessAll switch expressions must be exhaustive. For any selector value, there must be a matching case. This is necessary since the expression must always yield a value.

Classic switch statements need not be exhaustive. But “modern” switch statements have to. If you mean to do nothing when none of the cases match, add a default: break; or default -> {};

A switch is modern if it has a type pattern, record pattern, or case null.

Note that cases with when clauses are ignored for exhaustiveness checking (unless the when clause is a compile-time constant). This switch is not exhaustive:

Integer x = ...;String d = switch (x) { case 0 -> "zero"; case Integer n when n > 0 -> "positive"; case Integer n when n < 0 -> "negative";} The compiler isn’t a mathematician. It doesn’t try to reason that every integer must be zero, positive, or negative.

Remedy: case Integer _ or default in the last clause.

Exhaustiveness is particularly useful with sealed hierarchies:

switch (j) { case JSONNumber(var v) -> "" + v; case JSONString(var v) -> v; case JSONBoolean.FALSE -> "false";}; // oops--what about JSONBoolean.TRUE? Finally, note that null is never used in exhaustiveness checking. A switch can be exhaustive without case null. It is just null-hostile and throws a NPE with a null selector. Or a MatchError when there is a nested null in a record.

Puzzler #5What is wrong with this switch?

String d = switch (obj) { case Number n -> "a number"; case Integer i -> "an integer"; default -> "something else";}; Click arrow to reveal answerWith type and record patterns, order matters. The first case dominates the second. That is a compile-time error.

What about

Integer x = 0;String d = switch (x) { case Integer i when i > 0 -> "positive"; default -> "negative"; case 0 -> "zero";} Click arrow to reveal answerIt’s perfectly fine. For historical reasons, default has inconsistent dominance rules. Read on for the details.

Principle #5: DominanceType and record patterns are processed top to bottom. The compiler generates an error if one case dominates the other. For example:

case Number n dominates

case Integer i and

case Number n when n.intValue() == 0 The record pattern

case Box(var b) dominates

case Box(JSONString(var s)) As with exhaustiveness checking, the contents of when clauses is not analyzed (unless they are compile-time constants). The compiler can’t tell that

case Number n when n.intValue() >= 0 dominates

case Number n when n.intValue() == 0 The default clause must come after any patterns. But for historical reasons, it can come before constant cases.

With classic switch statements, the order of the cases doesn’t matter, except when there is fall through. Because you can fall through from the default clause, it can be anywhere:

switch (n) { case 0: log("zero"); break; default: log("ignore the next log entry"); // FALL THROUGH case 1: log("one"); break;} I couldn’t think of a realistic example where this behavior would be useful. Just put default last.

Puzzler #6Can you declare variables with the same name in different cases?

switch (n) { case 0, 1: String d = "binary"; log(d); break; default: String d = "not binary"; log(d); break;} Click arrow to reveal answerSince Java 1.0, it has been legal to declare a variable inside a switch. The scope extends from the point of declaration until the end of the switch.

Therefore, the switch above doesn’t compile. The variable d is declared twice. Remedy: Use braces to confine d to a block.

What about variables introduced in patterns?

JSONPrimitive j = ...;String d;switch (j) { case JSONNumber(var v): d = "" + v; break; case JSONString(var v): d = v; break; case JSONBoolean v: d = v.name(); break;}; Click arrow to reveal answerThis switch compiles. The scope of each pattern variable v extends to the end of the statements in the case.

Principle #6: Variable ScopesThere are three ways of declaring variables inside a switch:

  1. Inside a block: { var a = ...; ... }. These are unsurprising. The scope ends with the block.
  2. Inside a pattern: case JSONNumber(var v). The scope starts with the declaration, so you can use it in guards: case JSONNumber(var v) when v >= 0 The scope is confined to the case.
  3. In a statement following a colon of a case. This is a weird historical artifact. More below.

Ever since the switch statement in the C programming language, it has been legal to declare a variable anywhere in the switch. Its scope extends to the end of the statement. After all, the case labels are just jump targets. This is perfectly legal:

int n = ...;switch (n) { case 0, 1: String d = "binary"; log(d); // FALL THROUGH default: d = "default"; log(d);} Note that the default branch must assign something to d before using it. Otherwise, the compiler reports an error about a possibly uninitialized variable.

Because of the tracking of uninitialized variables, such switch-scoped variables are never useful. I have only seen them in certification exam questions. Just stick to block-scoped and pattern variables.

The alert reader may swonder what happens with fall-through into a pattern:

case Integer n: log(n); // FALL THROUGHcase String s: log(s.length()); break; // ERROR This is an error. When falling through from case Integer n, it is impossible to bind the selector value to s. But you can fall into a type pattern that does not bind the match to a variable:

case Integer n: log(n); // FALL THROUGHcase String \_: log("string"); break; // Ok Don’t code like that at home!

ConclusionPattern matching has the potential to make code easier to read, particularly when working with sealed type hierarchies that are designed with pattern matching in mind. This is common practice in functional programming languages. I imagine it will become much more common in Java when we have efficient value objects.

Java has chosen to incorporate pattern matching into the classic switch and instanceof syntax. That leverages programmer experience in straightforward cases. But it can create confusion in edge cases, as you can probably confirm from your performance on those puzzlers. (If you got them all correct, award yourself five gold rings.)

To keep out of trouble, I send you these six ~~geese a-laying~~ rules of thumb:

  • Don’t use fall through. It saves you a lot of grief and complexity!
  • Use switch expressions, not statements.
  • Have a case null unless you really want a null selector to throw a NPE
  • Put default at the end
  • Sort your cases so that the most specific ones come first (in particular, constants)
  • Don’t use switch-scoped variables

The post Twelve Days of Pattern Matching appeared first on JVM Advent.

View Details

What does it mean to be a good Java Cloud Citizen? It’s definitely more than just putting an application in a container and deploying it. It is essential to consider factors such as providing real-time health status through fine-grained metrics to optimize your Java application’s performance and resilience in the cloud, . You’ll also need to ensure fast startup, and avoid excessive resource consumption within the cluster.

Being a good Cloud Citizen also involves streamlining configuration, deployment and upgrade processes. By integrating these tasks seamlessly, the application can facilitate smooth deployments and upgrades. This will lead to more efficiency and ease of management. This article gives a concise and opinionated overview of the Kubernetes basics from a Java developer’s perspective and learn step-by-step how to get your application production ready on Kubernetes.

Startup time and small footprintKubernetes is a highly sophisticated orchestration engine for containerized applications. Usually organizations install Kubernetes clusters across several nodes (servers). Kubernetes automatically and dynamically spreads out workloads across these different nodes for optimal usage. Typically you will want to create multiple instances/pods of your application for high availability. When one pod gets killed, stopped, moved to a different node or what have you, your application is still up and running from your user’s perspective. This is called “horizontal scaling”.

Because pods can stop and start relatively frequently, it is important that the applications inside those pods can start up quickly as well. The longer it takes to start up, the less flexible Kubernetes will be at scheduling its workloads and the less advantage you have of using a cloud solution. Similarly, it is important that your applications have an as small as possible footprint. This allows Kubernetes to schedule applications across nodes more flexibly. For example, if your application’s resources take up 60% of your node’s available capacity, Kubernetes will not be able to schedule another instance on that same node, even though 40% of its capacity goes underused. If however, your application uses 40%, it’s able to spread the load to multiple instances of one or more of your applications. Only 20% of that node would go to waste. Imagine if your workloads would be even smaller. Kubernetes would have a lot more flexibility of scheduling pods, as well as increase the node’s usage and efficiency.

JavaWhat does this mean for your Java applications? Java was (unsurprisingly) not originally built for cloud deployments. The typical deployment target for “traditional” Java applications is rather large. They usually have dedicated servers with the goal to keep the applications running as long as possible. If the application needs to scale, it’s usually done by adding more hardware resources to these servers (vertical scaling) instead of creating more instances (horizontal scaling). Because of this, startup time and footprint were not a top priority for Java developers. With Kubernetes however, Java has had to reinvent itself.

There are several initiatives in the Java world to reduce the startup time and footprint of Java applications. There are eg. OpenJDK projects like Leyden and CRaC, or projects like GraalVM Native Image that compile Java applications down to very fast and compact native binaries. There are newer frameworks/stacks like Quarkus, Micronaut and Helidon. These have been conceived with cloud native and kubernetes deployment targets in mind. Even the more traditional Spring (Boot) or JakartaEE have been making a lot of improvements to make applications more Kubernetes friendly.

QuarkusWe’ll focus in this article on Quarkus because it makes working with “Kube-Native” Java quite a bit easier and more performant. Feel free to explore the other stacks and compare and see what makes the most sense for your project.

Quarkus moves as much “heavy lifting”, such as classpath scanning, resolving annotations, etc to the application build time instead of during the application startup. This reduces both the startup time and the amount of memory needed. Actually, this is just the tip of the iceberg, you can read more about Quarkus optimization for container workloads here.

Optimizing your Java application for Kubernetes is the first, but also one of the most important steps to create a production-grade, Kubernetes-native Java application. Let’s now take a look at some tips and tricks to create production-grade cloud native Java workloads.

Containerize your applicationTo deploy an application to Kubernetes, you will need to package it up as a container image first. A few years ago that meant creating or finding a Dockerfile, adding commands to copy your artifacts and dependencies, and building the container image with a docker build command. While this is still a valid way of building containers, there are now many more ways and tools to create container images, such as Podman, Jib, BuildPacks, Kaniko, Buildah, etc.

While each of these tools have their advantages and challenges, you will likely use a base image which you can customize to your needs. It is important to be very conscious of where this base image comes from. There are a surprising amount of container images out in the wild. While probably not purposefully malignant, these images often contain vulnerabilities that can be exploited relatively easily. To create a production-grade container image, it is thus very important that you start from a verified/certified base container image. Ideally one that comes from a source you trust and that you can expect to maintain the base images you’re using going forward as well. Red Hat’s Universal Base Images (UBI) are an example of base images you can use and redistribute (license free).

Deploying to KubernetesOnce you have a container image, the next step is for you (or someone else in your organization) to deploy it to Kubernetes. To build and release a production-grade application it makes sense to test your applications in an environment as similar to the production environment as possible. This will help you to get ahead of discrepancies between your local environment and production as soon as possible as well. It is however not a trivial task to learn all the ins and outs of Kubernetes and its ecosystem. Fortunately there are solutions for Java developers to be able to deploy applications to a local or remote Kubernetes instance.

Quarkus for instance makes things painless through the use of a ‘quarkus-kubernetes’ extension. Adding this dependency to your project will generate Kubernetes manifests for you automatically (in an aptly named target/kubernetes/ folder). You can then deploy these manifests by either applying the (yaml or json) file, or calling a quarkus deploy command. Quarkus supports remote debugging on Kubernetes out of the box as well. Even its Dev Mode can work with a Kubernetes deployment.

Alternatively, projects like JKube are worth checking out as well to work with Kubernetes in an easy and straightforward way.

Is my application actually ready to receive requests?A production-grade Kubernetes application needs more than just a deployment though. If you deploy a container (in a pod), a Kubernetes Service will by default start sending traffic to it as soon as the container starts. However the application inside the container might still be starting up. Even if you’ve optimized your application to start up super fast, there will still be a gap of (milli)seconds where it is not available. It might for example also be establishing connections to a database or a messaging system, so requests coming are likely to fail during this startup time. Fortunately Kubernetes has a concept of “health probes” that can point to an endpoint in your application where you can advertise whether your application is actually able to receive requests. There are 3 different health probes:

  • Startup Probe
  • Readiness Probe
  • Liveness Probe

Quarkus leverages the MicroProfile Health spec through the Smallrye implementation. Adding the “smallrye-health” extension will, in combination with the ‘kubernetes’ extension, add the 3 health endpoints to your application’s Kubernetes manifests automatically. You can create custom health endpoints using simple MicroProfile-based annotations. You can also modify the parameters of the health endpoints by adding configuration values to the application.properties file.

Declare your application’s needs and limitsAs mentioned before, organizations typically deploy a Kubernetes cluster across several nodes. They each have a certain amount of processing power (CPU) and memory (RAM) available. If you do not specify any limits to your application’s pods, they will by default be able to consume as much of the resources of the node they’re running on. This can become problematic when you have multiple applications running and they start competing for the available resources. When resource starvation starts to occur on a node, the Kubernetes controller will step in and effectively kill pods on this node. If it’s not able to reschedule the killed pods on a different node, these workloads will not be able to start up anymore. This will result in a degraded user experience (at best).

To avoid these kinds of scenarios, you can leverage the concepts of “requests” and “limits” in Kubernetes. Adding a “request” parameter to your deployment will communicate to the Kubernetes controller that your application needs a minimum amount of resources (memory and/or cpu) to work correctly. This helps the Kubernetes controller to schedule your pod appropriately on one (or more) of its nodes. “limits” on the other hand tell Kubernetes that if your application goes beyond a given amount of resource usage it should kill and restart the pod. This helps avoid situations where your application is unexpectedly starting to use more resources than you anticipated. Eg. due to a memory leak or another unforeseen buildup of resource usage. Instead of the pod taking up more and more resources and eventually potentially bringing down an entire node or cluster, the “blast radius” of a resource issue is now contained to just one instance.

Adding LimitsAdding limits and resources is therefore likely a good practice. Your kubernetes admin might have defined some default limits and requests already for each pod. It is however also a good idea for the developer to be aware of the (predicted) resource usage of their application and specify the request and limit values they would like for the application.

For Quarkus, you can specify these values to the generated Kubernetes manifest by adding request and limit values to the application.properties file. eg.

quarkus.kubernetes.resources.limits.cpu=300mquarkus.kubernetes.resources.limits.memory=300Mi Security considerationsNo production-ready application and deployment is complete without considering the security implications of such an endeavor. At the minimum you should scan your application’s source code and dependencies for vulnerabilities. An IDE plugin like Dependency Analytics (for VScode or IntelliJ) gives you feedback while you’re developing your code. You should integrate code and container scanning in your CI/CD pipeline as well and fail your pipeline if critical vulnerabilities are found. Think of code scanning tools like SonarQube, or container scanning tools like Clair and/or Trivy. Your Kubernetes admins or security team should also have runtime scanning capabilities installed as well.

SecretsYou should also make sure to keep sensitive data safe. Passwords and other sensitive information are stored in Kubernetes in the form of “Secrets”. Though authenticated users and service accounts have access to these objects in Kubernetes, they are typically encrypted at rest, making them less vulnerable to be exploited. Unless a hacker somehow gets admin access to a cluster, or is able to exploit a container that has viewing privileges to the secrets.

Accessing and using secrets is again straightforward with Quarkus. Adding the “kubernetes-config” extension gives you the ability to interact with Kubernetes configuration options such as secrets. All you have to do is set the “secrets.enabled” flag to true such as in the following example. After that, specify which secrets you would like to interact with (the ‘postgresql’ secret in this case). Quarkus creates the necessary Kubernetes constructs such as a ServiceAccount, Role and RoleBinding in the background that allow the application to access the secret.

%prod.quarkus.kubernetes-config.secrets.enabled=true%prod.quarkus.kubernetes-config.secrets=postgresql To further encrypt secrets, there are tools such as Vault and Sealed Secrets.

Observe and measure your application on KubernetesOnce your application has landed on a Kubernetes instance, you’ll want to keep an eye on how it is behaving and whether your requests and limits are set appropriately. Exposing metrics from your application to an observability stack is a must when you have distributed loads and ephemeral containers that can come and go. From a Java perspective, the OpenTelemetry and MicroMeter projects are good solutions to add observability to your application. With Micrometer for example, you can expose a “metrics” endpoint to your application that a monitoring tool (eg. Prometheus) collects metrics from. This allows you to search through or create graphs and dashboards with (eg. with Grafana). You can also see in detail all the various metrics coming out of the JVM running inside your application (memory used, statistics related to the garbage collector, etc). This in turn will allow you to proactively make modifications to your code, or your deployment manifests.

Observability is also important to be able to access logs in a centralized place, and trace through requests in case issues are happening. The OpenTelemetry project supports Java. It integrates easily into your code to forward traces and logs to a collector that you can plug in to a tracing tool such as Jaeger.

Automate your deploymentsSetting up container builds, kubernetes deployments, configuring your application, adding observability are all important steps. The most important of all is perhaps to automate your application’s configuration and deployment as much as possible. Automation is important to release your application in a smooth and controlled manner that is repeatable. It is also important because it allows you, your team, and those who come after you, to know exactly how to build and deploy the application, and with what kind of configurations.

You should make sure even your CI/CD tool itself, as well as its pipelines can be automated as well. This allows you to (re)create entire stacks with ease and enable you to create new production-grade applications without much hassle. Tekton for example is a CI/CD solution that can be fully defined as a set of Custom Resources in Kubernetes. It allows you to automate your pipeline creation as well as the creation of Tekton instances. In addition, it integrates with signing tools such as Sigstore. With this you can sign not only your artifacts, but each task that’s part of your pipelines as well. In a new world of Software Supply Chain Attacks, this is another invaluable step on your way to productizing applications.

GitopsFinally, GitOps tools such as ArgoCD or FluxCD can help you define a desired state of your environment, deployment and configuration, and make sure your Kubernetes environment actually matches this desired state. This helps you to know exactly what your environment and deployments should look like. With GitOps, you can see (in your source repository) who changed something, what they changed, and when it changed. GitOps and adjacent tools such as Argo Rollouts also allow you to roll out applications in advanced ways. With it you can use blue/green or canary rollouts and release in progressive way that minimizes the impact to your users.

This is just the beginningProductizing Java applications for Kubernetes can seem like a daunting task. With some careful consideration and planning it can make developer’s lives easier and vastly more productive. It can also make a huge difference for your organization’s ability to execute and deliver applications faster, more secure and more robust.

This article tried to give you a quick and concise overview as well as some pointers to Open Source projects you could use to build and deploy production-grade, Kubernetes-native Java applications. This should get you well on your way to becoming a good Cloud-Native citizen.

The post Production-Grade Kubernetes for Java Developers appeared first on JVM Advent.

View Details

Would you believe me if I said it was possible to run Java almost anywhere as long as you have access to Jupyter Notebooks ?

Jupyter Notebooks are an interactive computing environment that allows you to create and share documents that contain live code, equations, visualizations, and narrative text.

Notebooks always intrigued me as they allow for easy exploration. Java generally has not allowed for easy exploration in its early days. In recent years we have seen tools such as JBang and OpenJDK improvements that makes getting started with Java inherently simpler. For example single script files (JEP 330) and single main methods (JEP 445) have helped.

Jupyter Notebooks have started to become ubiquitous. I believe it’s valuable to make Java function seamlessly within Jupyter Notebooks or, at the very least, to understand how to use them together effectively. This article will provide insights into why integrating Java into Jupyter Notebooks is useful. Additionally, I will demonstrate how to enable Java in Jupyter on virtually any Jupyter-enabled platform, whether local or cloud-based. Finally – encourage you to checkout out https://github.com/jupyter-java where this story will hopefully evolve further.

So lets start with some of the reasons why Java in Jupyter Notebooks are interesting!

Interactive Learning and Explorative DevelopmentJupyter Notebooks provide an interactive interface where you can write and execute code in segments (called cells), see the output immediately, and make changes on the fly. This is different from the traditional Java development process, where you typically write a complete program, compile it, and then run it.

Not only does it let you experiment – it can also be used to let you setup notebooks that can interactively teach and educate others how to use the basics of Java or the API of your library.

Instead of just documenting how the API works one can put in snippets of code just next to the documentation that let you tinker and fix code to understand how it works. All without having to setup several layers of IDE and tooling to get started. That is powerful.

Support for Multiple LanguagesAlthough Jupyter started with Python (hence the name, a play on Julia, Python, and R), it supports many languages by having Kernels that be written in and support any language, including Java.

Surprisingly to many there are already quite a list of Java based kernels. Especially for the non-Java JVM languages.

The main challenge with all these are that to install and enable them they each have their own install setup and not all “just works” in online IDE’s. Would it not be great to have a way to use these kernels anywhere?

Data Science and VisualizationJupyter is popular in data science due to its support for data manipulation and visualization libraries. You can easily visualize data with graphs and charts, which is integrated directly in the notebook alongside the code that produces them.

Python is king in data science and although I think it is possible for Java to improve in that area it is not data science that drives my interest. No, the interesting part for those visualizations in Jupyter Notebooks are not just text and images – it can be anything that renders to html/css/javascript. You can imagine a Kernel that understands how to turn any Java Collection into a table, a BufferImage into a png, a JSONObject into a collapsible tree or A File or Path into a link to open. The possibilities are endless.

Web-based and local IDE InterfacesJupyter runs in the browser, offering a web-based interface. This makes it platform-independent and accessible from anywhere. Jupyter style notebook are available in VSCode based IDE’s as well as Jupyter Labs online services as well as being rendered on GitHub – meaning Jupyter Notebook works locally and in the cloud – making notebooks ubiquitously available.

Documentation and SharingNotebooks allow you to combine code with Markdown text, equations written in LaTeX, and more. This makes it easy to create documents that explain the code and its output, which can be useful for educational purposes, note taking, data analysis reports, and more.

The key thing is to make these notebooks accessible with Java available so everyone can open and access them.

In conclusion, think of Jupyter Notebooks as an IDE where you can not only write and test your code in chunks but also document your process and visualize results in real-time, all within the same document.

Now how do you get to use Java in all these various notebook environments?

Meet Jupyter-JavaUntil recently finding information about how to use Jupyter with Java been a challenge. Especially how to use the various java kernels and installing them in a way that worked across the various environments.

I created jupyter-java a Git Hub organization with its own landing page, discussion forum and a few repos all related to working with Java using Jupyter Notebooks. It is still in its infancy but contributions are very welcome.

I made it when I realized I could make Java Kernel installation a breeze compared to today’s complexity.

Easy Java Kernel InstallationThis is available as a JBang script that you can run as follows:

jbang install-kernel@jupyter-java By default it currently installs IJava based kernel but already supports installing the other following Kernels:

| Name | Language | | --- | --- | | IJava | Java | | Raipao | Java, requires Java 21 | | Kotlin | Kotlin | | Ganyemede | Java, Groovy, Kotlin |

Java kernels and language supported by install-kernel@jupyter-javaIf you run jbang install-kernel@jupyter-java <kernel name> and have VSCode with Jupyter Extension installed you can now open a New Jupyter Notebook and choose Jupyter Kernel > jbang-<kernel name> to have that notebook working with the respective kernel.

The install-kernel script has various options to tweak the setup. For example if you want to be using latest greatest Java 21 preview features you can use:

jbang install-kernel@jupyter-java --java 21 —-enable-preview

And with that you can use String templates in your Jupyter Notebooks.

Java in any cloud Jupyter notebookNotebooks are available in many places but they rarely if at all have Java enabled. With a little bit of additional JBang magic you can put the following into most Jupyter notebook environments and make a Java kernel with preview features available:

!pip install jbangimport jbangjbang.exec(“trust add https://github.com/jupyter-java”)jbang.exec(“install-kernel@jupyter-java --java 21 —-enable-preview”) Those four lines do the following:

  1. Install jbang-python, a Python module that installs JBang and provide API to use it from Python
  2. Import jbang module in Python
  3. Trust https://github.com/jupyter-java as a trusted url (an additional layer of protection so JBang doesn’t execute the code of a completely random stranger)
  4. Run install-kernel@jupyter-java to generate a Java Kernel setup.

All fairly simple stuff – but together it enables Java on many online services. Below are the various services it works on already today:

| Online Notebook | Description | | --- | --- | | Google Colab | Jupyter notebooks hosted by Google – just need a google account | | Mybinder | Free service that offer to run docker images with Jupyter included | | Red Hat Data Science | Accessible for free via Red Hat OpenShift Sandbox – provides Jupyter notebooks | | GitPod.io | GitPod.io VSCode online environment, has free tier | | GitHub Codespaces | GitHub codespaces based on VSCode, requires subscription – has OpenSource friendly free tier | | OpenShift Workspaces | Red Hat OpenShift VSCode online environment, has free tier |

Online services that install-kernel@jupyter-java works withIf you want to try it out immediately go to github.com/jupyter-java/anywhere and use one of the direct links present in that repository.

Challenges and Future ProspectsMy idea is that Jupyter-Java can if nothing else provide a starting point for Java developers to get started with Jupyter Notebooks. My hope is that it encourages you and others to evolve and improve the support for Java in Jupyter Notebooks as it is really nice to have available.

Maybe Java API’s will provide custom renderings ? Maybe a JBang enabled kernel is in the future? Maybe Jupyter Notebooks in Quarkus dev-ui would be possible ?

Lots of interesting opportunities.

ConclusionJupyter Notebooks and Java are possible and have multiple advantages and their combined use will help make Java more accessible and useful.

My encouragement to you in this jolly season is to go out and try Jupyter Notebooks – get started with https://github.com/jupyter-java/anywhere. With install-kernel@jupyter-java it is trivial to get started.

Explore the different Kernel variants, try use your favorite API’s, get a feel for it and then post questions or ideas in the discussions – lets spread the word and make the (Java) world a better place.

The post Jupyter Notebooks and Java? appeared first on JVM Advent.

View Details

“99 little bugs in the code,
99 little bugs.
Track one down, patch it around,
There’s 113 little bugs in the code.”
—Anonymous

This article is based on chapter 18 of Spring Security in Action, 2nd edition (a book I wrote). Find more detailed content in the book.

Over time, as software grew more complex and development teams expanded, it became unfeasible for individual developers to keep track of all the functionalities added by their peers. To ensure that new bug fixes or features didn’t disrupt existing functionalities, developers needed an effective strategy. The primary purpose of writing unit and integration tests is to validate the newly implemented functionalities and prevent the inadvertent disruption of existing ones when modifying code. This process is known as regression testing.

In modern development practices, when a developer completes a code modification, they upload these changes to a shared server for code version management. This action triggers a continuous integration tool that automatically executes all pre-existing tests. If a test fails due to the recent changes, indicating a disruption in existing functionality, the continuous integration tool alerts the entire team (as illustrated in figure 1). This approach significantly reduces the risk of introducing changes that negatively impact established features.

Figure 1 Testing is part of the development process. Anytime a developer uploads code, the tests run. If any test fails, a continuous integration tool notifies the developer.

It’s important to recognize that testing your application involves more than just examining your own code. Equally crucial is testing how your application interacts with the frameworks and libraries it utilizes (as depicted in figure 2). At some point, you might update these frameworks or libraries to their newer versions. During such updates, it’s essential to verify that your application still seamlessly integrates with these updated dependencies. If the integration isn’t as smooth as before, you’ll want to swiftly identify and rectify the areas in your application that need adjustments to resolve any integration issues.

Figure 2 The functionality of an application relies on many dependencies. When you upgrade or change a dependency, you might affect existing functionality. Having integration tests with dependencies helps you to discover quickly if a change in a dependency affects the existing functionality of your application.

Understanding how to test your application’s integration with Spring Security is a vital aspect of this article. The Spring framework, including Spring Security, is rapidly evolving. As you update your application with new versions of these frameworks, it’s crucial to assess whether these updates introduce any vulnerabilities, errors, or incompatibilities. Prioritizing security from the initial design stage of your app is essential. Implementing tests for security configurations should be a standard procedure, and a task shouldn’t be considered complete without these security tests.

This article delves into various strategies for testing an application’s integration with Spring Security. We’ll analyse some examples to guide you on crafting effective integration tests for the functionalities you’ve implemented. Testing, as a general concept, is fundamental, and gaining an in-depth understanding of it offers numerous advantages.

Our focus will be on the interaction between an application and Spring Security. Before jumping into examples, I recommend a few resources that have deepened my understanding of this topic. For a more detailed understanding or a quick refresher, consider these insightful books:

  • “JUnit in Action, 3rd ed.” by Cătălin Tudose et al. (Manning, 2020)
  • “Unit Testing Principles, Practices, and Patterns” by Vladimir Khorikov (Manning, 2020)
  • “Testing Java Microservices” by Alex Soto Bueno et al. (Manning, 2018)

I. Using mock users for testsThis section focuses on the use of mock users for testing authorization configurations, a method that is both straightforward and popular. By employing a mock user in a test, you bypass the authentication process entirely (as illustrated in figure 3).

Testing authorization configurations often involves skipping the authentication step. This is because it’s not necessary to verify the authentication process each time you’re assessing whether the system correctly implements an authorization rule. It’s important to remember that while authentication and authorization are interdependent, they are separated within the security context. To isolate and test an authorization configuration, you can create a mock security context, allowing you to control and test various authorization scenarios as needed.

In most applications, there are typically a few authentication methods (often just one), but a much broader array of authorization rules that apply to different use cases or endpoints. Therefore, it’s more efficient to test authorization rules in isolation without repeating authentication tests each time you need to confirm that the authorization for a specific component is functioning correctly.

The mock user, which is only active during the test, can be configured with any attributes necessary to validate specific scenarios. For instance, you can assign the user certain roles (like ADMIN or MANAGER) or various authorities to ensure that the application behaves as expected under these conditions.

Figure 3 We skip the shaded components in the Spring Security authentication flow when executing a test. The test directly uses a mock SecurityContext, which contains the mock user you define to call the tested functionality.

We need a couple of dependencies in the pom.xml file to write the tests. The next code snippet shows you the classes we use throughout the examples in this article. You should make sure you have these in your pom.xml file before starting to write the tests. Here are the dependencies:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope></dependency> In the test folder of your Spring Boot project, we add a class named MainTests. We write this class as part of the main package of the application. In listing 1, you can find the definition of the empty class for the tests. We use the @SpringBootTest annotation, which represents a convenient way to manage the Spring context for our test suite.

Listing 1 A class for writing the tests@SpringBootTest public class MainTests { // test class contents go here} A convenient way to implement a test for the behavior of an endpoint is by using Spring’s MockMvc. In a Spring Boot application, you can autoconfigure the MockMvc utility for testing endpoint calls by adding an annotation over the class, as the next listing presents.

Listing 2 Adding MockMvc for implementing test scenarios@SpringBootTest@AutoConfigureMockMvc public class MainTests { @Autowired private MockMvc mvc; } Now that we have a tool we can use to test endpoint behavior, let’s get started with the first scenario. When calling the /hello endpoint without an authenticated user, the HTTP response status should be 401 Unauthorized.

You can visualize the relationship between the components for running this test in figure 4. The test calls the endpoint but uses a mock SecurityContext. We decide what we add to this SecurityContext. For this test, we need to check that if we don’t add a user that represents the situation in which someone calls the endpoint without authenticating, the app rejects the call with an HTTP response having the status 401 Unauthorized. When we add a user to the SecurityContext, the app accepts the call, and the HTTP response status is 200 OK.

Figure 4 During the test execution, we bypass the authentication step. The test employs a mock SecurityContext and accesses the /hello endpoint provided by HelloController. To ensure the app’s behavior aligns with the set authorization rules, a mock user is introduced within the test’s SecurityContext. In scenarios where a mock user isn’t defined, we anticipate the app will deny authorization for the call. Conversely, if a user is defined, the expectation is that the call will be successfully authorized.

The following listing presents this scenario’s implementation.

Listing 3 Testing that you can’t call the endpoint without an authenticated user@SpringBootTest@AutoConfigureMockMvcpublic class MainTests { @Autowired private MockMvc mvc; @Test public void helloUnauthenticated() throws Exception { mvc.perform(get("/hello")) #A .andExpect(status().isUnauthorized()); }} Mind that we statically import the methods get() and status(). You find the method get() and similar methods related to the requests we use in the examples of this article in this class:

org.springframework.test.web.servlet.request.MockMvcRequestBuilders Also, you find the method status() and similar methods related to the result of the calls that we use in the next examples of this article in this class:

org.springframework.test.web.servlet.result.MockMvcResultMatchers You can run the tests now and see the status in your IDE. Usually, in any IDE, to run the tests, you can right-click on the test’s class and then select Run. The IDE displays a successful test with green and a failing one with another color (usually red or yellow).

NOTE In the projects provided with the book, above each method implementing a test, I also use the @DisplayName annotation. This annotation allows us to have a longer, more detailed description of the test scenario. To occupy less space and allow you to focus on the functionality of the tests we discuss, I took the @DisplayName annotation out of the listings in the book.

To test the second scenario, we need a mock user. To validate the behavior of calling the /hello endpoint with an authenticated user, we use the @WithMockUser annotation. By adding this annotation above the test method, we instruct Spring to set up a SecurityContext that contains a UserDetails implementation instance. It’s basically skipping authentication. Now, calling the endpoint behaves like the user defined with the @WithMockUser annotation successfully authenticated.

With this simple example, we don’t care about the details of the mock user like its username, roles, or authorities. So we add the @WithMockUser annotation, which provides some defaults for the mock user’s attributes. Later in this article, you’ll learn to configure the user’s attributes for test scenarios in which their values are important. The next listing provides the implementation for the second test scenario.

Listing 4 Using @WithMockUser to define a mock authenticated user@SpringBootTest@AutoConfigureMockMvcpublic class MainTests { @Autowired private MockMvc mvc; // Omitted code @Test @WithMockUser public void helloAuthenticated() throws Exception { mvc.perform(get("/hello")) .andExpect(content().string("Hello!")) .andExpect(status().isOk()); }} Run this test now and observe its success. But in some situations, we need to use a specific name or give the user specific roles or authorities to implement the test.

II. Testing with users from a User Details ServiceThis section explores the method of sourcing user details for testing from a UserDetailsService, offering an alternative to the use of mock users. The key variation here is that, rather than fabricating a user, we retrieve the user information from a specified UserDetailsService. This technique is particularly useful when you aim to test the integration with the data source that your application utilizes for loading user details (as shown in figure 5).

Figure 5 Instead of creating a mock user for the test when building the SecurityContext used by the test, we take the user details from a UserDetailsService. This way, you can test authorization using real users taken from a data source. During the test, the flow of execution skips the shaded components.

Note that, with this approach, we need to have a UserDetailsService bean in the context. To specify the user we authenticate from this UserDetailsService, we annotate the test method with @WithUserDetails. With the @WithUserDetails annotation, to find the user, you specify the username. The following listing presents the implementation of the test for the /hello endpoint using the @WithUserDetails annotation to define the authenticated user.

Listing 5 Defining the authenticated user with the @WithUserDetails annotation@SpringBootTest@AutoConfigureMockMvcpublic class MainTests { @Autowired private MockMvc mvc; @Test @WithUserDetails("john") public void helloAuthenticated() throws Exception { mvc.perform(get("/hello")) .andExpect(status().isOk()); }} III. Using custom Authentication objects for testingIn most cases when employing a mock user for testing, the specific class used by the framework to generate Authentication instances in the SecurityContext is not a concern. However, there might be scenarios where your controller logic depends on the type of the object within the SecurityContext. This raises the question: is it possible to guide the framework to create a specific type of Authentication object for testing purposes? The answer is affirmative, and that’s the focus of this section.

The strategy is straightforward. We introduce a factory class tasked with constructing the SecurityContext. This gives us complete command over the creation process of the SecurityContext for the test, including its contents (as demonstrated in figure 6). For instance, this allows for the inclusion of a custom Authentication object in the SecurityContext.

Figure 6 For complete mastery over the definition of the SecurityContext in testing, we construct a factory class. This class provides guidance on constructing the SecurityContext for the test, offering enhanced flexibility. It allows us to select specific details, such as the type of object to employ as an Authentication object. In the accompanying figure, the components that are bypassed in the test flow are highlighted for clarity.

Let’s write a test in which we configure the mock SecurityContext and instruct the framework on how to create the Authentication object. An interesting aspect to remember about this example is that we use it to prove the implementation of a custom AuthenticationProvider. The custom AuthenticationProvider we implement in our case only authenticates a user named John. However, as in the other two previous approaches we discussed in sections I and II, the current approach skips authentication.

For this reason, you see at the end of the example that we can give our mock user any name. We follow three steps to achieve this behavior (figure 7):

  1. Write an annotation to use over the test similarly to the way we use @WithMockUser or @WithUserDetails.
  2. Write a class that implements the WithSecurityContextFactory interface. This class implements the createSecurityContext() method that returns the mock SecurityContext the framework uses for the test.
  3. Link the custom annotation created in step 1 with the factory class created in step 2 via the @WithSecurityContext annotation.

Figure 7 To enable the test to use a custom SecurityContext, you need to follow the three steps illustrated in this figure.

STEP 1: DEFINING A CUSTOM ANNOTATIONIn listing 6, you find the definition of the custom annotation we define for the test, named @WithCustomUser. As properties of the annotation, you can define whatever details you need to create the mock Authentication object. I added only the username here for my demonstration. Also, don’t forget to use the annotation @Retention (RetentionPolicy.RUNTIME) to set the retention policy to runtime. Spring needs to read this annotation using Java reflection at runtime. To allow Spring to read this annotation, you need to change its retention policy to RetentionPolicy.RUNTIME.

Listing 6 Defining the @WithCustomUser annotation@Retention(RetentionPolicy.RUNTIME)public @interface WithCustomUser { String username();} STEP 2: CREATING A FACTORY CLASS FOR THE MOCK SECURITYCONTEXTThe second step consists in implementing the code that builds the SecurityContext that the framework uses for the test’s execution. Here’s where we decide what kind of Authentication to use for the test. The following listing demonstrates the implementation of the factory class.

Listing 7 The implementation of a factory for the SecurityContextpublic class CustomSecurityContextFactory implements WithSecurityContextFactory<WithCustomUser> { @Override public SecurityContext createSecurityContext( WithCustomUser withCustomUser) { SecurityContext context = SecurityContextHolder.createEmptyContext(); var a = new UsernamePasswordAuthenticationToken( withCustomUser.username(), null, null); context.setAuthentication(a); return context; }} STEP 3: LINKING THE CUSTOM ANNOTATION TO THE FACTORY CLASSUsing the @WithSecurityContext annotation, we now link the custom annotation we created in step 1 to the factory class for the SecurityContext we implemented in step 2. The following listing presents the change to our @WithCustomUser annotation to link it to the SecurityContext factory class.

Listing 8 Linking the custom annotation to the SecurityContext factory class@Retention(RetentionPolicy.RUNTIME)@WithSecurityContext(factory = CustomSecurityContextFactory.class)public @interface WithCustomUser { String username();} With this setup complete, we can write a test to use the custom SecurityContext. The next listing defines the test.

Listing 9 Writing a test that uses the custom SecurityContext@SpringBootTest@AutoConfigureMockMvcpublic class MainTests { @Autowired private MockMvc mvc; @Test @WithCustomUser(username = "mary") public void helloAuthenticated() throws Exception { mvc.perform(get("/hello")) .andExpect(status().isOk()); }} Running the test, you observe a successful result. You might think, “Wait! In this example, we implemented a custom AuthenticationProvider that only authenticates a user named John. How could the test be successful with the username Mary?” As in the case of @WithMockUser and @WithUserDetails, with this method we skip the authentication logic. So you can use it only to test what’s related to authorization and onward.

IV Summary* Writing tests is a best practice. You write tests to ensure your new implementations or fixes don’t break existing functionalities. * You need to test your code and its integration with the libraries and frameworks you use. * Spring Security offers excellent support for implementing tests for your security configurations. * You can test authorization directly by using mock users. You write separate tests for authorization without authentication because you generally need fewer than authorization tests. * It saves execution time to test authentication in separate tests, which are fewer in number, and then test the authorization configuration for your endpoints and methods. * To test security configurations for endpoints in non-reactive apps, Spring Security offers excellent support for writing your tests with MockMvc.

The post Mastering Spring Security integration testing for your apps appeared first on JVM Advent.

View Details

At the beginning there was only Elasticsearch …It all started when Elasticsearch and Kibana license was changed from Apache 2.0 to EL (Elastic License) and SSPL (Server Side Public License). OpenSearch (formerly OpenDistro for Elasticsearch) is a fork of the Apache 2.0 licensed versions (7.10.2) of Elasticsearch and Kibana developed by AWS (Amazon Web Services).

The OpenSearch projectIn a nutshell OpenSearch projects aims to be a completely open-source equivalent of Elasticsearch oferring many of the enterprise capabilities of Elasticsearch also for free. OpenSearch is developed by Amazon and offered as a managed AWS service, a replacement of the Elasticsearch one. The projects has also forked many of the other appliations in the Elastic stack like Kibana (renamed to OpenSearch Dashboards) and also the various Elasticsearch language clients. In essence OpenSearch and all applications around it evolve as separate products that have no compatility with newer versions of Elasticsearch. The capabilities of features in OpenSearch are developed as plugins (available in the public repositories of the OpenSearch Project organization in GitHub) including the ones that are distributed as part of the core Elasticsearch application. A new mechanism called extensions is being developed to serve as a replacement of the standard plugin system in OpenSearch (inherited by Elasticsearch) with the aim to solve its main disadvantages:

org.opensearch.clientopensearch-rest-high-level-client2.9.0org.opensearch.clientspring-data-opensearch1.2.0

  • run in the same process as OpenSearch;
  • require cluster restart on plugin installation/update;
  • require updates across OpenSearch versions.

Extensions run as separate processes that interact with the OpenSearch cluster and use the same protocol as OpenSearch nodes for communcation. They may also invoke actions on other extensions that enables a mechanism for communication between the various extensions. Some of the plugins in the OpenSearch ecosystem are being reimplemented as extensions.

OpenSearch also ships and open source distribution of Logstash which however is currently planned to be discontinued in favor of standard Logstash oferring by Elastic. In terms of Logstash later versions of the Elasticsearch output plugin are not compatible with OpenSearch and for that reason OpenSearch provides also a third-party output plugin for LogStash. In addition a new capability called OpenSearch Integrations is being developed as part of OpenSearch Dashboards which provides the possiblity to collect and visualize data from external sources into OpenSearch. In certain cases OpenSearch Integrations can be used as an alternative to Logstash.

In terms of Spring Framework OpenSearch also provides a third-party Spring Data OpenSearch framework, an equivalent to the Spring Data Elasticsearch one.

OpenSearch vs Elasticsearch: a brief comparison guideLet’s do a brief comparison between traditional Elasticsearch and OpenSearch using a few different criteria.

Licensing and subscriptions

Elasticsearch provides a more restrictire license as of 7.11 for service providers and subscription-based model for enterprise Elasticsearch features. On the other hand there is no subscription model in OpenSearch: it is a fully open source project with certain limitations on features bound to the AWS OpenSearch offering.

Support

Elasticsearch provides support options as per subscription plan while OpenSearch has only community support and discussion can be conducted in the community forum of OpenSearch.

Documentation

Elasticsearch has a rich documentation with some nice capabilities like running examples directly in Kibana or copying them as CURL commands. OpenSearch has less examples and is not structured so well.

Roadmap

Elasticsearch does not have a public roadmap exposed and provides overview of new capabilities throughout i.e. blogpost. OpenSearch roadmap on the other hand is publicly available.

Migrating from Elasticsearch to OpenSearch (and vice-versa)In the aim to position itself as a competitor product OpenSearch even provides a migration guide from Elasticsearch OSS and even a tool that aids that process. While no official guide on Elasticsearch a reverse process can be applied to migrate from OpenSearch to Elasticsearch in practice.

Let’s see an example on how to migrate a simple Java application that uses both Elasticsearch Java client and Spring Data Elasticsearch. The process may be as simple as:

  • assuming we use High Level Elasticsearch client and Spring Data Elasticsearch we can simply replace the dependencies for OpenSearch Java High Level Rest client and the one for Spring Data OpenSearch:

org.opensearch.clientopensearch-rest-high-level-client2.9.0org.opensearch.clientspring-data-opensearch1.2.0

  • update imports: boils down to just replacing org.elasticsearch to org.opensearch for the Java client and no change for Spring Data framework.
  • if you use a secure version of Elasticsearch which is typically the case with later Elasticsearch versions and use username, password and fingerpring to connect to Elasticsearch such as provided by the following method:

private static RestClientBuilder createClientBuilder() { final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("elastic", "ZldXf5gREgh2OaE5HiIw")); String fingerprint = "<fingerprint>"; SSLContext sslContext = TransportUtils.sslContextFromCaFingerprint(fingerprint); RestClientBuilder builder = RestClient.builder(new HttpHost("localhost", 9200, "https")) .setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() { @Override public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) { return httpClientBuilder .setSSLContext(sslContext) .setDefaultCredentialsProvider(credentialsProvider); } }); return builder;} If you start OpenSearch by default you get a generated CA certificate and you can generate a fingerprint out of the certificate using a tool like openssl as follows:

openssl x509 -in ca.pem -sha256 -fingerprint | grep SHA256 | sed ‘s/://g’

After that you can replace that fingerprint along with the username and password in the code above. The sslContextFromCaFingerprint from the TransportUtils class can be copied over and reused when working with OpenSearch as it is not present in the OpenSearch Java client dependency.

  • For Spring Data OpenSearch it is sufficient to define a proper client configuration component for i.e. a RestHighLevelClient as per previous step and use the same setup as for Spring Data Elasticsearch. There should also be a configuration class that extends from the AbstractOpenSearchConfiguration class provided by the Spring Data OpenSearch framework

ConclusionOpenSearch provides a good open source alternative of Elasticsearch with many of its enterprise features offered for free. However Elasticsearch remains backend by a solid foundation and continues to invest into new features and performance improvements while OpenSearch tried to gain contributions more from the community. While it might be a good choice for applications that really need the core capabilities of an enterprise-level production-ready search offering Elasticsearch has still more to offer in terms certain aspects such as support, documentation and enterprise features missing from OpenSearch. The two applications will continue diverging over time and evolving as separate products.

The post Free enterprise search with OpenSearch appeared first on JVM Advent.

View Details

MotivationMost applications in the world follow a simple format. They ingest some data, do some transformation on it, and then expose this transformed data to their consumers. If we look at a high enough level, we will see this pattern in all applications. The transformation part can be very complex and follow a lot of business logic, or it could be very simple and consist of just renaming some fields, removing some fields or adding some fields. As we all can guess, writing code for simple transformation can feel very repetitive, boring and thus error-prone. So, it comes as no surprise that there are libraries out there, built to tackle this in an easier and simpler way. MapStruct is just one library like that one.My team used it a long time ago, on a project where our application needed to read data from DB and expose it to the world over REST API. The problem was that we were not the owners of the DB and we were not able to change DB schema definitions and make data look in DB how it looked to end consumers of our API. So, we were faced with two possible solutions to our problem. Write a lot of boilerplate code to do transformations, and invoke a lot of maintenance costs on ourselves, or use a library that would do most of the heavy lifting for us. Our choice landed on MapStruct, and we were saved on multiple occasions by it, especially as the requirements of our consumers underwent massive changes. Changing transformations was easy because we only needed to change some settings and MapStruct did all the work for us automatically.Let us take a look at MapStruct.ContextIn our example, we shall assume that we need to build a REST API that exposes some data that don’t belong to us. We are either receiving it via some 3rd party API or reading from some other team DB.To make it self-contained, In our use case, we will assume that data is coming from DB. For simplicity reasons in this example, we will use H2, in the memory database to simulate this use case. Let us assume that we are interested in Customer data, which is stored in table Customers.The first thing that we need to do, is to create a simple Java class Customer, mark it as Entity and map all columns from the table into this class. Once we are done, we will get some code like this. @Entitypublic class Customer { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @NotNull private String firstName; @NotNull private String lastName; @NotNull private Integer dayOfBirth; @NotNull private Integer monthOfBirth; @NotNull private Integer yearOfBirth; private String address; private Integer houseNumber; private String houseNumberAddition; private String city; private String country; // getters and setters .....} Unfortunately, our consumers are expecting data in different format. So, let us create class Customer2DTO which will be in the format that our consumers expect to receive the data.We would end up with code like this. public class Customer2DTO { private Long id; private String name; private String familyName; private String fullName; private LocalDate birthDay; private String address; private Integer houseNumber; private String houseNumberAddition; private String city; private String country; // getters and setters ....} As we can see, some fields are the same in both Customer and Customer2DTO. However, some fields have different names in Customer2DTO like name and familyName. Also, there are fields in Customer2DTO that don’t exist in Customer, fullName and birthDay. Of course, we can write code that would copy all the same fields, and adjust for name changes, this would mean that we would increase the size of our code, and the amount of code that we now need to maintain and look after. A more elegant solution would be to use MapStruct, so let us see how we can do exactly that in this use case.Adding MapStruct to our projectThe first thing that we need to do, is to add MapStruct to our project as a dependency.Let us add this piece of code in the dependencies block of our pom.xml file org.mapstruct mapstruct 1.5.5.Final Since MapStruct will generate code for us, we need to tell Maven to invoke MapStruct during compile time for the generation of code to happen. We can achieve this by extending the build plugin phase, by adding this piece of code in pom.xml org.apache.maven.plugins maven-compiler-plugin 3.8.1 org.mapstruct mapstruct-processor ${org.mapstruct.version} Now that MapStruct has been added to our project, and the build phase adjusted, we can start leveraging the power of MapStruct.First usage of MapStructWe need to create one interface, let us call it Customer2Mapper, and let us annotate it with @Mapper.In this way, we are telling MapStruct to create an implemenation of this interface. import org.mapstruct.Mapper;@Mapperpublic interface Customer2Mapper {} The next thing is to get INSTANCE of this interface that we can use in our code. For that, we need to add one line to our code. Customer2Mapper INSTANCE = Mappers.getMapper(Customer2Mapper.class); The final thing that we need to do is to create a method signature that will do the transformation from Customer to Customer2DTO. Customer2DTO customerToCustomerDTO(Customer customer); full code will look something like this import org.mapstruct.Mapper;import org.mapstruct.factory.Mappers;import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer2DTO;import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;@Mapperpublic interface Customer2Mapper { Customer2Mapper INSTANCE = Mappers.getMapper(Customer2Mapper.class); Customer2DTO customerToCustomerDTO(Customer customer);} To utilise MapStruct transformation, we just need to call this code on the instance of a Customer. Customer2DTO c2dto = Customer2Mapper.INSTANCE.customerToCustomerDTO(customer); In case we run this code and check what the result is, we will see that MapStruct did its best to map all the fields from Customer to Customer2DTO which have the same name and compatible types. In our case, all fields should be mapped, except name, familyName, fullName and birthDay.Renaming fieldsLet us see how we can use MapStruct to rename the fields.All that we need to do, to tell MapStruct to map field firstName from Customer to field name in Customer2DTO, is to add this line before the method signature of customerToCustomerDTO @Mapping(source = "firstName", target = "name") here we are telling MapStruct to use field firstName in Customer as source and field name in Customer2DTO as target. MapStruct will generate code that will do exactly that. We can repeat this for as many fields as we want, we just need to match the correct source and target fields. So for mapping lastName to familyName we need to add this @Mapping(source = "lastName", target = "familyName") Combining multiple fieldsIn the case of the fullName, we need to combine two fields from Customer into one field in Customer2DTO. Again we will use annotation @Mapping. Parameter target will be fullName. However, we will not use source. Instead, we will use expresion. The code will look like this @Mapping(target="fullName", expression = "java(customer.getFirstName() +\" \"+ customer.getLastName())") We can put almost any Java code in expresion. However, it would make most sense to keep it simple and don’t go nuts. Think about the future you who will maintain this code :-).To map the field birthDay in Customer2DTO, we will use a similar approach as for fullName. We will add this code @Mapping(target="birthDay", expression = "java(java.time.LocalDate.of(customer.getYearOfBirth(), customer.getMonthOfBirth(), customer.getDayOfBirth()))") so full code will look something like this import org.mapstruct.Mapper;import org.mapstruct.Mapping;import org.mapstruct.factory.Mappers;import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer2DTO;import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;@Mapperpublic interface Customer2Mapper { Customer2Mapper INSTANCE = Mappers.getMapper(Customer2Mapper.class); @Mapping(source = "firstName", target = "name") @Mapping(source = "lastName", target = "familyName") @Mapping(target="fullName", expression = "java(customer.getFirstName() +\" \"+ customer.getLastName())") @Mapping(target="birthDay", expression = "java(java.time.LocalDate.of(customer.getYearOfBirth(), customer.getMonthOfBirth(), customer.getDayOfBirth()))") Customer2DTO customerToCustomerDTO(Customer customer);} Creating “sub-objects” inside Data Transfer objectsVery often once we have done some work, requirements change or get extended. So let us assume that happened to our use case. Instead of getting data in the format of Customer2DTO, all of a sudden, we need to send data in a different format. First, we will create a class in a new format and will call it Customer3DTO. It would look something like this public class HomeAddressDTO { private String street; private Integer houseNumber; private String addition; private String city; private String country; //getters and setters ....}

public class Customer3DTO { private Long id; private String name; private String familyName; private String fullName; private LocalDate birthDay; private HomeAddressDTO homeAddress; // getters and setters ....} as we can see, info about home address now needs to be an object in itself, instead of individual fields in response of our API.Let us make a copy of our Mapper interface Customer2Mapper, and let us call it Customer3Mapper. The only thing that we need to do to create an instance of object HomeAddressDTO in Customer3DTO and fill in with appropriate data is to add a few mappings that we used in past with source and target arguments. Only this time values for the target will have the prefix “homeAddress.” . The result should look like this import org.mapstruct.Mapper;import org.mapstruct.Mapping;import org.mapstruct.factory.Mappers;import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer3DTO;import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;@Mapperpublic interface Customer3Mapper { Customer3Mapper INSTANCE = Mappers.getMapper(Customer3Mapper.class); @Mapping(source = "firstName", target = "name") @Mapping(source = "lastName", target = "familyName") @Mapping(target="fullName", expression = "java(customer.getFirstName() +\" \"+ customer.getLastName())") @Mapping(target="birthDay", expression = "java(java.time.LocalDate.of(customer.getYearOfBirth(), customer.getMonthOfBirth(), customer.getDayOfBirth()))") @Mapping(target="homeAddress.street",source="address") @Mapping(target="homeAddress.houseNumber",source="houseNumber") @Mapping(target="homeAddress.addition",source="houseNumberAddition") @Mapping(target="homeAddress.city",source="city") @Mapping(target="homeAddress.country",source = "country") Customer3DTO customerToCustomerDTO(Customer customer);} If we run this code and check output we should see that all is as expected. We can easily check this by using simple JUnit tests, for example public class DummyCustomerBuilder { public static Customer dummyCustomer() { Customer customer = new Customer(); customer.setId((long)1); customer.setFirstName("Sherlock"); customer.setLastName("Holmes"); customer.setCity("London"); customer.setCountry("Great Britan"); customer.setHouseNumber(221); customer.setHouseNumberAddition("B"); customer.setAddress("Baker Street"); customer.setDayOfBirth(6); customer.setMonthOfBirth(1); customer.setYearOfBirth(1854); return customer; }}

import org.junit.jupiter.api.Assertions;import org.junit.jupiter.api.Test;import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer3DTO;import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;import java.time.LocalDate;public class Customer3MapperTest { // ......... @Test public void testMappingHomeAddress() { //given Customer customer = DummyCustomerBuilder.dummyCustomer(); //when Customer3DTO cDto = Customer3Mapper.INSTANCE.customerToCustomerDTO(customer); //then Assertions.assertNotNull(cDto); Assertions.assertNotNull(cDto.getHomeAddress()); Assertions.assertEquals("Baker Street",cDto.getHomeAddress().getStreet()); Assertions.assertEquals(Integer.valueOf(221), cDto.getHomeAddress().getHouseNumber()); Assertions.assertEquals("B",cDto.getHomeAddress().getAddition()); Assertions.assertEquals("London", cDto.getHomeAddress().getCity()); Assertions.assertEquals("Great Britan",cDto.getHomeAddress().getCountry()); }} ConclusionAs we saw from our simple realistic example, with very little code using MapStruct we can handle a lot of everyday transformation without the need to create a lot of boilerplate code. This means that maintaining and modifying code to meet the demands of tomorrow will be easier, due to the simple fact that there is less of it.In this blog post, we just scratched the surface of all the things MapStruct can help us with, and I highly recommend checking official website for more info.My suggestion in day-to-day usage of MapStruct would be make sure to keep it simple and always think if something is easier and better to achieve using MapStruct or your custom code. The fact that you can do something using one, doesn’t always mean that you should. Maybe there is a simpler and better solution.## Resources* Full code with tests https://github.com/vladimir-dejanovic/making-data-transformation-easy-with-mapstruct-blog * https://mapstruct.org/ The post Making data transformation easy with MapStruct appeared first on JVM Advent.

View Details

I tweet technical content that I consider interesting, but the funny tweets are the ones that get the most engagement.

I attended the JavaLand conference in March, stumbled upon the Gradle booth, and found this gem:

Of course, at some point, a fanboy hijacked the thread and claimed the so-called superiority of Gradle. In this post, I’d like to shed some light on my stance, so I can direct people to it instead of debunking the same “reasoning” repeatedly.

To manage this, I need to get back in time. Software development is a fast-changing field, and much of our understanding is based on personal experience. So here’s mine.

My first build tool: AntI started developing in Java in 2002. At the time, there were no build tools: we compiled and built through the IDE. For the record, I first used Visual Age for Java; then, I moved to Borland JBuilder.

Building with an IDE has a huge issue: each developer has dedicated settings, so artifact generation depends on the developer-machine combination.

Non-repeatable builds are an age-old problem. My first experience with repeatable builds is Apache Ant:

Apache Ant is a Java library and command-line tool whose mission is to drive processes described in build files as targets and extension points dependent upon each other. The main known usage of Ant is the build of Java applications. Ant supplies a number of built-in tasks allowing to compile, assemble, test and run Java applications. Ant can also be used effectively to build non Java applications, for instance C or C++ applications. More generally, Ant can be used to pilot any type of process which can be described in terms of targets and tasks.

— https://ant.apache.org/

Ant is based on three main abstractions:

  • A task is an atomic unit of work, e.g., javacto compile Java files, war to assemble a Web Archive, etc. Ant provides lots of tasks out-of-the-box but allows adding custom ones.
  • A target is a list of tasks
  • You can define dependencies between tasks, such as package depending on compile. In this regard, you can see Ant as a workflow execution engine.

I soon became “fluent” in Ant. As a consultant, I went from company to company, project to project. Initially, I mostly set up Ant, but Ant became more widespread as time passed, and I encountered existing Ant setups. I was consistent in my projects, but other projects were very different from each other.

Every time, when arriving at a new project, you had to carefully read the Ant setup to understand the custom build. Moreover, each project’s structure was different. Some put their sources in src, some in sources, some in a nested structure, etc.

I remember once a generic build file that tried accommodating the whole of an organization’s project needs. It defined over 80 targets in over 2,000 lines of XML. It took me a non-trivial amount of time to understand how to use it with help and even more time to be able to tweak it without breaking projects.

My second build tool: MavenThe above project got me thinking a lot. I wanted to improve the situation as the maintainers had already pushed Ant’s limits. At the time, I was working with my friend Freddy Mallet (of Sonar fame). We talked, and he pointed me to Maven. I had once built a project with Maven but had no other prior experience. I studied the documentation for hours, and through trial-and-error attempts, under the tutelage of Freddy, migrated the whole Ant build file to a simple parent POM.

In Ant, you’d need to define everything in each project. For example, Ant requires configuring the Java files location for compilation; Maven assumes they are under src/main/java, though it’s possible to override it. Maven did revolutionize the Java build field with its Convention over Configuration approach. Nowadays, lots of software offer sensible configuration by default.

For developers who go from project to project, as I did, it means there’s much less cognitive load when joining a new project. I expect Java sources to be located under src/main/java. Maven conventions continue beyond the project’s structure. They also define the project’s lifecycle, from compilation to uploading the artifact in a remote registry, via unit and integration testing.

Finally, junior developers tend to be oblivious about it, but Maven defined the term dependency management. It introduced the idea of artifact registries, where one can download immutable dependencies from and push artifacts to. Before that time, each project had to store dependencies in its dedicated repository.

For the record, there were a couple of stored dependencies on the abovementioned project. When I migrated from Ant to Maven, I had to find the exact dependency version. For most, it was straightforward, as it was in the filename or the JAR’s manifest. One, however, had been updated with additional classes. So much for immutability.

Maven had a profound influence on all later build tools: they defined themselves in reference to Maven.

No build tool of mine: GradleGradle’s primary claim was to fix Maven’s shortcomings, or at least what it perceived as such. While Maven is not exempt from reproach, Gradle assumed the most significant issue was its lack of flexibility. It’s a surprising assumption because that was precisely what Maven improved over Ant. Maven projects have similar structures and use the same lifecycle: the principle of least surprise in effect. Conversely, Gradle allows customizing nearly every build aspect, including the lifecycle.

Before going to confront the flexibility argument, let me acknowledge two great original Gradle features that Maven implemented afterward: the Gradle daemon and the Gradle wrapper.

Maven and Gradle are both Java applications that run on the JVM. Starting a JVM is expensive in terms of time and resources. The benefit is that long-running JVM will optimize the JIT-ed code over time. For short-term tasks, the benefit is zero and even harmful if you take the JVM startup time into account. Gradle came up with the Gradle daemon. When you run Gradle, it will look for a running daemon. If not, it will start a new one. The command-line app will delegate everything to the daemon. As its name implies, the daemon doesn’t stop when the command line has finished. The daemon leverages the benefits of the JVM.

Chances are that your application will outlive your current build tools. What happens when you need to fix a bug five years from now, only to notice that the project’s build tool isn’t available online? The idea behind Gradle’s wrapper is to keep the exact Gradle version along with the project and just enough code to download the full version over the Internet. As a side-effect, developers don’t need to install Gradle locally; all use the same version, avoiding any discrepancy.

Debunking Gradle’s flexibilityGradle brought the two above great features that Maven integrated, proving that competition is good. Despite this, I still find no benefit of Gradle.

I’ll try to push the emotional side away. At its beginning, Gradle marketing tried to put down Maven on every possible occasion, published crazy comparison charts, and generally was very aggressive in its communication. Let’s say this phase lasted far more than would be acceptable for a young company trying to find its place in the market. You could say that Gradle was very Oedipian in its approach: trying to kill its Maven “father”. Finally, after all those years, it seems it has wised up and now “loves Maven”.

Remember that before Maven took over, every Ant project was ad hoc. Maven did put an end to that. It brought law to the World Wild West of custom projects. You can disagree with the law, but it’s the law anyway, and everybody needs to stand by it. Maven standards are so entrenched that even though it’s possible to override some parameters, e.g., source location, nobody ever does it.

I did experience two symptoms of Gradle’s flexibility. I suspect far more exist.

Custom lifecycle phasesMaven manages integration testing in four phases, run in order:

  1. pre-integration-test: set up anything the tests need
  2. integration-test: execute the tests
  3. post-integration-test: clean up the resources, if any
  4. verify: act upon the results of the tests

I never used the pre- and post-phases, as each test had a dedicated setup and teardown logic.

On the other side, Gradle has no notion of integration tests whatsoever. Yet, Gradle fanboys will happily explain that you can add the phases you want. Indeed, Gradle allows lifecycle “customization”: you can add as many extra phases into the regular lifecycle as you want.

It’s a mess, for each project will need to come up with both the number of phases required and their name: integration-test, integration-tests, integration-testing, it (for the lazy), etc. The options are endless.

The snowflake syndromeMaven treats every project as a regular standard project. And if you have specific needs, it’s possible to write a plugin for that. Writing a Maven plugin is definitely not fun; hence, you only write one when it’s necessary, not just because you have decided that the law doesn’t apply to you.

Gradle claims that lack of flexibility is an issue; hence, it wants to fix it. I stand by the opposite: lack of flexibility for my build tool is a feature, not a bug. Gradle makes it easy to hack the build. Hence, anybody who thinks their project is a special snowflake and deserves customization will happily do so. Reality check: it’s rarely the case; when it is, it’s for frameworks, not regular projects. Gradle proponents say that it still offers standards while allowing easy configuration. The heart of the matter is that it’s not a standard if it can be changed at anybody’s whim.

Gradle is the de facto build tool for Android projects. In one of the companies I worked for, somebody wrote custom Groovy code in the Gradle build to run Sonar and send the metrics to the internal Sonar instance. There was no out-of-the-box Sonar plugin at the time, or I assume it didn’t cut it. So far, so good.

When another team created the company’s second Android project, they copy-pasted the first project’s structure and the build file. The intelligent thing to do would have been, at this time to make an internal Gradle plugin out of the Sonar-specific code. But they didn’t do it because Gradle made it so easy to hack the build. And I, the Gradle-hater, took it upon myself to create the plugin. It could have been a better developer experience, to say the least. Lacking quality documentation and using an untyped language (Groovy), I used the console to print out the objects’ structure to progress.

ConclusionCompetition is good, and Gradle has brought new ideas that Maven integrated, the wrapper and the daemon. However, Gradle is built on the premise that flexibility is good, while my experience has shown me the opposite. Ant was very flexible, and the cognitive load to go from one project to the next was high.

We, developers, are human beings: we like to think our projects are different from others. Most of the time, they are not. Customization is only a way to satisfy our ego. Flexible build tools allow us to implement such customization, whether warranted or not.

Irrelevant customizations bring no benefit and are easy to develop but expensive to maintain. If managing software assets is part of my responsibilities, I’ll always choose stability over flexibility for my build tool.

The post My Final Take on Gradle (vs. Maven) appeared first on JVM Advent.

View Details

Welcome back, my dear Java Geek!

Last year we compared WebAssembly and discussed in what ways it differs from the JVM. A lot of things have happened in the meantime. If you want to dive deeper into that kind of detail, I warmly suggest reading this beautiful blog series by Chris Dickinson.

For this Java Advent, I wanted to get back to the topic from a different angle. Last year we saw that, because the JVM and WebAssembly are only shallowly similar, there is friction when it comes to putting the two together.

But this is not just a matter of taste; a Wasm VM is generally simpler, smaller, and easier to embed than a full, modern JVM. This on the one hand, might allow JVM languages to run in spaces that they normally would not fit (for instance, in a plug-in system for a native executable); and, on the other hand, it might allow the JVM to host languages that normally would not be supported (by implementing Wasm support on top of a JVM).

Last year we listed a few projects that were starting to approach the space of compiling JVM bytecode to Wasm bytecode, and a few others that were addressing hosting a Wasm binary on top of a JVM.

In this post, we will see what things have changed since last time, and we will revisit some of those projects. Has the ecosystem matured? And in what ways should you, the Java Geek, care?

What’s With All The Fuss?Original: Jave SE Platform at a Glance

In essence, you can think of a Wasm VM as a JDK without the class library, with no built-in way to interact with its surroundings, except importing and exporting functions. If some function you import is able to perform I/O, then great! you can actually do something useful, otherwise, you have got yourself a fancy calculator.

Then, if it is so useless, why are we interested in it at all? The Wasm VM is small, so it is relatively easy to implement, and thus, it is also easy to port to many platforms. An unmodified Wasm VM is able to run in a browser as well as outside a browser. And, if you remember, this is the thing I am personally more excited about.

Now, in the last year, you might have heard that Wasm VMs are being proposed as an alternative to containers because you can produce a self-contained binary that will run in a sandboxed environment: this is an ambitious goal, and people are making some progress there.

However, there is also another, much less ambitious use case, where I feel a greater potential lies: because Wasm VMs are relatively small and easy to embed, they are great for implementing sandboxed plug-in and extension systems.

When a Wasm VM is embedded to provide a plug-in system, the Wasm VM is extended by providing a collection of functions that the Wasm binaries can invoke. A proposed standard called WASI provides a set of functions that, if you squint enough, could be considered some sort of POSIX compatibility layer, or, more generally, a set of OS-like primitives to access things like the console, a file system and, in some cases, even the network. A subset of these APIs is often expected to be available, usually to be able to provide simple input and output capabilities (such as logging). Each language usually rebuilds its own standard library on top of these APIs, with the aim of making a port easier for the end-users.

Several projects have adopted Wasm as an extension language; for instance, the Envoy proxy allows writing middleware through a Wasm interface; Redpanda is a streaming platform implementing the Kafka protocol, that allows writing data transforms in-core using Wasm. The list could go on and it is always growing.

The Extism project by Dylibso is proposing a unified API to provide cross-platform extensions and plug-ins, regardless of the language: they provide a battery-included system to experiment with Wasm plug-ins. This includes the JVM, where the underlying VM is currently Wasmtime, and Go, where the VM is wazero.

The JVM has always provided ways to load code dynamically. I think we could even dare to say that class loading is in fact a defining feature of a JVM, so, it is pretty easy to write a plug-in system that loads class files or jar files. However, as with any language VM, the JVM has always been limited to the languages that in fact support it.

As the Wasm landscape expands, more and more languages are deciding to target it. For instance, there is support for C, C++, Rust, Go, Python, Ruby just to name a few. Even the .NET ecosystem has added support to Wasm in its toolchain. Fermyon keeps a list of languages with their degree of support.

So for a Java Geek there are two opportunities here:

  1. JVM languages can support Wasm as a compilation target so that they can be used to write Wasm binaries that will run outside a traditional JVM and inside a Wasm VM. This could be used to write software for the browser, as well as plug-ins for other software.
  2. JVM may run Wasm binaries to support languages that traditionally would not be available on a plain JVM.

What Has Changed Since Last Time?Quite a lot! First of all, let me talk just a little about my favorite topic in the whole world, i.e. myself. I am no longer at Red Hat, and I have joined Tetrate to join the team of, guess what, an open-source WebAssembly runtime that I mentioned earlier called wazero. In the process, I also had to switch to a different language in my day-to-day; that is, Go, since that’s wazero’s language.

Burn in hell you, traitor! — I hear you say. But, in my defense: first, I still hold the JVM dear to my achy-breaky heart, and, second, I think getting exposed to a different ecosystem helps you to see the bigger picture. And in fact, there are a few things that all these ecosystems (JVM, Go, Wasm) could learn from each other.

But enough about me! While I was busy learning Go, the JVM space has also started to get more involved in Wasm. And, at the same time, Wasm has gained a few features that, in the future, might make it easier for JVM languages to target it. For instance, in the previous post, we mentioned the lack of support for threading, exception handling, and garbage collection. All these features made progress, and they are slowly starting to get experimental support in some languages.

Caffeinated GophersAs surprising as it may sound, while working on wazero I have learned that there are in fact similarities between the Go and the Java runtime (at least in principle); for instance:

  • compiling Go and Java to a Wasm binary requires some massaging. But such a massage is not that different from building a native executable. We will see in what ways in the next section
  • evaluating a Wasm binary on the JVM and the Go runtime can be achieved by implementing a Wasm runtime on top of them or depending on an existing runtime. Both the JVM and Go have similar caveats (we will see them later) when it comes to depending on a native library, so writing a Wasm VM specific to that language might be more convenient.

Let us see both aspects in detail.

Compiling JVM Software into WasmWe already mentioned targeting the Wasm bytecode in its current version of the spec (2.0) is more similar to targeting a native platform than a high-level language VM like the JVM.

It is not by chance that many of the languages that support Wasm as a compilation target are based on the LLVM toolchain. Because the LLVM toolchain supports Wasm among its native targets, it is relatively easy to add support for it.

Thus, it is no surprise that C and C++ support Wasm via Clang/LLVM, that Rust supports Wasm, Zig supports Wasm, and that the TinyGo flavor of the Go language gained support for Wasm relatively early on. Guess what common trait they all share? That’s right, they all leverage LLVM.

Wasm as a Native TargetBut in what ways does Wasm behave as a native target, rather than, more intuitively, as JavaScript?

Well, for instance, Wasm 2.0 does not provide primitives to manipulate structured data. It only deals with numbers – integers and floats (and 128-bit vectors, but that does not make much difference).

So, what if you want to deal with fancy data structures such as — gasp!arrays? Well, fear not: Wasm 2.0 provides to each module its own, isolated linear memory space; which is a cool way to call an array of unsigned bytes that your program will treat as if it were real operating system memory. Thus, if you need to allocate an array, then you just reserve a slice of that larger array.

And what if you need a fancier structure such as strings? Well, you just reserve a slice of that array for the characters, and then maybe some extra meta-data for the size (depending on how your language represents strings).

And what if you need an even fancier data structure such as Point(int x, int y)? Well, you get the idea.

Now, as long as you only have to allocate, things will work out pretty well. You can just keep track of the last allocation you did; that is, effectively keeping an index into the array. Which is another way to say, err, a pointer. And every time you allocate more, you can just update that ~~pointer~~ I mean index.

However, at some point, that memory space will finish. And obviously, we don’t want that to happen, so you also want to keep track of things you no longer need, and free that space. And you want to keep things neat and avoid fragmentation. And there you go: you got yourself a memory manager. Or, as some runtimes call it, a garbage collector.

Note. It is worth mentioning that for all intents and purposes, the linear memory space can be thought about as real operating system memory: however, there is one big caveat that makes it quite different; such a memory space is not shared across Wasm programs, even within the same VM, and even across modules. Each module gets its own, isolated memory space, and the only way to pass structured data across modules is essentially to copy it over. This is a big difference that is often highlighted as one of the benefits of Wasm binaries over traditional, native binaries.

All of this might evolve in the future because the WasmGC spec has moved further and the multiple memories and threading proposals have moved too. The WasmGC spec deals exactly with allocating and deallocating structured data and delegating memory management to the underlying runtime. The flat memory space will still be available, but a compiler may pick the allocation strategy that suits the language best. The “multiple memories” proposal allows modules to define different, isolated memory spaces, while the threading proposal includes a way to declare a memory space as shared. However, at the time of writing the WasmGC proposal is only supported in browsers, and the other two are not widely available yet; until all these proposals gain wider adoption, a bare-bone Wasm 2.0 runtime only provides a flat, linear memory space as described above.

So, from the JVM perspective, there are two strategies to target Wasm:

  • compiling a JVM into an executable and then letting it load and evaluate class files
  • compiling a JVM application into a self-contained executable, from bytecode similar to a native image
  • compiling a JVM application into a self-contained executable, from source code with a language-specific compiler.

Compiling a JVM into WasmIn the first case, we are effectively porting and compiling a JVM into Wasm, and then we are evaluating JVM bytecode inside such a JVM that runs inside a Wasm VM. If this gives you a headache, that’s alright. It is a little mind-bending. However, this is a perfectly valid approach, and it is also the approach dynamic languages such as Ruby and Python are adopting. But is this the best approach? As usual, it depends on your use case and your performance requirements. This approach potentially allows for the largest degree of compatibility, with fewer limitations.

There is at least one project that is doing exactly that: the fine people at Leaning Technologies are developing a JVM that runs in-browser (CheerpJ) that is especially well-suited to modernize legacy software that would require, say, an applet runtime (they also provide a browser extension that does exactly that).

However, a modern JVM tends to be large; as such, a Wasm binary of this kind might not be well suited to write tiny executables such as plug-ins and extensions.

Compiling Java Bytecode into WasmThis is the most general approach. If you are able to compile Wasm into bytecode, then potentially all Java software can be compiled into Wasm. This is similar to the approach that GraalVM Native Image takes to produce a native executable. In fact, Native Image would be the most natural candidate, to the point that this was mentioned as a possibility in the post about RISC-V support which, guess what, leveraged LLVM.

Because it is the most general approach, just like the Native Image Builder: it should deal with all the worst cases, and cannot make any assumptions about the program that will be run.

  • In order to preserve the semantics of your program, you will have to emulate most of the features of the JVM, including, in some form, reflection, and class loading (even if with some limitations, such as the infamous “closed world assumption”).
  • you want to reduce the program surface as much as possible, just like GraalVM’s Native Image Builder does when it produces a native executable: this however may impose limitations on reflection and class loading (the infamous “closed world assumption”).
  • Then, just like the Native Image Builder, you will still have to ship a full-blown garbage collector.
  • Finally, to keep your boot time low, you might want to move some computation at build time.

At the time of writing, there are at least two projects that are able to compile class files into Wasm JWebAssembly and TeaVM. However, if you want to produce a self-contained Wasm executable that runs outside the browser, TeaVM is the most promising project so far.

If you are lazy like me, I found that in order to get started with TeaVM, the most effective way is to clone the repository, build with ./gradlew publishToMavenLocal, and then try out the example under samples/pi which is already configured for WASI support. The program computes the first N digits of π a given N, supplied via command line argument, then prints them with the elapsed time., and, if the build was successful you will find a pre-built Wasm binary under samples/pi/build/libs/wasi/pi.wasm

You can test it out with your favorite Wasm runtime, that is, obviously wazero. Just kidding, obviously, the choice is relevant, the output will be always the same; that’s the point after all!

❯ wazero run build/libs/wasi/pi.wasm 3314 :3Time in millis: 0❯ wazero run build/libs/wasi/pi.wasm 531415 :5Time in millis: 0❯ wazero run build/libs/wasi/pi.wasm 103141592653 :10Time in millis: 0❯ wazero run build/libs/wasi/pi.wasm 1003141592653 :105897932384 :206264338327 :309502884197 :401693993751 :500582097494 :604592307816 :704062862089 :809862803482 :905342117067 :100Time in millis: 7 Appendix: Does It Really Boot Fast?It is often claimed that Wasm VMs are super-fast to boot. This is not false, but the reason is kind of underwhelming; there is no secret sauce: they start fast because they don’t need to do much anyway. In a typical Java program, a JVM might need to load and initialize thousands of classes before it reaches a steady state. All these initializers add up, and that is the reason why the Native Image Builder makes the pragmatic choice of moving some of that computation at build time, taking a snapshot of that heap, and then restoring it at boot time to get reasonable startup performance.

Even Wasm modules may define a startup function to perform initialization. Guess what you might need to do in Wasm too if you want to keep those precious milliseconds down?

It is interesting how the Wasm community has already produced a tool to perform build-time initialization called wizer. Instead of producing a native binary, wizer produces a new Wasm binary, that is, what Project Leyden would call a condenser.

Compiling Source Code into WasmSome JVM languages have supported compiling to a different target for a long time. However, in my research, I have found that in general the primary target for these compilers is execution in the browser. I will still give a brief overview of these alternative compilers for completeness.

The Scala compiler gained soon support for targeting JavaScript with Scala,js. And, later, the Scala Native project started to explore the space of native compilation through an LLVM-based backend. More recently this Native backend has experimented with adding support to target Wasm. However, this experiment needs support for features such as automated garbage collection and exception handling, that are emulated via JavaScript shims (this is generated automatically by the Emscripten toolchain, that you can think of as an extension to LLVM).

The Kotlin compiler was born with multi-platform in mind: Kotlin supported a JavaScript output for front-end development since its very first versions. It is only natural to support Wasm with the same goal. The Kotlin compiler for Wasm (Kotlin/Wasm) originally was born as an extension to the Kotlin/Native backend (based on LLVM). The most recent version, however, targets Wasm directly, and, in particular, it leverages the WasmGC proposal, which, at the time of writing, has been enabled in Chrome and Firefox. Node, being based on V8 like Chrome, it supports it behind a flag, and Deno has been reported to run Kotlin fine.

The GWT compiler for Java evolved into the J2CL compiler in recent years. Originally targeting Java source-to-JavaScript compilation, it has also become a test bed for experimenting with the WasmGC spec.

Evaluating Wasm Binaries on the JVMIn the introduction, we briefly mentioned that Wasm might bring support to languages that otherwise would not be available on the JVM. But we have JRuby, we had Jython, and with Truffle we have other, state-of-the-art dynamic language implementations, including JavaScript: while these get best-in-class performance only when run on a GraalVM JDK, they are still portable and work on any JVM. So the question might be: why would you need Wasm, if you already have GraalVM?

For different reasons, we briefly discussed GraalVM in the previous post. While Truffle/GraalVM supports a number of languages with great performance it still requires implementing such language-specific support from scratch. There is one Python implementation, one Ruby implementation, one R implementation, one JavaScript implementation… you get the idea.

But, as we have seen earlier, Wasm was designed as a compilation target, and a lot of compiler toolchains already support it. This means that with relatively few changes, it is often possible to bring Wasm support to first-party language implementations. For instance, the Python and the Ruby runtimes that run on Wasm are the traditional CPython and CRuby (Ruby MRI) runtimes, with obvious compatibility benefits.

Picking a Wasm Runtime for the JVMAssuming that we have now bought into Wasm as a way to host end-user extensions in our Java environment, the most complete and battle-tested implementations of a Wasm VM are written in languages such as C/C++ and Rust. These are native libraries that will require some form of integration.

Now, while Java is improving the developer experience over JNI with Project Panama (finally being released with JDK 22) linking against a native library still imposes a number of restrictions.

Interestingly enough, this is another place where the Go runtime oddly behaves like a Java Runtime. While the developer experience for Go developers is probably better than JNI, linking against a non-Go, native binary requires a Go developer to reach for the Cgo system. This is completely transparent from a development perspective, as it is just a matter of importing the right library. But as you opt-in to CGo, under the hoods, the compilation pipeline changes dramatically: it requires your system to provide a C compiler, and cross-platform build capabilities that usually work out-of-the-box, will require much more work.

The restrictions imposed by both JNI/Panama and Cgo are essentially the same:

    • There are portability concerns, because you will have to compile the native library for all the platforms you want to support, and this obviously hinders the portability of your code
    • There is overhead crossing the boundary to and from the managed environment to “native code”
  • There are security and safety implications because the native library has access to the entire space of the process memory
  • There are runtime concerns because every native call hogs an operating system thread: this will not play nicely with virtual threads (i.e., in the case of Go, goroutines).

This means that, while it is perfectly possible for a native Wasm runtime to be imported into a Java or Go application, this comes with costs that have to be carefully evaluated, and that may ultimately lead to avoiding adopting it altogether.

This is the reason for the wazero project: it is a zero-dependency WebAssembly runtime for Go, where zero means literally no dependencies, but in particular, zero Cgo dependencies. So, depending on it and using it, for a Go developer is a no-brainer: there is essentially no overhead, and you keep all the benefits of your Go runtime. So, what about Java? Is there anything similar?

Indeed, there are. The GraalVM project already proved that it is possible to run a lower-level compilation target on top of Truffle: this is called Sulong, and it is an implementation of a runtime for the LLVM IR, that is, the Intermediate Representation that a compiler based on LLVM uses internally, that then, in the final stages of the compilation pipeline, gets translated into the target architecture.

So, there is an experimental GraalVM Wasm language implementation for Truffle. Obviously, besides this being experimental, it is also worth noticing that, as it is for all the Truffle-based language implementations, you will need a GraalVM JDK to get the best performance out of it.

I also wanted to mention another project that is being developed by some friends, and I’m keeping myself involved in it from afar, called Chicory. Chicory is a Wasm VM implementation that aims to support the entire spec. It currently does not aim for best-in-class performance, but focuses on correctness, by implementing a Wasm interpreter validated against the Wasm test suite. Nonetheless, the people involved are already considering adding support for a bytecode translation layer, which potentially could provide reasonable performance. Chicory was initiated by one of the founders at Extism (the Wasm plug-in system), so one of the goals will be to rebase the Extism SDK for Java on top of it, once it is mature enough.

ConclusionsI could go on and on about Wasm and this article has reached a considerable length already. The space is always evolving and for a newcomer, it might be intimidating to get started. I mentioned all of the ways Wasm could be useful from the perspective of the Java Geek, but I also overlooked some important limitations that will need to be addressed before Wasm can expect to gain a wider adoption, beyond early adopters and enthusiasts.

For instance, to this day, there are few options for debugging (especially dire is the landscape when it comes to stepwise debugging, where tooling is still dramatically limited — see for instance this recent talk by Ashwin Kumar Uppala and Shivay Lamba).

The work to stabilize the WASI set of APIs is also ongoing.

Finally, there is a lot of buzz around the so-called Component Model. The component model aims to provide a polyglot system to define APIs and compose libraries together, while retaining the safe, isolated architecture of the Wasm VM (remember: memory is not shared by default). These are however early days and the work here is still in flux.

I still hope that this new article has caught your attention; in the meantime, enjoy your panettone and have a sip of spumante, and see you at some conference in 2024!

The post A Return to WebAssembly for the Java Geek appeared first on JVM Advent.

View Details

The rabbit hole goes infinitely deep if you want to do latency measurements, benchmarking, or performance testing/tuning/analysis. They are very hard to do right and it is very easy to mess them up. In this post, I would like to show you a very common mistake of latency measurements and a simple solution to fix it.

Let’s say you want to record the duration of a method call. I guess most people will be able to mention at least one thing about what is wrong with this. I usually expect to hear error handling and “misusing” the Date class but there is a lot more.

Response handleRequest(Request request) { Date start = new Date(); Response response = doSomething(request); record(new Date().getTime() - start.getTime()); return response;} Believe it or not, I saw this in the wild but this is not the very common mistake I referred to previously and want to talk about. The very common mistake I would like to talk about is this:

Response handleRequest(Request request) { long start = System.currentTimeMillis(); try { return doSomething(request); } finally { record(System.currentTimeMillis() - start); }} Unfortunately, you can see latency measurement code similar to this all the time and if this snippet seems reasonable to you, you are not alone: the duration is measured without using the Date class, exceptional cases are handled, what could go wrong?
I’m glad you asked.

System.currentTimeMillis() returns:

the difference, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC.

javadoc

This is a so-called “wall-clock”, it just tells you what is the current system time. And the usage of it can lead to lots of problems.

ProblemsGranularitySystem.currentTimeMillis() returns the current time in milliseconds but the granularity of the value depends on the underlying operating system and might be larger than the base unit. For example, many operating systems measure time in units of tens of milliseconds.

Let’s say your OS measures time in chunks of 100ms. In this scenario, there is no way to measure anything that needs to be more precise than this. So if an operation takes somewhere between 20 and 80ms, your measurements are either 0 or 100ms (this precision can be worse, e.g.: 1s), not even mentioning measuring sub-millisecond operations. If you look into the javadoc of System.currentTimeMillis(), you can read about this problem.

Time UniformityYou might have heard that there are irregularities in Earth’s rotation (it slows down and speeds up in complex ways). Also, there is a long-term slowdown in Earth’s rotation. There is a standard, called UT1 (a version of Universal Time), that is based on astronomical observations and Earth’s rotation. Because of this, UT1 does not always flow uniformly. So if your time source is based on this, you have a non-uniform source of time which can speed up and slow down leading you to measurement errors (see: javadoc of Date). System.currentTimeMillis() uses UTC instead of UT1 so it does not have this exact problem.

Leap SecondUTC is measured by precise atomic clocks while UT1 is based on astronomical observations (see the previous section). To keep the two somewhat close to each other (< 0.9s), UTC is occasionally adjusted by one second. This adjustment is called the leap second. Since climatic and geological events affect Earth’s rotation, UTC leap seconds are irregular and unpredictable. These adjustments can lead you measurement errors since the clock can change during your measurements (see: javadoc of Date).

Synchronizing the System ClockWhen clocks do not run at the same rate, clock drifting can happen (they get out-of-sync). To keep them somewhat close to each other, clock synchronization is necessary (see: NTP). Since your system clock precision is far from the precision of atomic clocks, it needs to be adjusted regularly to be within a few milliseconds of UTC. This can mean jumps in time or making the clock go faster or slower so that it will drift towards UTC. These adjustments can also lead to measurement errors.

Daylight Saving TimeSomething needs to be really messed up if Daylight Saving Time causes a problem in such measurements, though it can happen if you use the Date class for measuring elapsed time. It is also a quite frequent issue if you need to deal with time; I have seen a service once that crashed twice per year, can you guess why?

SolutionThe solution is pretty simple: do not use the “wall-clock”. In Java (but other platforms have a solution for this too), System.nanoTime() gives you access to a high-resolution time source (nanoseconds) that is designed for measuring elapsed time and it is not related to the system clock or any “wall-clock”, see the javadoc of System.nanoTime(). So you can do this:

Response handleRequest(Request request) { long start = System.nanoTime(); try { return doSomething(request); } finally { record(System.nanoTime() - start); }} Doing this can be ok but after this point, the rabbit hole goes infinitely deep so I do not recommend doing this if you want to measure latency. There are lots of other problems you can run into (see later) so what I recommend instead is using the right tool for the job:

  • If you are doing (nano/micro/milli/macro) benchmarking: JMH
  • If you want to collect metrics for your application: Micrometer
  • If you want to perf-test your application: Gatling

Here’s an example for collecting metrics with Micrometer:

@TimedResponse handleRequest(Request request) { return doSomething(request);} or

Response handleRequest(Request request) { return timer.record(() -> doSomething(request));} or, if you want better control:

Response handleRequest(Request request) { Timer.Sample sample = Timer.start(); try { return doSomething(request); } finally { sample.stop(timer); }} But there is so much more you can do, please check the docs.

What else can go wrong?Everything. Here are a few things I recommend watching and reading to dig deeper:

  • How NOT to Measure Latency by Gil Tene
  • Java Microbenchmark Harness: The Lesser of Two Evils by Aleksey Shipilëv
  • Falsehoods programmers believe about time
  • More falsehoods programmers believe about time; “wisdom of the crowd” edition
  • The Problem with Time & Timezones by Computerphile

The post How Not to Measure Elapsed Time in Java appeared first on JVM Advent.

View Details

Whether you are a beginner or senior Java developer, you strive to accomplish ambitious goals through your code while enjoying incremental progress. Along with many performance, stability, and security updates, Java 21 delivers new features and enhancements aiming to boost Java development productivity. And the best way to learn these language features is by using them in a Java project.

SetupAs the winter festivities approach, let’s build a Java application where you can order a wrapped gift for someone. Project wrapup is a simple http handler implementation that returns a gift as JSON from a sender to a receiver via HTTP POST method.

Before jumping into action, you should know that you need an IDE, at least JDK 21 and maven installed on your local machine to reproduce the examples. I generated my project with Oracle Java Platform Extension for Visual Studio Code via View > Command Palette > Java: New Project > Java with Maven, named the project wrapup and chose the package name org.ammbra.advent.

So, let’s check out how we can use Java 21 language constructs to package gifts as JSONs.

Towards a simplified beginning with JavaThe IDE generated project contains a starter class Wrappup.java in the package org.ammbra.advent.

package org.ammbra.advent;public class Wrapup { public static void main(String[] args) { System.*out*.println("Hello World!"); }} Although the main method declares arguments, those are not later processed within its scope. In JDK 21, JEP 445 introduced unnamed classes and instance main methods as a preview feature to reduce the verbosity when writing simple programs. In consequence, you can refactor the previous code to:

package org.ammbra.advent; class Wrapup { **void main()** { System.*out*.println("Hello, World!");}} To run the previous snippet, go to a terminal window and type the following command:

java **--enable-preview --source 21** \ src/main/java/org/ammbra/advent/Wrapup.java For the moment, let’s evolve the Wrapup class to process only HTTP POST requests and produce a JSON output, by implementing com.sun.net.httpserver.HttpHandler.

record Wrapup() implements HttpHandler { void main() throws IOException { var server = HttpServer.create( new InetSocketAddress("", 8081), 0); var address = server.getAddress(); server.createContext("/", new Wrapup()); server.setExecutor( Executors.newVirtualThreadPerTaskExecutor() ); server.start(); System.out.printf("http://%s:%d%n", address.getHostString(), address.getPort()); } @Override public void handle(HttpExchange exchange) throws IOException { int statusCode = 200; String requestMethod = exchange.getRequestMethod(); if (!"POST".equalsIgnoreCase(requestMethod)) { statusCode = 400; } // Get the request body input stream InputStream reqBody = exchange.getRequestBody(); // Read JSON from the input stream JSONObject req = RequestConverter.asJSONObject(reqBody); String sender = req.optString("sender"); String receiver = req.optString("receiver"); String message = req.optString("celebration"); String json = "{'receiver':'" + receiver + "', 'sender':'" + sender + "','message':'" + message + "'}"; exchange.sendResponseHeaders(statusCode, 0); try (var stream = exchange.getResponseBody()) { stream.write(json.getBytes()); } }} To launch the program, go to the terminal window and run the following commands:

```

export path to .m2 json libraryexport $JSON_PATH=//.m2/repository/org/json/json/20231013#launch the appjava -classpath target/classes:$JSON_PATH/json-20231013.jar \ --enable-preview --source 21 \ src/main/java/org/ammbra/advent/Wrapup.java

``` Let’s try a simple curl request to check the output:

curl -X POST http://127.0.0.1:8081 \ -H 'Content-Type: application/json' \ -d '{"receiver":"Duke","sender":"Ana","celebration":"Happy New Year!"}' You should receive the following response:

{'receiver':'Duke', 'sender':'Ana','message':'Happy New Year!'} Greeting someone is a nice gesture, but the application should also serve more complex responses when the sender wishes to send a more substantial gift. To address that, let’s model the application domain.

Data modelling with records and sealed typesGifting on a special occasion can vary from a postcard to a more substantial gift.

For the wrapup project let’s consider the following requirements:

  • A sender can do a nice gesture and offer a gift.
  • A gift can be either a postcard or add to it one of the following: an online coupon, buy an experience or a material present.
  • A postcard does not have an associated cost, all the other 3 types of gifts have a price.
  • An online coupon has an expiry date.
  • A present can be placed inside a box, which has an extra cost.
  • A sender can give a different postcard or surprise depending on celebration, but never send 2 postcards as a gift.

Fig.1 System Class Diagram

The diagram above shows a possible way to model previously described scenario. Postcard,``Coupon, Experience and Presentare records because they should be carriers of immutable data representing possible surprise options. They also share a common formatting process to JSON through the sealed interface Intention.

package org.ammbra.advent.surprise;import org.json.JSONObject;public **sealed interface** Intention **permits** Coupon, Experience, Present, Postcard { JSONObject asJSON();} A Gift is another record type containing a Postcard and an Intention.

package org.ammbra.advent.surprise;import org.json.JSONObject;public record Gift(Postcard postcard, Intention intention) { public JSONObject merge(String option) { JSONObject intentionJSON = intention.asJSON(); JSONObject postcardJSON = postcard.asJSON(); return postcardJSON.put(option, intentionJSON); }} Celebration is an enum storing the defined occasions for sending a gift. Depending on the value of the Choice enum, wrapup will return the appropriate gift in JSON format. Next, let’s define Coupon, Experience, Postcard and Present records and format their data using String templates.

Syntax flexibility with expressive String templatesThe sealed interface Intention limits inheritance, by only allowing specific subtypes, but is also a useful language construct to communicate the purpose of Coupon, Experience, Postcard and Present records.

For example, the characteristics of a Coupon object are its price, the date when it expires and the currency of its cost. As a gift representation should follow a JSON format, let’s leverage string templates to achieve that.

String templates became available as a preview feature in Java 21 and mix literal text with embedded expressions and template processors to produce specialized results, like JSONObject. To return a JSONObject, a template expression would need:

  • A template processor (JSON)
  • A dot character (U+002E) and
  • A template which contains an embedded expression (Coupon record fields).

The Coupon, Experience, Postcard and Present records can share the same template processor from String to JSON:

package org.ammbra.advent.surprise;import org.json.JSONObject;public sealed interface Intention permits Coupon, Experience, Present, Postcard { StringTemplate.Processor<JSONObject, RuntimeException> JSON = StringTemplate.Processor.of( (StringTemplate st) -> new JSONObject(st.interpolate()) ); JSONObject asJSON();} And with this template processor, the Coupon record becomes:

package org.ammbra.advent.surprise;import org.json.JSONObject;import java.time.LocalDate;import java.util.Currency;public record Coupon(double price, LocalDate expiringOn, Currency currency) implements Intention { @Override public JSONObject asJSON() { return JSON. """ { "currency": "\{currency}", "expiresOn" : "\{ expiringOn}", "cost": "\{price}" } """ ; }} Experience and Postcard records share a similar template formatting logic. As the cost of a Present varies depending on the gift-wrapping cost, the asJSON method implementation looks as follows:

package org.ammbra.advent.surprise;import org.json.JSONObject;import java.util.Currency;public record Present(double itemPrice, double boxPrice, Currency currency) implements Intention { @Override public JSONObject asJSON() { return JSON. """ { "currency": "\{currency}", "boxPrice": "\{boxPrice}", "packaged" : "\{ boxPrice > 0.0}", "cost": "\{(boxPrice > 0.0) ? itemPrice + boxPrice : itemPrice}" } """ ; }} Now that the project has each element of the data model, let’s investigate how to prototype the HTTP response containing the gift as JSON.

A clear control flow with pattern matching in switch expressionsAn user of the wrapup application should be able to emit different requests to send a personalized gift to someone:

```

send a postcard with a greeting for current yearcurl -X POST http://127.0.0.1:8081 \ -H 'Content-Type: application/json' \ -d '{"receiver":"Duke","sender":"Ana","celebration":"CURRENT_YEAR", "type":"NONE"}' #send a coupon and a postcard with a greeting for current year curl -X POST http://127.0.0.1:8081 \ -H 'Content-Type: application/json' \ -d '{"receiver":"Duke","sender":"Ana","celebration":"CURRENT_YEAR", "option":"COUPON", "itemPrice": "24.2"}' #send a birthday present and postcardcurl -X POST http://127.0.0.1:8081 \ -H 'Content-Type: application/json' \ -d '{"receiver":"Duke","sender":"Ana","celebration":"BIRTHDAY", "option ":"PRESENT", "itemPrice": "27.8", "boxPrice": "2.0"}' #send a happy new year postcard and an experience curl -X POST http://127.0.0.1:8081 \ -H 'Content-Type: application/json' \ -d '{"receiver":"Duke","sender":"Ana","celebration":"NEW_YEAR", "option ":"EXPERIENCE", "itemPrice": "47.5"}'

`` To support all these operations, the behaviour ofHTTPHandler` should be capable to process each of these request bodies and return an appropriate gift as JSON. Given the complexity of the POST request body, let’s represent it as a record which builds based on potential data:

package org.ammbra.advent.request;import org.ammbra.advent.surprise.Celebration;public record RequestData(String sender, String receiver, Celebration celebration, Choice choice, double itemPrice, double boxPrice) { private RequestData(Builder builder) { this(builder.sender, builder.receiver, builder.celebration, builder.choice, builder.itemPrice, builder.boxPrice); } public static class Builder { private String sender; private String receiver; private Celebration celebration; private Choice choice; private double itemPrice; private double boxPrice; public Builder sender(String sender) { this.sender = sender; return this; } public Builder receiver(String receiver) { this.receiver = receiver; return this; } public Builder celebration(Celebration celebration) { this.celebration = celebration; return this; } public Builder choice(Choice choice) { this.choice = choice; return this; } public Builder itemPrice(double itemPrice) { this.itemPrice = itemPrice; return this; } public Builder boxPrice(double boxPrice) { this.boxPrice = boxPrice; return this; } public RequestData build() throws IllegalStateException { return new RequestData(this); } }} RequestData uses an alternative constructor to pass the Builder instance to the record constructor. With this record definition, the logic inside handle(HttpExchange exchange) method refactors to:

@Overridepublic void handle(HttpExchange exchange) throws IOException { // ... // Get the request body input stream InputStream reqBody = exchange.getRequestBody(); // Read JSON from the input stream JSONObject req = RequestConverter.asJSONObject(reqBody); RequestData data = RequestConverter.fromJSON(req);// ... } Next, let’s evaluate the surprise content based on the gift option present in the request and make sure each case is treated accordingly using an exhaustive switch expression:

double price = data.itemPrice();double boxPrice = data.boxPrice();Choice choice = data.choice();Intention intention = switch (choice) { case NONE -> { Currency usd = Currency.getInstance("USD"); yield new Coupon(0.0, null, usd); } case COUPON -> { Currency usd = Currency.getInstance("USD"); LocalDate localDate = LocalDateTime.now() .plusYears(1).toLocalDate(); yield new Coupon(itemPrice, localDate, usd); } case EXPERIENCE -> { Currency eur = Currency.getInstance("EUR"); yield new Experience(itemPrice, eur); } case PRESENT -> { Currency ron = Currency.getInstance("RON"); yield new Present(itemPrice, boxPrice, ron); }}; Without a default branch, adding new Choice values will lead to compilation errors, which will make us consider how to handle those new cases.

As the gift intention is now clear, let’s process the final JSONObject response by using pattern matching for switch.

Postcard postcard = new Postcard(data.sender(), data.receiver(), data.celebration());Gift gift = new Gift(postcard, intention);JSONObject json = switch (gift) { case Gift(Postcard p1, Postcard p2) -> { String message = "You cannot send two postcards!"; throw new UnsupportedOperationException(message); } case Gift(Postcard p, Coupon c) when (c.price() == 0.0) -> p.asJSON(); case Gift(Postcard p, Coupon c) -> { String option = choice.name().toLowerCase(); yield gift.merge(option); } case Gift(Postcard p, Experience e) -> { String option = choice.name().toLowerCase(); yield gift.merge(option); } case Gift(Postcard p, Present pr) -> { String option = choice.name().toLowerCase(); yield gift.merge(option); }}; In this scenario, the switch expression uses the nested record pattern of Gift to determine the final JSON. As mentioned in the initial requirements, a sender cannot send two postcards as a gift so that operation is not supported. Another special scenario is when the sender offers only a complimentary postcard and the final gift has no associated cost. Hence, the switch expression first treats this situation in a guarded case label – case Gift(Postcard p, Coupon c) when (c.price() == 0.0) – because an unguarded pattern case label –case Gift(Postcard p, Coupon c)– dominates the guarded pattern case label with the same pattern.

Records and record patterns are great to streamline data processing, but Wrapup program needed only some of the components for further processing.

Concise code with unnamed patterns and variablesWhen a switch executes the same action for multiple cases, you can improve its readability by using unnamed pattern variables. The unnamed patterns and variables became a preview feature in JDK 21 and target to be finalized in JDK 22 (see JEP 456).

Some cases from the previous switch expression required Postcard, Coupon, Experience and Present, but never used further values from these records. After refactoring the switch with unnamed pattern variables, it becomes:

Gift gift = new Gift(postcard, intention);JSONObject json = switch (gift) { case Gift(Postcard \_, Postcard \_) -> { String message = "You cannot send two postcards!"; throw new UnsupportedOperationException(message); } case Gift(Postcard p, Coupon c) when (c.price() == 0.0) -> p.asJSON(); case Gift(\_, Coupon \_), Gift(\_, Experience \_), Gift(\_, Present \_) -> { String option = choice.name().toLowerCase(); yield gift.merge(option); }}; Now that the Wrapup implementation reached its final state, build the project and launch it again from a terminal window:

```

build the projectmvn clean verify#launch the app java -classpath target/classes:$JSON_PATH/json-20231013.jar \ --enable-preview --source 21 \ src/main/java/org/ammbra/advent/Wrapup.java

``` and issue a POST request via curl:

curl -X POST http://127.0.0.1:8081 \ -H 'Content-Type: application/json' \ -d '{"receiver":"Duke","sender":"Ana","celebration":"NEW\_YEAR", "option ":"EXPERIENCE", "itemPrice": "47.5"}' If you would like to further try the code used in this article, go to the wrapup repository.

Final thoughtsJDK 21’s preview features like string templates, unnamed patterns, variables, unnamed classes and instance main methods help you minimize the amount of repetitive and verbose code, enabling you to express intent more clearly and concisely. Use records and sealed types to model your domain and enable a powerful form of data navigation and processing with record patterns and pattern matching for switch. As the year comes to a close, I encourage to try these features to boost your productivity with Java.

The post Wrapping up the year with powerful Java language features appeared first on JVM Advent.

View Details

As Java developers, we know that the festive season is a time for joy, reflection, and perhaps a bit of well-deserved downtime away from the keyboard. But as we hang our virtual stockings by the metaphorical chimney with care, there’s one gift none of us wants to receive: a lump of coal, symbolizing security vulnerabilities in our code that could invite unwanted guests to the holiday party.

Here’s how you can ensure that your Java stocking remains coal-free and full of the season’s cheer.

Embrace the Gift of Best Practices

Santa’s list isn’t the only one you should be checking twice. Regular code reviews are like the elves’ quality control, ensuring every toy (or line of code) is up to the North Pole’s high standards. Incorporate static code analysis tools into your build process to automatically review your code for security vulnerabilities. Tools like FindBugs, PMD, or Checkmarx can be the Rudolph that guides your secure coding sleigh.

Wrap Your Code Tight with Encapsulation

Good object-oriented design is like wrapping your presents neatly — it keeps prying eyes and hands away. Encapsulation ensures internal data is not exposed where it shouldn’t be. Use private and final modifiers where appropriate to prevent unintended access or modification. It’s like keeping the wrapping paper intact until Christmas morning, ensuring no surprises are spoiled (or exploited).

Keep Your Dependencies on the Nice List

All Java projects stand on the shoulders of giants — the libraries and frameworks that make our lives easier. However, each dependency is a potential entry point for security vulnerabilities. Regularly check your dependencies for known vulnerabilities. Keeping your dependencies up to date is akin to ensuring your holiday lights are in working order, preventing a short circuit that could ruin the festive mood.

Don’t Let Your Secrets Spill Like Unattended Eggnog

Hard-coding secrets in your source code is like leaving your front door unlocked during the holiday festivities. Utilize environment variables, or better yet, a secure secrets management system like HashiCorp Vault or AWS Secrets Manager to keep your sensitive information under wraps. That way, even if someone gets a peek at your code, they won’t find the keys to the kingdom.

Be a Scrooge with Your Resources

Resource management in Java, especially when handling user input, needs to be as tight as Scrooge’s purse strings. Beware of DoS (Denial of Service) attacks by imposing limits on user input sizes, and validate inputs as if you were interrogating a suspicious Grinch. Ensure that you’re not giving any potential attackers the gift of overconsumption of your system’s resources.

Testing: The Elves’ Workshop

Unit testing, integration testing, and security testing are the elf workshops where toys are tested for durability. Use testing frameworks like JUnit or TestNG to automate your testing process. Incorporate penetration testing into your development cycle to catch any security issues that would otherwise be as obvious as a red nose on a reindeer’s face.

Silent Night, Secure Night

Logging and monitoring might not seem festive, but the silence of a secure night is golden. Logging should be like Santa’s careful notes of who’s naughty and nice — detailed enough to be useful without revealing sensitive information. Use tools like Log4j 2 responsibly (we all remember when Uncle Buck found the Log4Shell keys to the pantry), ensure you don’t log sensitive information, and monitor your application with an APM tool to detect unusual patterns that could indicate a security breach.

In the end, ensuring your Java stocking is devoid of coal comes down to a consistent practice of secure coding principles.

This holiday season, gift yourself the peace of mind that comes with knowing you’ve taken the steps to secure your applications. Here’s to a festive season filled with joy, peace, and secure code — Merry Christmas to all, and to all a good night()!

The post How Not to Get Coal in Your Java Stocking: A Developer’s Guide to Secure Coding appeared first on JVM Advent.

View Details

IntroductionFirst, let’s level-set some expectations. This definition isn’t meant for the extreme levels of quality usually associated with the software used in

avionics, implanted medical devices, nuclear power plants, heavy machinery, weapons systems, and so on. It’s meant for the other five-nines of us, writing consumer-grade systems like web or mobile apps, where, if something goes wrong, or it’s unclear and the user makes a mistake, there may be frustration on their part and embarrassment on ours, but nobody’s going to die. Those other kinds of industries already have their own approaches, often including regulations and much closer inspection than our software will ever get.

Now let’s start things off with a question. Do you like low-quality software? Presumably not! So let’s try another question. Have you written any low-quality software? I know I sure have! To those of you who said yes, congratulations! As the saying goes, Step Number One is to realize you have a problem! For the rest of you: welcome to software development; I hope you enjoy this career you’ve obviously just started.

So, we’ve got plenty of people writing low-quality software, but we don’t like it. It seems pretty clear to me: we need more software quality! But that leads us to one tiny little question: what is it?! If we don’t have a usable definition, it’s hard to improve even our own software quality, let alone the entire state of the art.

Several years ago, I was looking for a good definition, but the ones I found all had serious problems. Most were

long lists of complicated terms, full of developer jargon. Jargon is fine for talking among ourselves, but I wanted a definition that other people would understand, even non-technical people, so they could understand our challenges better, and give us more precise feedback about exactly how our software sucks.

Some definitions were

proprietary, requiring us to buy expensive tools or documents. Some were only applicable within the context of certain technologies, often also proprietary. I felt that all of that was just plain wrong. I wanted something that everybody could use, for free.

Some definitions focused exclusively on issues of interest to

us developers, ignoring the needs of the users and other stakeholders. For instance, many were completely about maintainability — which nobody else knows or cares about, at least not directly, and omitted other important things, like whether the software is easy to use. Management may know and care about the effects of poor maintainability, like changes taking longer and introducing bugs and developer headaches. However, they generally don’t know that these symptoms are caused by poor maintainability, and wouldn’t recognize poorly maintainable software if it bit them in the proverbial posterior.

Some definitions weren’t even about the software at all, but all about

the process, or the byproducts, dictating that you must hold these meetings or produce those documents. Some of these meetings and documents may be helpful, but to make them the definition misses the whole point. It’s certainly possible to do all that and yet produce horrible software, or to produce great software without them. I wanted something more flexible, and more focused on the software itself.

I didn’t see any that I liked, nor that were commonly accepted, so in the spirit of XKCD,

I decided to make my own. To keep it simple, I zoomed out from down in the weeds, where we developers tend to live, past the 40,000-foot view, up to about low earth orbit, so I could look at continents, not pebbles. That let me trim it down to just six aspects, with simple names and relatively simple explanations. The result is so short, it literally fits easily on the back of my business card.

The Big RevealI call this list of aspects ACRUMEN, but what does that mean? Originally, it was a Latin word, meaning sour fruit, like grapefruit, limes, and lemons. But what is it in this context? The acronym ACRUMEN (try saying that ten times fast!), simply takes those six aspects, and puts them in priority order. By now you’re probably wondering, SO WHAT ARE THE @#$%^& ASPECTS ALREADY?! They are that software should be Appropriate, Correct, Robust, Usable, Maintainable, and E*fficient. But what does all that mean?!

First and foremost, it needs to be doing what the stakeholders need it to do, in other words, do the right job. Then it needs to be doing that job correctly, or in other words, do the job right. It should be hard for anyone to make it malfunction, which is mainly about being insecure or fragile, or even seem so, and we’ll get much deeper into that later. However, it should be easy for the users to use and for the developers to change. (The other way round, not so much. Generally, you don’t want your users changing what your software does, and if we find our own software easy to use, but we’re not the intended users, then what good is that?) Last, dead last despite how we developers tend to worship this, it should be easy on resources, not only the technical ones that we usually think of, but other kinds as well, and again we’ll get deeper into that later.

To put that all together in one easily grabbable chunk:

| Appropriate | : doing the right job | | Correct | : doing the job right | | Robust | : hard to make it malfunction, or seem to | | Usable | : easy for the users to use | | Maintainable | : easy for the developers to change | | Efficient | : easy on resources |

Now, I’ve said a few times that it consists of six aspects, and I’ve told you about six, but ACRUMEN has seven letters! So, what does the N stand for? Nnnnnnothing, I just tacked it on to make a real word, even if an obsolete one.

FAQsWhile the basic definition is fresh in your minds, I’ll address a few frequently asked questions.

Aside from going into detail on the tips, how do we actually use ACRUMEN itself, the list?

Mainly, we can keep it in mind as a checklist when writing or evaluating software. We can ask, is it appropriate, is it correct, and so on, or how good is it in each aspect, on a scale of 1 to 10, or by simple triage, or is it good enough for our needs? And if the answer is ever that it’s not good enough, we can ask what can be done to

make it so? In the more immediate term, we can ensure that our current projects are likely to meet these criteria. In the longer term, we can ensure that our processes support these criteria, by including various helpful activities and requirements, and maybe even an explicit evaluation against the ACRUMEN aspects. We can also set

targets, for how good we need it to be in each aspect.

How can we quantify this, and boil it down to one number that shows the quality of a piece of software?

Mainly, I advise that you don’t do that! Instead, at the very least, keep six numbers, one for each aspect. Otherwise, you lose too much valuable information. A single number might tell you that the software is good or bad, but a set of six numbers will tell you how. For instance, with a chart like this:

| Aspect | Score (out of 10) | | --- | --- | | Appropriateness | 8 | | Correctness | 10 | | Robustness | 7 | | Usability | 3 | | Maintainability | 7 | | Efficiency | 4 |

we’re probably talking about a program that does most of what’s needed, does it absolutely correctly, but not very efficiently, I would bet slowly, impacting the usability. It’s also fairly robust and maintainable, but could still use some improvement there too. These numbers can help prioritize further work on it.

Is ACRUMEN, or rather ACRUME, always the right ordering? Some projects seems a little different.

No, ACRUMEN is just the typical case. Your mileage may well vary. Consider the case of a company-internal command-line physics simulation tool, using a standard algorithm that will never change. It needs to do the right job, may need to be very efficient, but maybe we can make do with a rough approximation, rather than a precisely correct number that would take much longer to calculate. It might not need to be so usable because it’s for ourselves, not customers, nor so robust because of the limited interfaces and fewer things to go wrong, nor so maintainable because the logic is never going to change. So, its list may well look more like AECURM, rather than ACRUME.

The only real constant is that appropriate will always be at the top. We’ll see shortly why because now we’re going to look at each aspect in more detail, and up first is of course:

AppropriatenessIf our software doesn’t have this, then Nothing Else Matters. If our software is doing the wrong job, then it doesn’t matter how well it’s doing the wrong job. So, appropriateness is not only more important than any other aspect, it’s even more important than all the others put together! And yet, we developers are generally not taught that this is even a thing, let alone one that we need to think about.

To prove the importance of being appropriate, let’s try a little thought experiment. Suppose you want a program to play

checkers, and I write for you the world’s greatest chess playing program. It’s as correct, robust, usable, maintainable, and efficient as anyone could ever want. But will you be happy with it? Probably not. But why not, if it’s such a great program? Because it’s not checkers! It’s not what you asked for. It’s not what you need. Or in ACRUMEN terms, it’s not appropriate.

So, now that we know how important this is, how do we achieve it? In an ideal world, we would have

frequent direct contact with the stakeholders. Ideally face to face, or as close to that as possible. We can ask what they want, and break it down into smaller and smaller pieces. (Developers should be good at that, it’s how programming basically works!) But, we should go a step further, and ask why they want things. This will help reveal what they really need, which is what we really need to satisfy — as opposed to what they say they want, which makes it two steps removed from there.

Unfortunately, we don’t usually get that opportunity. Second best is to bring in the experts, which in this case would be Requirements Analysts. But we usually don’t get those either, at least outside huge companies. So, we usually have to settle for occasional remote or indirect contact with at least a representative of some stakeholders, like a Product Owner in Scrum. It doesn’t work quite as well, but having some communication with someone with a clue, is vital.

Once we think we have a good grasp of their needs, we can show them

mockups and prototypes of what we intend to do, and demos of what we have done. This gives them a chance to correct our wrong ideas of their needs, before we go too far down the wrong rabbit-hole. I think we’ve all been there, wasting time implementing the wrong thing. Ideally, show them these frequently, as a sort of continuous course correction. Frequent feedback from the stakeholders is even more important than being able to ask them questions.

There’s another thing, though, that I’ll be returning to over and over in this talk. We can propose

tests! In particular, I recommend the Given/When/Then pattern:

  • Given these preconditions, such as data being in a certain state;
  • When this happens, usually some kind of input from users, or a timer, or a sensor, or another system over a queue or an API;
  • Then this is the result, usually either something the user sees, or data being in a desired new state.

This makes a great link between the worlds of business and tech because the business people can understand it, and we can turn it into a runnable test.

CorrectnessIf our software doesn’t have this, then, it has bugs. It could be giving obviously incorrect results, or worse yet, subtly incorrect, so we don’t notice so soon. It could be putting data, whether correct or not, in the wrong records or files, or even deleting them!

Nothing can actually stop us from writing code that isn’t correct, at least with the decently productive tools we have today. So, the big question is: just like the

Thermos (or in British English, the Dewar Flask) that keeps hot things hot and cold things cold, how do we know? I mentioned the answer just a moment ago: tests prove whether our code is correct — assuming of course that the tests themselves are correct. (Actually, even then, it’s not quite true, but I’ll get to that shortly.)

I’ll skip over a lot of the advice about how many of what kinds of tests to write, and how, as you can find that in a bazillion other articles, blog posts, videos, books, and so on. But, I will point out that typical types of tests, like end-to-end/system, feature, integration, unit, and so on, can only prove the correctness of cases that we thought to test. There are some advanced techniques, though, that can help find unusual cases we didn’t think of.

Property-based testing tests whether some desired property of our code, what formal computer scientists would call an “invariant”, holds true for all valid inputs. A property testing tool makes up lots of random test data to try, somewhat like the security concept of “fuzzing”, but staying within defined bounds of validity, rather than trying to find and exceed them. If it finds an input that makes our property fail, that means that there is an edge case that we didn’t consider.

Mutation testing runs our tests against slightly altered versions of our code. Each altered version should make at least one test fail. If not, that means that our code isn’t “meaningful” enough for the mutation to make a difference in its behavior, such as if it’s redundant or unreachable, or our tests aren’t strict enough to catch the difference the mutation made, or maybe both. (I also speak on mutation testing at conferences, and you can check out my Youtube playlist of versions of that talk.)

We should have enough test coverage, of assorted kinds and levels, and verified to actually test our code rather than game a metric, to have strong confidence in the correctness of our code.

RobustnessIf our software doesn’t have this, then, at best, it may simply show a lot of error messages, and seem fragile and unreliable, or it may crash a lot and actually be fragile and unreliable. It may even get hacked because Robustness includes Security.

The short explanation is that it’s hard to make the software malfunction (or even seem to), but what does that even mean?! There are a few other things, but most of what I mean is covered by a core concept of information security:

the CIA Triad. No, it’s nothing to do with spies and gangsters, it’s this triangle up here, of Confidentiality, Integrity, and Availability. So, robust software does not reveal data when it’s not supposed to, alter data when it’s not supposed to, or become unavailable when it’s not supposed to, even when an attacker is trying to force it to violate them.

So, how do we achieve all that?
Once again, we could bring in the experts, and in this case, that would be…

penetration testers, or for short, pen testers. (You can see why I couldn’t resist using that image!) The good news is, you don’t have to work for a huge company to use them. Many work for independent computer security companies, that you can hire on contract. However, they are usually expensive, and disruptive because they need to test the production system.

So, once again, we’ll usually have to do without the experts, but, we can use some of their tools, especially software such as static analyzers (which simulate the execution of our program), fuzzers (which test our program’s reactions to various kinds of invalid inputs, in the “fuzzing” technique I mentioned earlier), and probes (which test our system for vulnerability to specific known attacks). Many of these are available as open source.

Even without their software, we can still get a long way by using their mindset. The main part of that is to ask ourselves what could go wrong. Here the tone of voice is critical, it’s not “What could go wrong?”, as though we think nothing could, but almost statement-like, “What could go wrong.”, as if to say, “I know a lot could go wrong, I’m trying to list it, I don’t need a demonstration thankyouverymuch, God!”

For instance, if the system wants the user to type a filename, the user could type it wrong, or type correctly the name of a file they don’t have access to, and so on. The program should not crash, or show a mysterious error message like “ENOENT” or “HTTP 500”, but instead show a clear and friendly error message, and let the user try again.

There may even be external factors, like losing a network connection, or other hardware problems. Our software should handle all reasonably foreseeable types of problems as gracefully as practical.

That may sound like a lot, but so far, we’ve only covered innocent mistakes and mishaps. To make it really robust, we must make it secure, which means we must think like an attacker. We must ask ourselves, what are the system’s weak points? What can attackers make happen, that would get them one step closer to their goal? In what unusual ways can someone get information out of – or into – our system?

Once we’ve brainstormed and run out of answers to such questions, then for everything we’ve come up with, we must somehow handle it. Yes, that’s extremely vague, but how to handle something is going to vary immensely, depending on exactly what it is. If the user types a bad filename, ask for another! But if the system detects an attack in progress and data may be getting corrupted, the proper response may be to shut the whole thing down, and not bring it back until someone goes into the data center and presses the big green button! In-between, there are many possibilities. Perhaps we can prevent the situation, mitigate the negative effects, or recover from them, perhaps with the help of insurance. But whatever response we decide on, we must test it, as it is now an important part of our system.

Our next aspect is one often seen as a tradeoff with security:

UsabilityIf our software doesn’t have this, our users will become frustrated, and may stop using or recommending our software. That could be disastrous for a software vendor, or a software-as-a-service company! Also, hard-to-use software can lead the user to do the wrong thing. Remember what happened in Hawai’i in January 2018, due to software that was hard to use? They had a false alarm about an incoming nuclear missile! Just think what could happen if that were the launch system, not just an alarm!

Unfortunately, if we Google software usability, we find mostly things about ensuring that users with various challenges can use our software about as well as the rest of us. In other words, accessibility. That’s a good goal in itself, but I’m adding on that it should be easy for everyone to use, not just equally difficult!

To go into more depth: it should be clear at all times what the user can do, should do, and must do, how they can do it, and what else the software can do, especially any help facilities. And, all of that should be easy to do, despite any challenges the user may be facing.

We can start with the things that accessibility usually addresses, like lack of vision, color vision, hearing, fine motor control, and so on. But there are other whole types of challenges we should be aware of, like lack of literacy, at least in our character set. The user may lack certain knowledge, such as culture references, like the significance of traffic light colors. They may even be of low intelligence! Yes, we may joke about stupid users, but statistically, about half of them will be below average.

Also, again, there may be external factors, like a noisy or shaky environment. Imagine someone uses your mobile app on a small phone, while standing up on a crowded bus in downtown rush-hour traffic! I don’t know what that’s like where you live, but at least in Washington DC, accurate tapping is Not Happening.

Another often-overlooked part of usability is that all software should be usable, whether it’s a web app, a mobile app, a desktop GUI app, or a command-line app — or an API, be it through function calls like with a library or framework, or a wire protocol, whether binary or textual, or whatever.

So, how do we achieve all this? Once again, ideally we can bring in the experts. The bad news is, the people called Usability Experts are mostly really about accessibility. The good news is, we have a wide range of other professions we can get help from! We mainly want a User Experience expert, or at least a User Interface expert. But even a web designer, or even an old-fashioned print graphic designer, has training in principles of practical visual design that can help us, at least in that aspect of usability. However, as usual, we’ll often have to do without any help, but we can go a long way by applying the principles of these experts. For instance:

here we see an illustration of the KISS Principle, meaning “Keep It Simple, Stupid!” (Or if we don’t want to be so negative, “Keep It Super-Simple”.) Note the simplicity of these stereotypical apps from two highly successful companies with reputations for simple ease of use, compared to the cluttered unusable mess from “your company”. I think many of us, even the front-enders who are usually expected to do better at visual design than back-enders like myself, will recognize some of our own work in that.

Another thing we can do, if the software is something we can use ourselves, is to

“eat our own dog food”. But remember, if we find our own software easy to use, that does not mean that our users will! We have inside knowledge, that makes it much easier. But if there’s anything we find difficult or unclear, it will be much worse for our users. So, dog food it mainly to find the pain points.

Lastly, it may not be as definable and quantifiable as correctness, but a user interface can still be

tested! We can bring in some of our typical users, even ones that don’t already know our system, and have them try to do common tasks. We can watch them use it (which is what’s going on in the photo above), and look for signs, on their faces and screens, of confusion or frustration, or if we’re lucky, satisfaction or happiness. Afterward, ask them what they found hard or easy, unclear or obvious? Then fix their pain points, do more of the good parts, and lather, rinse, repeat.

The next aspect is the one we usually think of most:

MaintainabilityIf our software doesn’t have this, then changes take longer, and are more likely to introduce bugs, and developer headaches. Delays could make the company miss opportunities. Bugs damage the company’s reputation. Developer headaches are bad enough for us, but for the company, they could make key personnel quit in frustration. I’d bet most of us have been there, as either the quitter or a survivor who had to pick up the slack.

We’d probably all agree that the basic concept is that “maintainable” software is easy to change. (Thank you, Captain Obvious!) But I’m going to add that it’s easy to change, with low chance of error, and low fear of error, even for a novice programmer, who is also new to our project.

So how do we achieve all this? For better or worse, the vast majority of software engineering advice is aimed squarely at this. So, rather than expound on countless generic principles like good naming, or the Single Responsibility Principle, or low coupling and high cohesion, I’m going to stick to my theme and tell you how testing can help with maintainability.

Some of you may already usually use tests as a sort of documentation of how the code should be used. But old tests, like the ones we wrote to verify any prior changes we made, like adding a feature or fixing a bug, can be useful in other ways. They form a regression test suite, to catch anything we break that used to work. Just knowing that that is there, will reduce our fear of error, like a safety net. And that will allow us to progress at a quick pace with a clear and focused mind, rather than creeping along slowly because we’re terrified of breaking something accidentally and not discovering it until users complain. And that is why I mentioned fear at all.

There are also numerous tools we can use, like linters, complexity analyzers, and just cranking up the warnings on our compilers or interpreters. These will give us plenty of hints how to improve our code, mainly in its maintainability, and occasionally uncovering subtle bugs. It’s astonishing how many nasty bugs you can catch, just by cranking up the warnings!

EfficiencyIf we don’t have this, then our programs may run slowly, or make the users buy more resources. They could clog the network, or even crash machines by running out of memory or disk space, or drain other resources. Mainly we know about technical resources, but there are others, such as the user’s patience and brainpower, and the company’s money!

So, how do we achieve efficiency? Just as there are many kinds of resources, there are many different kinds of inefficiency we could fix, but for this discussion I’m going to focus on fixing the most obvious and common kind: slowness.

I’m sure we’ve all had a program run slowly, then we stare at the code, spot where we think it’s inefficient, spend a long time optimizing that little piece, run the program again, and… it’s still slow! So … don’t do that!

Measure it instead! Humans aren’t really good at spotting the inefficiencies, but there are profilers and packet capture programs and such, that will tell us exactly where, or at least when, we’re using too much CPU, RAM, bandwidth, etc.

Once we’ve found where or when it’s slow, though, there’s still the question of why it’s slow? Certain kinds of programs tend to have certain problems. For instance, a distributed system may be doing too much communication, or using a slow network. A database-driven system may have an inefficient query or data model. But in the general case, usually the problem is either something architectural, which is more complex than I want to get into right now, or a bad algorithm. Maybe we’re using something with a polynomial or exponential runtime, when thinking about the problem a little differently could let us use a better algorithm, such as one with linear, root or logarithmic, or could be even constant runtime. Perhaps we’re using a bad data structure, and that is forcing us to use a bad algorithm.

The upshot is that we should be familiar with the basic common data structures and algorithms, and how to recognize them when we see them in real-world problems, analyze and compare their demands on our assorted resources, and choose and change and combine them. That way, we can use solutions that have stood the test of time, sometimes with ready-made implementations that are well tested and maybe even optimized. Once it’s fast enough, we can slap a performance test around it (you knew I had to mention writing some kind of test eventually!), to ensure we don’t have that kind of regression.

In conclusion, if we make sure that our software is Appropriate, Correct, Robust, Usable, Maintainable, and Efficient, then Nobody should have any cause to be sour about the fruits of our labors.

The post ACRUMEN: What is “software quality” anyway?! appeared first on JVM Advent.

View Details

Databases are no message queues is a well-established claim that has been discussed in many blog postings and conference presentations. But with advancements in relational databases, does this claim still stand up to scrutiny? Looking at modern versions of Postgres, the answer is often no. Therefore, this article looks into Postgres’ lightweight notification mechanism and discusses how it can be leveraged to implement a simple, but effective push-based message queue. It also looks into using this queue for communicating among replicas on a Kubernetes deployment, and into implementing a generic task processing framework.

Postgres as a message queuePostgres is, of course, a relational database that implements a large fraction of the SQL standard. But beyond that, Postgres implements many other, non-standardized features that can also be executed via its extension upon SQL. One such feature is the LISTEN and NOTIFY mechanism which allows for sending asynchronous messages across database connections. And of course these commands can be issued via JDBC. For a simple hello-world example, consider a JVM to listen on a given hello_world_channel:

try (Connection conn = getConnection()) { try (Statement stmt = conn.createStatement()) { stmt.execute(“LISTEN hello\_world\_channel"); } PGNotification[] notifications = conn .unwrap(PgConnection.class) .getNotifications(0); System.out.println( "Hello " + notifications[0].getParameter() + "!");} To receive notifications, one needs to specify the name of a channel to LISTEN to. The name of the channel can be chosen arbitrarily. To receive notifications, one needs to unwrap the connection to the Postgres JDBC driver’s PgConnection. From there, received notifications can be read with a timeout, or 0 if one wants to wait indefinitely. A second JVM can now send a notification using a similarly simple setup:

try ( Connection conn = getConnection(); Statement stmt = conn.createStatement()) { stmt.execute("NOTIFY hello\_world\_channel, ‘World’");} which will cause the first JVM to print Hello World!.

Defining triggers to create a simple message queueOften, a notification is not sent directly, but via a trigger on a table. For example, to implement the mentioned message queue, one could start basis in a simple table as:

CREATE TABLE MY\_MESSAGES ( RECEIVER VARCHAR(200), ID SERIAL, PAYLOAD JSON, PROCESSED BOOLEAN); To fire a notification whenever a message is inserted into the table, a function such as the following implements this in Postgres’ procedural language pgSQL without altering the inserted row:

CREATE FUNCTION MY\_MESSAGES\_FCT()RETURNS TRIGGER AS$BODY$BEGIN PERFORM pg\_notify(‘my\_message\_queue’, NEW.RECEIVER); RETURN NEW;END;$BODY$LANGUAGE PLPGSQL; In the above function, the pg_notify function is invoked, which simply triggers a NOTIFY with the second argument as a payload but avoids possible SQL injection which could occur with string concatenation. This function can now be installed as a trigger on any insertions in MY_MESSAGES:

CREATE TRIGGER MY\_MESSAGES\_TRGAFTER INSERT ON MY\_MESSAGESFOR EACH ROWEXECUTE PROCEDURE MY\_MESSAGES\_FCT(); This way, one or several listeners can be notified on the arrival of new messages, for example as replicas within a Kubernetes deployment.

Postgres notifications and connection poolingOne caveat with Postgres’ notification mechanism is that it typically requires the creation of a dedicated Connection for receiving notifications. This is due to the connection being used for sending notifications back via the channel that the JDBC client established when opening a connection and executing the LISTEN statement. This requires that the connection is long-lived, which does not normally play well with pooled DataSources. Instead, one should create a dedicated Connection via the DriverManager API.

Note that this also occupies a full connection on the Postgres server where connections typically are pooled, as well. For this reason, a Postgres server might start rejecting new connection attempts if too many JVMs already occupy a dedicated connection for listening for notifications. It might therefore become necessary to increase the maximum number of allowed concurrent connections in the Postgres server instance. As connections for receiving notifications do often run idle and require few machine resources, this is not normally a consequential change. Quite the opposite, if the listening for notifications can substitute frequent polling against the database, this approach might even free resources.

With this downside, the approach of Postgres does also bring a less obvious upside. With Oracle, for example, the database does not require a dedicated connection. However, this requires that the database can actively call the notified application on a given host and port. This might not always be possible, for example on Kubernetes when multiple replicas share a common host.

Using Spring integration’s JDBC message queue on PostgresThis functionality will be available in Spring integration with imminent arrival of version six. Spring integration does already offer a JDBC-based queue implementation. But as of today, it only offers polling messages, or to receive push messages when operating on the same queue object within a single JVM. By defining a trigger, similarly to the one above, as suggested in Spring integration’s schema-postgres.sql file, Spring integration allows for receiving messages that are sent via a regular JdbcChannelMessageStore.

The message allows to send a message with any serializable payload to a given channel as follows:

JdbcChannelMessageStore messageStore = new JdbcChannelMessageStore(getDataSource());messageStore.setChannelMessageStoreQueryProvider( new PostgresChannelMessageStoreQueryProvider());messageStore.addMessageToGroup( “some-channel”, new GenericMessage<>(“World”); which Spring integration 6 now allows to receive via push notification from any other connected JVM via:

PostgresChannelMessageTableSubscriber subscriber = new PostgresChannelMessageTableSubscriber(() -> DriverManager.getConnection( getJdbcUrl(), getUsername(), getPassword()).unwrap(PgConnection.class);subscriber.start()PostgresSubscribableChannel channel = new PostgresSubscribableChannel( messageStore, "some-channel", subscriber);channel.subscribe(message -> System.out.println( “Hello “ + message.getPayload() + “!”); Before, inter-JVM communication like this was previously only possible by polling the channel for new messages while the above mechanism allows for quasi-instant communication among different VMs. When creating a multi-node application that already uses Postgres, this can be used as an easy way to communicate between VMs. For example, one could use Spring integration’s LockRegistryLeaderInitiator to determine a node that executes unshared work. If multiple nodes can receive an HTTP message that is meant for this leader node to process, those nodes can now forward this call via a JDBC message store which notifies the leader instantaneously. This can be achieved with only a few lines of code and without a need to expand the technical stack to additional technology such as Zookeeper.

Implementing a generic task processor with push notifications to workersFor a real-world example of inter-JVM communication using Postgres, the Norwegian tax authority offers a thin library for generic task processing using the database’s notification API. If a batch of new tasks is created, multiple worker nodes are notified of the additional work and wake up to poll for new messages. This work will continue until no additional tasks are available when the workers will get back to sleep.

This shows another strength of the notification mechanism where it allows any amount of listeners to a given channel to be notified simultaneously and without preallocating table rows to a given node. Thanks to Postgres’ multiversion concurrency control, this allocation can be decided upon selecting from a database where each node can acquire row locks to determine its tasks from a table, without the need for a separate allocation implementation within a possible alternative notification framework. All this makes Postgres a good choice for using the database as a queue, especially if Postgres already is part of the technological stack.

The post Using Postgres as a Message Queue appeared first on JVM Advent.

View Details

When many Java developers hear the word WebAssembly, the first thing they think is “browser technology”. The second thing: “it’s the JVM all over again”. After all, for a Java developer, in-browser apps are prehistory.

In the last few weeks, there have been quite a few announcements around WebAssembly, such as the Docker+Wasm Technical Preview. As a Java geek myself, I think we should not dismiss this technology as just a fad.

Indeed, WebAssembly is “a bytecode for the Web” (I mean, that’s the name after all), but the similarities between Java and Wasm (lower-cased: it’s a contraction, not an acronym!) really end here.

If you want to know more about how we came to define the WebAssembly standard, you can learn more about its history on my own blog. In the following, I will try to argue that there is more to WebAssembly than “just the web”.

First of all, a WebAssembly runtime is only shallowly similar to a JVM. For instance, WebAssembly was always meant to be a proper compilation target for different programming languages, while the JVM was not, at least, not originally.

Myth #1: The JVM Is A Polyglot Compilation TargetOf course, everyone knows the JVM is one of richest, interoperable language ecosystems there is. We don’t have just Java, we also have Scala, Jython, JRuby, Clojure, Groovy, Kotlin and many many others.

However, the sad, sad reality is that Java bytecode was never really meant to be a general-purpose compilation target. In fact, you can even find literary references that spell that out clearly; in “Bytecodes meet combinators: invokedynamic on the JVM”, John Rose writes (bold mine):

The Java Virtual Machine (JVM) has been widely adopted in part because of its classfile format, which is portable, compact, modular, verifiable, and reasonably easy to work with. However, it was designed for just one language—Java— and so when it is used to express programs in other source languages, there are often “pain points” which retard both development and execution.

The paper describes how and why the invokedynamic opcode was introduced in the JVM; in fact, it was specifically introduced to support dynamic languages targeting the JVM as a runtime. At the time, those were many: JRuby, Jython, Groovy, etc… This opcode was not added because the JVM was supposed to support such languages; but because people were doing it anyway: so, it was better just to acknowledge it!

In other words, the JVM, as it was at the time, was not an adequate compilation target for dynamic languages. We may even argue that the JVM became a compiler target not because it was the best compilation target, but because people wanted to interoperate with it because of adoption and support …just like JavaScript!

GraalVM: One VM to Rule Them AllThe GraalVM project has recently gone mainstream. This project includes a Just-in-Time compiler targeting regular Java bytecode, an API to build efficient language interpreters, and, recently, a native image compiler.

One of the original goals for GraalVM was to be “One VM to rule them all”, i.e. to be a polyglot runtime.

But Truffle does not define a polyglot compilation target. Instead, the Truffle API allows you to build an efficient, JITting interpreter for dynamic programming languages using a very high-level representation (an AST-based interpreter, if you are interested).

Note for the nitpicker. Now, once you enter the programming-language-rabbit-hole everything gets kind of “meta”. Indeed, with Truffle you can write a JITting interpreter for some other “proper” bytecode format.

In fact, there is a Truffle-based interpreter for LLVM (Sulong); and, sure, LLVM bitcode is meant to be a multi-platform/multi-target compilation target. So, by the transitive property, you may argue that GraalVM/Truffle do support a multi-platform compilation target.

This is technically correct (which is the best kind of correct), but there are many considerations to be made, and there is not enough space here to discuss them all. In short, LLVM bitcode is meant to be a compilation target, but it was not necessarily meant to be a cross-platform runtime language (e.g., there are slight variations in the instructions you may have to use, depending on the CPU/OS you want to target). Moreover, as opposed to WebAssembly, which is a multi-vendor standard, GraalVM and Truffle are, to this day, open source, community-driven, but single-implementation efforts (work has recently started to bring it to the OpenJDK and possibly to the Java Language Specification).

Ultimately, WebAssembly is also another language that GraalVM/Truffle is able to support, so if you want to use GraalVM, you might even target Wasm!

Myth #2: It’s Just Another Stack-based Language VMWebAssembly is defined as a virtual instruction set architecture (ISA) for a structured stack-based virtual machine.

The word structured here is key, because it is a very significant departure from the way, say, the JVM works. In practice, in a structured stack machine most computations use a stack of values, but control flow is expressed in structured constructs such as blocks, ifs, and loops. Moreover, in the WebAssembly language, some instructions can be represented both as “simple” and as “nested”.

Let’s see an example. In the stack-based Wasm machine the expression:

( x + 2 ) * 3

int exp(int); Code: 0: iload\_1 1: iconst\_2 2: iadd 3: iconst\_3 4: imul 5: ireturn Could be translated in the following sequence of instructions:

(local.get $x) (i32.const 2) i32.add (i32.const 3) i32.mul * local.get puts the value of the local variable $x on the stack * then the i32.const pushes the 32-bit integer (i32) constant 2 on the stack * i32.add pops the two values from the stack, and push the result $x+2 on the stack * we then push the integer constant 3 * i32.mul pops the two integer values and pushes the i32 result of the multiplication (($x+2)*3)

You may have noticed how instructions that take at least one argument are parenthesized. The one we just saw is the “linearized” version of WebAssembly. It is the one that is straightforwardly translated into its binary representation in a .wasm file. There is however another, semantically equivalent “nested” representation:

(i32.mul (i32.add (local.get $x) (i32.const 2)) (i32.const 3)) The nested representation is particularly interesting because it shows a peculiar difference with other types of bytecodes (such the JVM’s), i.e. operations nest and read like operations in a more conventional programming language. Well, for some definition of conventional: it reads like Scheme (a language in the family of LISPs), and the convention for parenthesization is a clear homage to it. Of course, this is not by accident; if you know a bit about JavaScript’s evil origin story you’ll definitely know that it was originally written in 10 days; and you may also know that Brendan Eich initially was hired to develop a Scheme dialect.

However, the even more interesting detail (at least to me) is that the nested sequence naturally linearizes to the other version; in fact, if you follow the precedence rule for parenthesized expressions, you have to start at the innermost parentheses:

(i32.add (local.get $x) (i32.const 2)) so first you get $x, then you evaluate the constant to 2, then you sum them; then you continue with the outermost expression:

(i32.mul (i32.add ...) (i32.const 3)) Now you have evaluated the contained i32.add, you evaluate the constant 3 and you can multiply them. That’s exactly the same order of evaluation of the stack-based version!

We have also mentioned structured control flow. The reason for this choice is, again, safety; but also simplicity:

The WebAssembly stack machine is restricted to structured control flow and structured use of the stack. This greatly simplifies one-pass verification, avoiding a fixpoint computation like that of other stack machines such as the Java Virtual Machine (prior to stack maps). This also simplifies compilation and manipulation of WebAssembly code by other tools.

Let’s see an example:

void print(boolean x) { if (x) { System.out.println(1); } else { System.out.println(0); }} This translates to the bytecode:

void print(boolean); Code: 0: iload\_1 1: ifeq 14 4: getstatic #7 // java/lang/System.out:Ljava/io/PrintStream; 7: iconst\_1 8: invokevirtual #13 // java/io/PrintStream.println:(I)V11: goto 2114: getstatic #7 // java/lang/System.out:Ljava/io/PrintStream;17: iconst\_018: invokevirtual #13 // java/io/PrintStream.println:(I)V21: return You will notice the unstructured jump instructions ifeq and goto which are missing from the equivalent WebAssembly definition, replaced instead by proper if...then...else blocks!

(module ;; import the browser console object, ;; you'll need to pass this in from JavaScript (import "console" "log" (func $log (param i32))) (func ;; change to positive number (true) ;; if you want to run the if block (i32.const 0) (call 0)) (func (param i32) local.get 0 (if (then i32.const 1 call $log ;; should log '1' ) (else i32.const 0 call $log ;; should log '0' ))) (start 1) ;; run the first function automatically) You can see and play with the original example on the Mozilla Developer Network

Obviously, this also linearizes to a non-nested version:

(module (type (;0;) (func (param i32))) (type (;1;) (func)) (import "console" "log" (func (;0;) (type 0))) (func (;1;) (type 1) i32.const 1 call 0) (func (;2;) (type 0) (param i32) local.get 0 if ;; label = @1 i32.const 1 call 0 else i32.const 0 call 0 end) (start 1)) More Differences: Memory ManagementFor better or worse, another area where WebAssembly virtual machines greatly differ from a JVM is memory management. As you probably know, Java languages do not require you to allocate and deallocate memory, or care about stack vs. heap allocations; at least in general: you may care about those and there are ways to deal with them explicitly if you really need to. But the reality is that most people won’t.

This is not a language-level feature, it is really also how the VM works. You do not have primitives to deal with memory at the VM-level; in fact, primitives for heap allocation are available, but they are exposed as JDK APIs. There is no way for you to opt out of managed memory: you cannot just say “I don’t care about the garbage collected heap, I am going to do my own memory management”.

At this time, WebAssembly is quite the opposite. It is no coincidence that most languages targeting WebAssembly today really manage their own memory. Some languages do garbage collection; but in those cases, they have to roll their own garbage collection routines, because the VM does not provide such a facility.

Instead, with WebAssembly you get a slice of linear memory, and then you can do whatever you want with it. Allocate, deallocate; even move it around if you’d like. While this is, in a way, more powerful than what the JVM provides, it also comes with caveats.

For instance, the JVM does not require you to specify the memory layout of an object, because it is up to the VM to deal with structure packing, word alignment, etc. In the case of WebAssembly, you deal with those issues.

On the one hand, this makes it perfect as a target for manually-managed programming languages, where a higher degree of control is expected and desired. On the other hand, it could make it harder for such languages to interoperate with each other.

Now, structure and object layout is an ABI concern: a thing of the past for JVM developers, except for some very limited and notable exceptions.

Interestingly enough, the draft GC spec for WebAssembly has recently moved forwards, and it does not just deal with garbage collection, but it effectively describes how to deal with structures, and how to make them interoperate, regardless of the originating language. So, while this is still not ready, things are continuously evolving and multiple concerns are being addressed.

More Than WebNow, in all we have learned so far, you might have noticed that I never mentioned the word Web once.

Indeed, it took me a while to get to the point, but this is where I tell you, the Java Geek, why you should care.

Even if you do not care about front-end, you should not dismiss WebAssembly as a purely front-end technology. There is nothing in the design and specification of WebAssembly that makes it specifically tied to the front-end. In fact, most mainstream JavaScript runtimes are now able to load and link WebAssembly binaries, even outside the browser; so you can run a Wasm executable in a Node.js runtime, with a thin layer of JS glue code to interact with the rest of the platform.

But there are also many pure-WebAssembly runtimes, such as Wasmtime, WasmEdge, wasmCloud, Wazero that are completely untied from a JavaScript host. These runtimes are usually lighter-weight than a full-blown JavaScript engine, and they are often easy to embed inside larger projects.

In fact, many projects are starting to embrace WebAssembly as a polyglot platform to host extensions and plug-ins.

One notable example, for instance, is the Envoy proxy: the codebase is mostly C++; it does support plug-ins, but with the same caveats as browser plug-ins: you have to compile them, you have to ship them, they may not run at the right level of privileges, they may even tear down the entire process in case of a fatal fault. Now, you could embed a Lua or a JS interpreter and let your users script their way to success: the interpreter is safer because it is isolated from your main business logic, and it only interacts in a safe way with the host environment; the main downside: you have to pick a language for your users.

Or, you could just embed a WebAssembly runtime, let your users pick their own language and just compile it to Wasm. You will have the same safety guarantees, and happier users.

These pure WebAssembly runtimes are not just for extensions. Many projects are creating thin layers of Wasm-native APIs to provide stand-alone platforms.

For instance Fastly has developed a platform for serverless computing at the edge, where the serverless functions are implemented by user-provided WebAssembly executables.

Fermyon is a startup that is developing a rich ecosystem of tooling and Web-based APIs to write Web apps using only Wasm. One of their latest announcement is their Fermyon Cloud offering.

These solutions offer custom, ad-hoc APIs for specific use cases; and this is indeed one way to use WebAssembly. But that is not the end of it. In 2019, Docker founder Solomon Hykes wrote:

If WASM+WASI existed in 2008, we wouldn’t have needed to created Docker. That’s how important it is. Webassembly on the server is the future of computing. A standardized system interface was the missing link. Let’s hope WASI is up to the task! https://t.co/wnXQg4kwa4

— Solomon Hykes (@solomonstre) March 27, 2019

https://platform.twitter.com/widgets.js

If you pull this out of context your first question may be “What the hell has Wasm to do with Docker?” and, of course, “What the hell is WASI?”.

WASI is the WebAssembly System Interface. You can think of it as of a collection of (POSIX-like) APIs that allow a Wasm runtime to interact with the operating system. Is this like the JDK Class Library? Not quite. It is a thin layer of capability-oriented APIs for interaction with the operating system. You can read more on the Mozilla announcement blog., but, in short, this is the last piece of the puzzle: WASI allows to define backend applications that directly interact with the operating system without any extra layer, and without ad-hoc APIs. The current effort is to make WASI widely-adopted and, in a way, a standard de facto for backend development.

WASI APIs include things like file system access, networking and even threading APIs. These APIs work hand-in-hand with the lower-level capabilities of the runtime, enabling easier ports to the platform.

Porting JavaWith all its challenges, for the first time, we have a technology with the potential to become a truly multi-vendor, multi-platform, safe, polyglot programming platform. I believe that we, as Java geeks, should not lose the occasion to be relevant in this space.

The WebAssembly specification and the WASI effort are still in flux. But all these pieces together are paving the way to allow an easier port of any programming language, not just those with a manual memory management.

Indeed, some garbage collected languages are already available, albeit not all of them take the same approach. For instance, Go can be compiled to Wasm (albeit with some limitations). For instance, the Python port is a port of the interpreter. So they compiled the CPython interpreter to Wasm, and then that is used to evaluate Python scripts, just like in a traditional execution environment.

In fact, memory management is really just part of the story, and only one of the many caveats that at this time would allow to port Java. You can always stick a GC in your executable (indeed, this is how GraalVM Native Image currently work); in my opinion, however it is harder to support other CPU features or system calls that are currently still unstable or not widely supported.

For instance:
– threading support is still lacking or experimental in most stand-alone Wasm runtimes;
– even browser support is experimental, and simulated through WebWorkers.
– there is not a standardized support for socket access: all the services that allow you to write custom HTTP handlers usually provides you with a pre-configured socket, limiting low-level access
– Exception handling is another experimental feature that is harder to simulate, because of the lack of unstructured jumps in the Wasm bytecode: this will likely need proper support in Wasm VMs before it can be adopted.
– each language brings its own constraints on memory layout and object shapes: it is therefore harder for languages to share data across boundaries, hindering compatibility between different languages and thus limiting the suitability of Wasm as a polyglot plaform (this is however being addressed as part of the GC spec itself).

In short, there are many challenges to porting Java to the WebAssembly platform inside and outside the browser.

Java Support on WebAssemblyCurrently, multiple projects and library that deal with WebAssembly and Java. I have compiled a list of those that I found around the web. At this time, however, most of these are hobby projects.

Running Java in the BrowserMany projects target Java translation to WebAssembly. Most of them, however, do not emit code that is compatible with leaner Wasm runtimes: in general, they are meant for running in the browser.

  • Bytecoder, JWebAssembly, and TeaVM are all translators from Java bytecode into WebAssembly that take a slightly different approach to translating Java bytecode to browser-friendly code. Among the others, TeaVM seems the most promising, as shown in Fermyon’s fork which includes initial support for WASI
  • CheerpJ is a very promising, albeit proprietary, attempt to support the full extent of Java, including Swing. There is also a Chrome extension to run good ol’ applets through Web tech

Here are also some honorable mentions of projects that target browser runtimes (with experimental Wasm support in some cases):

  • J2CL (successor to GWT) is a source-to-source translator (i.e. a transpiler) from Java to JavaScript, which has recently gained support for Wasm. This compiler has also bleeding-edge support for the GC spec.
  • Bck2Brwsr is another compiler from bytecode that targets JavaScript and the browser
  • Kotlin/Native also supports being compiled to Wasm via LLVM. It comes with all the caveats of Kotlin/Native (e.g. it may not support all of your Java libraries)
  • DoppioJVM is an interesting project that I wish to mention because it takes a completely different approach, similar to Python’s: instead of compiling bytecode to Wasm, it is instead an in-browser VM (written in JavaScript) that is able to interpret JVM bytecode. Unfortunately, the project is currently unmaintained.

Running WebAssembly on the JVMWe have been talking about running Java programs on a Wasm runtime. But of course, you may want to be able to do the opposite, too. In all fairness, the JVM already provides quite a few programming languages, and the current programming model that most Wasm runtimes offer (with manual memory management) seems kind of off when hosted on a JVM. But I still want to mention these for completeness, and because in general, they may still be interesting.

  • The prime candidate is obviously the aforementioned GraalVM’s Truffle implementation of a WebAssembly interpreter, which benefits from all the JIT superpowers and polyglot interoperability of the GraalVM/Truffle platform
  • asmble is a suite of tools that includes a compiler from Wasm to bytecode and a Wasm interpreter
  • Happy New Moon With Report (JVM) is a WebAssembly runtime for the JVM (that I am including in this list because I just love the silly name!)
  • There are also bindings to native Wasm runtimes, such as kawamuray/wasmtime-java
  • The Extism project has recently launched: it provides a unified API across different host languages to interface with a native WebAssembly runtime (Wasmtime)
  • Katai WebAssembly is a Wasm parser written using the Katai Struct binary parser generator that I am currently maintaining (PRs welcome!): this is not meant necessarily for running Wasm on the JVM, but it is actually useful when you want to be able to manipulate or query Wasm executables for information. In fact, a Kaitai grammar allows one to generate a binary parser for any supported language, so not just Java, but also Python, Ruby, Go, C++, and many others.

ConclusionI hope that this post sparked some interest in you. It is still early days for Java-on-Wasm, but I invite you to explore this brand-new world with an open mind: it may surprise you!

The post WebAssembly for the Java Geek appeared first on JVM Advent.

View Details

Writing a “Hello World” program is often a rite of passage for a software engineer when learning a new language.

If you’re a Java developer, you might even remember the first time you typed public static void main(String[] args) in your editor of choice. But did you ever wonder what’s inside that “.class” file that the compiler spits out? Let’s look at how we can write a JVM “Hello World” by creating a class file programmatically.

We’ll work through creating a class file for the following simple Java Hello World application.

public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); }} By the end of this post you’ll have made your first steps into the world of Java bytecode: being able to generate a Java class file without a Java compiler (OK, technically we’ll still need a Java compiler, since we’re going to write Java code to generate the class file!).

What is a class file anyway?A Java class file is a container for the compiled Java class, interface, enum or record definitions along with their corresponding members such as fields & methods. The methods in-turn contain the Java bytecode instructions that will be executed by a Java Virtual Machine (JVM).

At a high-level, a Java class file, as defined in the Java Virtual Machine Specification, contains the following structure:

  • The magic number 0xCAFEBABE used to identify the file as a Java class file
  • The major and minor version of the class file
  • A constant pool containing all the literal constants used within the class file
  • Access flags indicating whether the class is public, abstract etc
  • The name of the class and its superclass
  • The list of interfaces implemented by the class
  • Fields and methods
  • Attributes

In this post, we’re going to write code to generate a class that contains a main method and that method will contain bytecode which contains instructions to print “Hello World”.

Creating a classSo, how can we create a Java class file without starting from Java source code? Technically, a class file is just a bunch of bytes so we could just start writing out a stream of bytes:

DataOutputStream dataOutputStream = new DataOutputStream( new FileOutputStream("HelloWorld.class"));dataOutputStream.writeInt(0xCAFEBABE);//...dataOutputStream.close(); But once we get past the magic number things get more complicated and we’d benefit from a higher-level API to help us out.

This is when a library like ProGuardCORE, ASM or ByteBuddy comes in handy.

ProGuardCOREProGuardCORE is a Java bytecode manipulation & analysis library that contains the tools required to read, write and manipulate Java class files and their bytecode. It abstracts away some of the details and provides model classes, editors and builders for all things class file related.

In order to create a representation of a Java class for our Hello World program we can use the ClassBuilder utility. We simply need to provide, at minimum, the Java class file version, the access flags, the class name and the super class name:

ClassBuilder classBuilder = new ClassBuilder(/* version = */ CLASS\_VERSION\_1\_6,/* accessFlags = */ PUBLIC,/* className = */ "HelloWorld",/* superClass = */ "java/lang/Object");ProgramClass helloWorldClass = classBuilder.getProgramClass(); Using ProGuardCOREProGuardCORE is published to Maven Central, so you can simply create a new Java project and add a dependency to start using it. For example, a Gradle build.gradle file could look like the following:

plugins { id 'java'}repositories { mavenCentral()}dependencies { implementation 'com.guardsquare:proguard-core:9.0.6'} Writing a Java class fileOnce we’ve created a Java class representation in memory we can write it to a file with a ProgramClassWriter.

ProGuardCORE heavily uses the visitor pattern to implement functionality that can be applied to the model classes. The ProgramClassWriter visitor implements the functionality to write the class model to an output stream.

The class can be written to a file HelloWorld.class using a DataOutputStream, a FileOutputStream and a ProgramClassWriter as follows:

ClassBuilder classBuilder = new ClassBuilder(/* version = */ CLASS\_VERSION\_1\_6,/* accessFlags = */ PUBLIC,/* className = */ "HelloWorld",/* superClass = */ "java/lang/Object");ProgramClass helloWorldClass = classBuilder.getProgramClass();DataOutputStream dataOutputStream = new DataOutputStream( new FileOutputStream("HelloWorld.class"));helloWorldClass.accept( new ProgramClassWriter(dataOutputStream));dataOutputStream.close(); You can now use the command line tool javap to check that we’ve created a valid class file:

$ javap -c -v -p HelloWorld.classClassfile HelloWorld.classLast modified 12 Nov 2022; size 62 bytesSHA-256 checksum 650610b365dac2ca00fee4b090a6089b90d0086c862141a3ac43030911f07489public class HelloWorldminor version: 0major version: 50flags: (0x0001) ACC\_PUBLICthis\_class: #2 // HelloWorldsuper\_class: #4 // java/lang/Objectinterfaces: 0, fields: 0, methods: 0, attributes: 0Constant pool:#1 = Utf8 HelloWorld#2 = Class #1 // HelloWorld#3 = Utf8 java/lang/Object#4 = Class #3 // java/lang/Object{} Notice that the generated file already contains the class name, version, superclass and a small constant pool containing the strings representing the class and superclass names.

There are, however, no fields or methods in the class!

Adding a main methodAdding a method using the ClassBuilder is easy with the addMethod builder methods. You must provide, at minimum, the access flags, the name and the descriptor (see “type descriptors”):

ClassBuilder classBuilder = new ClassBuilder(/* version = */ CLASS\_VERSION\_1\_6,/* accessFlags = */ PUBLIC,/* className = */ "HelloWorld",/* superClass = */ "java/lang/Object");ProgramClass helloWorldClass = classBuilder.getProgramClass();classBuilder.addMethod( PUBLIC | STATIC, "main", "([Ljava/lang/String;)V");DataOutputStream dataOutputStream = new DataOutputStream( new FileOutputStream("HelloWorld.class"));helloWorldClass.accept( new ProgramClassWriter(dataOutputStream));dataOutputStream.close(); If you try to run the generated class file now, you’ll receive an error:

$ java HelloWorldError: LinkageError occurred while loading main class HelloWorldjava.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file HelloWorld We added a method, but the method doesn’t contain any code!

Type descriptorsAs you may have noticed, the descriptor doesn’t look like a Java signature as you would write in Java source code.

The types in descriptors in Java class files are encoded using characters which represent the types on the JVM and class names are always fully qualified, with the / as a separator instead of ..

For example, the descriptor for the main method in Java (public static void main(String[] args)) is ([Ljava/lang/String;)V.

| Character | Java type | | B | byte | | C | char | | D | double | | F | float | | I | int | | J | long | | LClassName; | class | | S | short | | Z | boolean | | [ | array |

Java bytecode instructionsWe’ll need to add some code to our main method to actually get our Hello World program to print “Hello World”. The code that we need to generate is, of course, Java bytecode.

Since our Hello World program is very simple we’ll just need a few instructions to:

  1. load the string “Hello World”
  2. execute System.out.println

A Java virtual machine is a stack-based machine: many of the instructions deal with pushing and popping from the operand stack. For example, the instruction ldc is used to load a constant onto the stack and the invoke instructions will pop their operands from the stack.

In order to execute an instance method, such as println, we can use the invokevirtual instruction. The first operand for invokevirtual is a reference to the instance on which the method will be called: in our case a reference to System.out. The System.out instance and the string “Hello World” will be popped from the stack and the method will be executed.

In total, for our Hello World program, we’ll need 4 different bytecode instructions:

| Instruction | Stack before | Stack after | Example | Example Description | | getstatic | …, | …, value | getstatic Ljava/lang/System; out | Pushes a reference to the System.out instance onto the stack | | ldc | …, | …, value | ldc “Hello World” | Pushes the constant “Hello World” onto the stack | | invokevirtual | …, objectref, [arg1, arg2, argN] | …, [return value] | invokevirtual Ljava/io/PrintStream; println(Ljava/lang/String;)V | Pops the reference to System.out and the “Hello World” string, and executes println | | return | …, | empty | return | Returns from a method |

CompactCodeAttributeComposerWe’ve already added a main method to our program using a ClassBuilder but without any code. As we learnt in the previous section we’ll need to generate four instructions: getstatic, ldc, invokevirtual and return.

The ClassBuilder provides a second addMethod which allows building code with a CodeBuilder. The CodeBuilder interface declares a single method compose that provides a CompactCodeAttributeComposer parameter.

The CompactCodeAttributeComposer is one of the core tools in the ProGuardCORE toolbox for creating code snippets. The API closely resembles the JVM instruction set, so our code snippet to print “Hello World” uses 4 methods with familiar names to generate the getstatic, ldc, invokevirtual, and return instructions:

ClassBuilder classBuilder = new ClassBuilder(/* version = */ CLASS\_VERSION\_1\_6,/* accessFlags = */ PUBLIC,/* className = */ "HelloWorld",/* superClass = */ "java/lang/Object");classBuilder.addMethod(PUBLIC | STATIC, "main", "([Ljava/lang/String;)V", 100, composer -> composer .getstatic("java/lang/System", "out", "Ljava/io/PrintStream;") .ldc("Hello World") .invokevirtual("java/io/PrintStream", "println", "(Ljava/lang/String;)V") .return\_());ProgramClass helloWorldClass = classBuilder.getProgramClass();DataOutputStream dataOutputStream = new DataOutputStream( new FileOutputStream("HelloWorld.class"));helloWorldClass.accept( new ProgramClassWriter(dataOutputStream));dataOutputStream.close(); Finally, “Hello World”Using the ProGuardCORE toolbox we’ve written a Java program that produces a Java class file that when executed prints “Hello World”.

You should be able to execute the generated HelloWorld.class file and see the result yourself:

$ java HelloWorldHello World Congratulations! You’ve taken your first step into the world of Java bytecode in which you’ve learnt your first 4 Java bytecode instructions!

Next stepsWe’ve only just scratched the surface of Java class files, Java bytecode and the toolbox provided by ProGuardCORE.

ProGuardCORE provides many tools to read, write and analyse Java bytecode and is the underlying library used by software such as the open-source ProGuard shrinker, the Android security solution DexGuard and the application security testing tool AppSweep.

For your next steps, take a look at the ProGuardCORE manual, ProGuardCORE examples or this small Brainf*ck compiler that uses ProGuardCORE to generate Java bytecode.


The post JVM Hello World appeared first on JVM Advent.

View Details

Elasticsearch is as one of the leading solutions for Enterprise search (and not only). As such it is worth understanding how does it work internally in order to better leverage its capabilities. Let’s follow a short journey to understand how does Elasticsearch work internally.

At the beginning there was only Lucene …The Apache Lucene library is an open-source library for full-text indexing. It is used by a number of applications to build a number of advanced search capabilities with Elasticsearch being one of them. Lucene is also used by a number of other Enterprise search applications such as Apache Solr. Why would one choose to use Apache Lucene or a search application build on top of it to implement search capabilities ? Why not use simple queries on top of i.e. a relational database already used by an application ? The key to this answer is the basic data structure used by Apache Lucene: an inverted index.

In a nutshell when we store (index) a text (a document) in Lucene it is split into tokens. Each distinct token in the inverted index points to the documents that contain it. This provides the possibility to implement faster algorithms for full text search covering a wider and more complex range of search scenarios. A traditional relational databases uses standard indexes based on data structures like a B-tree to improve performance and that provides lesser options for optimization. How is an inverted index stored by Apache Lucene internally ? It is stored in separate files on disk called Lucene segments:

Elasticsearch: a web server on top of Lucene …Yes, Elasticsearch can be considered a web server build on top of the Lucene library or even a document-oriented database. It provides a number of important capabilities that are missing from the library itself such as:

  • custering: Elasticsearch provides a robust mechanism to build a cluster of Elasticsearch instances for scalability and high availability
  • JSON-based REST API
  • caching
  • a lot more …

An index in Elasticsearch is ditributed in one or more primary shards and zero or more replica shards. In effect an Elasticsearch shard resides on a node from the Elasticsearch cluster and corresponds to a Lucene index:

An index in Elasticsearch may not have a field mapping (schema) defined explicitly. In that case Elasticsearch tries to deduct one automatically. A field may also have multiple types associated at the same time (i.e. text and keyword).

Every search document returned is scored to determine how relevant that document is according to the search query. Earlier versions of Elasticsearch (prior to 5.0) used the tf-idf algorithm to determine score relevance but later versions use the Okapi BM25 algorithm.

Elasticsearch is designed with clustering in mind. It tries to balance the number of shards across the nodes in a cluster so that load is distributed evenly. Even replica shards may paticipate in search queries instead of only providing high availability. The shard for a document is determined based on a simple hash function on the document routing key (which is the document ID by default):

shard = hash(routing_key) % number_of_primary_shards

There are two options to add a new node to the cluster: either using a multicast address or unicast: with a list of one more existing nodes in the cluster.

A mechanism in place to deal with potential conflicts is implemented by means of optimistic locking. This is achieved by explicitly specifying a version of the document expected to be currently in the index and if that is not the case the operation fails. Traditional relational databases in contrast implement pessimistic locking where certain parts of the schema can be locked to prevent unexpected modifications. This is not the case with Elasticsearch: we cannot lock an index or parts of it during a write request.

Some general recommendations related to the number of shards and size of index are:

  • too small number of shards introduces a scalability bottleneck
  • too many shards introduces performance and management overhead
  • determining the number of primary shards should be based on an upfront planning
  • putting large amounts of data in a single index should be avoided: if that is required the index should be split into montly/weekly/daily indices so that we keep the size of each index ideally between 5 and 10 GBs of data
  • aliases to reference indexes should be used as much as possible

How are requests processed in an Elasticsearch cluster ?
Let’s first see how an index request is processed:

  • the index request is sent to a coordinating node in the cluster
  • the coordinating node routes the request to a shard
  • the shard does not write the document imediately on disk by default (it can be forced though with a parameter) but to two in-memory areas: the memory buffer and the transaction log
  • the in-memory areas are then flushed to disk

Now let’s see how is a search request processed:

  • a search request is processed in two phases: fetch and query
  • during the fetch phase the search request is forwarded by the coordinating node to all shards to determine which ones contain data matching the query
  • during the query phase these shards are queried to retrieve data that is then aggregated by the coordinating node and returned back to the client

Modules here, modules there, modules everywhere …An Elasticsearch node is comprised internally of different modules. Earlier versions of Elasticsearch used a modified version of the Google Guice library for dependency injection. Effectively latest versions of Elasticsearch are moving away from it. Modules were bound to a Guice binder effectively enabling them to be injected and used wherever needed:

// b is a Guice binder modules.add(b -> { b.bind(Node.class).toInstance(this); b.bind(NodeService.class).toInstance(nodeService); b.bind(NamedXContentRegistry.class).toInstance(xContentRegistry); b.bind(PluginsService.class).toInstance(pluginsService); b.bind(Client.class).toInstance(client); b.bind(NodeClient.class).toInstance(client); b.bind(Environment.class).toInstance(this.environment); b.bind(ThreadPool.class).toInstance(threadPool); b.bind(NodeEnvironment.class).toInstance(nodeEnvironment); … } Some core modules are:

  • discovery and cluster formation: used for node discovery
  • HTTP: for the HTTP REST API
  • plugins: for managing the Elasticsearch plug-ins
  • thread pools: thread pools used internally by Elasticsearch
  • transport: communication layer for the Elasticsearch nodes

The evolving codebase …The open source version of Elasticsearch can be cloned from the official Github repo. Each version has a corresponding tag (i.e. v8.4.3) and there are also branches for minor versions (i.e. 8.5). The code is well structured and easy to understand. Here are some of the root folders of the repo:

  • client: implementation of the low level and high level Java REST clients
  • distribution: gradle build scripts for building the various distributions (i.e. RPMs)
  • docs: official Elasticsearch documentation organized in asciidoc format
  • server: core Elasticsearch application, contains built-in core modules
  • x-pack: implementation of the XPack extension
  • plugins: Additional plugins part of Elasticsearch distribution
  • modules: Additional modules implementing Elasticsearch functionality

To understand how Elasticsearch boots up you can start from the org.elasticsearch.node.Node#start() method:

ConclusionWe did a brief deep-dive into how Elasticsearch works. As with any software project the truth is in the code so you can checkout the Elasticsearch repo and analyse certain parts of it, i.e. particular modules. This is particularly useful if you find yourself in a situation where you need to understand how something works and it is not quite clear from documentation or if you need to write an Elasticsearch plugin and cannot find a good reference example.

The post Elasticsearch Internals appeared first on JVM Advent.

View Details

Quality mattersAs software engineers, we all agree, that quality is an important part of systems that we build. In the end, what is the point of the most interesting feature in the world, if in 90% of cases it doesn’t work, right?

So, we all agree that quality is important, however usually, we are not all align on the way how to get there. Some people are advocates of TDD, writing test first and code later. There are those who first write code and add test later, and also there are ones somewhere in between these two approaches. The fact that usually in universities, courses and trainings not enough time is spent on this topic doesn’t help.

There is a chance that one might have heard of Pyramid of Quality. However, people don’t always spend enough time on translating it to the real world, and how to actually do it. So let us try together to change this.

Assumption of real use caseLet us assume that we work in some company X and that we are building a simple REST API. We expose few end points. We have Service layer for more complex process. Also, we have a Repository that is used to connect to some database. Standard stuff that most of us encounter in our companies. The question at hand is how to implement the Pyramid of Quality in this example, which layers should be present and what tools should be used for them.

Code of REST API example and everything else shared in this blog post can be found at https://github.com/vladimir-dejanovic/test-pyramid-blog

PyramidThe basic logic behind the Pyramid is that the first, bottom layer is the largest and need to cover the whole application. Every layer after that is more complex and specialized, so with each we will cover a bigger area, and not all areas need to be covered.

Unit LayerEveryone will agree that the first layer is the Unit Test Layer.

Unit tests need to be written in a way that they are small, execute fast, and that they test only a small part, a unit, of the application. They shouldn’t depend on anything else, or make calls to any other systems, or need special setup to be run. The idea is for them to be run all the time during development stage, but also to be run at every code review/merge request stage also. If they run for a long time, people will not run them often enough, and in that way we would lose the benefit of them.

When it comes to Unit tests in the Java world, my recommendation is to use JUnit5. In case that you can’t use it for some reason, JUnit4 with some extra libraries will also do the trick.

In our use case, one simple Unit test might look something like this.

@SpringBootTestclass PostServiceTest { @Mock PostRepository postRepository; @InjectMocks PostService postService = new PostService(); @BeforeEach void setUp() { List list = new ArrayList<>(); Post post = new Post(); post.setTitle("title 1"); list.add(post); Mockito.when(postRepository.findAll()).thenReturn(list); } @Test void getAllPosts() { List list = postService.getAllPosts(); Assertions.assertEquals(1,list.size()); Assertions.assertEquals("title 1", list.get(0).getTitle()); }} As we see in this code example, we are mocking the Repository with Mockito. We are not using the original one and do any database calls.

The rule of thumb is to always mock or use duplicates for any dependency that code might have, for which we are writing the Unit Test. We do this to make sure that we don’t get false positives, cases where there is a bug or issue in dependency and our unit test fails as a result, while our code was bug free.

Additionally, by using mocks we short circuit execution, and in that way keep unit tests as small and as fast as needed.

Component LayerThe next layer is optional from my point of view, and it is the Component Layer. The idea behind components tests is to test bigger parts, components, of our system.

In practice, we can easily create component tests by using JUnit, and leverage mocking dependencies on the border of the component that we are testing. In this way, we can easily decide how big or small our component test will be. Since they are bigger, we can cover the whole system with a smaller number of tests.

Although I understand the logic and reasoning behind them, I rarely encountered them in real life. For them to provide value, we need to have some very complex systems, with complex components. Even in those use cases, there is a big question mark on the return on investment of Component Test overhead over Unit Tests and some other tests in later layers.

Functional LayerThe next layer in my mind should be the Layer of Functional Tests. Here we need to cover all interactions with our system from the user perspective, and validate that in these cases everything performs as expected. Some people might argue that this should be called system tests.

My personal preference is to use the term functional because it is easier for people, including non-techies, to understand what is being tested. Additionally, over the years, I saw people use system testing in a variety of contexts and meant numerous different things under it.

Functional tests need to meet few requirements that are different to unit and component tests. They need to be able to be run stand alone, since the idea is to execute them against “working systems”, usually against local version first, and then version of application in different environments, like dev, test, staging, maybe eve production. From this, it also doesn’t come as a surprise that there should be present a way to indicate either different base URL or something else for running them against those different environments.

Here it is also critical that different environments (dev, test, staging and prod) are as identical as possible. In this way, we can easily run the same functional test over all environments and be sure that any errors that we get by running functional tests are real bugs and not environment specific. Over the years I saw setups which were very different, and the effect was that, in reality, different test were run in different environment. And this led to lower test quality that inevitably led to lower product quality.

Over time, I noticed that BDD (Behaviour-Driven Development) is the best approach to creating functional tests. The tool I use most often for it is Cucumber.

In our use case one functional test BDD might look like this

Scenario: load data Given base url 'http://localhost:8080' When user hit end point 'posts' Then I expect list of data And that would translate to code like this

public class StepDefinitions { HttpClient client; String baseUrl; private HttpResponse data; public StepDefinitions() { client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP\_1\_1) .followRedirects(HttpClient.Redirect.NORMAL) .connectTimeout(Duration.ofSeconds(20)) .build(); } private HttpResponse hitURL(String urlPath) throws IOException, InterruptedException { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(urlPath)) .build(); return client.send(request, HttpResponse.BodyHandlers.ofString()); } @Given("base url {string}") public void base\_url(String url) { baseUrl = url; } @When("user hit end point {string}") public void user\_hit\_end\_point(String endPoint) { try { data = hitURL(baseUrl + "/" + endPoint) ; } catch (IOException e) { data = null; e.printStackTrace(); } catch (InterruptedException e) { data = null; e.printStackTrace(); } } @Then("I expect list of data") public void i\_expect\_list\_of\_data() { if(data == null) throw new io.cucumber.java.PendingException(); }} Since in our use case we are testing the interaction with our simple REST API, we are leveraging Java HTTP Client to make REST calls and validate the results with Cucumber. In case our users are interacting with a more complex system, we might use Selenium instead of HTTPClient.

Personally, I find BDD fits very nicely in the ecosystem of Functional Tests because it is easy to read and also non-tech people can easily contribute & add to them.

End-to-End LayerThe next layer that we need in our pyramid are End-to-End tests. The logic here is to really test the whole chain. Something or someone interacts with our system, that triggers our system to interact with some other system, and so on. At one point, response starts to go back via this chain. These responses need validation, so we know everything is working as expected. Writing tests like this is more difficult. They need to run on as-close-as-possible production-like environments. It should come without saying that stable environments are crucial for success. And all shareholders need to buy into this type of test. Since they are complex, writing them will take time, and if they send false positives due to instability of environment in which they are tested people will stop running and writing them.

The good thing about them, is that they need to cover only parts of the system that interact with other systems.

Tools and libraries that we should use to write end-to-end test, are usually the same ones we use for functional tests.
End-to-end tests need to be standalone, for the same reason as functional tests. They shouldn’t be tied to any specific environment, and we should run them against multiple environments of our application. Running them in staging is a must, and running them in any previous environment is a good bonus.

Performance LayerThe next layer is very regularly overlooked: the Performance Layer. In most cases, all previous layers are testing one user one click situations, and as we all know that is not how real users interact with our systems. That’s why we need to test how our system performs in real-world scenarios. This is where load tests, also known as performance test, are helpful.

My weapon of choice for this is Gatling. It does the job perfectly, is easy to configure and very versatile. In our use case, the load test might look something like this.

public class LoadTestSimulation extends Simulation { ChainBuilder query = exec( http("get posts").get("/posts")) .pause(1); HttpProtocolBuilder httpProtocol = http.baseUrl("http://localhost:8080") .acceptHeader("application/json") .acceptLanguageHeader("en-US,en;q=0.5") .acceptEncodingHeader("gzip, deflate") .userAgentHeader( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0" ); ScenarioBuilder users = scenario("Users").exec(query); { setUp( users.injectOpen(rampUsers(10).during(10)) ).protocols(httpProtocol); }} To run it, we just need to execute the following command:

$ mvn gatling:test The rule of thumb is that performance tests must be run in staging before pushing our code to production. As we discussed while looking at end-to-end tests, the staging environment needs to be as close to production as possible and stable. Performance tests shouldn’t be tied to any environment.

Bonus LayerThe last layer in our pyramid is a bonus layer due to the simple reason that most people don’t have it, or don’t consider it as a test layer. In all fairness, it isn’t really related to test, it is more related to protection in case things go wrong in production.

I strongly advise everyone that all features, that are developed, are put behind feature flags.

The idea of feature flags is simple: if the flag is “On” the feature is active. When it’s deactivated, users will not see the feature. In essence, a feature flag is like a switch that we can flip and activate or deactivate a feature. So in case we have an issue in production, simply flip the feature flag and disable the faulty feature and make sure that everything is back to normal, instead of patching and pushing new code to production.

Feature flags are a powerful concept and yet, they are very frequently overlooked. As I stated before, it isn’t really a testing layer, but a powerful layer of protection that all applications should have.

Resources* Full code example https://github.com/vladimir-dejanovic/test-pyramid-blog * https://junit.org/junit5/ * https://site.mockito.org/ * https://cucumber.io/docs/installation/java/ * https://gatling.io/

The post How to Tackle the Pyramid of Quality in the Real World appeared first on JVM Advent.

View Details

IntroductionEclipse Adoptium is securing Java for the community. This is a journey and it is important to establish that the title says “securing” not “secured”. We are in unprecedented times where there is pressure on all software developers to secure the software supply chain due in part to an increasing wave of creative software attacks and also with new mandates to protect against the growing threat of attacks.

To restate what everyone already knows, Java is everywhere. Java is the “write once, run everywhere” language. It continues to share its position in the list with a handful of other top programming languages and arguably is the most pervasive choice for enterprise software for many reasons, including its stability, portability, ease of learning, affordability and community support.

This post covers our motivation at Eclipse Adoptium and shares the steps we have taken as well as our next steps in the secure software development journey. It is also a call to the community for feedback, input and action to join the on-going effort to ensure Java is the safest and most trusted choice for enterprise software.

Large Install BaseEclipse Temurin is the Adoptium distribution of OpenJDK. You may know it by its former name, AdoptOpenJDK. It is the most popular distribution of OpenJDK in production and with that comes a great sense of responsibility. So, given the Temurin distribution is everywhere, running banking software on mainframe servers, on millions of phones, in the software in your car or your beer fridge, we arrive at one of our fundamental needs and motivation.

Ensure Temurin is secure in order to secure the world around us. We want to protect our consumers from someone hacking into their fridges resulting in tepid beer (which will serve as a running analogy, as you can extrapolate, of any other critical infrastructure attacks).

False Sense of SecuritySo, we are all now duly scared. No one wants room-temperature beer.

Does this drive everyone away from open-source software? The answer is no, instead of the old model of trusting a vendor to build a secure thing behind its veiled corporate wall, the world has changed. Open and transparent development using a model that comes with ways to verify the output continues to be the preferred direction of travel. Philosophically, instead of trying to figure out how to trust, the world is figuring out how to ‘trust but verify’.

Screen grab from a Smart Beer Fridge ApplicationSecure Development ActivitiesHow we startedKnowing all of this, our efforts at Adoptium started years ago and are now ramping up at a more rapid pace. Some of our visible efforts began as an Outreachy project called Project RASPberry, to improve the Reproducibility, Auditability, Security, and Presentability (RASP) of our product builds through enhancing our approach to tracking dependencies and the automated creation of a Software Bill of Materials (SBOM) as part of our build process. This work also led us to further engage with different security experts, get involved in the OWASP CycloneDX community and learn by doing.

Guidelines for Secure Software DevelopmentThere are plenty of resources to help projects and organizations on this journey. When we began, we assessed many existing security frameworks available and selected the Secure Software Development Framework (SSDF), the US government based NIST framework as a model to follow, since it outlined a thorough and rigorous set of criteria to ensure secure software development. We have also added the Supply-chain Levels for Software Artifacts (SLSA) framework to our toolkit. While we have covered these frameworks in other posts and presentations, it is useful to summarize them here.

SSDFThe checklist provided by the SSDF framework encompasses many activities that we had already done, or have initiated at the project. Much of the effort at Adoptium, under this tracker issue, includes assessment, documentation and identifying whether there are any gaps that we need to fill.

SSDF practices are divided into 4 groups.

SSDF Best PracticesThese SSDF practices and our effort to ensure we meet all of the criteria further stabilizes our project and is a step along our path of continuously improving how we build, test, and deliver software.

SLSAWhen we began our work and assessed frameworks, SLSA v0.1 was available. Since that initial assessment, SLSA v0.2 has arrived and the Eclipse Foundation has selected SLSA to guide Eclipse projects in Software Supply Chain best practices. Given this, we additionally utilize the SLSA framework to guide our efforts and rate our progress.

At the time of writing, Temurin is at SLSA Level 2 and marching quickly towards the SLSA Level 3 badge.

SBOMsBoth SSDF and SLSA require the collection, safeguarding, maintenance, and sharing of provenance data in the form of a software bill of materials (SBOM). An SBOM is a complete, formally structured list of components, libraries and modules that are required to build a given piece of software and the supply chain relationships between them. That is the long way of saying that an SBOM is a list of ingredients.

As per the helpful SBOM explainer video from NTIAGov, the list of ingredients needs to include the atomic parts. In their granola bar example, compound parts like caramel are broken into atomic components like cream and sugar.

Granola Bar Supply Chain (courtesy of ntia.gov)For our Temurin builds of OpenJDK, we currently produce and offer up SBOMs with both our general availability builds and our nightly early access builds. We are in the process of refining the contents of our SBOMs, to ensure that we break all compound parts down into the atomic parts.

Simplified Temurin Supply ChainSBOMs contain the list of ingredients, those atomic parts that went into producing the binary and help answer many questions that consumers may have about their supply chain. When used to scan for vulnerabilities in the security context, they help answer the question “Am I at risk?”. Used informationally for dependency tracking and identifying most used components, they could help answer “Do I invest?” or “Do I change suppliers?”. For the reproducibility use case, where we want to use the SBOM not just as a list of ingredients, but also as a recipe for reproducible builds, the question answered might be “Can I accurately reproduce this build?”.

Reproducible BuildsRemember that promise of ‘trust but verify’? Reproducible builds are the ‘verify’ piece. Of course, you can just ‘trust us’ when we say that the contents of the SBOM accurately describe the binary you just downloaded, but it would be even better if you and others could verify it for yourselves.

Why is it the mechanism for providing ‘evidence’?

The Adoptium releases can be rebuilt within multiple secure environments each with stringent bill of material checks, and then the binaries compared to ensure byte for byte identical output. If the builds from the multiple secure environments are identical to the one from the open-source projects, the likelihood of malicious tampering would be extremely low.

Reproducible Builds at Eclipse Adoptium Andrew Leonard

The Adoptium project has done extensive work to find and resolve non-deterministic code and processes and push changes upstream to the OpenJDK project. Through this effort, and with Temurin’s supporting changes to build scripts, reproducible builds are now possible in JDK 19+ and back-ported to JDK 17.

| JDK 19+ | JDK 17 | | Linux | | | | Mac | | | | Windows | | (requires testing) |

Availability of reproducible builds by OS / versionMore enhancements are underway for verifying and rating reproducible builds and to improve the tooling at the project. We want to accommodate industry adoption of the ‘trust but verify’ model.

SBOM refinementsDepthAs mentioned, we are currently working on refining our SBOM contents, ensuring that all atomic parts are included in this provenance data. What this means in practice is that we are running the strace tool to see everything that touches the final product during production, and ensuring it is all accounted for in the SBOM. The goal is to have all atomic components with all transitive dependencies and known-unknowns declared.

Specification UpdatesSpecifications are evolving quickly. In the first incarnation of our automated SBOM generation, the CycloneDX specification did not support a type that was quite suitable to describe components of runtime environments, like Temurin. We started off as type:application, currently are type:framework, and in CycloneDX v1.5, we may shift to type:platform. In order to progress, we will choose to work with the CycloneDX team and supply the best options we have at the time.

"component" : { "name" : "Eclipse Temurin", "version" : "17.0.4.1+1", "description" : "Temurin JDK Component", "type" : "framework" }, We are now providing feedback on the proposed CycloneDX formulation enhancement, which offers a way to describe more than the list of ingredients, but how to assemble them into a recipe. These updates are expected to be available later in 2023.

VEXWhile working on the automated creation of release notes, the question arose around how to capture the list of Common Vulnerabilities and Exposures (CVEs) fixes that are in the product being released and also, how can we share this information in a useful manner to the community. In the course of that discussion, we identified the Vulnerability Exploitability Exchange (VEX) which is a part of CycloneDX specification.

We plan to explore generating a VEX BOM that can be linked from the Temurin SBOM. This deep-linking capability supported by CycloneDX is referred to as BOM-Link and promises to be an adaptable approach to communicating on security advisories. This may facilitate better automation for consumers when they want to verify vulnerabilities and patches.

Use Case DocumentationWe will be developing documentation and common user stories for sharing. One example for developers may be when they are creating and shipping an application that may contain Temurin, like a native image created with jlink.

Call for Participation from the CommunityWe would love to hear what you are doing to secure your software supply chains.

What challenges are you facing that we should understand better in order to be more useful and relevant?

Even while we are refining the contents of the Temurin SBOM, do you already envision integrating it into your secure supply chain process? You can already pull the signed Temurin SBOM from the Adoptium API alongside the Temurin binaries it describes.

You can generate your own queries using api.adoptium.net/q/swagger-ui/#/Binary/getBinary and changing image_type to sbom. For convenience, here are some example API queries for SBOMs for various versions, platforms and release types.

| JDK version | Platform | Release Type | API query | | --- | --- | --- | --- | | 8 | x64 Windows | GA / latest | https://api.adoptium.net/v3/binary/latest/8/ga/windows/x64/sbom/hotspot/normal/eclipse?project=jdk | | 11 | x64 Mac | GA / latest | https://api.adoptium.net/v3/binary/latest/11/ga/mac/x64/sbom/hotspot/normal/eclipse?project=jdk | | 17 | x64 Linux | GA / latest | https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/sbom/hotspot/normal/eclipse?project=jdk | | 19 | ppc64le Linux | GA / latest | https://api.adoptium.net/v3/binary/latest/19/ga/linux/ppc64le/sbom/hotspot/normal/eclipse?project=jdk | | 20 | aarch64 Mac | EA / Nightly build | https://api.adoptium.net/v3/binary/latest/20/ea/mac/aarch64/sbom/hotspot/normal/eclipse?project=jdk |

Example API queries for Temurin SBOMsSummaryWhether it is in response to the Cyber Resilience Act (CRA), the Presidential Executive Order 14028 or some other sweeping mandate, we understand the need to increase our pace and deliver on our goals. Whether it is your beer fridge, your bank account or your fuel pipeline, our project serves to protect it. Please collaborate with Adoptium in any way you can, joining the conversation in the Adoptium Slack workspace, sharing requirements by raising Github issues, active development on some of our ‘next step’ features, or early adoption and use of our SBOMs. We want your help.

“Securing” not “secured”. This is an on-going and ever-improving effort and we encourage you to join us to help secure Java and ‘keep the beer cold’.

The post Securing Java for the Community appeared first on JVM Advent.

View Details

Mandatory memeUsing Http4k and Loom

If you read my blog or listened to a talk of mine, you may know that my favorite library to serve HTTP requests in Kotlin is Http4k. It’s easy to understand and lets me map each request as a function that transforms a Request into a Response. Powerful and straightforward: I love it.

Its most complained about drawback is that it’s not supporting asynchronous handling of requests. In Kotlin, there is an excellent way to handle asynchronous calls, which is using coroutines.

Http4k doesn’t support it. It’s so often required that the authors are tired of hearing people asking about it. Why not add it?

Well, long story short, adding it would make everything more fragile and complicated. Since it’s an open-source project, the authors decided (rightly in my opinion) to leave Http4k simple and easy to use and drop the coroutine support.

But… but… surely not having coroutines would greatly impact performance? Well, not necessarily. In the vast majority of use cases, asynchronously handling web requests doesn’t bring any advantage.

This may require an explanation, since all top web server benchmarks show that handling calls asynchronously is faster (and they are not wrong).

Concurrency and Multi-ThreadingLet’s look at how the asynchronous model can improve performance, particularly for web servers. For example, my laptop’s CPU has eight cores, which is pretty typical in 2022, so it can do (at most) 8 things in parallel.

A web server is an application that listens on a TCP socket, creates an HTTP Request, processes it somehow, and transmits the HTTP Response back to the sender.

So, no matter how many requests arrive at your web server, my laptop cannot process more than eight in parallel.

Now, let’s assume we have 32 requests coming. If we only care about the total processing time, or throughput, the fastest way is to process 8 requests first and then the other 8 and so on:

Each core is processing a request after the other, no wasted CPUBut this is a bit unfair because only 8 users get their request immediately served, and the rest must wait before starting the connection, and since the allocation is somehow random, some users risk waiting so much that the connection will time out.

A more polite web server would process all 32 requests simultaneously but taking the same overall time, so about 4x the time for a single request. For simplicity, let’s ignore the latency and bandwidth limitations of real networks.

How can a CPU with 8 cores handle 32 requests in parallel? It is possible if each core of the CPU switch context between four calls, dedicating a quarter of the time to each. So, we can assign each call to its own processing context that will keep all the variables, the stack trace, and everything else that the CPU needs to work on it.

Using threads 8 cores can process more than 8 requests at same time, but with no performance improvementThe total time is close to the previous case, but all clients will be accepted immediately, reducing the chance of connection timeouts.

I wrote close and not equal because it will take a little more time. After all, switching the context takes some time (and some memory). This “processing context” is what we call a thread.

The CPU can handle threads very efficiently, but they still slow down the system (CPU cache misses) and the use more memory (thread stack memory and GC roots).

Roughly speaking on a modern CPU, the overhead is negligible with tens of threads; it became noticeable around 100 threads, and it is problematic when the number of threads approaches 1000.

A common strategy to limit the resources utilized is to use a ThreadPool to reuse threads once they have finished their task, instead of creating new ones every time.

So far, so good. You cannot do better than this if you need the CPU working full-time to process your requests. In other words, increasing the number of threads to more than the CPU cores won’t speed up the whole operation time (but it can improve fairness).

Don’t Waste CPU Time, The Problem of WaitingWhat about those benchmarks, then? Well, probably, during the request processing, there is some time when the CPU is just sitting there waiting without doing any work.

Why should the CPU wait when we want to respond as fast as possible? Usually, it’s because it’s blocked waiting for data from a much slower IO channel, like a net socket or the file system. It can also be blocked by lock, required for synchronization or some other reason. In any case, when a thread is waiting, it doesn’t consume CPU, and having a bunch of other threads ready to be run can improve the global performance:

Requests with long waiting time waste CPU resourcesIf this is the case, we can see that increasing the number of threads keeps speeding up things beyond the number of physical CPU cores until we arrive at a situation where the CPU is fully utilized again. At that point, adding new threads will only slow down the total time.

Requests that put a thread in waiting can be handled more efficiently, increasing the number of threadsFor example, if all requests spend 50% of the time waiting, we should double the number of CPU cores to find the optimal number of threads. If 90% of the time is spent waiting, we should multiply the CPU cores by 10.

Let’s say we are writing a typical RESTful backend, and each thread handles a request with these timings (invented but realistic):

  • 1 ms reading the request data
  • 9 ms calling an external service to retrieve the data
  • 180 ms waiting for the response from the external service
  • 9 ms analyzing the data and rendering it in JSON
  • 1 ms sending the response to the caller

The total time is 200 ms., and we spent roughly 20 ms using the CPU and 180 ms waiting for the network. So 90% of our time is spent waiting, and indicatively using 80 threads should maximize the outcome if the CPU has eight cores. If the external service instead had taken 1980 ms to reply, our ratio would have been 99% waiting and 1% processing, and we would need 800 threads to utilize the CPU fully, but probably less than that because we have to add the CPU load to handle all those threads.

Let’s verify it with some code. The full sources are on GitHub (https://github.com/uberto/Http4kLoom)

Using Http4k as the web server, I created a trivial application that returns a welcome message:

fun testApp(request: Request): Response = Response(OK).body("Hello, ${request.query("name")}!") In Http4k, any function of type Request -> Response can be used as a web server with a single line of code:

val server = ::testApp.asServer(Jetty(port = 9000)).start() //normal jetty Let’s first find out how many requests my laptop can handle without any sleep calls.

I used autocannon to stress my server. Note that calling the server from several other computers on a fast network would be better for obtaining a meaningful measurement, but since I just want to show the difference and not to measure absolute performance, my laptop would do fine.

From the measurements, my server handles at most 94.000 requests per second, regardless of the number of threads (see all the measurements at the end of the post). This means less than 0.1 millisecond for core to handle a request. Not bad Jetty!

This is our baseline, we can probably do better with specific optimizations, but it’s still a ridiculously high number, considering that the whole Twitter handles 3000 requests for second on average (7B visits for month).

To simulate a realistic web application, we can add 50ms of sleep. Which similar to having to do a few interactions on database.

fun testApp(request: Request) = Response(OK).body("Hello, ${request.query("name")}!") .also { Thread.sleep(50)//simulating some async operation without cpu load } For this test, I created a specialized JettyLoom class that uses the thread pool passed in the constructor. In this way, I can easily swap the thread pool implementation. For example, let’s use 500 threads:

val server = ::testApp.asServer( JettyLoom(port = 9000, threadPool = ExecutorThreadPool(500))).start() //fixed threads Note that Jetty uses the “eat what you kill” execution strategy, which it’s not only very fast but also allows us to play with different thread pools.

On my laptop, I couldn’t measure any improvement using more than 500 threads, and having more than 1000 will actually slow things down.

We should take these numbers with more than a grain of salt: a high number of threads can impact your application in many other ways, such as memory consumption and longer GC pause. I cannot say this enough: base all your decisions on your own measurements and not from some blog on the internet (including my own).

To continue our analysis, since my laptop has 8 cores, about 99% of sleep is the max you can do with threads.

Let’s Cooperate aka The Asynchronous ModelThings start becoming interesting if our requests wait for 99.9% of the time or more. Unfortunately, we cannot continue to increase the number of threads over 500, so what can we do?

Well, one possibility we won’t explore in this post is to change your API to avoid such long waits. For example, make things event-based or use callback instead of long waits. So, if it’s possible to reduce the waiting time to 99% of total or less, this is the best solution.

Alas, for a series of reasons, this is not always possible, so for the rest of this post, we assume that we cannot avoid keeping the CPU idle most of the time, and we need a way to wait without blocking the whole thread.

Roughly speaking, the idea is to pause or park the waiting task and use the thread to work on something else instead of waiting. Once done, we check if the waiting task is ready to continue.

In this way, a single thread can handle many asynchronous tasks at the same time.

But here is the catch: asynchronous APIs are more complicated to use. After all, it’s like when a cook tries to cook several dishes at the same time: it can be done, but it’s challenging. It’s easy to forget something on the gas and ruin the dinner.

Java Futures, RXJava, and Kotlin coroutines, are all brilliant solutions to somehow simplify asynchronicity. They help, but they still add complexity to the straightforward synchronous model.

On the other hand, they shine on benchmarks: it’s relatively easy to tune them for simple test cases and obtaining amazing results. Real production where some requests take seconds and other milliseconds is a different scenario. But in this post we will continue pretending that benchmarks are meaningful because we are super optimistic!

Loom and Virtual ThreadsThe best approach would be to handle the threads overload and the asynchronous API at the JVM level, closer to the operative system, and let the high-level programming language be blissfully ignorant of what is happening at the low level. This would mean having your cake (performance) and eating it too (synchronous code).

Given the title of this post, you may have guessed that this is the goal of the JDK project Loom. It’s been available for the first time with Java19, released in October 2022.

Without further ado, let’s inject Loom’s virtual threads into our previous example and measure the difference in performance for long waiting calls.

val server = ::testApp.asServer( JettyLoom(port = 9000, threadPool = LoomThreadPool())).start() //loom And now the results:

normal jetty with defaultsaverage 80k req/s with no sleep and 100 connections (top 200% CPU)average 94k req/s with no sleep and 10000 connections (top 200% CPU)average 1970 req/s with 50ms sleep and 100 connections (top 25% CPU)average 3600 req/s with 50ms sleep and 10000 connections (top 80% CPU)threadpool with 500 threadsaverage 80k req/s with no sleep and 100 connections (top 200% CPU)average 94k req/s with no sleep and 10000 connections (top 200% CPU)average 1970 req/s with 50ms sleep and 100 connections (top 35% CPU)average 9500 req/s with 50ms sleep and 10000 connections (top 130% CPU)loomaverage 77k req/s with no sleep and 100 connections (top 250% CPU)average 94k req/s with no sleep and 10000 connections (top 250% CPU)average 1970 req/s with 50ms sleep and 100 connections (top 40% CPU)average 94k req/s with 50ms sleep and 10000 connections (top 400% CPU) The value of CPU utilization is quite imprecise, but still indicative. I’ve used the Linux app top to get the data.

A screenshot from the best run:

And now the mandatory graph (higher is better). The green bar is the important one, and Loom is really doing well!

Finally, some comments on the code:

class LoomThreadPool : ThreadPool { var executorService: ExecutorService = Executors.newVirtualThreadPerTaskExecutor() @Throws(InterruptedException::class) override fun join() { executorService.awaitTermination(Long.MAX\_VALUE, TimeUnit.NANOSECONDS) } override fun getThreads(): Int = 1 override fun getIdleThreads(): Int = 1 override fun isLowOnThreads(): Boolean = false override fun execute(command: Runnable) { executorService.submit(command) }} This is how I’ve implemented a constructor of virtual threads disguised as a thread pool, using the new Loom API.

class JettyLoom( private val port: Int, override val stopMode: ServerConfig.StopMode, private val server: Server): PolyServerConfig { constructor(port: Int, threadPool: ThreadPool) : this( port, ServerConfig.StopMode.Graceful(Duration.ofSeconds(5)), Server(threadPool).apply { addConnector(http(port)(this) )} ) This is the necessary boilerplate code to allow Http4k to use Jetty with an external Threadpool. The full code is on the GitHub repository.

Conclusions

The new virtual threads have radically changed the panorama of JVM concurrency model. In future versions of the JDK, they will introduce better constructs for structured concurrency, and the APIs will be finalized, so they can only get better from now.

It’s fascinating how virtual threads can provide many benefits in terms of performance and flexibility without needing changes to our code style or hard-to-tune custom optimizations.

I hope this post can help other people to experiment with them!

If you liked this post, you may consider following me on Twitter (@ramtop)

PS. in case you want to know more about Http4k and how to build quickly back-ends in Kotlin using functional programming, I’ve written a book about it!

https://pragprog.com/titles/uboop/from-objects-to-functions/The post Asynchronous Functional Web Server in Kotlin appeared first on JVM Advent.

View Details

IntroductionIn the world of data science, Python is the most popular programming language. There are plenty of other contenders in the non-JVM world including C++, R, MATLAB, Julia, and JavaScript. Python is often preferred for its friendly syntax, many built-in capabilities, and widely available libraries.

JVM languages have also been widely used for data science. Languages like Java and Scala are often considered for data science projects since running on the JVM can bring many benefits. Groovy sits alongside those languages as a great alternative to consider for your next data science project. It is sometimes referred to as the Python of the JVM. It also offers a friendly syntax, many built-in capabilities and can use a wide variety of available libraries.

In this blog post, we’ll examine several common data science and machine learning tasks and see how to perform them using Groovy and JVM libraries. Along the way, we’ll mention beneficial aspects that Groovy brings and highlight advantages such as speed and ability to scale that comes from using the JVM.

We’ll cover the following data science activities:

  • Using dataframes and visualization libraries to explore candle ratings and reviews.
  • Predicting house price using linear regression.
  • Classifying Iris flowers using traditional algorithms and neural networks.
  • Clustering single-malt Scotch whiskies by flavor characteristics.
  • Various natural language processing tasks.
  • Detecting objects within images. Key takeaways for Groovy are:

  • Groovy offers a friendly Java-like syntax with dynamic or static typing capabilities. Its metaprogramming capabilities often simplify the code.

  • Groovy aligns closely with Java, which has multiple benefits:
    • The learning curve is reduced. Data scientists can cut and paste most Java examples when learning a new library and add Groovy idioms over time.
    • As the JVM evolves, Groovy automatically obtains new features and improvements by piggy-backing on the great work of the JVM developers.
    • No special Groovy support is needed for frameworks. Frameworks which offer Java support, automatically offer Groovy support. Additional Groovy enhancements can be added if desired.
  • Groovy data science implementations can take advantage of the many options for scaling that exist on the JVM About GroovyApache Groovy is a multi-faceted programming language for the JVM. Its goal is to provide a Java-like experience to users of the language but allow for greatly simplified code in many scenarios.

As an example, we could write the following Java program to calculate Fibonacci numbers using matrix manipulation:

We can set up Groovy to know about this library and customize the output, to instead allow code and execution like this:

This simplifies the code and cognitive load for the data scientist yet makes identical calls to our matrix library. The output is a lot prettier too!

We’ll see a little more output customization when we get to the natural language examples.

We just illustrated a matrix example. Data scientists will likely end up using matrices all the time but might rarely do so directly. They will often be used under the covers by higher level algorithms. Readers interested in matrices can have a look at a Groovy example that creates neural networks by hand using matrices for digit recognition.The following blog post may also be of interest. It looks at a range of additional matrix calculations including one exciting area which is speeding up matrix calculations using the (currently incubating) Vector API.

Before diving further into our examples, it is worthwhile briefly talking about typing. Groovy was originally designed as a dynamically-typed complement to Java. Groovy’s dynamic nature allows the language to be augmented at runtime using techniques similar to those found in Python, Ruby, Smalltalk and Clojure. Groovy also has a static nature allowing improved compile-time type checking similar to Java, Scala and Kotlin. Both the dynamic and static natures offer extensibility options.

While it isn’t the focus of this blog, Groovy has great support for writing Domain Specific Languages (DSLs). As an example, here is a line of code that might be used to control a Mars rover robot:

move right by 2.m at 5.cm/s To compile such code, we might declare a move method, and use metaprogramming to define m and cm properties for numbers, among other things. We call this process “defining our DSL”. We have options to leverage Groovy’s dynamic or static natures when designing the DSL. If we have a very dynamic DSL, we can catch accidental (or malicious) incorrect commands for the rover, e.g. we might throw an exception or return some error code for the following command:

move forward by 2.kgs If we have a type-rich DSL, attempts to compile the above line might result in a compile-time error like this:

[Static type checking] - Cannot call by(Quantity<Length>) with arguments [Quantity<Mass>] Another aspect we might want to incorporate into our rover DSL is speed limiting the rover for energy conservation or safety reasons. Perhaps the speed should be limited to 5 cm/s, so that the following line would be considered an invalid rover command:

move right by 2.m at 6.cm/s We could put the appropriate defensive programming guards into our move method to detect invalid speeds at runtime. However, the type checker itself is also extensible, so we can bake such constraints into the type system if we choose in which case we might see a compile-time error like this:

[Static type checking] - Speed of 6 is too fast! There are numerous ways to encode such a constraint into a type system. The approach shown here puts the burden of doing such an encoding on the DSL designer, not the DSL user. To see more about how to design such DSLs, including incorporating Java’s Units of Measurement API 2.0 (JSR 385) see this blog post.

Most of our examples use only the built-in metaprogramming enhancements in Groovy. These simplify code using lists, maps and Strings among other things. We will show one example later of more specialised metaprogramming when we look at using Apache Beam for scaling linear regression. It is worthwhile keeping in mind that if we have many similar data science scripts to write, creating a DSL may further increase productivity when writing those scripts.

For more information on Apache Groovy,you can visit the project website, read more aboutGroovy’s history, and see some more informationabout Groovy and data science.

Data Science LibrariesWhen performing data science tasks, Groovy has many useful built-in general purpose features, but there are many libraries you’ll probably want to use for more data science or machine learning specific tasks.The following table provides a non-exhaustive list of such libraries that we use or mention in this blog.

| Technologies/libraries covered | | --- | | Data manipulation | Weka, Tablesaw, Apache POI, Apache Camel, Apache Commons CSV, Encog, Datavec, Tribuo | | Data science algorithms | Weka, Smile, Encog, Tribuo, DeepLearning4J, Deep Netts, Apache Commons Math | | Scaling data science | Apache Spark, Apache Ignite, Apache Beam, Apache Wayang (incubating), GPars, Spark-NLP, DJL with Tensorflow, DeepLearning4J with Apache MXNet, GraalVM | | Visualization | XChart, Tablesaw Plot.ly, JavaFX, GroovyFX |

Candles

An example that looks at reading spreadsheets, using dataframes, and creating graphs.

An interesting series of tweets around scented candles emerged about a year into the pandemic. One of the symptoms of COVID was loss of smell. About the time that infection rates were increasing, complaints about the lack of scent in scented candles were also increasing. Several folks explored the data in more detail including as shown in the following tweet.

Let’s explore the same data using Groovy.

We’ll look first at the review data which is contained in the spreadsheet Scented_all.xlsx which is on the classpath.

var url = getClass().classLoader.getResource('Scented\_all.xlsx')var table = new XlsxReader().read(builder(url).build()) Here we are using the Tablesaw library which provides a dataframe abstraction and has an add-on for reading Excel spreadsheet files.

For most of these code snippets we have not shown the relevant imports (they are in the complete listing in the repo). For this example, we use import aliasing to make the code more succinct. Since aliasing may be less familiar to some readers, we’ll show one example import:

import static tech.tablesaw.api.StringColumn.create as sCol This is the same as a standard static import but also renames (or rather provides an alias for) the method. This is handy for our example where we’d otherwise have multiple create methods, and we’d only be able to have one of them as a static import. Now we can use the imported sCol method to create a new String column in our table and later we’ll use similarly defined dCol and bCol aliases for creating double and Boolean columns.

Our table has a Date column already but we’ll create an additional Month column containing just the month name as a string:

var monthCol = sCol('Month', table.column('Date').collect { it.month.toString()}) Then we’ll create an additional Noscent Boolean column. Values in that column will be true if the review text matches any of a number of regex patterns:

var candidates = ['[Nn]o scent', '[Nn]o smell', '[Ff]aint smell', '[Ff]aint scent', "[Cc]an't smell", '[Dd]oes not smell like', "[Dd]oesn't smell like", '[Cc]annot smell', "[Dd]on't smell", '[Ll]ike nothing']var noScentCol = bCol('Noscent', table.column('Review').collect { review -> candidates.any { review =~ it }}) We’ll add our newly created columns to our table.

table.addColumns(monthCol, noScentCol) Next, let’s collect the reviews which happened after COVID started and summarize the counts per month and counts of negative reviews per month.

var start2020 = LocalDateTime.of(2020, JANUARY, 1, 0, 0) var byMonth2020 = table .where(r -> r.dateTimeColumn('Date').isAfter(start2020)) .sortAscendingOn('Date') .summarize('Noscent', countTrue, count) .by('Month') Next, we’ll count the proportion of “noscent” to total reviews.

double[] nsprop = byMonth2020.collect { it.getDouble('Number True [Noscent]') / it.getDouble('Count [Noscent]') } Now, we’ll calculate the standard error and high and low values for the error bars. Some libraries might be able to show error bars automatically. That’s not the case here, but it’s easy enough to create them ourselves:

var indices = 0..<byMonth2020.size()double[] se = indices.collect { sqrt(nsprop[it] * (1 - nsprop[it]) / byMonth2020[it].getDouble('Count [Noscent]')) }double[] barLower = indices.collect { nsprop[it] - se[it] }double[] barHigher = indices.collect { nsprop[it] + se[it] }byMonth2020.addColumns(dCol('nsprop', nsprop), dCol('barLower', barLower), dCol('barHigher', barHigher)) Now, we graph the results of our calculations:

var title = 'Proportion of top 5 scented candles on Amazon mentioning lack of scent by month 2020'var layout = Layout.builder(title, 'Month', 'Proportion of reviews') .showLegend(false).width(1000).height(500).build()var trace = BarTrace.builder( byMonth2020.categoricalColumn('Month'), byMonth2020.nCol('nsprop')) .orientation(VERTICAL).opacity(0.5).build()var errors = ScatterTrace.builder( byMonth2020.categoricalColumn('Month'), byMonth2020.nCol('barLower'), byMonth2020.nCol('barHigher'), byMonth2020.nCol('barLower'), byMonth2020.nCol('barHigher')) .type("candlestick").opacity(0.5).build()var chart = new Figure(layout, trace, errors)var parentDir = new File(url.file).parentFilePlot.show(chart, new File(parentDir, 'ReviewBarchart.html')) Our example uses the Tablesaw Plot.ly integration which fires open a browser page showing the following chart:

We can use similar code to look at how ratings for the top 3 best-selling candles have changed before and after COVID for scented and unscented candles. This results in the following graphs:

While this analysis doesn’t attempt to analyze all reasons for the change in candle ratings, we can see that the ratings for the scented candles drop off more dramatically than unscented ones once COVID infections increased.

As a final topic for this candle example, data scientists are often familiar with SQL, so instead of using Tablesaw’s Excel integration and subsequent table aggregation functions, we could just as easily read the spreadsheet using Apache POI (details here) and calculate the “noscent” proportions using Groovy’s language integrated query capability (also known as Ginq or GQuery).

from row in tablewhere row.Date > start2020groupby row.Monthorderby row.Dateselect row.Month, agg(\_g.toList().count{ it.row.NoScent }) / count(row.Date) We could similarly go on to calculate the error bars and display our results graphically.

Linear Regression

An example covering reading CSV files, using ordinary least squares, some additional graphing options including GroovyFX, and how to scale regression using Apache Beam.

Regression analysis is widely used for prediction and forecasting. It provides a statistical process for determining the relationship between some independent variables (or features) and some dependent variable (or desired outcome). Linear regression looks for a linear relationship between such variables.

For us, house price is the desired outcome, and we’ll look for a relationship with features like, number of bedrooms, number of bathrooms, square feet of living space, and others. We’ll use the Kaggle dataset for King County between May 2014 and May 2015.

Preliminary stepsIn our Candle example, we dived right in and started working with the data. In general, we might want to explore the data first and potentially perform some clean-up to remove anomalous data and work out how to handle potentially missing data.

Let’s look at the data again with Tablesaw:

var file = getClass().classLoader.getResource('kc\_house\_data.csv').fileTable rows = Table.read().csv(file) println rows.shape()println rows.structure()println rows.column("bedrooms").summary().print()println rows.where(rows.column("bedrooms").isGreaterThan(10)) It has this output:

kc\_house\_data.csv: 21613 rows X 21 cols Structure of kc\_house\_data.csv Index | Column Name | Column Type |------------------------------------------- 0 | id | LONG | 1 | date | STRING | 2 | price | DOUBLE | 3 | bedrooms | INTEGER | 4 | bathrooms | DOUBLE | 5 | sqft\_living | INTEGER | 6 | sqft\_lot | INTEGER | 7 | floors | DOUBLE | ... | ... | ... | Column: bedrooms Measure | Value |----------------------------------- Count | 21613 | sum | 72854 | Mean | 3.370841623097218 | Min | 0 | Max | 33 | Range | 33 | Variance | 0.8650150097573497 | Std. Dev | 0.930061831147451 |kc\_house\_data.csv id | price | bedrooms | bathrooms | sqft\_living | sqft\_lot | floors | ...----------------------------------------------------------------------------------1773100755 | 520000 | 11 | 3 | 3000 | 4960 | 2 | ...2402100895 | 640000 | 33 | 1.75 | 1620 | 6000 | 1 | ... The summary for the bedroom feature showed a maximum value of 33, so we displayed all houses with more than 10 bedrooms. Given the number of bathrooms and sqft_living size, the second of these appears like an anomaly in the data. Possibly someone typed 33, rather than 3, when entering the bedroom value.

Let’s remove all properties with more than 30 bedrooms and examine the number of bedrooms as a histogram. We’ll use Apache Commons CSV to read the CSV file, Apache Commons Math to collate our histogram and produce statistics, and GroovyFX for our graph.

``` var full = getClass().classLoader.getResource('kc_house_data.csv').file
var csv = CSV.withFirstRecordAsHeader().parse(new FileReader(full))
var all = csv.collect { it.bedrooms.toInteger() }.findAll{ it < 30 }
var stats = new SummaryStatistics()
all.each{ stats.addValue(it as double) }println stats.summary
var dist = new EmpiricalDistribution(all.max()).tap{load(all as double[])}var bins = dist.binStats.withIndex().collectMany { v, i -> [i.toString(), v.n] }

start { stage(title: 'Number of bedrooms histogram', show: true, width: 800, height: 600) { scene { barChart(title: 'Bedroom count', barGap: 0, categoryGap: 2) { series(name: 'Number of properties', data: bins) }
}
}
} ``` Which has this output:

StatisticalSummaryValues:n: 21612min: 0.0max: 11.0mean: 3.3694706644456733std dev: 0.907981787328914variance: 0.8244309261210092sum: 72821.0 And produces the following graph.

If we have more heavy duty data integration needs, we could consider incorporating Apache Camel into our workflow. We might for instance use it when exploring for outliers. We might also want to become a little more systematic in finding our outliers by using ZScores as found in Apache Commons Math, or a Support Vector Machines anomaly detector as found in Tribuo.

Once we are happy with exploring and potentially cleaning the data, we can move into building and using our prediction model.

Ordinary least squaresOrdinary least squares finds our regression relationship by minimizing residual errors.

The work is already done for us, we just need to use the appropriate regression class.

Let’s start by exploring a model with just the bedrooms feature as our independent variable. We’ll use Apache Commons CSV, Apache Commons Math, and GroovyFX.

var feature = 'bedrooms'var nonOutliers = feature == 'bedrooms' ? { it[0] < 30 } : { true }var file = getClass().classLoader.getResource('kc\_house\_data.csv').filevar csv = CSV.withFirstRecordAsHeader().parse(new FileReader(file))var all = csv.collect { [it[feature].toDouble(), it.price.toDouble()] }.findAll(nonOutliers)var reg = new SimpleRegression().tap{ addData(all as double[][]) }def (min, max) = all.transpose().with{ [it[0].min(), it[0].max()] }var predicted = [[min, reg.predict(min)], [max, reg.predict(max)]]start { stage(title: "Price vs $feature", show: true, width: 800, height: 600) { scene { lineChart(stylesheets: resource('/style.css')) { series(name: 'Actual', data: all) series(name: 'Predicted', data: predicted) } } }} This produces the following graph:

You should note that the data is spread widely on this graph and hence our model won’t be particularly good at predicting house prices.

To improve our model, we can also use multi linear regression which factors multiple features into the model. The algorithm automatically adjusts the coefficients for each feature. Features with a large positive impact on price will have a large coefficient. Features with a negative impact on price will have a negative coefficient. Features which aren’t really related to price will have a coefficient close to zero.

Let’s try multi-regression with Smile. Smile supports dataframes, various machine learning algorithms, NLP, and visualization. Here we’ll use its OLS regression class.

``` var price = table.column('price').toDoubleArray()
var model = OLS.fit(Formula.lhs('price'), table)
var predicted = model.predict(table)
double[][] data = [price, predicted].transpose()

var from = [price.toList().min(), predicted.min()].min()
var to = [price.toList().max(), predicted.max()].max()
var pts = [[from, from], [to, to]]
var ideal = LinePlot.of(pts as double[][], DASH, RED)

ScatterPlot.of(data, BLUE).canvas().with {
title = 'Actual vs predicted price'
setAxisLabels('Actual', 'Predicted')
add(ideal)
window()
}

`` We tell thefitmethod thatprice` is our dependent variable. It will attempt to find the relationship between that variable and all other variables.

It produces this output:

Here we are using Smile’s Java Swing-based visualization capabilities.

Note that the spread is much smaller than for simple regression, but it is still fairly spread. What this tells us is that multi regression is much better than simple regression, but overall, predicting house prices based solely on this raw data is hard.

Other algorithmsOrdinary least squares is only one algorithm we have up our sleeve for regression. We might consider Scalable Vector Machine (SVM), Stochastic Gradient Descent (SGD), or Classification and Regression Trees (CART). Unfortunately, for our dataset, all models have similar prediction capability.

Scaling optionsFor our small dataset, scaling is not a high priority. For datasets with millions of rows or hundreds of features, scaling becomes paramount. The good news is that numerous options exist for us to scale linear regression on the JVM.

Two great options are to use Apache Spark (covered next) or Apache Ignite (as shown here) to run our regression calculations. The standard ordinary least squares algorithm isn’t particularly well suited for parallel distribution but alternative algorithms which are better suited for execution within parallel clusters are included as part of those platforms machine learning libraries.

We can slightly adapt ordinary least squares to get reasonable results with concurrent evaluation. We essentially place random subsets of the data across our clusters, and later average out the slopes and intercepts found within the models from each cluster. This adapted algorithm can be used with any framework which supports concurrentexecution. For example, this example shows how to code that algorithm using GPars. If you are interested in GPars, you might want to also check out this blog post which goes into further GPars examples including how to use it with virtual threads.

For this blog post, we are going to show how to implement the adapted algorithm with Apache Beam, but first we’ll look at the cluster-friendly algorithm that comes with Apache Spark’s machine learning library.

Scaling with Apache SparkApache Spark is an open-source unified analytics engine for large-scale data processing. We’ll use the spark-mllib component to calculate our regression in a cluster:

def spark = builder().config('spark.master', 'local[8]').appName('HousePrices').orCreatedef file = HousePricesSpark.classLoader.getResource('kc\_house\_data.csv').fileint k = 5Dataset<Row> ds = spark.read() .format('csv') .options('header': 'true', 'inferSchema': 'true') .load(file)double[] splits = [80, 20]def (training, test) = ds.randomSplit(splits) String[] colNames = ds.columns().toList() - ['id', 'date', 'price']def assembler = new VectorAssembler(inputCols: colNames, outputCol: 'features') Dataset<Row> dataset = assembler.transform(training) def lr = new LinearRegression(labelCol: 'price', maxIter: 10) def model = lr.fit(dataset) println 'Coefficients:' println model.coefficients().values()[1..-1] .collect { sprintf '%.2f', it }.join(', ')def testSummary = model.evaluate(assembler.transform(test))printf 'RMSE: %.2f%n', testSummary.rootMeanSquaredErrorprintf 'r2: %.2f%n', testSummary.r2spark.stop() We’ll split our data into training and test datasets; using the test dataset to see how well our model performs. When run, we’ll see the following output (we show the coefficients for our model and the root mean squared error):

22/12/05 16:49:00 INFO SparkContext: Running Spark version 3.3.122/12/05 16:49:01 INFO SparkContext: Submitted application: HousePrices...41979.78, 80853.89, 0.15, 5412.83, 564343.22, 53834.10, 24817.09, 93195.29, -80662.68, -80694.28, -2713.58, 19.02, -628.67, 594468.23, -228397.19, 21.23, -0.42RMSE: 187242.12r2: 0.70...22/12/05 16:49:09 INFO SparkContext: Successfully stopped SparkContext Scaling with Apache BeamApache Beam provides a unified batch and streaming data processing framework which works with multiple languages like Java, Python and Groovy. It lets workloads be run across different runners like Apache Spark, Apache Flink and numerous others. We’ll show two implementation which use the native Java runner.

First, we define some helper methods which split our data into chunks to be run on different clusters and combine the results when we are done:

``` def features = [ 'price', 'bedrooms', 'bathrooms', 'sqft_living', 'sqft_living15', 'lat', 'sqft_above', 'grade', 'view', 'waterfront', 'floors']

def readCsvChunks = new DoFn() { @ProcessElement void processElement(@Element String path, OutputReceiver receiver) throws IOException { def chunkSize = 6000 def table = Read.csv(new File(path).toPath(), CSV.withFirstRecordAsHeader()) table = table.select(*features) table = table.stream().filter { it.apply('bedrooms') <= 30 }.collect(DataFrame.collect()) def idxs = 0..<table.nrows() for (nextChunkIdxs in idxs.shuffled().collate(chunkSize)) { def all = table.toArray().toList() receiver.output(all[nextChunkIdxs] as double[][]) }
}
}
def fitModel = new DoFn() {
@ProcessElement
void processElement(@Element double[][] rows, OutputReceiver receiver) throws IOException { def model = OLS.fit(Formula.lhs('price'), DataFrame.of(rows, features as String[])).coefficients() receiver.output(model)
}
}

def evalModel = { double[][] chunk, double[] model ->
double intercept = model[0]
double[] coefficients = model[1..-1] def predicted = chunk.collect { row -> intercept + dot(row[1..-1] as double[], coefficients) } def residuals = chunk.toList().indexed() .collect { idx, row -> predicted[idx] - row[0] } def rmse = sqrt(sumSq(residuals as double[]) / chunk.size()) [rmse, residuals.average(), chunk.size()] as double[]
}

def model2out = new DoFn() {
@ProcessElement
void processElement(@Element double[] ds, OutputReceiver out) {
out.output("** intercept: ${ds[0]}, coeffs: ${ds[1..-1].join(', ')}".toString())
}
}

def stats2out = new DoFn() {
@ProcessElement
void processElement(@Element double[] ds, OutputReceiver out) {
out.output("** rmse: ${ds[0]}, mean: ${ds[1]}, count: ${ds[2]}".toString())
}
} ``` With these helper methods in place, we can define our execution pipeline:

var csvChunks = p .apply(Create.of(filename)) .apply('Create chunks', ParDo.of(readCsvChunks)) var model = csvChunks .apply('Fit chunks', ParDo.of(fitModel)) .apply(Combine.globally(new MeanDoubleArrayCols())) var modelView = model .apply(View.<double[]>asSingleton()) csvChunks .apply(ParDo.of(new EvaluateModel(modelView, evalModel)).withSideInputs(modelView)) .apply(Combine.globally(new AggregateModelStats())) .apply('Log stats', ParDo.of(stats2out)).apply(Log.ofElements()) model .apply('Log model', ParDo.of(model2out)).apply(Log.ofElements()) If we apply a little bit of Groovy metaprogramming, we can tweak the execution pipeline to look like this:

``` var csvChunks = p | Create.of(filename) | 'Create chunks' >> ParDo.of(readCsvChunks)
var model = csvChunks | 'Fit chunks' >> ParDo.of(fitModel) | Combine.globally(new MeanDoubleArrayCols())
var modelView = model | View.asSingleton()

csvChunks | ParDo.of(new EvaluateModel(modelView, evalModel)).withSideInputs(modelView) | Combine.globally(new AggregateModelStats())
| 'Log stats' >> ParDo.of(stats2out) | Log.ofElements()
model | 'Log model' >> ParDo.of(model2out) | Log.ofElements() ``` It may seem like a small difference, but this code now looks very similar to the Python code that achieves the same thing. This could be a great productivity gain for projects which have a mix of Python and Groovy BEAM code.

Classification

An example covering reading CSV files, using traditional and neural network based classification algorithms, a glimpse at Jupyter Notebook options. Neural network solutions use Encog, Eclipse DeepLearning4J, and Deep Netts. Speeding up classification using GraalVM is also explored.

A classic data science dataset captures flower characteristics of Iris flowers. It captures the width and length of the sepals and petals for three species (Setosa, Versicolor, and Virginica).

The Iris project in the groovy-data-science repo is dedicated to this example. It includes a number of Groovy scripts and a Jupyter/BeakerX notebook highlighting this example comparing and contrasting various libraries and various classification algorithms.

Let’s look at how to classify the flowers using Weka’s decision tree algorithm:

def file = getClass().classLoader.getResource('iris\_data.csv').file as File def species = ['Iris-setosa', 'Iris-versicolor', 'Iris-virginica'] def loader = new CSVLoader(file: file) def model = new J48() def allInstances = loader.dataSet allInstances.classIndex = 4 model.buildClassifier(allInstances) println model Which has this output:

J48 pruned tree------------------Petal width 0.6| Petal width <= 1.7| | Petal length 4.9| | | Petal width 1.5: Iris-versicolor (3.0/1.0)| Petal width > 1.7: Iris-virginica (46.0/1.0)Number of Leaves : 5Size of the tree : 9 This model can visualized as follows:

Feel free to browse the other examples and the Jupyter/BeakerX notebook if you are interested in exploring additional classification techniques like naive Bayes or logistic regression.

For this blog, let’s dive further into just the deep learning classification examples.

Deep LearningWe’ll look at solutions using Encog, Eclipse DeepLearning4J and Deep Netts (with standard Java and as a native image using GraalVM) but first a brief introduction.

About Deep LearningDeep learning falls under the branches of machine learning and artificial intelligence. It involves multiple layers (hence the “deep”) of an artificial neural network. There are lots of ways to configure such networks and the details are beyond the scope of this blog post, but we can give some basic details. We will have four input nodes corresponding to the measurements of our four characteristics. We will have three output nodes corresponding to each possible class (species). We will also have one or more additional layers in between.

Each node in this network mimics to some degree a neuron in the human brain. Again, we’ll simplify the details. Each node has multiple inputs, which are given a particular weight, as well as an activation function which will determine whether our node “fires”. Training the model is a process which works out what the best weights should be.

The math involved for converting inputs to output for any node isn’t too hard. We could write it ourselves (as shown here using matrices and Apache Commons Math for a digit recognition example) but luckily we don’t have to. The libraries we are going to use do much of the work for us. They typically provide a fluent API which let’s us specify, in a somewhat declarative way, the layers in our network.

If you want to see more content about deep learning, consider also checking out this earlier JVM Advent blog post.

Just before exploring our examples, we should pre-warn folks that while we do time running of the examples, no attempt was made to rigorously ensure that the examples were identical across the different technologies. The different technologies support slightly different ways to set up their respective network layers. The parameters were tweaked so that when run there was typically at most one or two errors in the validation. Also, the initial parameters for the runs can be set with random or pre-defined seeds. When random ones are used, each run will have slightly different errors. We’d need to do some additional alignment of examples and use a framework like JMH if we wanted to get a more rigorous time comparison between the technologies. Never-the-less, it should give a very rough guide as to the speed to the various technologies.

EncogEncog is a pure Java machine learning framework that was created in 2008. There is also a C# port for .Net users. Encog is a simple framework that supports a number of advanced algorithms not found elsewhere but isn’t as widely used as other more recent frameworks.

The complete source code for our Iris classification example using Encog is here, but the critical piece is:

``` def model = new EncogModel(data).tap {
selectMethod(data, TYPE_FEEDFORWARD)
report = new ConsoleStatusReportable()
data.normalize()
holdBackValidation(0.3, true, 1001) // test with 30%
selectTrainingType(data)
}

def bestMethod = model.crossvalidate(5, true) // 5-fold cross-validation
println "Training error: " + pretty(calculateRegressionError(bestMethod, model.trainingDataset<))println "Validation error: " + pretty(calculateRegressionError(bestMethod, model.validationDataset)) ``` When we run the example, we see:

**paulk@pop-os**:**/extra/projects/iris\_encog**$ time groovy -cp "build/lib/*" IrisEncog.groovy 1/5 : Fold #11/5 : Fold #1/5: Iteration #1, Training Error: 1.43550735, Validation Error: 0.733022371/5 : Fold #1/5: Iteration #2, Training Error: 0.78845427, Validation Error: 0.73302237...5/5 : Fold #5/5: Iteration #163, Training Error: 0.00086231, Validation Error: 0.004271265/5 : Cross-validated score:0.10345818553910753Training error: 0.0009Validation error: 0.0991Prediction errors:predicted: Iris-virginica, actual: Iris-versicolor, normalized input: -0.0556, -0.4167, 0.3898, 0.2500Confusion matrix: Iris-setosa Iris-versicolor Iris-virginica Iris-setosa 19 0 0 Iris-versicolor 0 15 1 Iris-virginica 0 0 10real0m3.073suser0m9.973ssys0m0.367s We won’t explain all of the stats, but it basically says we have a pretty good model with low errors in prediction. If you see the green and purple points in the notebook image earlier in this blog, you’ll see there are some points which are going to be hard to predict correctly all the time. The confusion matrix shows that the model predicted one flower incorrectly on the validation dataset.

One very nice aspect of this library is that it is a single jar dependency!

Eclipse DeepLearning4jEclipse DeepLearning4j is a suite of tools for running deep learning on the JVM. It has support for scaling up to Apache Spark as well as some integration with python at a number of levels. It also provides integration to GPUs and C/++ libraries for native integration.

The complete source code for our Iris classification example using DeepLearning4J is here, with the main part shown below:

``` MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder()
.seed(seed)
.activation(Activation.TANH) // global activation
.weightInit(WeightInit.XAVIER)
.updater(new Sgd(0.1))
.l2(1e-4)
.list()
.layer(new DenseLayer.Builder().nIn(numInputs).nOut(3).build())
.layer(new DenseLayer.Builder().nIn(3).nOut(3).build())
.layer(new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
.activation(Activation.SOFTMAX) // override activation with softmax for this layer
.nIn(3).nOut(numOutputs).build())
.build()

def model = new MultiLayerNetwork(conf)
model.init()

model.listeners = new ScoreIterationListener(100)

1000.times { model.fit(train) }

def eval = new Evaluation(3)
def output = model.output(test.features)
eval.eval(test.labels, output)
println eval.stats() ``` When we run this example, we see:

**paulk@pop-os**:**/extra/projects/iris\_dl4j**$ time groovy -cp "build/lib/*" IrisDl4j.groovy [main] INFO org.nd4j.linalg.factory.Nd4jBackend - Loaded [CpuBackend] backend[main] INFO org.nd4j.nativeblas.NativeOpsHolder - Number of threads used for linear algebra: 4[main] INFO org.nd4j.nativeblas.Nd4jBlas - Number of threads used for OpenMP BLAS: 4[main] INFO org.nd4j.linalg.api.ops.executioner.DefaultOpExecutioner - Backend used: [CPU]; OS: [Linux]...[main] INFO org.deeplearning4j.optimize.listeners.ScoreIterationListener - Score at iteration 0 is 0.9707752535968273[main] INFO org.deeplearning4j.optimize.listeners.ScoreIterationListener - Score at iteration 100 is 0.3494968712782093...[main] INFO org.deeplearning4j.optimize.listeners.ScoreIterationListener - Score at iteration 900 is 0.03135504326480282========================Evaluation Metrics======================== # of classes: 3 Accuracy: 0.9778 Precision: 0.9778 Recall: 0.9744 F1 Score: 0.9752Precision, recall & F1: macro-averaged (equally weighted avg. of 3 classes)=========================Confusion Matrix========================= 0 1 2---------- 18 0 0 | 0 = 0 0 14 0 | 1 = 1 0 1 12 | 2 = 2Confusion matrix format: Actual (rowClass) predicted as (columnClass) N times==================================================================real0m5.856suser0m25.638ssys0m1.752s Again the stats tell us that the model is good. There is only one error in the confusion matrix for our testing dataset. DeepLearning4J does have an impressive range of technologies that can be used to enhance performance in certain scenarios. For this example, I enabled AVX (Advanced Vector Extensions) support but didn’t try using the CUDA/GPU support nor make use of any Apache Spark integration. The GPU option might have sped up the application but given the size of the dataset and the amount of calculations needed to train our network, it probably wouldn’t have sped up much.

What does this tell us? For this little example, the overheads of putting the plumbing in place to access native C++ implementations and so forth, outweighed the gains. Those features generally would come into their own for much larger datasets or massive amounts of calculations; tasks like intensive video processing spring to mind.

A downside of the impressive scaling options is the added complexity. The code was slightly more complex (around 30% greater line count) than the other options we are comparing. This stems from requirements that would be needed if we did want to make use of Spark integration even though we didn’t here. The good news is that once the work is done, if we did want to use Spark, that would now be relatively straight forward.

The other increase in complexity is the number of jar files needed in the classpath. I went with the easy option of using the nd4j-native-platform dependency plus added the org.nd4j:nd4j-native:1.0.0-M2:linux-x86_64-avx2 dependency for AVX support. This made my life easy but brought in over 170 jars including many for unneeded platforms. Having all those jars is great if users on other platforms want to try the example but it can be a little troublesome with certain tooling that breaks with long command lines on certain platforms. I could certainly do some more work to shrink those dependency lists if it became a real problem.

[For the interested reader, the groovy-data-science repo has other DeepLearning4J examples. The Weka library can wrap DeepLearning4J as shown for this Iris example here. There are also two variants of the digit recognition example we alluded to earlier using one and two layer neural networks.]

Deep NettsDeep Netts is a company offering a range of products and services related to deep learning. Here we are using the free open-source Deep Netts community edition pure java deep learning library. It provides support for the Java Visual Recognition API (JSR381). The expert group from JSR381 released their final spec earlier this year, so hopefully we’ll see more compliant implementations soon.

The complete source code for our Iris classification example using Deep Netts is here and the important part is below:

var splits = dataSet.split(0.7d, 0.3d) // 70/30% splitvar train = splits[0]var test = splits[1]var neuralNet = FeedForwardNetwork.builder() .addInputLayer(numInputs) .addFullyConnectedLayer(5, ActivationType.TANH) .addOutputLayer(numOutputs, ActivationType.SOFTMAX) .lossFunction(LossType.CROSS\_ENTROPY) .randomSeed(456) .build()neuralNet.trainer.with { maxError = 0.04f learningRate = 0.01f momentum = 0.9f optimizer = OptimizerType.MOMENTUM}neuralNet.train(train)new ClassifierEvaluator().with { println "CLASSIFIER EVALUATION METRICS\n${evaluate(neuralNet, test)}" println "CONFUSION MATRIX\n$confusionMatrix"} When we run this command we see:

**paulk@pop-os**:**/extra/projects/iris\_graalvm**$ time groovy -cp "build/lib/*" Iris.groovy 16:49:27.089 [main] INFO deepnetts.core.DeepNetts - ------------------------------------------------------------------------16:49:27.091 [main] INFO deepnetts.core.DeepNetts - TRAINING NEURAL NETWORK16:49:27.091 [main] INFO deepnetts.core.DeepNetts - ------------------------------------------------------------------------16:49:27.100 [main] INFO deepnetts.core.DeepNetts - Epoch:1, Time:6ms, TrainError:0.8584314, TrainErrorChange:0.8584314, TrainAccuracy: 0.525252516:49:27.103 [main] INFO deepnetts.core.DeepNetts - Epoch:2, Time:3ms, TrainError:0.52278274, TrainErrorChange:-0.33564866, TrainAccuracy: 0.52820516...16:49:27.911 [main] INFO deepnetts.core.DeepNetts - Epoch:3031, Time:0ms, TrainError:0.029988592, TrainErrorChange:-0.015680967, TrainAccuracy: 1.0TRAINING COMPLETED16:49:27.911 [main] INFO deepnetts.core.DeepNetts - Total Training Time: 820ms16:49:27.911 [main] INFO deepnetts.core.DeepNetts - ------------------------------------------------------------------------CLASSIFIER EVALUATION METRICSAccuracy: 0.95681506 (How often is classifier correct in total)Precision: 0.974359 (How often is classifier correct when it gives positive prediction)F1Score: 0.974359 (Harmonic average (balance) of precision and recall)Recall: 0.974359 (When it is actually positive class, how often does it give positive prediction)CONFUSION MATRIX none Iris-setosa Iris-versicolor Iris-virginica none 0 0 0 0 Iris-setosa 0 14 0 0Iris-versicolor 0 0 18 1 Iris-virginica 0 0 0 12real0m3.160suser0m10.156ssys0m0.483s This is faster than DeepLearning4j and similar to Encog. This is to be expected given our small data set and isn’t indicative of performance for larger problems.

Another plus is the dependency list. It isn’t quite the single jar situation as we saw with Encog but not far off. There is the Encog jar, the JSR381 VisRec API which is in a separate jar, and a handful of logging jars.

Deep Netts with GraalVMAnother technology we might want to consider if performance is important to us is GraalVM. GraalVM is a high-performance JDK distribution designed to speed up the execution of applications written in Java and other JVM languages. We’ll look at creating a native version of our Iris Deep Netts application. We used GraalVM 22.1.0 Java 17 CE and Groovy 4.0.3. We’ll cover just the basic steps but there are other places for additional setup info and troubleshooting help like here, here and here.

Groovy has two natures. Its dynamic nature supports adding methods at runtime through metaprogramming and interacting with method dispatch processing through missing method interception and other tricks. Some of these tricks make heavy use of reflection and dynamic class loading and cause problems for GraalVM which is trying to determine as much information as it can at compile time. Groovy’s static nature has a more limited set of metaprogramming capabilities but allows bytecode much closer to Java to be produced. Luckily, we aren’t relying on any dynamic Groovy tricks for our example. We’ll compile it up using static mode:

**paulk@pop-os**:**/extra/projects/iris\_graalvm**$ groovyc -cp "build/lib/*" --compile-static Iris.groovy Next we build our native application:

**paulk@pop-os**:**/extra/projects/iris\_graalvm**$ native-image --report-unsupported-elements-at-runtime \ --initialize-at-run-time=groovy.grape.GrapeIvy,deepnetts.net.weights.RandomWeights \ --initialize-at-build-time --no-fallback -H:ConfigurationFileDirectories=conf/ -cp ".:build/lib/*" Iris We told GraalVM to initialize GrapeIvy at runtime (to avoid needing Ivy jars in the classpath since Groovy will lazily load those classes only if we use @Grab statements). We also did the same for the RandomWeights class to avoid it being locked into a random seed fixed at compile time.

Now we are ready to run our application:

**paulk@pop-os**:**/extra/projects/iris\_graalvm**$ time ./iris...CLASSIFIER EVALUATION METRICSAccuracy: 0.93460923 (How often is classifier correct in total)Precision: 0.96491224 (How often is classifier correct when it gives positive prediction)F1Score: 0.96491224 (Harmonic average (balance) of precision and recall)Recall: 0.96491224 (When it is actually positive class, how often does it give positive prediction)CONFUSION MATRIX none Iris-setosa Iris-versicolor Iris-virginica none 0 0 0 0 Iris-setosa 0 21 0 0Iris-versicolor 0 0 20 2 Iris-virginica 0 0 0 17real 0m0.131suser 0m0.096ssys 0m0.029s We can see here that the speed has dramatically increased. This is great, but we should note, that using GraalVM often involves some tricky investigation especially for Groovy which by default has its dynamic nature. There are a few features of Groovy which won’t be available when using Groovy’s static nature and some libraries might be problematical. As an example, Deep Netts has log4j2 as one of its dependencies. At the time of writing, there are still issues using log4j2 with GraalVM. We excluded the log4j-core dependency and used log4j-to-slf4j backed by logback-classic to sidestep this problem.

Clustering

Looks at K-Means and other algorithms for clustering as well as using Apache Wayang and Apache Ignite for scaling clustering.

In an attempt to find the perfect single-malt Scotch whiskey, the whiskies produced from 86 distilleries have been ranked by expert tasters according to 12 criteria (Body, Sweetness, Malty, Smoky, Fruity, etc.).

While those rankings might prove interesting reading to some Whiskey advocates, it is difficult to draw many conclusions from the raw data alone. Clustering is a well-established area of statistical modelling where data is grouped into clusters. Members within a cluster should be similar to each other and different from the members of other clusters. Clustering is an unsupervised learning method. The categories are not predetermined but instead represent natural groupings which are found as part of the clustering process.

K-Means is the most common form of centroid clustering. The K represents the number of clusters to find. If we imagine points in 2D space, for k=3, we would start by picking 3 random points as our starting centroids.

We allocate all points to their closest centroid:

Given this allocation, we re-calculate each centroid from all of its points:

We repeat this process until either a stable centroid selection is found, or we have reached a certain number of iterations. For our case, we don’t have two dimensions but twelve. This makes it a little harder to visualize. We’ll cover that topic shortly.

Let’s first look at how we might use Tablesaw and Smile to create our KMeans model.

``` def file = getClass().classLoader.getResource('whiskey.csv').file
def helper = new TablesawUtil(file)
def rows = Table.read().csv(file)

def cols = ['Body', 'Sweetness', 'Smoky', 'Medicinal', 'Tobacco', 'Honey',
'Spicy', 'Winey', 'Nutty', 'Malty', 'Fruity', 'Floral']
def data = rows.as().doubleMatrix(*cols)

def pca = PCA.fit(data)
def dims = 3
pca.projection = dims
def projected = pca.project(data)
def clusters = KMeans.fit(data, 5)
def labels = clusters.y.collect { 'Cluster ' + (it + 1) }
rows = rows.addColumns(
*(0..
DoubleColumn.create("PCA${idx+1}", (0..<data.size()).collect{
projected[it][idx]
})
},
StringColumn.create('Cluster', labels),
DoubleColumn.create('Centroid', [10] * labels.size())
)
def centroids = pca.project(clusters.centroids)
def toAdd = rows.emptyCopy(1)
(0..
toAdd[0].setString('Cluster', 'Cluster ' + (idx+1))
(1..3).each { toAdd[0].setDouble('PCA' + it, centroids[idx][it-1]) }
toAdd[0].setDouble('Centroid', 50)
rows.append(toAdd)
}

def title = 'Clusters x Principal Components w/ centroids'
def type = dims == 2 ? ScatterPlot : Scatter3DPlot
helper.show(type.create(title, rows, *(1..dims).collect { "PCA$it" }, 'Centroid', 'Cluster'), 'KMeansClustersPcaCentroids') ``` There are a few points to note here. In order to display a graph, we need to reduce the number of dimensions. We use a technique called Principle Component Analysis (PCA) to do that.

The output will be:

Scaling OptionsWe can scale clustering in numerous ways. We might decide to use Apache Spark directly (shown here) since it has a clusterable K-Means implementation in its spark-mllib module. Lets instead explore Apache Wayang over the top of either a Java runner or Apache Spark. We’ll also look at using Apache Ignite.

Scaling with Apache WayangApache Wayang (incubating) is an API for big data cross-platform processing. It provides an abstraction over other platforms like Apache Spark and Apache Flink as well as a default built-in stream-based “platform”. The goal is to provide a consistent developer experience when writing code regardless of whether a light-weight or highly-scalable platform may eventually be required. Execution of the application is specified in a logical plan which is again platform agnostic. Wayang will transform the logical plan into a set of physical operators to be executed by specific underlying processing platforms.

We’ll start with defining a Point record:

record Point(double[] pts) implements Serializable { static Point fromLine(String line) { new Point(line.split(',')[2..-1]*.toDouble() as double[]) } } Our class is Serializable (more on that later) and contains a fromLine factory method to help us make points from a CSV file. We’ll do that ourselves rather than rely on other libraries which could assist. It’s not a 2D or 3D point for us but 12D corresponding to the 12 criteria. We just use a double array, so any dimension would be supported but the 12 comes from the number of columns in our data file.

We’ll define a related TaggedPointCounter record. It’s like a Point but tracks a cluster Id and count used when clustering the “points”:

record TaggedPointCounter(double[] pts, int cluster, long count) implements Serializable { TaggedPointCounter plus(TaggedPointCounter that) { new TaggedPointCounter((0..<pts.size()).collect{ pts[it] + that.pts[it] } as double[], cluster, count + that.count) } TaggedPointCounter average() { new TaggedPointCounter(pts.collect{ double d -> d/count } as double[], cluster, 0) } } We have plus and average methods which will be helpful in the map/reduce parts of the algorithm.

Another aspect of the KMeans algorithm is assigning points to the cluster associated with their nearest centroid. For 2 dimensions, recalling pythagoras’ theorem, this would be the square root of x squared plus y squared, where x and y are the distance of a point from the centroid in the x and y dimensions respectively. We’ll do the same across all dimensions and define the following helper class to capture this part of the algorithm:

``` class SelectNearestCentroid implements ExtendedSerializableFunction {
Iterable centroids

void open(ExecutionContext context) {  
    centroids = context.getBroadcast("centroids")    }  
TaggedPointCounter apply(Point p) {        def minDistance = Double.POSITIVE\_INFINITY  
    def nearestCentroidId = -1  
    for (c in centroids) {            def distance = sqrt((0..<p.pts.size()).collect{                p.pts[it] - c.pts[it]            }.sum{ it ** 2 } as double)            if (distance < minDistance) {                minDistance = distance                nearestCentroidId = c.cluster            }  
    }        new TaggedPointCounter(p.pts, nearestCentroidId, 1)    }

} `` In Wayang parlance, theSelectNearestCentroid` class is a UDF, a User-Defined Function. It represents some chunk of functionality where an optimization decision can be made about where to run the operation.

Once we get to using Spark, the classes in the map/reduce part of our algorithm will need to be serializable. Method closures in dynamic Groovy aren’t serializable. We have a few options to avoid using them. I’ll show one approach here which is to use some helper classes in places where we might typically use method references. Here are the helper classes:

``` class Clusterimplements SerializableFunction {
Integer apply(TaggedPointCounter tpc) { tpc.cluster() }}
class Average implements SerializableFunction {
TaggedPointCounter apply(TaggedPointCounter tpc) { tpc.average() }
}

class Plus implements SerializableBinaryOperator {
TaggedPointCounter apply(TaggedPointCounter tpc1, TaggedPointCounter tpc2) { tpc1.plus(tpc2) }
} ``` Now we are ready for our KMeans script:

``` int k = 5
int iterations = 20

// read in data from our file
def url = WhiskeyWayang.classLoader.getResource('whiskey.csv').file
def pointsData = new File(url).readLines()[1..-1].collect{ Point.fromLine(it) }
def dims = pointsData[0].pts.size()

// create some random points as initial centroids
def r = new Random()
def initPts = (1..k).collect { (0..<dims).collect { r.nextGaussian() + 2 } as double[] }

// create planbuilder with Java and Spark enabled
def configuration = new Configuration()
def context = new WayangContext(configuration)
.withPlugin(Java.basicPlugin())
.withPlugin(Spark.basicPlugin())
def planBuilder = new JavaPlanBuilder(context, "KMeans ($url, k=$k, iterations=$iterations)")

def points = planBuilder
.loadCollection(pointsData).withName('Load points')

def initialCentroids = planBuilder
.loadCollection((0.. new TaggedPointCounter(initPts[idx], idx, 0) })
.withName("Load random centroids")

def finalCentroids = initialCentroids
.repeat(iterations, currentCentroids ->
points.map(new SelectNearestCentroid())
.withBroadcast(currentCentroids, "centroids").withName("Find nearest centroid")
.reduceByKey(new Cluster(), new Plus()).withName("Add up points")
.map(new Average()).withName("Average points")
.withOutputClass(TaggedPointCounter)).withName("Loop").collect()

println 'Centroids:'
finalCentroids.each { c ->
println "Cluster$c.cluster: ${c.pts.collect{ sprintf('%.3f', it) }.join(', ')}"
} `` Here,kis the desired number of clusters, anditerationsis the number of times to iterate through the KMeans loop. ThepointsDatavariable is a list ofPointinstances loaded from our data file. We’d use thereadTextFilemethod instead ofloadCollectionif our data set was large. TheinitPts` variable is some random starting positions for our initial centroids. Being random, and given the way the KMeans algorithm works, it is possible that some of our clusters may have no points assigned.

Our algorithm works by assigning, at each iteration, all the points to their closest current centroid and then calculating the new centroids given those assignments. Finally, we output the results.

Using Wayang with the Java streams-backed platformAs we mentioned earlier, Wayang selects which platform(s) will run our application. It has numerous capabilities whereby cost functions and load estimators can be used to influence and optimize how the application is run. For our simple example, it is enough to know that even though we specified Java or Spark as options, Wayang knows that for our small data set, the Java streams option is the way to go.

Since we prime the algorithm with random data, we expect the results to be slightly different each time the script is run, but here is one output:

```

Task :WhiskeyWayang:run
Centroids:
Cluster0: 2.548, 2.419, 1.613, 0.194, 0.097, 1.871, 1.742, 1.774, 1.677, 1.935, 1.806, 1.613
Cluster2: 1.464, 2.679, 1.179, 0.321, 0.071, 0.786, 1.429, 0.429, 0.964, 1.643, 1.929, 2.179
Cluster3: 3.250, 1.500, 3.250, 3.000, 0.500, 0.250, 1.625, 0.375, 1.375, 1.375, 1.250, 0.250
Cluster4: 1.684, 1.842, 1.211, 0.421, 0.053, 1.316, 0.632, 0.737, 1.895, 2.000, 1.842, 1.737... ``` Which if plotted looks like this:

If you are interested, check out the examples in the repo links at the end of this article to see the code for producing this centroid spider plot or the Jupyter/BeakerX notebook in this project’s github repo.

Using Wayang with Apache SparkGiven our small dataset size and no other customization, Wayang will choose the Java streams based solution. We could use Wayang optimization features to influence which processing platform it chooses, but to keep things simple, we’ll just disable the Java streams platform in our configuration by making the following change in our code:

Now when we run the application, the output will be something like this (a solution similar to before but with 1000+ extra lines of Spark and Wayang log information – truncated for presentation purposes):

[main] INFO org.apache.spark.SparkContext - Running Spark version 3.3.0[main] INFO org.apache.spark.util.Utils - Successfully started service 'sparkDriver' on port 62081....Centroids:Cluster4: 1.414, 2.448, 0.966, 0.138, 0.034, 0.862, 1.000, 0.483, 1.345, 1.690, 2.103, 2.138Cluster0: 2.773, 2.455, 1.455, 0.000, 0.000, 1.909, 1.682, 1.955, 2.091, 2.045, 2.136, 1.818Cluster1: 1.762, 2.286, 1.571, 0.619, 0.143, 1.714, 1.333, 0.905, 1.190, 1.952, 1.095, 1.524Cluster2: 3.250, 1.500, 3.250, 3.000, 0.500, 0.250, 1.625, 0.375, 1.375, 1.375, 1.250, 0.250Cluster3: 2.167, 2.000, 2.167, 1.000, 0.333, 0.333, 2.000, 0.833, 0.833, 1.500, 2.333, 1.667...[shutdown-hook-0] INFO org.apache.spark.SparkContext - Successfully stopped SparkContext[shutdown-hook-0] INFO org.apache.spark.util.ShutdownHookManager - Shutdown hook called A goal of Apache Wayang is to allow developers to write platform-agnostic applications. While this is mostly true, the abstractions aren’t perfect. As an example, if I know I am only using the streams-backed platform, I don’t need to worry about making any of my classes serializable (which is a Spark requirement). In our example, we could have omitted the “implements Serializable” part of the TaggedPointCounter record, and we could have used a method reference TaggedPointCounter::average instead of our Average helper class. This isn’t meant to be a criticism of Wayang, after all if you want to write cross-platform UDFs, you might expect to have to follow some rules. Instead, it is meant to just indicate that abstractions often have leaks around the edges. Sometimes those leaks can be beneficially used, other times they are traps waiting for unknowing developers.

To summarise, if using the Java streams-backed platform, you can run the application on JDK17 (which uses native records) as well as JDK11 and JDK8 (where Groovy provides emulated records). Also, we could make numerous simplifications if we desired. When using the Spark processing platform, the potential simplifications aren’t applicable, and we can run on JDK8 and JDK11 (Spark isn’t yet supported on JDK17).

Scaling with Apache IgniteApache Ignite is a distributed database for high-performance computing with in-memory speed. It makes a cluster (or grid) of nodes appear like an in-memory cache.

This explanation drastically simplifies Ignite’s feature set. Ignite can be used as:

  • an in-memory cache with special features like SQL querying and transactional properties
  • an in-memory data-grid with advanced read-through & write-through capabilities on top of one or more distributed databases
  • an ultra-fast and horizontally scalable in-memory database
  • a high-performance computing engine for custom or built-in tasks including machine learning

It is mostly this last capability that we will use. Ignite’s Machine Learning API has purpose built, cluster-aware machine learning and deep learning algorithms for Classification, Regression, Clustering, and Recommendation among others. We’ll use the distributed K-means Clustering algorithm from their library.

Apache Ignite has special capabilities for reading data into the cache. We could use IgniteDataStreamer or IgniteCache.loadCache() and load data from files, stream sources, various database sources and so forth. This is particularly relevant when using a cluster.

For our little example, our data is in a relatively small CSV file and we will be using a single node, so we’ll just read our data using Apache Commons CSV:

var file = getClass().classLoader.getResource('whiskey.csv').file as File var rows = file.withReader {r -> RFC4180.parse(r).records*.toList() } var data = rows[1..-1].collect{ it[2..-1]*.toDouble() } as double[][] We’ll configure our single node Ignite data cache using code (but we could place the details in a configuration file in more complex scenarios):

var cfg = new IgniteConfiguration( peerClassLoadingEnabled: true, discoverySpi: new TcpDiscoverySpi( ipFinder: new TcpDiscoveryMulticastIpFinder( addresses: ['127.0.0.1:47500..47509'] ) ) ) Next, we’ll create a few helper variables:

var features = ['Body', 'Sweetness', 'Smoky', 'Medicinal', 'Tobacco', 'Honey', 'Spicy', 'Winey', 'Nutty', 'Malty', 'Fruity', 'Floral']var pretty = this.&sprintf.curry('%.4f')var dist = new EuclideanDistance()var vectorizer = new DoubleArrayVectorizer().labeled(FIRST) Now we start the node, populate the cache, run our k-means algorithm, and print the result.

Ignition.start(cfg).withCloseable { ignite -> println ">>> Ignite grid started for data: ${data.size()} rows X ${data[0].size()} cols" var dataCache = ignite.createCache(new CacheConfiguration<Integer, double[]>( name: "TEST\_${UUID.randomUUID()}", affinity: new RendezvousAffinityFunction(false, 10))) data.indices.each { int i -> dataCache.put(i, data[i]) } var trainer = new KMeansTrainer().withDistance(dist).withAmountOfClusters(5) var mdl = trainer.fit(ignite, dataCache, vectorizer) println ">>> KMeans centroids:\n${features.join(', ')} var centroids = mdl.centers*.all() centroids.each { c -> println c*.get().collect(pretty).join(', ') dataCache.destroy() Here is the output:

[18:13:11] \_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_[18:13:11] / \_/ \_\_\_/ |/ / \_/\_ \_\_/ \_\_/[18:13:11] \_/ // (7 7 // / / / / \_/[18:13:11] /\_\_\_/\\_\_\_/\_/|\_/\_\_\_/ /\_/ /x\_\_\_/[18:13:11][18:13:11] ver. 2.14.0#20220929-sha1:951e8deb[18:13:11] 2022 Copyright(C) Apache Software Foundation...[18:13:11] Configured plugins:[18:13:11] ^-- ml-inference-plugin 1.0.0[18:13:14] Ignite node started OK (id=f731e4ab)...>>> Ignite grid started for data: 86 rows X 13 cols>>> KMeans centroidsBody, Sweetness, Smoky, Medicinal, Tobacco, Honey, Spicy, Winey, Nutty, Malty, Fruity, Floral2.7037, 2.4444, 1.4074, 0.0370, 0.0000, 1.8519, 1.6667, 1.8519, 1.8889, 2.0370, 2.1481, 1.66671.8500, 1.9000, 2.0000, 0.9500, 0.1500, 1.1000, 1.5000, 0.6000, 1.5500, 1.7000, 1.3000, 1.50001.2667, 2.1333, 0.9333, 0.1333, 0.0000, 1.0667, 0.8000, 0.5333, 1.8000, 1.7333, 2.2667, 2.26673.6667, 1.5000, 3.6667, 3.3333, 0.6667, 0.1667, 1.6667, 0.5000, 1.1667, 1.3333, 1.1667, 0.16671.5000, 2.8889, 1.0000, 0.2778, 0.1667, 1.0000, 1.2222, 0.6111, 0.5556, 1.7778, 1.6667, 2.0000[18:13:15] Ignite node stopped OK [uptime=00:00:00.663] We can plot the centroid characteristics in a spider plot.

Natural Language Processing

Covers various natural language processing examples including detecting the language in use, parts of speech, entities, sentiment analysis, and universal sentence encoding using Apache OpenNLP, Stanford CoreNLP, and Datumbox. Also looks at scaling natural language processing using Spark NLP and DJL with TensorFlow

Natural Language Processing is certainly a large and sometimes complex topic with many aspects. Some of those aspects deserve entire blogs in their own right. For this blog, we will briefly look at a few simple use cases illustrating where you might be able to use NLP technology in your own project.

Language DetectionKnowing what language some text represents can be a critical first step to subsequent processing. Let’s look at how to predict the language using a pre-built model and Apache OpenNLP. Here, ResourceHelper is a utility class used to download and cache the model. The first run may take a little while as it downloads the model. Subsequent runs should be fast. Here we are using a well-known model referenced in the OpenNLP documentation.

``` def helper = new ResourceHelper('https://dlcdn.apache.org/opennlp/models/langdetect/1.8.3/')
def model = new LanguageDetectorModel(helper.load('langdetect-183'))
def detector = new LanguageDetectorME(model)

[ spa: 'Bienvenido a Madrid', fra: 'Bienvenue à Paris',
dan: 'Velkommen til København', bul: 'Добре дошли в София'
].each { k, v ->
assert detector.predictLanguage(v).lang == k
} `` TheLanguageDetectorME` class lets us predict the language. In general, the predictor may not be accurate on small samples of text but it was good enough for our example. We’ve used the language code as the key in our map and we check that against the predicted language.

A more complex scenario is training your own model. Let’s look at how to do that with Datumbox. Datumbox has a pre-trained models zoo but its language detection model didn’t seem to work well for the small snippets in the next example, so we’ll train our own model. First, we’ll define our datasets:

def loader = getClass().classLoaderdef datasets = [ English: loader.getResource("training.language.en.txt").toURI(), French: loader.getResource("training.language.fr.txt").toURI(), German: loader.getResource("training.language.de.txt").toURI(), Spanish: loader.getResource("training.language.es.txt").toURI(), Indonesian: loader.getResource("training.language.id.txt").toURI()] The de training dataset comes from the Datumbox examples. The training datasets for the other languages are from Kaggle.

We set up the training parameters needed by our algorithm:

def trainingParams = new TextClassifier.TrainingParameters( numericalScalerTrainingParameters: null, featureSelectorTrainingParametersList: [new ChisquareSelect.TrainingParameters()], textExtractorParameters: new NgramsExtractor.Parameters(), modelerTrainingParameters: new MultinomialNaiveBayes.TrainingParameters() ) Here, we’ll use a Naïve Bayes model with Chisquare feature selection.

Next we create our algorithm, train it with our training dataset, and then validate it against the training dataset. We’d normally want to split the data into training and testing datasets, to give us a more accurate statistic of the accuracy of our model. But for simplicity, while still illustrating the API, we’ll train and validate with our entire dataset:

def config = Configuration.configurationdef classifier = MLBuilder.create(trainingParams, config)classifier.fit(datasets)def metrics = classifier.validate(datasets)println "Classifier Accuracy (using training data): $metrics.accuracy" When run, we see the following output:

Classifier Accuracy (using training data): 0.9975609756097561 Our test dataset will consist of some hard-coded illustrative phrases. Let’s use our model to predict the language for each phrase:

println 'Classifying Predicted Probability' [ 'Bienvenido a Madrid', 'Bienvenue à Paris', 'Welcome to London', 'Willkommen in Berlin', 'Selamat Datang di Jakarta' ].each { txt -> def r = classifier.predict(txt) def predicted = r.YPredicted.center(10) def probability = sprintf '%6.2f', r.YPredictedProbabilities.get(predicted) println "${txt.padRight(30)}$predicted$probability When run, it has this output:

Classifying Predicted ProbabilityBienvenido a Madrid Spanish 0.83Bienvenue à Paris French 0.71Welcome to London English 1.00Willkommen in Berlin German 0.84Selamat Datang di Jakarta Indonesian 1.00 Given these phrases are very short, it is nice to get them all correct, and the probabilities all seem reasonable for this scenario.

Parts of SpeechParts of speech (POS) analysers examine each part of a sentence (the words and potentially punctuation) in terms of the role they play in a sentence. A typical analyser will assign or annotate words with their role like identifying nouns, verbs, adjectives and so forth. This can be a key early step for tools like the voice assistants from Amazon, Apple and Google.

We’ll start by looking at a perhaps lesser known library Nlp4j before looking at some others. In fact, there are multiple Nlp4j libraries. We’ll use the one from nlp4j.org, which seems to be the most active and recently updated.

This library uses the Stanford CoreNLP library under the covers for its English POS functionality. The library has the concept of documents, and annotators that work on documents. Once annotated, we can print out all of the discovered words and their annotations:

var doc = new DefaultDocument() doc.putAttribute('text', 'I eat sushi with chopsticks.') var ann = new StanfordPosAnnotator() ann.setProperty('target', 'text') ann.annotate(doc) println doc.keywords.collect{ k -> "${k.facet - 'word.'}(${k.str})" }.join(' ') When run, we see the following output:

PRP(I) VBP(eat) NN(sushi) IN(with) NNS(chopsticks) .(.) The annotations, also known as tags or facets, for this example are as follows:

| PRP | Personal pronoun | | VBP | Present tense verb | | NN | Noun, singular | | IN | Preposition | | NNS | Noun, plural |

The documentation for the libraries we are using give a more complete list of such annotations.

A nice aspect of this library is support for other languages, in particular, Japanese. The code is very similar but uses a different annotator:

doc = new DefaultDocument() doc.putAttribute('text', '私は学校に行きました。') ann = new KuromojiAnnotator() ann.setProperty('target', 'text') ann.annotate(doc) println doc.keywords.collect{ k -> "${k.facet}(${k.str})" }.join(' ') When run, we see the following output:

名詞(私) 助詞(は) 名詞(学校) 助詞(に) 動詞(行き) 助動詞(まし) 助動詞(た) 記号(。) Before progressing, we’ll highlight the result visualization capabilities of the GroovyConsole. This feature lets us write a small Groovy script which converts results to any swing component. In our case we’ll convert lists of annotated strings to a JLabel component containing HTML including colored annotation boxes. The details aren’t included here but can be found in the repo. We need to copy that file into our ~/.groovy folder and then enable script visualization as shown here:

Then we should see the following when running the script:

The visualization is purely optional but adds a nice touch. If using Groovy in notebook environments like Jupyter/BeakerX, there might be visualization tools in those environments too.

Let’s look at a larger example using the Smile library.

First, the sentences that we’ll examine:

def sentences = [ 'Paul has two sisters, Maree and Christine.', 'No wise fish would go anywhere without a porpoise', 'His bark was much worse than his bite', 'Turn on the lights to the main bedroom', "Light 'em all up", 'Make it dark downstairs' ] A couple of those sentences might seem a little strange but they are selected to show off quite a few of the different POS tags.

Smile has a tokenizer class which splits a sentence into words. It handles numerous cases like contractions and abbreviations (“e.g.”, “’tis”, “won’t”). Smile also has a POS class based on the hidden Markov model and a built-in model is used for that class. Here is our code using those classes:

def tokenizer = new SimpleTokenizer(true)sentences.each { def tokens = Arrays.stream(tokenizer.split(it)).toArray(String[]::new) def tags = HMMPOSTagger.default.tag(tokens)*.toString() println tokens.indices.collect{tags[it] == tokens[it] ? tags[it] : "${tags[it]}(${tokens[it]})" }.join(' ')} We run the tokenizer for each sentence. Each token is then displayed directly or with its tag if it has one.

Running the script gives this visualization:

|

| PaulNNP | hasVBZ | twoCD | sistersNNS | , | MareeNNP | andCC | ChristineNNP | . |

| NoDT | wiseJJ | fishNN | wouldMD | goVB | anywhereRB | withoutIN | aDT | porpoiseNN |

| HisPRP$ | barkNN | wasVBD | muchRB | worseJJR | thanIN | hisPRP$ | biteNN |

| TurnVB | onIN | theDT | lightsNNS | toTO | theDT | mainJJ | bedroomNN |

| LightNNP | ’emPRP | allRB | upRB |

| MakeVB | itPRP | darkJJ | downstairsNN |

|

[Note: the scripts in the repo just print to stdout which is perfect when using the command-line or IDEs. The visualization in the GoovyConsole kicks in only for the actual result. So, if you are following along at home and wanting to use the GroovyConsole, you’d change the each to collect and remove the println, and you should be good for visualization.]

The OpenNLP code is very similar:

def tokenizer = SimpleTokenizer.INSTANCEsentences.each { String[] tokens = tokenizer.tokenize(it) def posTagger = new POSTaggerME('en') String[] tags = posTagger.tag(tokens) println tokens.indices.collect{tags[it] == tokens[it] ? tags[it] : "${tags[it]}(${tokens[it]})" }.join(' ')} OpenNLP allows you to supply your own POS model but downloads a default one if none is specified.

When the script is run, it has this visualization:

|

| PaulPROPN | hasVERB | twoNUM | sistersNOUN | ,PUNCT | MareePROPN | andCCONJ | ChristinePROPN | .PUNCT |

| NoDET | wiseADJ | fishNOUN | wouldAUX | goVERB | anywhereADV | withoutADP | aDET | porpoiseNOUN |

| HisPRON | barkNOUN | wasAUX | muchADV | worseADJ | thanADP | hisPRON | biteNOUN |

| TurnVERB | onADP | theDET | lightsNOUN | toADP | theDET | mainADJ | bedroomNOUN |

| LightNOUN | ‘PUNCT | emNOUN | allADV | upADP |

| MakeVERB | itPRON | darkADJ | downstairsNOUN |

|

The observant reader may have noticed some slight differences in the tags used in this library. They are essentially the same but using slightly different names. This is something to be aware of when swapping between POS libraries or models. Make sure you look up the documentation for the library/model you are using to understand the available tag types.

Entity DetectionNamed entity recognition (NER), seeks to identity and classify named entities in text. Categories of interest might be persons, organizations, locations dates, etc. It is another technology used in many fields of NLP.

We’ll start with our sentences to analyse:

String[] sentences = [ "A commit by Daniel Sun on December 6, 2020 improved Groovy 4's language integrated query.", "A commit by Daniel on Sun., December 6, 2020 improved Groovy 4's language integrated query.", 'The Groovy in Action book by Dierk Koenig et. al. is a bargain at $50, or indeed any price.', 'The conference wrapped up yesterday at 5:30 p.m. in Copenhagen, Denmark.', 'I saw Ms. May Smith waving to June Jones.', 'The parcel was passed from May to June.', 'The Mona Lisa by Leonardo da Vinci has been on display in the Louvre, Paris since 1797.' ] For this example, we’ll use some well-known models, we’ll focus on the person, money, date, time, and location models:

def base = 'http://opennlp.sourceforge.net/models-1.5'def modelNames = ['person', 'money', 'date', 'time', 'location']def finders = modelNames.collect { model -> new NameFinderME(DownloadUtil.downloadModel(new URL("$base/en-ner-${model}.bin"), TokenNameFinderModel))} We’ll now tokenize our sentences:

def tokenizer = SimpleTokenizer.INSTANCEsentences.each { sentence -> String[] tokens = tokenizer.tokenize(sentence) Span[] tokenSpans = tokenizer.tokenizePos(sentence) def entityText = [:] def entityPos = [:] finders.indices.each {fi -> // could be made smarter by looking at probabilities and overlapping spans Span[] spans = finders[fi].find(tokens) spans.each{span -> def se = span.start..<span.end def pos = (tokenSpans[se.from].start)..<(tokenSpans[se.to].end) entityPos[span.start] = pos entityText[span.start] = "$span.type(${sentence[pos]})" } entityPos.keySet().sort().reverseEach { def pos = entityPos[it] def (from, to) = [pos.from, pos.to + 1] sentence = sentence[0..<from] + entityText[it] + sentence[to..-1] } println sentence} And when visualized, shows this:

|

| Acommitby | Daniel Sunperson | on | December 6, 2020date | improved Groovy 4’slanguage integrated query. |

| Acommitby | Danielperson | onSun., | December 6, 2020date | improved Groovy 4’slanguageintegrated query. |

| The Groovy inAction book by | Dierk Koenigperson | et. al. is abargain at | $50money | , or indeedany price. |

| Theconferencewrapped up | yesterdaydate | at | 5:30 p.m.time | in | Copenhagenlocation | , | Denmarklocation | . |

| I saw Ms. | May Smithperson | waving to | June Jonesperson | . |

| The parcel was passed from | May to Junedate | . |

| The MonaLisa by | Leonardo da Vinciperson | has been ondisplay inthe Louvre, | Parislocation | since 1797date | . |

|

We can see here that most examples have been categorized as we might expect. We’d have to improve our model for it to do a better job on the “May to June” example.

Scaling Entity DetectionFor large problems, we can also run our named entity detection algorithms on platforms like Spark NLP which adds NLP functionality to Apache Spark. We’ll use glove_100d embeddings and the onto_100 NER model.

var assembler = new DocumentAssembler(inputCol: 'text', outputCol: 'document', cleanupMode: 'disabled')var tokenizer = new Tokenizer(inputCols: ['document'] as String[], outputCol: 'token')var embeddings = WordEmbeddingsModel.pretrained('glove\_100d').tap { inputCols = ['document', 'token'] as String[] outputCol = 'embeddings'}var model = NerDLModel.pretrained('onto\_100', 'en').tap { inputCols = ['document', 'token', 'embeddings'] as String[] outputCol ='ner'}var converter = new NerConverter(inputCols: ['document', 'token', 'ner'] as String[], outputCol: 'ner\_chunk')var pipeline = new Pipeline(stages: [assembler, tokenizer, embeddings, model, converter] as PipelineStage[])var spark = SparkNLP.start(false, false, '16G', '', '', '')var text = [ "The Mona Lisa is a 16th century oil painting created by Leonardo. It's held at the Louvre in Paris."]var data = spark.createDataset(text, Encoders.STRING()).toDF('text')var pipelineModel = pipeline.fit(data)var transformed = pipelineModel.transform(data)transformed.show()use(SparkCategory) { transformed.collectAsList().each { row -> def res = row.text def chunks = row.ner\_chunk.reverseIterator() while (chunks.hasNext()) { def chunk = chunks.next() int begin = chunk.begin int end = chunk.end def entity = chunk.metadata.get('entity').get() res = res[0..<begin] + "$entity($chunk.result)" + res[end<..-1] } println res }} There is no need for us to go into all of the details here. In summary, the code sets up a pipeline that transforms our input sentences, via a series of steps, into chunks, where each chunk corresponds to a detected entity. Each chunk has a start and ending position, and an associated tag type.

This may not seem like it is much different to our earlier examples, but if we had large volumes of data and we were running in a large cluster, the work could be spread across worker nodes within the cluster.

Here we have used a utility SparkCategory class which makes accessing the information in Spark Row instances a little nicer in terms of Groovy shorthand syntax. We can use row.text instead of row.get(row.fieldIndex('text')). Here is the code for this utility class:

class SparkCategory { static get(Row r, String field) { r.get(r.fieldIndex(field)) } } If doing more than this simple example, the use of SparkCategory could be made implicit through various standard Groovy techniques.

When we run our script, we see the following output:

22/08/07 12:31:39 INFO SparkContext: Running Spark version 3.3.0...glove\_100d download started this may take some time.Approximate size to download 145.3 MB...onto\_100 download started this may take some time.Approximate size to download 13.5 MB...+--------------------+--------------------+--------------------+--------------------+--------------------+--------------------+| text| document| token| embeddings| ner| ner\_chunk|+--------------------+--------------------+--------------------+--------------------+--------------------+--------------------+|The Mona Lisa is ...|[{document, 0, 98...|[{token, 0, 2, Th...|[{word\_embeddings...|[{named\_entity, 0...|[{chunk, 0, 12, T...|+--------------------+--------------------+--------------------+--------------------+--------------------+--------------------+PERSON(The Mona Lisa) is a DATE(16th century) oil painting created by PERSON(Leonardo). It's held at the FAC(Louvre) in GPE(Paris). The result has the following visualization:

|

| The Mona LisaPERSON | is a | 16th centuryDATE | oil painting created by | LeonardoPERSON | . It’s held at the | LouvreFAC | in | ParisGPE | . |

|

Here FAC is facility (buildings, airports, highways, bridges, etc.) and GPE is Geo-Political Entity (countries, cities, states, etc.).

Sentiment AnalysisSentiment analysis is a NLP technique used to determine whether data is positive, negative, or neutral. Stanford CoreNLP has default models it uses for this purpose:

def doc = new Document(''' StanfordNLP is fantastic! Groovy is great fun! Math can be hard! ''') for (sent in doc.sentences()) { println "${sent.toString().padRight(40)} ${sent.sentiment()}" } Which has the following output:

[main] INFO edu.stanford.nlp.parser.common.ParserGrammar - Loading parser from serialized file edu/stanford/nlp/models/lexparser/englishPCFG.ser.gz ... done [0.6 sec].[main] INFO edu.stanford.nlp.sentiment.SentimentModel - Loading sentiment model edu/stanford/nlp/models/sentiment/sentiment.ser.gz ... done [0.1 sec].StanfordNLP is fantastic! POSITIVEGroovy is great fun! VERY\_POSITIVEMath can be hard! NEUTRAL In addition to using pre-trained models, we can also train our own. Let’s start with two datasets:

def datasets = [ positive: getClass().classLoader.getResource("rt-polarity.pos").toURI(), negative: getClass().classLoader.getResource("rt-polarity.neg").toURI() ] Initially, we’ll use Datumbox which, as we saw earlier, requires training parameters for our algorithm:

def trainingParams = new TextClassifier.TrainingParameters( numericalScalerTrainingParameters: null, featureSelectorTrainingParametersList: [new ChisquareSelect.TrainingParameters()], textExtractorParameters: new NgramsExtractor.Parameters(), modelerTrainingParameters: new MultinomialNaiveBayes.TrainingParameters() ) We now create our algorithm, train it with or training dataset, and for illustrative purposes validate against the training dataset:

def config = Configuration.configurationTextClassifier classifier = MLBuilder.create(trainingParams, config)classifier.fit(datasets)def metrics = classifier.validate(datasets)println "Classifier Accuracy (using training data): $metrics.accuracy" The output is shown here:

[main] INFO com.datumbox.framework.core.common.dataobjects.Dataframe$Builder - Dataset Parsing positive class[main] INFO com.datumbox.framework.core.common.dataobjects.Dataframe$Builder - Dataset Parsing negative class...Classifier Accuracy (using training data): 0.8275959103273615 Now we can test our model against several sentences:

['Datumbox is divine!', 'Groovy is great fun!', 'Math can be hard!'].each { def r = classifier.predict(it) def predicted = r.YPredicted def probability = sprintf '%4.2f', r.YPredictedProbabilities.get(predicted) println "Classifing: '$it', Predicted: $predicted, Probability: $probability"} Which has this output:

...[main] INFO com.datumbox.framework.applications.nlp.TextClassifier - predict()...Classifing: 'Datumbox is divine!', Predicted: positive, Probability: 0.83Classifing: 'Groovy is great fun!', Predicted: positive, Probability: 0.80Classifing: 'Math can be hard!', Predicted: negative, Probability: 0.95 We can do the same thing but with OpenNLP. First, we collect our input data. OpenNLP is expecting it in a single dataset with tagged examples:

def trainingCollection = datasets.collect { k, v -> new File(v).readLines().collect{"$k $it".toString() } }.sum() Now, we’ll train two models. One uses naïve bayes, the other maxent. We train up both variants.

def variants = [ Maxent : new TrainingParameters(), NaiveBayes: new TrainingParameters((CUTOFF\_PARAM): '0', (ALGORITHM\_PARAM): NAIVE\_BAYES\_VALUE)]def models = [:]variants.each{ key, trainingParams -> def trainingStream = new CollectionObjectStream(trainingCollection) def sampleStream = new DocumentSampleStream(trainingStream) println "\nTraining using $key" models[key] = DocumentCategorizerME.train('en', sampleStream, trainingParams, new DoccatFactory())} Now we run sentiment predictions on our sample sentences using both variants:

def w = sentences*.size().max()variants.each { key, params -> def categorizer = new DocumentCategorizerME(models[key]) println "\nAnalyzing using $key" sentences.each { def result = categorizer.categorize(it.split('[ !]')) def category = categorizer.getBestCategory(result) def prob = sprintf '%4.2f', result[categorizer.getIndex(category)] println "${it.padRight(w)} $category ($prob)}" }} When we run this we get:

Training using Maxent ...done....Training using NaiveBayes ...done....Analyzing using MaxentOpenNLP is fantastic! positive (0.64)}Groovy is great fun! positive (0.74)}Math can be hard! negative (0.61)}Analyzing using NaiveBayesOpenNLP is fantastic! positive (0.72)}Groovy is great fun! positive (0.81)}Math can be hard! negative (0.72)} The models here appear to have lower probability levels compared to the model we trained for Datumbox. We could try tweaking the training parameters further if this was a problem. We’d probably also need a bigger testing set to convince ourselves of the relative merits of each model. Some models can be over-trained on small datasets and perform very well with data similar to their training datasets but perform much worse for other data.

Universal Sentence EncodingThis example is inspired from the UniversalSentenceEncoder example in the DJL examples module. It looks at using the universal sentence encoder model from TensorFlow Hub via the DeepJavaLibrary (DJL) api.

First we define a translator. The Translator interface allow us to specify pre and post processing functionality.

``` class MyTranslator implements NoBatchifyTranslator { @Override
NDList processInput(TranslatorContext ctx, String[] raw) {
var factory = ctx.NDManager
var inputs = new NDList(raw.collect(factory::create))
new NDList(NDArrays.stack(inputs))
}

@Override  
double[][] processOutput(TranslatorContext ctx, NDList list) {  
    long numOutputs = list.singletonOrThrow().shape.get(0)  
    NDList result = []  
    for (i in 0..<numOutputs) {  
        result << list.singletonOrThrow().get(i)        }  
    result*.toFloatArray() as double[][]  
}

} ``` Here, we manually pack our input sentences into the required n-dimensional data types, and extract our output calculations into a 2D double array.

Next, we create our predict method by first defining the criteria for our prediction algorithm. We are going to use our translator, use the TensorFlow engine, use a predefined sentence encoder model from the TensorFlow Hub, and indicate that we are creating a text embedding application:

def predict(String[] inputs) { String modelUrl = "https://storage.googleapis.com/tfhub-modules/google/universal-sentence-encoder/4.tar.gz" Criteria<String[], double[][]> criteria = Criteria.builder() .optApplication(Application.NLP.TEXT\_EMBEDDING) .setTypes(String[], double[][]) .optModelUrls(modelUrl) .optTranslator(new MyTranslator()) .optEngine("TensorFlow") .optProgress(new ProgressBar()) .build() try (var model = criteria.loadModel() var predictor = model.newPredictor()) { predictor.predict(inputs) } } Now, let’s define our input strings:

String[] inputs = [ "Cycling is low impact and great for cardio", "Swimming is low impact and good for fitness", "Palates is good for fitness and flexibility", "Weights are good for strength and fitness", "Orchids can be tricky to grow", "Sunflowers are fun to grow", "Radishes are easy to grow", "The taste of radishes grows on you after a while", ] var k = inputs.size() Then, we’ll use our predictor method to calculate the embeddings for each sentence. We’ll print out the embeddings and also calculate the dot product of the embeddings. The dot product (the same as the inner product for this case) reveals how related the sentences are.

``` var embeddings = predict(inputs)

var z = new double[k][k]
for (i in 0..<k) {
println "Embedding for: ${inputs[i]}\n${embeddings[i]}"
for (j in 0..<k) {
z[i][j] = dot(embeddings[i], embeddings[j])
}
} ``` Finally, we’ll use the Heatmap class from Smile to present a nice display highlighting what the data reveals:

new Heatmap(inputs, inputs, z, Palette.heat(20).reverse()).canvas().with { title = 'Semantic textual similarity' setAxisLabels('', '') window()} The output shows us the embeddings:

Loading: 100% |========================================|2022-08-07 17:10:43.212697: ... This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2...2022-08-07 17:10:52.589396: ... SavedModel load for tags { serve }; Status: success: OK......Embedding for: Cycling is low impact and great for cardio[-0.02865048497915268, 0.02069241739809513, 0.010843578726053238, -0.04450441896915436, ...]...Embedding for: The taste of radishes grows on you after a while[0.015841705724596977, -0.03129228577017784, 0.01183396577835083, 0.022753292694687843, ...] Embeddings are an indication of similarity. Two sentences with similar meaning typically have similar embeddings.

Our heatmap is shown below:

This graphic shows that our first four sentences are somewhat related, as are the last four sentences, but that there is minimal relationship between those two groups.

Interested readers may also like to see this earlier JVM Advent blog post about OpenNLP.

Object detection

Looks at detecting objects within images using DJL and Apache MXNet.

Our final problem looks at using Apache Groovy with the Deep Java Library (DJL) and backed by the Apache MXNet engine to detect objects within an image.

About Deep Java Library (DJL) & Apache MXNet“DJL is engine agnostic, so it’s capable of supporting different backends including Apache MXNet, PyTorch, TensorFlow and ONNX Runtime. We’ll use the default engine which for our application (at the time of writing) is Apache MXNet.

Apache MXNet provides the underlying engine. It has support for imperative and symbolic execution, distributed training of your models using multi-gpu or multi-host hardware, and multiple language bindings. Groovy is fully compatible with the Java binding.

Using DJL with GroovyGroovy uses the Java binding. Consider looking at the DJL beginner tutorials for Java – they will work almost unchanged for Groovy.

For our example, the first thing we need to do is download the image we want to run the object detection model on:

Path tempDir = Files.createTempDirectory("resnetssd") def imageName = 'dog-ssd.jpg' Path localImage = tempDir.resolve(imageName) def url = new URL("https://s3.amazonaws.com/model-server/inputs/$imageName") DownloadUtils.download(url, localImage, new ProgressBar()) Image img = ImageFactory.instance.fromFile(localImage) It happens to be a well-known already available image. We’ll store a local copy of the image in a temporary directory and we’ll use a utility class that comes with DJL to provide a nice progress bar while the image is downloading. DJL provides its own image classes, so we’ll create an instance using the appropriate class from the downloaded image.

Next we want to configure our neural network layers:

def criteria = Criteria.builder() .optApplication(Application.CV.OBJECT\_DETECTION) .setTypes(Image, DetectedObjects) .optFilter("backbone", "resnet50") .optEngine(Engine.defaultEngineName) .optProgress(new ProgressBar()) .build() DJL supports numerous model applications including image classification, word recognition, sentiment analysis, linear regression, and others. We’ll select object detection. This kind of application looks for the bounding box of known objects within an image. The types configuration option identifies that our input will be an image and the output will be detected objects. The filter option indicates that we will be using ResNet-50 (a 50-layers deep convolutional neural network often used as a backbone for many computer vision tasks). We set the engine to be the default engine which happens to be Apache MXNet. We also configure an optional progress bar to provide feedback of progress while our model is running.

Now that we have our configuration sorted, we’ll use it to load a model and then use the model to make object predictions:

def detection = criteria.loadModel().withCloseable { model -> model.newPredictor().predict(img) } detection.items().each { println it } img.drawBoundingBoxes(detection) For good measure, we’ll draw the bounding boxes into our image.

Next, we save our image into a file and display it using Groovy’s SwingBuilder.

Path imageSaved = tempDir.resolve('detected.png')imageSaved.withOutputStream { os -> img.save(os, 'png') }def saved = ImageIO.read(imageSaved.toFile())new SwingBuilder().edt { frame(title: "$detection.numberOfObjects detected objects", size: [saved.width, saved.height], defaultCloseOperation: DISPOSE\_ON\_CLOSE, show: true) { label(icon: imageIcon(image: saved)) }} Building and running our application

Our code is stored on a source file called ObjectDetect.groovy.

The example uses Gradle for the build technology:

``` apply plugin: 'groovy'
apply plugin: 'application'

repositories {
mavenCentral()
}

application {
mainClass = 'ObjectDetect'
}

dependencies {
implementation "ai.djl:api:0.18.0"
implementation "org.apache.groovy:groovy:4.0.4"
implementation "org.apache.groovy:groovy-swing:4.0.4"
runtimeOnly "ai.djl:model-zoo:0.18.0"
runtimeOnly "ai.djl.mxnet:mxnet-engine:0.18.0"
runtimeOnly "ai.djl.mxnet:mxnet-model-zoo:0.18.0"
runtimeOnly "ai.djl.mxnet:mxnet-native-auto:1.8.0"
runtimeOnly "org.apache.groovy:groovy-nio:4.0.4"
runtimeOnly "org.slf4j:slf4j-jdk14:1.7.36"
} ``` We run the application with the gradle run task:

**paulk@pop-os**:**/extra/projects/groovy-data-science**$ ./gradlew DLMXNet:run**> Task :DeepLearningMxnet:run**Downloading: 100% |████████████████████████████████████████| dog-ssd.jpgLoading: 100% |████████████████████████████████████████|...class: "car", probability: 0.99991, bounds: [x=0.611, y=0.137, width=0.293, height=0.160]class: "bicycle", probability: 0.95385, bounds: [x=0.162, y=0.207, width=0.594, height=0.588]class: "dog", probability: 0.93752, bounds: [x=0.168, y=0.350, width=0.274, height=0.593] Our displayed image looks like this:

The full source code for this example can be found in the following repo:
https://github.com/paulk-asert/groovy-data-science/subprojects/DeepLearningMxnet

ConclusionWe have seen various data science tasks solved with Groovy and numerous JVM libraries and platforms. Hopefully, you’ve also seen some of the benefits of using Groovy for your data science implementations, including its:

  • friendly Java-like syntax and flexibility of dynamic or static typing
  • metaprogramming capabilities that often simplify code
  • close alignment with Java that reduces learning curves and allows Groovy to piggy-back on the great work of the JVM developers
  • ability to use the many options for scaling that exist on the JVM

You should be confident that Groovy allows you to create simple solutions for simple problemsbut also scale using a variety of JVM technologies to solve even the biggest data science problems.

The post Groovy and Data Science appeared first on JVM Advent.

View Details

Developer productivity seems to be front and center again these days. While projects like Backstage draw a lot of interest and we also see a lot of traction happening around productivity tools I personally believe that it is time to revisit the bigger picture and take a look at what developers are going through in this new distributed world and how things could be easier.

What we know best – The Inner LoopWhen we talk about developer productivity it is important to take a look at the whole picture. As developers we usually only seem to care about the so called “inner loop” development. This can be summarised as everything that happens on your development machine. From coding in your favourite development environment to unit- and container testing, debugging and code management. Our industry became pretty mature in the last decade in getting developers productive and setting up environments. We can check in workspace settings and even roll out complete project definitions simply with tools like Maven or projects like devfiles.

In the age of containers and Kubernetes some other things are starting to become important though. The ability to create small services (both in physical size and logically) and the ability to handle the various forms of containers including local service testing. There’s plenty of alternatives for so called microservices frameworks and they all have their strengths and weaknesses. Nobody should be surprised to see me cheering for Quarkus here. I have talked about it and it’s great features at plenty of occasions already. If you haven’t had a chance to check it out, there are simple getting-started guides available and the Microsoft team just recently published a hands-on-lab on Azure where you get to play with Quarkus and some others.

The outer-loop challengesThe challenge with modern application development stacks barely lies on the inner-loop side. Problems start when we think about the execution environment. It feels like hundred years back where developers were not only responsible for the applications but also the runtime environments and we had plenty of fancy ways to package and distribute everything on top of the OS to the ops teams. While long nights and heated discussions might lead to cozy memories for some, with the advent of DevOps and interdisciplinary teams we now find ourselves in the midst of being responsible not only for development, packaging and configuration but also the test, integration and production environments. What sounds like an organisational challenge mostly became much bigger in recent years. Looking at the above application stack example we quickly see the variety of topics that now fall into the responsibilities of development teams. And the above picture is just what I said: An example. Not even distantly capturing all the details and specifics we might see in our individual projects. Meeting the challenge of navigating complexity we do see different approaches in development teams.

Just lately I listened to an old friend of mine who works in a platform team that does nothing else than caring for developer productivity and curating a stack. What might work for mature and established companies isn’t necessarily where you’re at right now. Maybe you are starting out on your cloud adventures and just began looking at managed services. In situations like that you would need a curated and ready to roll platform that offers a great flexibility and tooling while still fitting into more or less traditional development processes. There’s some options out there and you might guess, what my answer would be if you’d ask me. But let’s dig deeper into the tooling for a minute. Because I think that besides the platform the accessibility and usability of developers day-to-day tooling is the most critical path besides a platform that broadly enables team velocity.

Only Code in Production is Valuable CodeThe following little six minute video gives you an opinionated view onto a subset of tools that I believe get you a head-start into container-based development.

The individual tools you see in this recording have been used in Natale’s and my O’Reilly book, too. And you can play around with the code and even download the book for free. The application itself is build with Quarkus, that I mentioned above already. What is new is that Podman-Desktop is used to build the containers locally. It is somewhat similar to Docker-Desktop but uses Podman-Engine underneath and gives you an easy on-ramp to Kubernetes deployments.

The local services is deployed to the OpenShift Developer Sandbox. A free playground where you can get to know OpenShift and learn about it’s features. Don’t miss out on the demo of Dev Spaces for easy browser based development! From here it gets really interesting. The services configuration usually is something that is kept with your code and configured in GitOps tools for automated environment management. Instead of hand-crafting the configuration you can also use the GitOps Primer Operator from the Konveyor project. This is an operator can be deployed with a Kubernetes environment to export objects out of the cluster and store them within a Git repository. And this was done in the above demo. From the sandbox directly into ArgoCD via a git repository. This approach might look unusual and for sure isn’t suited for ongoing development but what I wanted to share is that it takes a lot more than just a simple platform to successfully navigate today’s complex tech stacks. You’ll also need to find transitioning paths for existing projects and find a level approach for your teams to broaden their knowledge without slowing them down.

What does all of this have to do with JavaA good question. Glad you asked. I believe that Java will continue to play an important role in enterprises around the globe and it’s footprint will continue to expand into containers and Kubernetes. After all it is essential for developers to be able to use the programming languages and frameworks of choice to be productive. With GraalVM we have an alternative at hand for burst load driven applications. Monitoring across different JVMs with tools like Cryostat and it’s Operator also becomes more manageable. But more importantly the friction for developers to go from local service and application development to full scale deployment on Kubernetes is being addressed not only by processes but tools. To make the best out of new challenges we need to continue to invest into developer tools that remove complexity and don’t add more.

The post The Power of Two Rings – Another View onto Developer Productivity appeared first on JVM Advent.

View Details

WHAT IS CLOUD NATIVE JAVA Cloud native is a software approach to building, deploying and managing modern applications in cloud computing environments.

It allows companies to build highly scalable and resilient applications that can easily be enhanced to meet customer needs without breaking existing functionality. The cloud native approach involves immutable infrastructures which enables the servers that are hosting the applications to remain unchanged after deployment, microservices that allow the product to be broken apart into smaller applications that serve only one purpose, containers which are used to store services and allow for the application to run independent of OS and hardware, and many other tools that are making the cloud native paradigm a favorable choice of many modern applications.

MicroservicesMicroservices are small autonomous services that work together. They are the foundation that is essential to building cloud native applications.

advantages:
  • Time to market: The smaller the code base is, the easier it is to develop and release into production.
  • Productivity: When new developers join the team they can more easily set up their local development environment, as well as become productive faster.
  • Extensibility: When we need new functionality we can just add more microservices.
  • Replaceability: When we need to change a part of the application we can modify and redeploy only the respective microservice and not the whole application.
  • Scalability: Microservices can be independently scaled. If a certain service is highly used or is essential to the application, it is a great candidate for being scaled – either horizontally (by having multiple instances of it), or vertically (by adding more CPU and RAM to the machine on which it is deployed).
Microservice resilience with Spring Cloud

Spring Cloud is a project built on top of Spring Boot, which is designed to aid the development of distributed systems such as microservices. The Spring Cloud suite of projects contains many of the services you need to make your Java applications run in the cloud. These services include: API Gateway, Cloud Configuration, Circuit Breakers, Service Discovery, Tracing, Testing, Distributed messaging, Leadership election and cluster state, Global locks, Load balancing, Service-to-service calls, Routing. Click here for more information about Spring Cloud.

Simple Spring Cloud Architecture example ContainersContainers and the most widely adopted containerization solution, Docker, are another fundamental part of most modern cloud based Java applications.

They are executable units of software in which application code, along with their libraries and dependencies are packaged together.

The popularity of containers among IT professionals is growing.

According to a survey conducted by SlashData for Cloud Native Computing Foundation among around 4000 developers from all over the world, 62% of the respondents in Q1 2020 have been using containers and in Q1 2021 – 73%.

If we take a closer look at the containerization technology we would find some similarities from a conceptual point of view between it and the JVM(Java Virtual Machine). Just like JVM, Docker enables CPU and RAM usage limitation and monitoring. Both technologies achieve portability by isolating the application from its runtime environment. What Docker does additionally is that it packages the application, JVM and all other dependencies together. This ensures that the container will always include compatible versions of the JVM and the application.

Containers also improve on the idea of virtual machines(VMs), where the applications are run on a software emulation of a physical machine.

However, VMs have one big disadvantage – each application should have its own guest operating system – this leads to poor horizontal scaling.

advantages:
  • Lightweight: Containers share the machine OS kernel, eliminating the need for a separate full OS instance per application. Their smaller size means they can spin up quickly and better support cloud-native applications that scale horizontally.
  • Portable and platform independent: Containers are packing all their dependencies with them, meaning that software can be written once and then run on any hardware and OS without any additional reconfiguration.
  • Supports modern development and architecture: Due to their deployment portability across platforms and their small size, containers are an ideal fit for modern development and application approaches — DevOps, serverless, and microservices as they are built on regular code deployments in small increments.
  • Improves utilization: Thanks to their improved design and smaller size compared to VMs, provide more benefits in addition to improving CPU and memory utilization of physical machines. They also enable microservice architectures that can be deployed and scaled more granularly.
disadvantages:
  • Complexity: Containers add to the complexity of a project and their impact should be considered when the architecture is designed.
  • Insufficient expertise in container development and management: Finding talent familiar enough with the technology is another common difficulty among IT companies.
  • Redesign: This is an especially important drawback for applications that were not developed as cloud native in the first place (like monolith for example). The architecture redesign and code refactoring introduce a lot of risk that should be carefully calculated.

With all that said, containers will still grow in popularity in the near future and will be the best choice for modern Java applications. As for legacy solutions, the pros and cons will have to be weighed for each different case.

CI/CDCI/CD stands for continuous integration, continuous delivery and continuous deployment. This is a methodology for frequently developing code changes more frequently and reliably. Also it is a best practice in agile methodology.

Continuous integration

Continuous integration focuses on smaller commits and smaller code changes to integrate. A developer commits code at regular intervals, at minimum once a day. The developer pulls code from the code repository to ensure the code on the local host is merged before pushing to the build server. At this stage the build server runs the various tests and either accepts or rejects the code commit.

Continuous Delivery and Deployment

Continuous delivery (CD) is a software development practice where code changes are automatically built, tested, and prepared for production release. It expands on continuous integration by deploying all code changes to a testing environment, a production environment, or both after the build stage has been completed. Continuous delivery can be fully automated with a workflow process or partially automated with manual steps at critical points. When continuous delivery is properly implemented, developers always have a deployment-ready build artifact that has passed through a standardized test process. With continuous deployment, revisions are deployed to a production environment automatically without explicit approval from a developer, making the entire software release process automated. This, in turn, allows for a continuous customer feedback loop early in the product life cycle.

Common CI/CD tools in Java environment

All CI/CD tools do the same work: They run mundane, repetitive tasks to safely ship iterative code updates to end users. Their functionality covers:

  • build automation, where build means turning source code into a deployable version,
  • test automation
  • deploy automation

Due to the dynamically evolving cloud-native space in Java the most used CI tools often vary. Among the most popular are Jenkins, Buddy, TeamCity, Travis CI. These tools support a wide specter of platforms and features due to this fact it is hard to determine which one would suit you best. For reference, check out the link where the Java community provides feedback on the pros and cons of the different software.

A complete CI/CD workflow typically looks like this:
CI/CD in JAVA Spring Application

Sample project structure:

  • Gradle/Maven are used for dependency management, to build and run individual microservices.
  • Docker – you can check in 3. For more information on containers
  • Jenkins – essentially defines the automation pipeline with various stages such as build, test, dockerize, run docker container etc. It is used by Jenkins to trigger a job as per the defined pipeline.

Is Java ready for Cloud native?Java as a whole already has the necessary tools, and enhancements are being made to both the language and the tools that are supporting it, to be part of the cloud native community.

Even though, approaching 28 years of age the language still has a broad community of developers, frameworks and tools that are enabling it to remain in the fast-paced modern industry. Tool providers such as AWS, Docker, Kubernetes and many others, are passionate to integrate Java into their systems as it is a programming language that is still the leading force of a lot of applications and would be of financial benefit to them.

What is probably the biggest setback in Java being cloud native is that the majority of developers are not familiar with all the new tools and technologies that are being used to develop a cloud native application. Small to mid-sized companies do not have the resources to invest in the teaching of those developers which leaves them out of the cloud native space.

However, more and more companies are adopting the cloud native approach and are investing in the development of new skills through internal and external academies for their software engineers, which will inevitably put Java forward in the cloud native space.

The post Components of Cloud Native Java appeared first on JVM Advent.

View Details

Elasticsearch 8.0 became generaly available at the beginning of the year. The cornerstone of the 8x releases have been a number of performance, stability and security improvements. Apart from that new capabilities especially in the area of machine learning and NLP have also been introduced. In this article we will categorize some of the highlights of the Elasticsearch 8x release.

SecurityIn 7x and earlier versions Elasticsearch didn’t enable security features by default during installation. This however changed in 8x:

  • activation of Elasticsearch is enabled through Kibana by means of an enrollmnent token, the same mechanism can be used to add new nodes to a cluster;
  • authentication and authorization are enabled by default;
  • TLS between cluster nodes and on the HTTP API are enabled by default;
  • system indices have better protection by introducing a new allow_restricted_indices role.

PerformancePerformance improvements are also an essential part for every system that can operate on large ammounts of data as Elasticsearch does. In 8x:

  • faster indexing of certain field types such as range;
  • additional storage optimizations have been implemented for certail field type such as text and keyword;
  • performance improvements on ingest pipeline processing;
  • faster execution of filters, range and date_histogram aggregations;
  • faster execution of SQL queries by avoiding calculation of total number of hits using track_total_hits counter.

StabilityStability is improvement by a number of capabilities such as:

  • 7.x REST API compatibility;
  • complete removal of mapping types deprecated in 7x;
  • possibility to import and query indices created in Elasticsearch 5x and 6x;
  • upgrade to latest Lucene 9.

New capabilitiesNew capabilities are introduced with a focus on machine learning and NLP:

  • new k-nearest neighbor (kNN) search API;
  • import of PyTorch NLP models for ingest processing;
  • new frequent items aggregation;
  • new random sampler aggregation (technical preview).

Apart from that a new Elasticearch Java API Client is introduced replacing the now deprecated High Level REST client.

The post Elasticsearch 8x latest and greatest appeared first on JVM Advent.

View Details

So… YAML, our new XML! It’s not used to just store data any more, it’s everywhere we look: configuration of Kubernetes, Ansible, continuous integration systems, build systems, and much more. It’s advertised as user-friendly, easy to read by humans and process by machines. It turns out it’s not all roses… or is it just me?

I have the privilege to entertain and teach you today, on the 13th day of our Java Advent. Sit back and read a story on how I set out to make the world just a bit more type-safe and YAML-free, and how github-workflows-kt and github-actions-typing were born.

But… Why?It was late 2021 when we were still pushing forward the kotlin-python project where we decided to use GitHub Actions – GitHub’s first-party CI solution. I, personally, liked it a lot, but there was one piece that started to become less and less maintainable for this specific project: its YAML configuration. It was getting really convoluted; the two pipelines had around 20-30 steps each, lots of repetition across them and in each of them, iterating on them involved pushing changes to the repository, sometimes learning about a syntax error or some name mismatch after minutes or even hours. Additionally, I didn’t like the feeling of putting placeholders and logic inside YAML.

Just to give an example, here’s a part of our pipeline prior to converting to Kotlin:

View the code on Gist.(click here to see the whole file)

I see the following problems, with GitHub’s YAML config and later with YAML in general:

  • JDK_9="$JAVA_HOME" and ./gradlew form a standard preamble for steps whenever Gradle is called, and are repeated multiple times. I needed to copy and paste these, and what if something changes in the preamble, like an extra environment variable needs to be set? It has to be changed in all these places
  • what other inputs does the actions/upload-artifact action provide, and are my input names and types correct? Is it even a correct action name? What other versions are available – maybe v2 is already deprecated?
  • needs: build_and_test – what an interesting way to set a dependency on another job. It’s not a part of YAML, it’s another thing added by GitHub. On YAML level, it’s just a key-value pair and GitHub adds the semantics
  • there are two pipelines (separate YAML files) which share common parts, and there’s no way to deduplicate things with pure YAML
  • YAML relies on indents to get its structure. Is it always obvious to you how many spaces you need to put? Or maybe tabs? Even if you think you got the indent right (your IDE is smart enough to provide the vertical rulers), now it turns out the key is incorrect on this indent level. Sigh…
  • the reference to ${{ matrix.testTask }} keeps repeating, and there’s no mechanism in YAML to ensure that testTask is truly one of matrix parameters, and that even matrix is a thing. The expression mechanism ${{ ... }} is an extension provided by GitHub

Yes, this is my very own rant about YAML. As you see, YAML is indeed simple and readable, but only on the surface: too simplistic to express some constructs in a convenient and safe way. These are all strings, lists, dictionaries… Even if you think you know YAML, you still need to learn how certain mechanisms are implemented in a given tool that uses YAML, like here: expressions inside strings or setting a dependency on another job. It’s like a mirage: promising simplicity, providing headache in the longer run.

I thought there has to be a better way. I decided to try an experimental approach: express it all in a general-purpose programming language (like e.g. Jenkins does with Groovy) and see how it goes – so basically create “the better way” myself, in Kotlin.

Let there be DSLBefore you ask: DSL stands for Domain-Specific Language. In this case, we’re talking about an internal/embedded DSL. Simply put, it’s about using the host programming language’s features to be able to model a specific problem in a human-readable way, and let the DSL implementation handle the rest. Kotlin does provide utilities to efficiently create readable DSLs, so it seemed like a great fit for my little experiment. Some DSLs you may know: SQL is an external DSL, and fluent Hamcrest matchers create an internal DSL within Java/Kotlin.

The Kotlin internal DSL described further is available under https://github.com/krzema12/github-workflows-kt/. Let’s dig deeper into it together!

The foundationWhere do we start designing the DSL? My approach was to take a dead simple workflow and express it in soon-to-be Kotlin DSL. The most frequent task our CIs take care of is building the project, so let’s make the very first step by expressing it in YAML (.github/workflows/build.yaml):

View the code on Gist.These basic entities are brought to our attention:

  • a workflow, with its triggers
  • a collection of jobs, with its steps
  • steps, with its arguments

Using our DSL, it maps to such Kotlin file (.github/workflows/build.main.kts):

View the code on Gist.“Whoa, a lot of extra complexity!“, you may think. True, there are several more items, but they look scarier than they really are, so let’s go through them line by line.

This file is in fact an executable Kotlin script. Kotlin compiler allows passing a piece of Kotlin code, and thanks to putting .main.kts in the extension, we don’t even need the main function. To instruct the operating system that Kotlin should be used to interpret it, we need the shebang:

View the code on Gist.Then this line follows:

View the code on Gist.which adds a dependency on the DSL library. It’s a regular Maven artifact, hosted on Maven Central so we don’t need any extra repository. Think of it as a replacement for a Gradle/Maven configuration script, only really compact and with limited capabilities.

The bunch of imports speak for themselves. There are usually hidden in the IDE anyway.

Then, something interesting starts to happen. We have a top-level workflow(…) function call. After the call, at the very end of the file, there’s also a call to writeToFile(). Inside, we also see appropriate functions to model jobs and steps. Thanks to the arguments passed to the workflow(…) function and several conventions, executing this script writes a YAML file to .github/workflows/build.yaml. Great! Let’s see the file and confirm everything looks as expected:

View the code on Gist.Oh my, even more lines… What have we done – from 12 to 33 lines?! Usually this YAML is just a preview of what’s being run on GitHub, and only the Kotlin script is modified. Think of it as a compiler – it does produce some lower-level machine code that is less readable and more verbose, but we usually don’t care that much. Here we have to keep it in our repository because GitHub Actions are not aware of our Kotlin scripts – they only understand the YAMLs. Kotlin becomes our daily interface to author the workflows, and YAML is there only for the GitHub runtime, plus to double check that the DSL does what we expect.

One thing that stands out in the above YAML is the extra check_yaml_consistency job. As already explained, we need to keep both the Kotlin script and the YAML in our repository. Can it happen that someone is not aware of this DSL and edits the YAML by hand? Sure it can. The consistency mechanism is there to guard against the two files getting out of sync – it’s added implicitly by the DSL and runs before your actual workflow (you can opt out of adding the check if your way of using the DSL doesn’t need it). The approach is simple: take the Kotlin script, regenerate the YAML and compare it with the YAML present in the repository. If there’s a perfect match, carry on. Otherwise, fail fast.

Other elements that you may notice are explicit step IDs. In general they are optional in the YAML version, but the DSL adds them preemptively to be able to provide features like access to step outputs.

The rest is still readable, not minified or obfuscated in any way.

The power of KotlinAll right, we’ve entered the Kotlin world. What does it give us?

The first and foremost feature where all other useful features come from is the compilation phase. With YAML, we just put it in the repository and learn about certain issues at runtime. Here, we have a chance to catch a plethora of issues already in the IDE, and another chance when running the script and generating the YAML.

In the below sections, we’ll go through some of the pain points I enumerated at the beginning, and see how Kotlin and the DSL library addresses them.

Removing repeated partsThey can appear in various forms, like repeated strings inside commands or repeated steps in jobs. For repeated strings, the simplest solution is to extract the value to a variable/constant:

View the code on Gist.or, if you feel like creating a little piece of abstraction, it can be extracted to a function. This is an extension function because run(…) function is a part of the DSL, and job(…) function is a lambda with receiver of type JobBuilder<>*:

View the code on Gist.For using Gradle from within GitHub Actions, there’s a dedicated GitHub action providing some extra features like caching. Here I just want to show examples of extracting a common part of a command. You can come up with your own – you have the whole power of Kotlin at hand!

Because the extension function for a single run(…) occurrence works just fine, we can extract complex parts of workflows with multiple steps. These functions can even create jobs given some parameters, so the tool is pretty powerful.

Using actions type-safelyActions, apart from the name of the CI itself, ale also reusable pieces of logic, for example the most popular https://github.com/actions/checkout. This particular one has 14 inputs, and 0 outputs. A traditional way to learn about them is to go to the action’s repository and either browse the README hoping the inputs are documented and the descriptions are up-to-date, or browse action.y(a)ml file directly.

The DSL proposes another approach. It comes bundled with dozens of actions, each of them providing typed inputs and partially type-safe outputs. It means that you get suggestions in your IDE while writing the workflow in Kotlin, along with documentation provided by the action author. The script won’t compile if you provide incorrect input name or its type:

How is it done? I developed a standardized way of describing action types, see https://github.com/krzema12/github-actions-typing. For most actions where authors didn’t integrate with the typing solution yet, the typings are stored inside the DSL repository (e.g. https://github.com/krzema12/github-workflows-kt/blob/main/actions/actions/checkout/v3/action-types.yml). Some authors successfully onboarded github-actions-typing, like https://github.com/Vampire/setup-wsl where the typings are hosted in its repository: https://github.com/Vampire/setup-wsl/blob/master/action-types.yml. Maintaining the typings hosted as a part of the DSL does add some maintenance overhead, so I really appreciate when action owners decide to maintain them on their own. Automation created in scope of this project takes care of the rest.

Modeling dependencies between jobsAs another example of how a proper programming language can help us model stuff, here’s how we define that job_1 depends on job_2 (job_1 has to run before job_2 starts):

View the code on Gist.Jobs are represented with its own types (Job) and nothing stands in our way of storing a reference to it in a variable. Then, it’s as simple as passing a reference to a job in another job’s dependencies. It’s logical and type-safe, no need to work on strings and map keys, like in YAML.

SummaryWe’ve barely scratched the surface of what’s possible using github-workflows-kt. I hope I managed to draw your attention and show the power of the type-safe approach over plain YAML, especially for more complex workflows. It’s not for everyone – some people will prefer YAML, and Kotlin fans will be more eager to try it out.

If you like what you’ve seen so far, I encourage you to give it a test drive, there’s a documentation available here that will guide you further. I also love feedback – feel free to get in touch via the issues or on the dedicated Slack channel. Let’s work in the open-source spirit to improve the library together!

At this point I’d like to thank all the contributors and early adopters that already provided feedback and introduced great improvements to the library. Without your support, we wouldn’t manage to go this far. I’m looking forward to more adoption of the library and providing even greater feature coverage!

The post How I got rid of YAML in GitHub’s CI appeared first on JVM Advent.

View Details

Going beyond senior level in your career is an amazing opportunity, it may even offer you a lot of what you may be looking for:

  • Higher salaries and income.
  • Access to great opportunities.
  • Working on the most interesting projects.
  • Be more energized and happy with your work.
  • Amazing connections with other developers and leaders.
  • Respected inside and outside the company.
  • Having a larger impact in your area.

Going beyond senior positions also takes effort, it is not something that happens naturally…

The good news is that most of the effort, if not all, can be done in your current position, inclusive during your normal working hours.

4 Paths to Grow Beyond SeniorBefore we dive in into the what to do, let’s examine the 4 main paths for growing beyond senior:

The Path of PeopleYou may see yourself increasing your value and impact by having a team of developers that you manage and direct. That is the management path, and you can grow to become director, a VP and beyond

This is a path for those that prefer to manage people, and is a common desire for many developers. It is also arguably the path that most companies will offer you, because it is a clear, stablished path, that the company probably applies to other areas of the organization.

The Path of TechAnother possibility is to continue to grow deep in the tech side. There are different names for this path, like individual contributor or staff+. The idea it to become a specialist, an architect, tech lead or similar. You can grow to become a Principal Developer (or Engineer), and later a Distinguished Developer.

This path focus on technical work, with almost no or very limited people management. This path does require a healthy dose of mentoring and tech guidance. Right now this path is not very common, but it is getting a lot of attention lately.

The Path of IndependenceMaybe you see yourself with more independence, and being your own boss, looking to make a larger impact by choosing what you will work on. That path will lead you to be independent from a single company. You may become a consultant, instructor or a freelancer, sometimes working for multiple projects and companies.

This requires good skills on acquiring customers and negotiation, and you will need to have lots of visibility in the market. It can be very rewarding, specially in terms of autonomy and independence. It is a great path if you want a diversity of projects to work with, and it can be extremely positive financially.

The Path of EntrepreneurshipThis is the path of building your own company to serve others. That can be launching or joining a startup, building products either as full time or as a side project, and trying to work with customers and investors.

This can be very exciting and rewarding. At the same time, it can be tiring and consuming. The risks are big, but if they pay out, the rewards can be astronomical!

All those paths have similar requirements of higher levels of responsibility and autonomy from you. And all of them require that you develop and acquire new skills, some that you may have never needed or even wanted to have.

Most important: all of those paths are very rewarding! Financially for sure, but also in terms of increased impact, more independence and flexibility, and above all, with higher levels of personal realization and self actualization.

6 Actionable Steps that will take you Beyond SeniorIf any of those paths seem right for you, here are 6 actionable steps that that will get you to grow beyond senior level.

Focus and PurposeThose paths are rewarding, but all of them require new skills. Even if you plan to continue mainly on a technical work. This is not easy, and requires a good level of effort.

Also, independence and autonomy are needed, because none of those paths are simply handed to you. You will need to occupy new spaces, and create opportunities. Rarely those are positions that you can just apply to. You may have to create the conditions for those positions to be created in the first place.

Doing this can be even harder if you don’t understand your focus, and specially if you don’t see the purpose for it.

To solve that, identify your career focus. Not the tool or technology you will focus on (those are probably irrelevant), but the problem you are in love of solving, and the impact you want to cause in the world.

Learn to Go DeepThe path of autonomy and independence requires that you become self motivated and willing to pursue the skills needed for excellency.

Although you will work and have support from many people, since those are very collaborative positions, you will build your own path. That requires confidence, capacity of self-assessment, and a deeper understand of who you are and what you are capable of.

And specially, the drive to acquire and go deep in the needed skills.

Once your focus is clear, identify the missing or weak skills. Work on them. Create projects and challenges that will force yourself to acquire and improve the skills. You can do that at your work, or as side projects. Just be careful to closely align those activities with your focus, so you can move in the right direction.

Share, Inspire, InfluenceThe beyond senior paths all involve a higher impact, that is driven by having a stronger influence on people around you. Practice that.

Share what you know. That is a superpower that will attract to you the people, projects and opportunities that will build your own path.

Sharing is best done by people that listen well, so even if you feel like you are shy and introverted, that is actually a positive thing, not a liability.

Learn to listen, to write, and to speak. That’s the triad of skills that makes sharing extremely powerful. Interestingly enough, those are also the skills that will help you increase your knowledge and your confidence.

Build CommunitiesWhether they are internal — your team, the developers of your office, or maybe all the devs in your company — or external — user groups, events, meetups — communities are the place where your peers meet and share ideas, tips, tricks, problems, and solutions.

The place where you should be sharing your knowledge.

Being part of communities allow you to have access to information that is not easily available, gives you connection with people that have a diverse set of skills, and promotes trust, recognition and reputation.

The skill of building places like this will increase your influence and push you into leadership positions.

Start by participating in existing communities. They can be user groups, open source projects or any local community that you feel connected to.

Move on to engage your peers, and starting your own group to discuss specific interests and situations.

Take ResponsibilityLeadership can be many things, but it is above all, taking responsibility.

The more responsibility you take, the more influence you have, and that (should) reflect in independence and autonomy, more interesting challenges, larger salaries, and higher positions.

There is no question that all paths of growth beyond senior involve high levels of leadership and responsibility.

And there is no scenario where you achieve those higher positions without taking the corresponding level of responsibility.

One caveat: usually responsibility comes first. The benefits follows. Not the other way around.

Make an assesment of your current situation, company, or project. Are you taking the highest level of responsibility that you are capable of? If not, why not? Are there any opportunities for taking some more responsibility? Can you dig a space where you would be even more responsible?

Build Trust and ReputationAutonomy and independence require trust. All paths of growth beyond senior positions require high levels of trust.

Reputation is what people believe about you. Good reputation and trust are not exactly the same thing, but they are similar concepts.

I can’t trust you or believe good things about you if I don’t know you… Reputation and trust require visibility…

Visibility is a risky business. Your best skills becomes visible, but so do your lack of skills… Trust and reputation also depends on having good skills.

All the beyond senior paths will be more possible and accessible if you start building your reputation right now. Be visible. Share what you know. Participate in communities. Take responsibilities toward others and be helpful. Network with people and peers. And while doing all that, talk about it, and share your ideas and insights.

This is the time, start now!Those 6 actions will take you towards your next step. Whether you are trying to achieve a senior position, or you are looking to grow beyond senior. Whether you want to continue working for a company in the manager or technical path. Whether you would like to be more independent or have your own business.

One last word about the path to follow: they are not mutually-exclusive nor opposite. It is indeed possible to experiment more than one path in parallel, or even go back and try something different. So, don’t be paralyzed in a decision that may not even need to be taken right now.

Start applying the 6 steps highlighted in this article, and evaluate the opportunities when they show up.

The sooner you start, the faster you will have results.

And if you want some help to think about your career, identify your focus or have more clarity in a path that you want to take, early in 2023 I’ll run a series of conversations focused on building your best career year. To get started, download my Best Developer Year book for free, and you will be the first to know when those live conversations take place.

PS: The image on the top of this post was generated with the DALL-E 2 AI algorithm, using airbrush.ai.

The post 6 Actions to take Your Developer Career Beyond Senior Level appeared first on JVM Advent.

View Details

IntroductionThis blog builds on top of the great articles by Edoardo Vacchi leveraging all the goodies of a Typed Actor API and the working example of a Chat.

Now that Java 19 has been released this use-case is a perfect test bed to use the advanced capabilities of the Loom preview and evaluate what would be the gains and differences when using such a concurrency model for both library authors and final users.

In this blog, we assume some shallow knowledge of the previous posts and we invite you to look up references in case of doubt.

LoomLet’s start with the excellent definition of this project from the official wiki page:

Project Loom is to intended to explore, incubate and deliver Java VM features and APIs built on top of them for the purpose of supporting easy-to-use, high-throughput lightweight concurrency and new programming models on the Java platform. For this article we won’t dig into structured concurrency and we will focus on the usage of Virtual Threads.

This is arguably one of the most notable improvements happening on the JVM nowadays, Virtual Threads are an implementation of Green Threads and a Virtual Thread is not binding to a System Thread, instead, they can be created almost for free and they are extremely lightweight.

Using the “ExecutorService API” to spawn a new Virtual Thread looks as follows:

ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();executorService.execute(...); Loom promise is that the runtime will take care of the blocking code for us, and we should not really “care” anymore about concurrency, but write plain old blocking code. The Java standard library has been changed to account for this new paradigm and all blocking operations (on IO etc.) are going to release the carrier thread automagically.

Loom based Actor SystemLet’s start with the typed API surface we defined in the previous blog entry:

public interface TypedLoomActor { interface Effect<T> { Behavior<T> transition(Behavior<T> next); } interface Behavior<T> { Effect<T> receive(T o); } interface Address<T> { Address<T> tell(T msg); } static <T> Effect<T> Become(Behavior<T> next) { return current -> next; } static <T> Effect<T> Stay() { return current -> current; } static <T> Effect<T> Die() { return Become(msg -> { out.println("Dropping msg [" + msg + "] due to severe case of death."); return Stay(); }); } record System() { private static ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor(); public <T> Address<T> actorOf(Function<Address<T>, Behavior<T>> initial) { ??? } }} and now is the time to fill in the blanks powered by Loom, first of all, let’s define a RunnableAddress which is going to be our execution unit and enables the communication to Actors:

class RunnableAddress<T> implements Address<T>, Runnable { public Address<T> tell(T msg) { ??? } public void run() { ??? } } since Loom is offering us the scheduling mechanism we are going to simply bound each actor to exactly one Virtual Thread, and the actorOf implementations look straight-forward:

public <T> Address<T> actorOf(Function<Address<T>, Behavior<T>> initial) { var addr = new RunnableAddress<T>(initial); executorService.execute(addr); return addr; } as we have seen before, an Actor is usually implemented on top of a Mailbox, and this case is not any different. Since we want to take advantage of the blocking semantics of the JVM, we base our mailbox on a blocking data structure such as a LinkedBlockingQueue, with no additional fear for concurrent access we can implement the tell method of RunnableAddress:

final LinkedBlockingQueue<T> mailbox = new LinkedBlockingQueue<>(); public Address<T> tell(T msg) { mailbox.offer(msg); return this; } the only bit left is the actual Actor’s run method, as opposed to the async version of it, we don’t need anymore the async() duty cycle and we can provide a super dumb implementation leveraging the properties of the LinkedBlockingQueue:

public void run() { Behavior<T> behavior = initial.apply(this); while (true) { try { T message = mailbox.take(); Effect<T> effect = behavior.receive(message); behavior = effect.transition(behavior); } catch (InterruptedException e) { e.printStackTrace(); break; } } } more specifically, the initial behavior is computed right at the start and the rest of the code can be safely executed in a strict loop, knowing that the blocking operation will be handled by the Loom runtime.

The full implementation looks as follows:

public interface TypedLoomActor { interface Effect<T> { Behavior<T> transition(Behavior<T> next); } interface Behavior<T> { Effect<T> receive(T o); } interface Address<T> { Address<T> tell(T msg); } static <T> Effect<T> Become(Behavior<T> next) { return current -> next; } static <T> Effect<T> Stay() { return current -> current; } static <T> Effect<T> Die() { return Become(msg -> { out.println("Dropping msg [" + msg + "] due to severe case of death."); return Stay(); }); } record System() { private static ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor(); public <T> Address<T> actorOf(Function<Address<T>, Behavior<T>> initial) { var addr = new RunnableAddress<T>(initial); executorService.execute(addr); return addr; } } class RunnableAddress<T> implements Address<T>, Runnable { final Function<Address<T>, Behavior<T>> initial; final LinkedBlockingQueue<T> mailbox = new LinkedBlockingQueue<>(); RunnableAddress(Function<Address<T>, Behavior<T>> initial) { this.initial = initial; } public Address<T> tell(T msg) { mailbox.offer(msg); return this; } public void run() { Behavior<T> behavior = initial.apply(this); while (true) { try { T message = mailbox.take(); Effect<T> effect = behavior.receive(message); behavior = effect.transition(behavior); } catch (InterruptedException e) { e.printStackTrace(); break; } } } }} I’d argue that is shorter, cleaner and more beautiful than the previous one as we just got rid entirely of:

  • the async() duty cycle
  • concurrency primitives to handle access to the queue

The Chat exampleOn top of this shiny new API + Runtime, let’s rebuild the chat example.

The overall architecture will be almost identical, but we will differ in modeling the ChannelActor.java, which is the in-code representation of a wire in between client-server handling all the socket communication. Let’s focus on this important detail.

Using only completely asynchronous operations each ChannelActor is able to trigger the execution of write operations at any time, as it will be notified with a message when something has been read from a socket. On Loom we are supposed to use the old blocking code, where a read is performed with a in.readLine(), which means that our actor is eventually going to “stay stuck” in that condition waiting for more messages to arrive; if we model, as we used to, the Channel with a single actor it will be able to send messages only before/after an actual read from the Socket which seems a pretty bad user experience.

We need to slightly change our mind and enforce a deeper level of separation of concerns, we can do it by actually dividing the two functions contained in the ChannelActor:

  • a reader actor that will notify the parent any time a message is received
  • a writer actor that is able to write to the underlying socket without having to wait for read operations

The writer behavior can be defined by mechanically connecting the dots:

Effect<WriteLine> writer(WriteLine wl) { out.println(wl.payload()); return Stay(); } where out is a PrintWriter out defined in a context accessible by the function.

The reader the behavior will instead leverage the old java.io blocking operation:

Effect<PerformReadLine> read(Address<PerformReadLine> self) { try { return switch (in.readLine()) { case null -> { yield Die(); } case String line -> { addr.tell(fn.apply(line)); self.tell(new PerformReadLine()); yield Stay(); } }; } catch (IOException e) { throw new UncheckedIOException(e); } } with this encoding:

  • the actor execution is resumed by the Loom scheduler when IO is performed
  • the actor loops sending a message to still exercising the async boundary of the mailbox

There are a few minor details on how to keep around references to the java.io objects involved in reading and writing to a socket, here you have a peek at solving the problem with actual plain old classes:

class ChannelActors { record WriteLine(String payload) {} record PerformReadLine() {} final BufferedReader in; final PrintWriter out; ChannelActors(Socket socket) { try { in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(), true); } catch (IOException e) { throw new UncheckedIOException(e); } } final class Reader<T> { final Function<String, T> fn; final Address<T> addr; public Reader(Address<T> addr, Function<String, T> fn) { this.fn = fn; this.addr = addr; } void start(Address<PerformReadLine> readActor) { readActor.tell(new PerformReadLine()); } Effect<PerformReadLine> read(Address<PerformReadLine> self) { try { return switch (in.readLine()) { case null -> { yield Die(); } case String line -> { addr.tell(fn.apply(line)); self.tell(new PerformReadLine()); yield Stay(); } }; } catch (IOException e) { throw new UncheckedIOException(e); } } } <T> Reader<T> reader(Address<T> addr, Function<String, T> fn) { return new Reader<>(addr, fn); } Effect<WriteLine> writer(WriteLine wl) { out.println(wl.payload()); return Stay(); }} Having this basic building block available we can build, on top of it, a slightly modified version of the ChatServer:

public interface ChatServer { sealed interface ClientManagerProtocol { } record ClientConnected(Address<ChannelActors.WriteLine> addr) implements ClientManagerProtocol { } record LineRead(String payload) implements ClientManagerProtocol {} TypedLoomActor.System system = new TypedLoomActor.System(); int PORT = 4444; static void main(String... args) throws IOException, InterruptedException { var serverSocket = new ServerSocket(PORT); out.printf("Server started at %s.\n", serverSocket.getLocalSocketAddress()); Address<ClientManagerProtocol> clientManager = system.actorOf(self -> msg -> clientManager(msg)); while (true) { var socket = serverSocket.accept(); var channel = new ChannelActors(socket); ChannelActors.Reader<ClientManagerProtocol> reader = channel.reader(clientManager, (line) -> new LineRead(line)); reader.start(system.actorOf(self -> msg -> reader.read(self))); Address<ChannelActors.WriteLine> writer = system.actorOf(self -> msg -> channel.writer(msg)); clientManager.tell(new ClientConnected(writer)); } } static Effect<ClientManagerProtocol> clientManager(ClientManagerProtocol msg) { return clientManager(msg, new ArrayList<>()); } static Effect<ClientManagerProtocol> clientManager(ClientManagerProtocol msg, List<Address<ChannelActors.WriteLine>> clients) { return switch (msg) { case ClientConnected(var address) -> { clients.add(address); yield Become(m -> clientManager(m, clients)); } case LineRead(var payload) -> { clients.forEach(client -> client.tell(new ChannelActors.WriteLine(payload))); yield Stay(); } }; }} as before, we are waiting for incoming connections, and, as soon as one arrives it gets registered by the clientManager actor, which will take care of forwarding any incoming message to all the connected clients using ChannelActors.

The ChatClient is the dual implementation which is going to read lines from the standard input and simply forward them to the server through a ChannelActor:

public interface ChatClient { String host = "localhost"; int portNumber = 4444; TypedLoomActor.System system = new TypedLoomActor.System(); sealed interface ClientProtocol { } record Message(String user, String text) implements ClientProtocol {} record LineRead(String payload) implements ClientProtocol {} static void main(String[] args) throws IOException { var userName = args[0]; var socket = new Socket(host, portNumber); var channel = new ChannelActors(socket); Address<ChannelActors.WriteLine> writer = system.actorOf(self -> msg -> channel.writer(msg)); Address<ClientProtocol> client = system.actorOf(self -> msg -> client(writer, msg)); ChannelActors.Reader<ClientProtocol> reader = channel.reader(client, (line) -> new LineRead(line)); reader.start(system.actorOf(self -> msg -> reader.read(self))); out.printf("Login............... %s\n", userName); var scann = new Scanner(in); while (true) { switch (scann.nextLine()) { case String line when (line != null && !line.isBlank()) -> client.tell(new Message(userName, line)); default -> {} } } } static Effect<ClientProtocol> client(Address<ChannelActors.WriteLine> writer, ClientProtocol msg) { var mapper = new ObjectMapper(); try { switch (msg) { case Message m -> { var jsonMsg = mapper.writeValueAsString(m); writer.tell(new ChannelActors.WriteLine(jsonMsg)); } case LineRead(var payload) -> { switch (mapper.readValue(payload.trim(), Message.class)) { case Message(var user, var text) -> out.printf("%s > %s\n", user, text); } } } return Stay(); } catch(JsonProcessingException e) { throw new UncheckedIOException(e); } }} with jbang installed you can directly run and play with this example code with a few quick commands:

jbang code/ChatServer.java and in separate terminals:

jbang code/ChatClient.java <name> ConclusionsIn this blog, we presented how Loom can make life much easier for library developers and enable legacy code to be easily converted/ported into high-performing codebases without having to be re-written from scratch.

The experience has been pretty much positive overall and the ergonomics of Loom Virtual Threads nicely fit into the Actor System example, showing evidence of the potential but also practically exploiting the fact that is not going to be a straight “drop-in” replacement for other frameworks and libraries and changes will be required in the process.

Here we have just been scratching the surface of the problem and additional explorations and additional follow up and considerations will be required:

  • Loom offers nice StackTraces but, having a queue in front of each Virtual Thread/Actor we are going to lose this feature
  • What will happen in cases where there is high resource contingency
  • How the system will perform using Lock-free concurrent data structures?

Bye and see you next time!

The post Actors and Virtual Threads, a match made in heaven? appeared first on JVM Advent.

View Details

Christmas is coming, and Santa and the elves are working around the clock to build all toys. After a long day in the toy workshop, the elves want to spend their hard-earned NorthPoleCoins on jingle juice and sweets at the local pub. They always go in groups and often order collectively. They use a special application to keep track of payments so that every elf pays its fair share. We see the service that settles these payments below.

To preserve peace on the north pole, we want to add some tests to ensure that this service works as intended. One crucial property of the method above is that, at any point in time, the balances of all elves add up to zero. Otherwise, coins would be created or lost. A unit test for this method could look something like the one below.

First, the scenario is set up by initializing the objects with example values. Then, we invoke the method under test and write assertions to verify that the output is as expected. Of course, it doesn’t stop with this single test case. We generally write additional, similar tests to sufficiently cover all scenarios and edge cases. And when we see that all the test scenarios we came up with are passing, we can relievedly commit or deploy our code.

Downsides of example-based testingIf we take a step back, we see that many of these test cases are quite similar, except that each uses a slightly different example. There are a few downsides to this approach. First of all, developers need to spend more time developing and maintaining these test cases. This includes coming up with realistic examples, which requires a thorough understanding of the domain. New developers on the team might also have a hard time grasping the actual intent of the test. It takes time to see what angles are covered for each method and what is missing or redundant. And despite all this effort from the team, only a small portion of all possible examples are covered by the tests they wrote. Covering all combinations of inputs is just not feasible. And even worse, the scenarios covered are the same every run: essentially coupling between scenario and test suite.

When we implement our methods, we want the logic to be generic and not tied to specific cases. The code should have certain properties for all possible inputs. The tests we write for it are more the opposite: we write tests that are example-specific and not generic. The code and tests are designed on different levels of abstraction, where properties are more abstract than concrete examples.

Property-based testing to the rescueBut what if we could write tests like we write our code? Property-based testing (short: PBT) can help us out here. Instead of defining the concrete examples to test, you specify the domain (strings, positive integers, etc.) and let the framework generate many input values, including edge cases. Each test is executed multiple times with different possible inputs during a test run. Property-based testing allows us to write one test that tests all of the desired inputs, including typical edge cases, which allows for more efficient test writing and ensures that a test passes for all possible values within the domain.

Below we see how our test could look as a property-based test. The scenario function is replaced by Kotest’s checkAll, taking as its inputs a set of generators. The class containing all generators is called Arb, which is short for arbitrary. A predefined number of examples is generated for each test run, a thousand by default in Kotest. The rest of the test remains completely the same, except that we added some code to transform the generated values to the entities and values of our domain.

Our example-based tests previously all passed, which might lead us to believe that this property-based test will also be happy. Our test cases represented all possible paths, right? However, we are actually met with a failure this time. You might think that a property-based test is more difficult to debug because of its generated input values, but we did not talk about PBT’s superpower. Kotest not just mentions the failing example if the test fails, it starts ‘shrinking’ the input values. If the test fails with integer value 8 as input then this value is shrunk to value 7, and the test is retried. If the test failed when we tried to input a list of three strings integer then the list is shrunk to two strings. Shrinking continues until the test passes again, and the goal is to find the minimal reproducible example for which the test fails, which helps find the bug. Let’s have a look at the stack trace of our run.

The test failed when we tried to divide (a rounded) 77.93 NorthPoleCoins over four elves, and Kotest started shrinking both inputs. Interestingly, both shrinks uncovered a different bug for us. Dividing the coins led to a rounding error, while an empty list caused an exception to be thrown. We might not have thought of it when we created our examples, but now we can adapt our code based on these results.

When we make ourselves as developers responsible for coming up with realistic examples, we risk such bugs slipping through the cracks. Property-based tests make sure that all scenarios can be verified.

Custom generatorsThe property-based test we saw above is less readable than we would like because of the logic we added to transform the values from the built-in Arb generators to be suitable for our test. Kotest allows us to define custom Arb’s through the arbitrary builder, and we can use Kotlin’s extension functions to use these custom generators in the same way as their built-in counterparts.

Where Kotest knows how to shrink built-in generators, the shrinking behaviour for custom generators can be very dependent on your domain. Therefore, the arbitrary builder allows us to provide a shrink function that returns a list of values we deem to be a shrink result of the current generated value. Let’s have a look at what this could look like:

We ended up with a more concise test than the example-based test that we started with, but it covered many more examples than before.

ConclusionIn this article, we’ve seen what property-based testing is and how it differs from the more common example-based testing. Where defining representative examples in example-based testing is the programmer’s responsibility, property-based testing leaves this to generators so that the tests do not become partly dependent on the chosen examples. Property-based testing allows us to generalize concrete examples to focus on the characteristics of the code that underlie these examples, resulting in a cleaner and more compact test suite that is easy to maintain and better exposes subtle bugs. In this way, we bring what we test closer to what we claim to test. However, it is also important to note that property-based testing is not intended to replace example-based unit tests completely. It’s an addition to our testing arsenal that allows us to cover every possible input value with a single test to reveal bugs we might not come up with ourselves.

The post Property-based testing with Kotlin and Kotest appeared first on JVM Advent.

View Details

Science is what we understand well enough to explain to a computer. Art is all the rest.

Donald E. KNUTH, THINGS THAT A COMPUTER SCIENTIST RARELY TALKS ABOUT

The term “event” has become overloaded in today’s modern computing world. Event streaming, event processing, event messaging, event sourcing, event storming, event-driven architecture, and so on. Each represents different aspect of eventful computing. Just like anything, there are also challenges, but the benefits of the event-based approach should outweigh any obstacles in the long run, and will prove itself to be a viable and dynamic solution for today’s modern systems that are “hungry” for data.

A Brief Look Back in TimeEvent-driven style of programming appeared in the early days of computing, when the need arose to respond to requests. These inputs primarily came from user interactions with the machines, such as clicking on the mouse, resizing the window, pressing the key on the keyboard, and so on. Inputs also came from the devices, for example, on the instruction set level where events complement interrupts.

As we move up the computing layers closer to the current era, we see event-based models being used practically everywhere. A common pattern is the Model-View-Controller (MVC) and its variants are at the heart of graphical user interface (GUI) design. Briefly speaking, the controller mediates between the model and the view. It takes input from the user, and passes the data to the model for processing, and in turn, it receives the results from the model, then passes it to the view for rendering.

Events are indeed ubiquitous in almost every aspect of computing. In fact, we live in an eventful world. Everything that happens in our lives is an event. Birthing of a new baby, a world cup game, a rockstar performing at a concert, starting a new job, and so on. However, our minds have been conditioned to think procedurally from our initial training with computers. In order to adapt to the event-driven world, we need to shift our minds to think on a different level.

The Current Cloud EraFast forward to the present time, event style programming and systems design are no longer dealing with a single processor. The explosive expansion in the cloud and the exponential growth of data from all imaginable spaces have elevated the levels of event-based computing to new heights. These days we are handling messages between disparate devices in massive scale and velocity, as well as possibly across different geographical areas. Event streaming refers to the ongoing delivery of the data streams to their destinations in near real-time. The beauty of the approach is that there is no time wasted in between. Data gets ingested and processed as it arrives.

As a hands-on tech person you may be asking: “All those talks won’t buy me anything, show me some code!”. What may pique your interest is to show a very basic example of a publish-and-subscribe messaging pattern used by Apache Pulsar, a cloud-native event streaming platform. The same pattern is being used a number of other messaging and event streaming platforms, such as Apache Kafka, the MQTT broker, Google Pub/Sub, and so on. For this particular example usage, in order to simplify the setup, I am using DataStax’s Astra Streaming, the managed Apache Pulsar cloud platform. (Note: Anyone can register with the Astra platform and get $25 free-tier access.)

The Publish/Subscribe Messaging PatternThe Pub/Sub Messaging pattern is one of the most efficient approach to use for transmitting messages from the sender to the receiver(s). Notice that the receiver can be more than one. Because of the lack of coupling between the sender and the receiver(s), scalability is extremely high with this setup. At the heart of this approach is the broker, a stateless component that primarily handles message dispatching and delivery. The sender, also referred to as the publisher, does not send its message directly to the receiver(s) but publishes it to a topic. In fact, it does not bother knowing where the receiver(s) is/are. It lets the broker handle the delivery to the receiver(s) accordingly. It is up to the receivers, or subscribers, to subscribe to the topic(s) in order to be able to receive the messages that are of interests to them respectively.

Publisher / ProducerWriting an Apache Pulsar publisher client does not involve too many steps, as illustrated in the following example of a basic producer:

  • Establish the client connection (PulsarClient)
  • Create the publisher client (Producer)
    • associate it with the topic name
  • Send the message (*note: here we’re using the asynchronous sendAsync(), but send() is the one to use for synchronous send).

import org.apache.pulsar.client.api.PulsarClient;import org.apache.pulsar.client.api.Producer;import java.io.IOException;public class SimpleProducer { private static final String SERVICE\_URL = "pulsar+ssl://pulsar-gcp-uscentral1.streaming.datastax.com:6651"; public static void main(String[] args) throws IOException { // Create client object PulsarClient client = PulsarClient.builder() .serviceUrl(SERVICE\_URL) .authentication( AuthenticationFactory.token(YOUR\_PULSAR\_TOKEN) ) .build(); // Create producer on a topic Producer<byte[]> producer = client.newProducer() .topic("persistent://mg-twitch-tenant1/astracdc/data-9569760f-a558-4db4-8b05-7b50e57cdf94-mgtwitchkeyspace.movies\_and\_tv") .create(); // Send a message to the topic producer.sendAsync("Hello World".getBytes()); //Close the producer producer.close(); // Close the client client.close(); }} Subscriber / ConsumerLikewise, putting together a basic Apache Pulsar consumer client involves only another few steps:

  • Establish the client connection (PulsarClient)
  • Create the consumer client (Consumer)
    • associate it with the topic name
  • Loops until the message arrives, and consumes it

import org.apache.pulsar.client.api.PulsarClient;import org.apache.pulsar.client.api.Consumer;import org.apache.pulsar.client.api.Message;import java.io.IOException;import java.util.concurrent.TimeUnit;public class SimpleConsumer { private static final String SERVICE\_URL = "pulsar+ssl://pulsar-gcp-uscentral1.streaming.datastax.com:6651"; public static void main(String[] args) throws IOException { // Create client object PulsarClient client = PulsarClient.builder() .serviceUrl(SERVICE\_URL) .authentication( AuthenticationFactory.token(YOUR\_PULSAR\_TOKEN) ) .build(); // Create consumer on a topic with a subscription Consumer consumer = client.newConsumer() .topic("mg-twitch-tenant1/astracdc/data-9569760f-a558-4db4-8b05-7b50e57cdf94-mgtwitchkeyspace.movies\_and\_tv") .subscriptionName("my-subscription") .subscribe(); boolean receivedMsg = false; // Loop until a message is received do { // Block for up to 1 second for a message Message msg = consumer.receive(1, TimeUnit.SECONDS); if(msg != null){ System.out.printf("Message received: %s", new String(msg.getData())); // Acknowledge the message to remove it from the message backlog consumer.acknowledge(msg); receivedMsg = true; } } while (!receivedMsg); //Close the consumer consumer.close(); // Close the client client.close(); }} Closing ThoughtsThis article attempts to scratch the surface of one of the most fascinating subject areas in computing: event computing, which may not have been talked about as much as in other more “glamorous” areas in the market. The reason could be due to the fact that event computing often addresses the “hidden” spots and solves problems behind the scenes, much like the plumbing pipelines that are hiding underneath the building.

Think of events as the air that we breathe in, without which there would be no life. So it is with the event-driven systems, in which all of the moving parts – messages, streams, pipelines, connectors, transformers, remediators, etc. are operating independently and without direct dependencies on one another, and yet as a whole, they come together to produce the coherent results that we expect.

Water Flowing Over Derwent Dam by Tim Hallam is licensed under CC-BY-SA 2.0The post The Art and Benefits of Computing Eventfully appeared first on JVM Advent.

View Details

JavaDoc Code SnippetsIt is easy to include buggy code snippets into code documentation. Java 18 introduces an @snippet tag with which you can pull snippets from (hopefully) working source code. I explain how that works, and also look into a couple of alternative approaches for other forms of technical documentation.

“Inline” SnippetsWhen you include code in a document, it can be inline /* like this */ or, in typesetting speak, displayed:

/* like this */ CSS calls these inline and block display.

In HTML, you use the code tag for inline code and pre for a code block. In JavaDoc, the {@code ...} tag produces <code>...</code> in HTML, with an added benefit: The contents of the tag can contain left angle brackets < and ampersand &. You don’t escape them as < and &.

For code blocks in JavaDoc, the preferred approach, up to now, has been to use

/** * ... * <pre>{@code * ... * ... * }</pre> */ That way, you also don’t have to escape < and &.

As of Java 18, you can instead use the @snippet tag:

/** * ... * {@snippet : * ... * ... * } */ The text between the newline following the colon and the closing brace is enclosed in a pre tag. As with @code, leading whitespace and * are removed. (You don’t need to start each line with a *, but IDEs typically provide them automatically.) The remaining multi-line text has its common indent stripped, exactly like a text block.

A couple of caveats: In @snippet, just like in @code, braces { } must balance. As with all JavaDoc, you cannot have /* */ comments because the */ would close the JavaDoc /** comment opener. In Java, comments do not nest, and JavaDoc cannot change the processing of comments.

If you are familiar with the typesetting terminology, you may be irritated that JEP 413 refers to these as “inline snippets”, even though they yield code blocks, not inline code.

Even more confusingly, the JavaDoc documentation uses “inline” in a different way. It distinguishes between block tags such as @param, that appear at the start of a line, and inline tags such as @code{ } that can appear anywhere. In that sense, @snippet is an inline tag. If it contains code (and not a reference to external code), it is inline in both senses, as a tag and a snippet.

So far, this is not a monumental advance. The real power of snippets is to pull in code from external source files.

External Snippets and RegionsNow on to the interesting part. Suppose you have a code example that you want to include in your documentation. To make sure it is correct, you first make a unit test:

public class MarkdownBuilderTest { @Test void testCodeDelimitedByBackticks() { var builder = new MarkdownBuilder(); builder.code("`Hello, ${name}!`"); var result = builder.toString(); // `` `Hello, ${name}!` `` // Why this madness? See // https://meta.stackexchange.com/questions/82718/how-do-i-escape-a-backtick-within-in-line-code-in-markdown assertEquals(result, "`` `Hello, ${name}!` ``"); } ...} Of course, you don’t want to copy and paste the code into your JavaDoc. What if the API changes later? Instead, a snippet can read a region from a file:

/** * Adding an inline code element: * {@snippet file=MarkdownBuilderTest.java region=code-with-backticks} */ To mark the snippet in the source file, use markup comments:

public class MarkdownBuilderTest { @Test void testCodeDelimitedByBackticks() { // @start region=code-with-backticks var builder = new MarkdownBuilder(); builder.code("`Hello, ${name}!`"); var result = builder.toString(); // `` `Hello, ${name}!` `` // @end // Why this madness? See https://meta.stackexchange.com/questions/82718/how-do-i-escape-a-backtick-within-in-line-code-in-markdown assertEquals(result, "`` `Hello, ${name}!` ``"); } ...} The JavaDoc tool locates the file, copies the region, and strips the common indent.

The region can contain arbitrary code. The braces don’t have to match, and there can be /* ... */ comments.

Ok, not entirely arbitrary. The code cannot contain a comment of the form // @end

A file can have any number of regions. You can even have overlapping regions. Then the end comments must have the form

// @end region=regionName If you want to include an entire source file in a snippet, you don’t specify a region:

{@snippet file=MarkupBuilderTest.java} Instead of the file attribute, you can use a class attribute and specify the class name:

{@snippet class=com.horstmann.test.MarkupBuilderTest} Often, the files containing the snippets are not a part of the code that implements the features that you are documenting. In that case, you have two choices. You can place a file in a subdirectory snippet-files of the package to which the class belongs. (Note that due to the hyphen, snippet-files cannot be a part of a package name.) Or you can place them elsewhere, and invoke javadoc with the --snippet-path command-line argument, passing a list of directories separated by the platform path separator.

HighlightingSometimes you want to emphasize or highlight a part of your snippet. Use a @highlight markup comment to specify the highlighted parts. For example:

builder.code("`Hello, ${name}!`"); // @highlight substring=code Any matching substrings are displayed in bold:

builder.code("`Hello, ${name}!`"); This directive affects only the line preceding the comment. Instead, you can place the comment above the affected line:

// @highlight substring=code :builder.code("`Hello, ${name}!`"); Note the colon at the end of the comment line. This form is useful if the subsequent line is long, and it may be necessary if the subsequent line cannot have a trailing comment, such as a text block.

To apply a @highlight directive to multiple lines, define a region, like this:

// @highlight region substring=code...// @end You can name the region if you like:

// @highlight region=highlight-code substring=code...// @end region=highlight-code Caution: You cannot use this name to import the region as an external snippet. Only regions marked up with @start region=regionName can be imported.

If the substring (or in general, any attribute value of a markup comment) contains spaces, enclose it in single or double quotes. Here we need to use single quotes because the string contains double quotes.

builder.code("`Hello, ${name}!`"); // @highlight substring='"`Hello, ${name}!`"' Now the string "Hello, ${name}!" will be emphasized, including the quotation marks.

What if the substring to match contains both single and double quotes?

builder.code("`G'day, ${name}!`"); Then you can no longer use substring, but you can match with a regular expression. See the following section.

You can choose among three types of emphasis: type=bold (the default), type=italic, or type=highlighted. The type name becomes the class attribute of the generated HTML. Their appearance can be modified by adding or replacing the standard stylesheet (with the --add-stylesheet or --main-stylesheet option of the javadoc tool).

Regular ExpressionsTo select a regular expression, use the regex attribute instead of substring:

// @highlight regex=\bcode\b This matches all occurrences of code with word boundaries. If the string code occurs in a larger word, such as encode, the initial \b prevents a match.

Note that you do not escape the backslashes, even if the regular expression is enclosed in quotes:

// @highlight regex="\bcode\b" As another example, we might want to match quoted strings:

// @highlight regex='"[^"]*"' Here I had to enclose the regular expression in single quotes because it contains double quotes.

I know…if the quoted string contains \" escapes, that regular expression isn’t good enough. See this SO discussion for delightful improvements.

In a regular expression, you can use Unicode escapes. In particular, you can use \u0027 or \u0022 to denote single or double quotes. To match the pesky string from the preceding section, use

// @highlight regex='"`G\u0027day, ${name}!`"' Caution: Both javac and javadoc process Unicode escapes in source files before lexical analysis. However, javadoc does not process Unicode escapes in external snippets. In the preceding example, the six-character sequence \u0027 is a part of the regular expression.

Linking and ReplacingUse a @link markup comment to add links to the generated JavaDoc:

builder.code("`Hello, ${name}!`"); // @link substring=code target=MarkdownBuilder#code The target attribute value uses the same format as the {@link ...} tag for links outside snippets.

Instead of the substring attribute, you can use a regex attribute to specify the range of the link.

Sometimes, you want to elide inessential detail in the JavaDoc. This is achieved with the @replace markup comment:

builder.code("`Hello, ${name}!`"); // @replace regex='".*"' replacement='"..."' If the regular expression has groups, you can reference the group matches in the replacement text, using the general rules for Java regex replacement:

System.out.println("Hello, World!"); // @replace regex='"(.{3}).*(.{3})"' replacement='"$1...$2"' The replacement contains the first and last three characters in the string, yielding:

System.out.println("Hel...ld!"); Conversely, you need to escape $ and \ in the replacement string with another backslash.

As with @highlight, you can scope @link and @replace to a region:

// @link region=link-code substring=code target=MarkdownBuilder#code Other File TypesSnippets don’t have to be Java code. The standard doclet also supports the properties format. Here is an inline snippet:

/** * ... * This program writes a file such as the following: * {@snippet : * #Program Properties * #Sun Dec 1 12:54:19 PST 2022 * top=227.0 * left=1286.0 * width=423.0 * height=547.0 * filename=/home/cay/books/cj12/code/v1ch09/raven.html * } */ You can import an external properties file:

/** * ... * {@snippet file=sample.properties} */ In the properties file, you can use regions, highlights, links, and replacements, using markup comments that start with a # symbol:

```

@highlight regex=[0-9]+.[0-9]* :

``` Caution: Properties files don’t allow trailing comments. Comments must span an entire line. Either use the colon syntax, applying the markup comment to the next line, or use a region.

Hybrid SnippetsInline snippets are convenient for the JavaDoc author because the code is right there to see, in the source file. But they are only good for eye candy—highlighting and linking. External snippets allow for testing the snippet code. But they don’t show up in the source file that is being documented. Personally, I don’t see that as a huge problem. You can always generate the JavaDoc and look at that. But for those JavaDoc authors who would like to see the imported code, there are hybrid snippets.

A hybrid snippet is both inline and external:

/** * Adding an inline code element: * {@snippet file=MarkdownBuilderTest.java region=code-with-backticks : * var builder = new MarkdownBuilder(); * builder.code("`Hello, ${name}!`"); * var result = builder.toString(); // `` `Hello, ${name}!` `` * } */ Note that the @snippet tag has both an external reference and a body (between the colon and the closing brace).

If the inline and external code do not match, javadoc reports an error.

The Compiler Tree APISo far, I have focused on a workflow where the snippet code is contained in external source files that are tested independently. Suppose conversely that a JavaDoc author prefers inline snippets. Then it would be useful to have a tool that extracts the inline snippets and injects them into external code, such as unit tests. The JDK doesn’t contain such a tool, but it could be built with the snippet support in the Compiler Tree API. Java 18 adds a https://docs.oracle.com/en/java/javase/18/docs/api/jdk.compiler/com/sun/source/doctree/SnippetTree.html SnippetTree node to the Compiler Tree API. If you are interested in exploring this, have a look at this overview.

Scala mdocThe snippet support is a good solution for pulling in and formatting code into JavaDoc. It does not by itself run or verify the code. The Scala mdoc tool uses a different approach. It executes code snippets and pastes their results into the documentation.

Scala mdoc is based on Markdown, not HTML, which is just a sign of our modern times, not a fundamental difference. (When JavaDoc was first conceived, the birth of Markdown was still a decade away.)

Scala mdoc is not limited to producing API docs, but it can be used for arbitrarily structured documentation such as tutorials. I don’t want to dwell on the details. Here is the one interesting point: mdoc runs code snippets and splices the results into the documentation. If you write:

scala mdocval x = 1List(x, x)</pre>the Markdown snippet is transformed to<pre>scalaval x = 1// x: Int = 1List(x, x)// res0: List[Int] = List(1, 1) This is similar to running the code in the Scala REPL (with some subtle differences).

In Scala, this can be very useful. One can often describe an API with well-chosen examples of method invocations and their outputs. By using mdoc, one knows that the examples compile and that the documented results are always up to date.

There is also support for code snippets that shouldn’t compile or that throw an exception. You can hide setup code that is necessary for the code snippets to run.

Would something similar be useful for Java documentation? Code snippets could be piped into JShell and their values could be integrated into the documentation. It is certainly something to think about.

Articles and BooksWhen writing an article or book about programming, one is faced with the same problem as the API doc authors—to make sure that the code snippets actually work.

AsciiDoc has support for importing regions from external code files that is similar to JavaDoc snippets.

Github-flavored Markdown lets you import regions by line number, provided the file is stored on Github. That’s of course more limiting since the line numbers are not stable.

When I write books, I follow the inline snippet approach. I include the code in the manuscript, so that I can easily read and edit it. I use a very simple tool to extract the snippets into source files. For each source file, I have a template that splices the snippets into the source file. Here is an example from the third edition of Scala for the Impatient:

//SRC ../html/ch02.html//OUT ch2/src/main/scala/section4.worksheet.sc// print is like println, but doesn't add a newline: //INS #io-1// You can use string concatenation for complex outputs://INS #io-2// For formatted output, use the f interpolator: val name = "Fred"val age = 42//INS #io-4// The raw interpolator://INS #io-4b// Double an actual dollar in an interpolated string: val price = 19.95//INS #io-4c I write in HTML and the snippets are identified by their id attributes. In Markdown, one could use a fence label such as ```scala snippet #io-1. Such a template processor is easy to write. I describe the design in more detail in this blog.

You now know how to improve your JavaDoc with code snippets. Hopefully I have given you some food for thought how to use a similar approach for other technical writing, and a glimpse of what may become possible in the future.

The post JavaDoc Code Snippets and Friends appeared first on JVM Advent.

View Details

Performance, performance, performance – applications need performance! We hear, see, and breathe that with every change in Java applications. But making such changes in our code should result from a careful analysis: knowing what happened to the application and environment during a specific time. Understanding what is going on in your Java application is crucial when it comes to improving the performance of your application. A performance analysis needs tools, and this article gives you tips on using JDK tools like jcmd, jconsole, jstat, jmap, etc., to gain insights on classes and threads and perform live GC analysis or heap dump processing.

Command-line monitoring tools shipped with every JDK can help you get a better understanding about:

  • Basic virtual machine (VM), class, and thread information
  • Live garbage collection (GC) analysis
  • Capturing heap dumps for further processing

Let’s explore how tools like jps, jcmd, jinfo… can help you with that.

Querying The Running Java ProcessesWhen looking into performance, would be great to first get a list of the Java processes that run on target host. You can list those instrumented JVMs by using a tool named jps:

jps #list JVMS on localhost23296 Jps23276 example-nima-reactive.jar23294 NimaMain The right column of the output of this command shows the class or jar name, application or virtual arguments, and the left column contains the local virtual machine identifier (lvmid). Often the lvmid is the same as with the operating system process ID.

If you want to use jps output in your scripts, you can add the -q option to produce only the JVM identifiers. Also, you can run jps targeting a remote host by providing the host identifier using the syntax of an URI.

Ensure that the local host has the appropriate permissions to access the remote host and jstatd server is running on the target location, with an internal RMI registry bound to an open port.

The lvmid can further help you find out the uptime of the JVM by running the following:

jcmd 23276 VM.uptime23276:6642.570 s When querying details about a Java process, you can also run jcmd using the main class name:

jcmd NimaMain VM.uptime23294:32839.931 s In the following section, we will look closely at how you find JVM information and use that to tune its flags or detect memory issues.

Fine Tuning JVM Flags and Diagnosing Memory Leaksjcmd is a utility that can send diagnostic commands to a running JVM, and you should use this tool on the same machine as the JVM is running on. This powerful CLI tool can list all the JVM processes running on the local machine, offer you basic VM information about JVM tuning flags in use or JVM system properties, statistics about heap usage, managing a flight recording, etc.

Overusing jcmd to send diagnostic commands can affect the performance of the VM.

Probably you’ve seen the syntagm “reduce the memory footprint,” and you are wondering how to detect the current memory usage. For a Java application, the heap is the most significant memory consumer, but the JVM uses memory for its internal operations, and this non-heap memory is called native memory. You can use jcmd to find out more details about the native memory and, based on those, tune its usage or detect memory leaks.

You can find out how much native memory you are using by running the following:

jcmd 23276 VM.native\_memory23276:Native memory tracking is not enabled If native memory tracking is not enabled, you will receive the following message: Native memory tracking is not enabled. jcmd can offer you details about all the flags running on a JVM and thus detect if NativeMemoryTracking is enabled :

jcmd 23276 VM.flags #show the tuning flags and their value If you want to inspect an individual flag’s value, you can use another JDK CLI tool called jinfo. By running the following jinfo command, you can print information about a Java configuration for a specific Java process:

jinfo -flag NativeMemoryTracking 23276 -XX:NativeMemoryTracking=off To enable native memory tracking, you should restart your application using an additional VM argument: –XX:NativeMemoryTracking (NMT):

Please consider that having NMT enabled can add application performance overhead.

Once you enabled native memory tracking, the output of jcmd 23276 VM.native\_memory #print native memory usage will include a usage summary of JVM native memory types like:

  • Class which defines the JVM memory used to store class metadata.
  • Thread defines the memory used by application threads.
  • Code offers details about the memory used to store JIT-generated code
  • Compiler and GC space usage etc.

One of the easiest way to identify a memory leak in your JVM is by running jcmd with VM.native_memory option to first get a baseline:

jcmd 23276 VM.native\_memory baseline This will create a snapshot of the current memory usage to be compared with the later in time usage using summary_diff option:

jcmd 23276 VM.native\_memory summary.diff If diff reports show a significant increase of memory usage in areas like Heap, Thread, Code or Class, this can be a memory leak issue.

IntegratiNG JVM Monitoring ToolsKeeping track of the JVM flags or changes in native memory usage can be daunting, but you can easily integrate jcmd with Java Flight Recorder (JFR). JFR is a profiling and event collection framework built into the JDK and you can use it to gather low-level details about how the JVM and Java applications behave. You can generate a JFR file using jcmd via:

jcmd 23276 JFR.start name=example\_recording delay=10s duration=20s filename=./examplerecording.jfr The previous command creates a sample JFR recording file named examplerecording.jfr in the same location as the jar application. The recording starts in 10s after launching the command, captures events during 20s and uses the default JFR settings. To stop the recording simply run:

jcmd 23276 JFR.stop name=example\_recording Moreover, starting with JDK 14 you can use JFR Event Streaming to integrate JFR with different metrics APIs and send JVM monitoring data directly to the monitoring service of choice.

How about visualizing the threads run by Java applications? jconsole displays information in real-time about the number of threads running in an application. Just run jconsole in a terminal window, and you will see the desktop tool popping up:

jconsole Threads view

If you would like to take a closer look to the stack of running threads, jstack CLI tool can help you with that:

jstack 23276#prints Java stack traces of threads for this process How about monitoring Garbage Collectors activity? jcmd has capabilities that include performing GC operations, but also collecting heap dumps:

jcmd 23276 GC.heap\_dump # generate a JVM heap dumpx Moreover, you can obtain statistics about garbage collectors by running jstat and print its output with -gcutil option:

jstat -gcutil 23276 10 250 #take 10 samples every 250ms If you are interested in printing heap summaries or generating a heap dump you should give jmap a try:

jmap -histo 23276#print a histogram of the Java object heapfor this Java processjmap -dump:file=heap.bin 23276#generate a heap dump inheap.bin file ConclusionWhen writing this article, the latest JDK release is 19 and ships with more than 20 tools. These tools evolve rapidly with the Java landscape, and newer ones may supersede some. Gathering details using various CLI JDK tools allows you to obtain individual or time-distributed information about the JVM and Java applications’ performance. You can coordinate those pieces of information with JVM and infrastructure metrics collected by tools like Datadog or Prometheus. These actions help you understand how your application performed with specific JVM and infrastructure configurations, so you know where to apply improvements.

The post A Sneak Peek at The Java Performance Toolbox appeared first on JVM Advent.

View Details

Java introduced the concept of checked exceptions. The idea of forcing developers to manage exceptions was revolutionary compared to the earlier approaches.

Nowadays, Java remains the only widespread language to offer checked exceptions. For example, every exception in Kotlin is unchecked.

Even in Java, new features are at odds with checked exceptions:the signature of Java’s built-in functional interfaces doesn’t use exceptions.It leads to cumbersome code when one integrates legacy code in lambdas.It’s evident in Streams.

In this article, I’d like to dive deeper into how one can manage such problems.

The problem in the codeHere’s a sample code to illustrate the issue:

Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList") .map(it -> new ForNamer().apply(it)) // 1 .forEach(System.out::println); 1. Doesn’t compile: need to catch the checked ClassNotFoundException

We must add a try/catch block to fix the compilation issue.

Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList") .map(it -> { try { return Class.forName(it); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }) .forEach(System.out::println); Adding the block defeats the purpose of easy-to-read pipelines.

Encapsulate the try/catch block into a classTo get the readability back, we need to refactor the code to introduce a new class. IntelliJ IDEA even suggests a record:

var forNamer = new ForNamer(); // 1Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList") .map(forNamer::apply) // 2 .forEach(System.out::println);record ForNamer() implements Function<String, Class<?>> { @Override public Class<?> apply(String string) { try { return Class.forName(string); } catch (ClassNotFoundException e) { return null; } }} 1. Create a single record object 2. Reuse it

Trying with LombokProject Lombok is a compile-time annotation processor that generates additional bytecode. One uses the proper annotation and gets the result without having to write boilerplate code.

Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java. Never write another getter or equals method again, with one annotation your class has a fully featured builder, Automate your logging variables, and much more.

— Project Lombok

Lombok offers the @SneakyThrow annotation:it allows one to throw checked exceptions without declaring them in one’s method signature.Yet, it doesn’t work for an existing API at the moment.

If you’re a Lombok user, note that there’s an opened GitHub issue with the status parked.

Commons Lang to the rescueApache Commons Lang is an age-old project.It was widespread at the time as it offered utilities that could have been part of the Java API but weren’t.It was a much better alternative than reinventing your DateUtils and StringUtils in every project.While researching this post, I discovered it is still regularly maintained with great APIs.One of them is the Failable API.

The API consists of two parts:

  1. A wrapper around a Stream
  2. Pipeline methods whose signature accepts exceptions

Here’s a small excerpt:The code finally becomes what we expected from the beginning:

Stream<String> stream = Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList");Failable.stream(stream) .map(Class::forName) // 1 .forEach(System.out::println); Fixing compile-time errors is not enoughThe previous code throws a ClassNotFoundException wrapped in an UndeclaredThrowableException at runtime. We satisfied the compiler, but we have no way to specify the expected behavior:

  • Throw at the first exception
  • Discard exceptions
  • Aggregate both classes and exceptions so we can act upon them at the final stage of the pipeline
  • Something else

To achieve this, we can leverage the power of Vavr. Vavr is a library that brings the power of Functional Programming to the Java language:

Vavr core is a functional library for Java. It helps to reduce the amount of code and to increase the robustness. A first step towards functional programming is to start thinking in immutable values. Vavr provides immutable collections and the necessary functions and control structures to operate on these values. The results are beautiful and just work.

— Vavr

Imagine that we want a pipeline that collects both exceptions and classes. Here’s an excerpt of the API that describes several building blocks.

It translates into the following code:

Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList") .map(CheckedFunction1.liftTry(Class::forName)) // 1 .map(Try::toEither) // 2 .forEach(e -> { if (e.isLeft()) { // 3 System.out.println("not found:" + e.getLeft().getMessage()); } else { System.out.println("class:" + e.get().getName()); } }); 1. Wrap the call into a Vavr Try 2. Transform the Try into an Either to keep the exception. If we had not been interested, we could have used an Optional instead 3. Act depending on whether the Either contains an exception, left, or the expected result, right

So far, we have stayed in the world of Java Streams. It works as expected until the forEach, which doesn’t look “nice”.

Vavr does provide its own Stream class, which mimics the Java Stream API and adds additional features. Let’s use it to rewrite the pipeline:

var result = Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList") .map(CheckedFunction1.liftTry(Class::forName)) .map(Try::toEither) .partition(Either::isLeft) // 1 .map1(left -> left.map(Either::getLeft)) // 2 .map2(right -> right.map(Either::get)); // 3result.\_1().forEach(it -> System.out.println("not found: " + it.getMessage())); // 4result.\_2().forEach(it -> System.out.println("class: " + it.getName())); // 4 1. Partition the Stream of Either in a tuple of two Stream 2. Flatten the left stream from a Stream of Either to a Stream of Throwable 3. Flatten the right stream from a Stream of Either to a Stream of Class 4. Do whatever we want

ConclusionJava’s initial design made plenty of use of checked exceptions. The evolution of programming languages proved that it was not a good idea.

Java streams don’t play well with checked exceptions. The code necessary to integrate the latter into the former doesn’t look good. To recover the readability we expect of streams, we can rely on Apache Commons Lang.

The compilation represents only a tiny fraction of the issue. We generally want to act upon the exceptions, not stop the pipeline or ignore exceptions.In this case, we can leverage the Vavr library, which offers an even more functional approach.

You can find the source code for this post on GitHub.

To go further:

  • Exceptions in Java 8 Lambda Expressions
  • How to Handle Checked Exceptions With Lambda Expression
  • “Stackoverflow: Java 8 Lambda function that throws exception?”
  • Failable JavaDoc
  • Vavr
  • Exceptions in Lambda Expression Using Vavr
  • Java Streams vs Vavr Streams

Originally published at A Java Geek on October 16th, 2022

The post Exceptions in Java Lambdas appeared first on JVM Advent.

View Details

In this article, we discuss the new Spring authorization server framework. This framework comes as a replacement for the capability to build an OAuth authorization server after the deprecation of the Spring Security OAuth project. The Spring Security OAuth project needed to be deprecated. It was only offering support for the OAuth 1 specification, and building a reliable OAuth 2/OpenID Connect using it was not easy. The part that refers to the client and the resource server moved directly into Spring Security, while the authorization server part has been extracted into a separate framework, which we’ll discuss in this article. After reading this article, please visit and find more details directly on the project’s main page: Spring Security Authorization Server

  1. What is an authorization serverLet’s start with a briefing on OAuth 2 and what an authorization server is. In most organizations, people work with multiple software systems which cover different purposes in their day-to-day work. And I’m certain that even you are not comfortable having multiple accounts for each and every software system you use. We’d prefer a better way to manage access. With an OAuth 2 implementation, we have a separate component that manages the users credentials. We call this component the authorization server. Any (frontend) application would need to request the user to log in through the authorization server to access its backend. So instead of having each application managing its own access, we separate this responsibility.

Figure 1 The main actors in an OAuth 2 systemThe user will be able to log in using the authentication capabilities provided by the authorization server, and then use any of the apps integrated with that authorization server. In this article, we’ll implement the authorization server functionality using the new Spring Security authorization server framework.

  1. Building a minimal authorization serverIt’s time to build our authorization server using the new Spring Security authorization server framework. The first thing to do is make sure you have the dependencies for your project.

Listing 1 presents the pom.xml file of the Spring Boot project we’ll build for our demonstration. You observe that we only need to add the web dependency (since it will be a web app) and the Spring Security authorization server dependency for the authorization server framework.

Listing 1 The needed dependencies in your pom.xml file <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> <version>1.0.0-M2</version> </dependency></dependencies> Next thing we need to configure is the authorization server filter in the security filter chain. You can start from a minimal configuration provided by the utility method applyDefaultSecurity(HttpSecurity http) in the OAuth2AuthorizationServerConfiguration class. To apply several customizations, you use the getConfigurer() method as presented in listing 2.

Also, in listing 1, you can observe the use of authenticationEntryPoint() to make sure that any request that wasn’t authenticated will be correctly redirected to the login page provided by the server.

Listing 2 Configuration at security filter chain level @Configurationpublic class SecurityConfig { @Bean @Order(1) public SecurityFilterChain asSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))) return http.build(); }} Since we plan to use the authorization code grant type, the authorization server has to provide a way for the user to log in. For this reason, we’ll configure a second filter in the filter chain to set up the form login authentication on our server.

Definitely, you can implement any authentication method, but for our example, the simple way is to configure the form login with its convention configurations as provided by Spring Boot.

Listing 3 presents the minimum setup for enabling the form login authentication.

Listing 3 Configuration of form login support @Bean@Order(2)public SecurityFilterChain appSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests( (authorize) -> authorize .anyRequest().authenticated() ).formLogin(Customizer.withDefaults()); return http.build();} To make things easier to read and understand, in listing 4 I added both filters we previously defined in this article.

Listing 4 Full filter chain configuration @Configurationpublic class SecurityConfig { @Bean @Order(1) public SecurityFilterChain asSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .exceptionHandling( (exceptions) -> exceptions .authenticationEntryPoint( new LoginUrlAuthenticationEntryPoint("/login")) ) return http.build(); } @Bean @Order(2) public SecurityFilterChain appSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests( (authorize) -> authorize.anyRequest().authenticated() ).formLogin(Customizer.withDefaults()); return http.build(); }} Being an authorization server dedicated to support grant types related to user authentication, it needs to also manage the user details. Fortunately, the user details management is made the same way as we’d do with any usual Spring Security application: with a UserDetailsService implementation.

Listing 5 demonstrates a simple InMemoryUserDetailsManager plugged in as a bean in the Spring context to take care of the user management. To simplify our example, I only added one user in-memory. The PasswordEncoder may also be added separately as a bean in case you want to provide specific implementations for the password encrypt/hash.

Listing 5 User management @Beanpublic UserDetailsService userDetailsService() { UserDetails userDetails = User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build(); return new InMemoryUserDetailsManager(userDetails);} The authorization server also needs to manage all the details about clients (apps that request tokens and use them to access the resources protected by the resource server). To define the client management, one needs to provide an implementation of the RegisteredClientRepository interface. The object model that represents a client known by the authorization server is defined by the RegisteredClient type. Some of the details you have to define are:

  • the client credentials (client ID and secret)
  • the authentication method
  • the supported grant types for each client
  • the redirect URIs (in case of authorization code grant type)
  • the scopes

Additionally, one can also specify per client:

  • token settings (for example the expiration time)
  • specific client settings (for example, whether it requires or not a consent)

Listing 6 shows how client management can be defined. In this simple example, I used an in-memory implementation of the RegisteredClientRepository contract. However, you can provide any implementation of the RegisteredClientRepository interface. For example, you might want to store and get them from a database.

Listing 6 Client management @Beanpublic RegisteredClientRepository registeredClientRepository() { RegisteredClient c = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("client") .clientSecret("{noop}secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT\_SECRET\_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION\_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH\_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT\_CREDENTIALS) .redirectUri("http://127.0.0.1:8080/authorized") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .clientSettings(ClientSettings.builder() .requireAuthorizationConsent(true).build()) .build(); return new InMemoryRegisteredClientRepository(c);} In most cases, you’d use a non-opaque token. JSON Web Token (JWT) is the most known and used implementation of a non-opaque token.

The authorization server framework supports both opaque and non-opaque tokens, but in this example I chose to use a non-opaque JWT implementation.

If you choose to use a JWT, you need to provide key pairs to sign the tokens. The JWKSource is the bean you need to plug into the authorization server configuration to tell it what key pairs can it use to sign/validate the JWTs. Observe that in this example, I used javax.security capabilities provided directly by the JDK to generate the key pair at the app startup. Eventually, in a real-world app, you might want to load the keys from a vault or use a different practice to manage them. Whichever your approach would be, as long as you end up with a JWKSource component managing these keys, the authorization server will know how to use them.

Listing 7 shows the definition of a JWKSource that manages a set containing only one key pair. In a real-world app, you might want to have multiple key pairs and eventually use key rotation for additional safety.

Listing 7 Private public key-pairs management @Beanpublic JWKSource<SecurityContext> jwkSource() { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet);}private static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair;} The last piece of the needed configuration is the AuthorizationServerSettings bean, which provides the main server configurations (for example, the endpoints that can be called by the client). If you wish to use the defaults, you can simply build an instance and plug it into the Spring context, as presented in listing 8.

Listing 8 General server configurations @Beanpublic AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build();} Now you have a minimal working authorization server. You can start the app to test it.

  1. Testing the whole setupYou have a variety of things to test, but to prove the minimum, let’s just execute a full authorization code with PKCE grant type. We’ll act like the client and expect to go step by step through the flow and in the end get a JWT token.

Here are the steps we need to follow:

  1. Call the /authorize endpoint and expect to be redirected to the login page.
  2. Login with valid credentials (see the user we added with the UserDetailsService contract)
  3. After inserting correct credentials, we will be redirected to a page that doesn’t exist (the one we added as redirect URI) but we will get the authorization code as a request parameter in the URL.
  4. We take the authorization code, and we call the /token endpoint
  5. In the response, we get the access token (In the format of a JWT)

Figure 2 The authorization code grant typeStep 1. Call in browser the /authorize endpoint. The next code snippet gives you the full URL for test. http://localhost:8080/oauth2/authorize?response_type=code&client_id=messaging-client&scope=message.read&redirect_uri=http://127.0.0.1:8080/authorized&code_challenge=QYPAZ5NU8yvtlQ9erXrUYR-T5AGCjCF47vN-KsaI2A8&code_challenge_method=S256

Figure 3 The login screenAfter logging in and being redirected (steps 2 and 3) you get the authorization code.

Figure 4 The page you are redirect back to with the authorization code providedStep 4, you call the /token endpoint. The next code snippet provides you the full URL for calling the /token endpoint.

http://localhost:8080/oauth2/token?client_id=messaging-client&redirect_uri=http://127.0.0.1:8080/authorized&grant_type=authorization_code&code=Vw30RxdqfgdMw0xcWPV8tdxUH1nJdjny7tN5ub8td2dUrioCFXLnqvlgcFmkpbuwiviscp-wCvlxygjCn6pNEWYgo8tWlmkZg8LNgAY7lxTKZBuae7kabyX_0Tk_7ges&code_verifier=qPsH306-ZDDaOE8DFzVn05TkN3ZZoVmI_6x4LsVglQI

Figure 5 Calling the /token endpointFigure 6 Using HTTP Basic authentication to call the /token endpointFinally, you got a response with a JWT token. You can inspect your JWT token using jwt.io The next snippet shows an example of how the JWT might look like.

eyJraWQiOiIyY2E1MTU1ZS1iMzNkLTQzYWYtYWRmMy0yYWY4MmUzZTc4ODMiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoiY2xpZW50IiwibmJmIjoxNjY4MTU0NjI1LCJzY29wZSI6WyJvcGVuaWQiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiZXhwIjoxNjY4MTU0OTI1LCJpYXQiOjE2NjgxNTQ2MjV9.EUD5aqO0yKHa7k4wKMkl85aE\_LmbcfKrLWZC9jt\_-pfui5QOyOgmhQAuWQksEazpT09zajagPOtzwSelDNynPDCj\_rpciw8IqVZp4ePoYc2lhv95o9qheC5m2qE3wD-6lYDey\_pFckmEaGMuH9tp6btqetAyvvgJDaLGH9MGZhTcHFQi0ij93kUz3dxr8JX15eO\_RfoMlWxf3rKrMt9B7aQBqu8ijGIRqvzVPqVhxep4Qjff8n0v9UK6v1zuPJtyJ8VvT4\_y8iRz5uxC2scATTk6HywtdcZMix3ShNKqNSssnfZm5IhnjfT31lOl7YQWFLiyYbMSNinifnp3kMaiBw Figure 7 Decoding the JWT in jwt.io4. Summary* The Spring authorization framework is a Spring ecosystem project that enables you to easily build and customize an OAuth2/OpenID Connect server * The Spring authorization framework is not a product such as Keycloak (or similar third parties). You use the Spring authorization server framework to build your own authorization server, not just configure one. * Building a minimal authorization server is simple. The process includes the definition of a few base components among which we can enumerate: user management, client management, key pair management for token signing, and generic server configuration. * The framework offers very easy-to-customize ways, allowing you to easily and fast build an OAuth 2/OpenID Connect server.

The post A new Spring Security authorization server appeared first on JVM Advent.

View Details

Many years ago, microservices appeared as the solution to all the problems that the monoliths had, such as scalability, maintainability, and availability. The companies adopt this type of architecture on their platforms which implies massive migrations to old systems using different strategies, like slicing/splitting into small pieces as an alternative to creating a new platform using the old one just as a guide. After some time, it could take months or years; the microservice needs to be deprecated for many reasons, like performance issues, the frameworks having many bugs and not existing a new version, or the company or else the community not supporting the language. This situation offers a unique problem because now you have a microservice that receives requests from many other places. Hence, the deprecation process implies you need to have the plan to remove the microservice to communicate it to all the consumers.

Why can a migration fail?Let’s start with a real possible scenario; a travel company has a catalog microservice with all the information about countries, states, cities, airports, and many other entities. The microservice needs to be deprecated because it has some big problems related to performance issues, scalability, and maintainability, concentrating on many endpoints as well as the logic of multiple teams in one place.

The migration process to new microservices to replace the old one could be simple, but it’s not because you need to coordinate with multiple teams. Some strategies that could fail are:

  1. Notify all the consumers: Inform all the teams that a new microservice exists and that the old one will be deprecated, along with waiting for all the teams to respond to notify that the migration was successful. This approach has multiple problems because only some groups have the same availability on their backlogs to migrate. The migration could take weeks or months to end, implying that you need to maintain two different microservices spending time-solving issues.
  2. Not checking the new microservice: One common problem when you create a new microservice is to check if everything works fine in a real scenario, receiving a request for other microservices. This point is relevant because if the consumers start to migrate and problems appear in some of the environments, there will be a big chance that the consumers will stop the migration together with rollback the changes until you solve the problems. For some reason, it’s not strange that difficulties appear on the new microservice, but you need to reduce the risk that the issues arise during the migration using some mechanism to check if everything works fine or not.
  3. Continue receiving requests after the migration: You notified all the teams that use the microservices, and all of them migrated, but your microservices have a lot of requests, so you need to decide what you want to do with the old microservice, but you don’t know the real impact to stop it.

These are just a few problems you can find during migration, but many others depend on the company’s context and the consumers.

How to succeed in a migration?There isn’t a way to migrate from one microservice to another without problems. Still, you could mitigate the risk of significant issues appearing during the process by doing a plan that considers many aspects, like the number of consumers using your microservice and the window of time to migrate.

The migration process to deprecate an endpoint/microservice

The migration process could have 3 phases, each of which needs to be executed in sequence. First, you need to create a plan for the migration; after that, migrate with the coordination of all the consumers, and last, deprecate the microservice. Let’s see each of these phases in a little more detail.

Creation of the plan for the rolloutThe first step to deprecate an endpoint/microservice is to create a plan considering different aspects to communicate with all who will suffer the impact of the changes. Some of the elements that you need to consider are:

  • Find the consumers: Many tools help you to solve this problem, like the APM (New Relic, ELK, Dynatrace), which shows you a service map with the relation between the microservices, so you only need to take this information and find which team or person are responsible for maintaining those microservices. Sometimes not all consumers appear on the APMs, which imply a possible risk of the deprecation process partially failing; an excellent alternative to solve this problem is to add and check on the logs of your application some information that helps you to validate the list of consumers, this depends of how you implemented your infrastructure because if you use Kubernetes and inject extra information on the headers of the different requests like the name of service the task to find the consumers, it’s simple.
    APM – New Relic

Another thing to consider is if someone outside of the company uses the endpoint/microservice because this will affect the deadline to deprecate or remove it. Also, you need to consider alternatives if some of the external consumers won’t migrate to the new microservice. * Define the strategy: This point is one of the most relevant parts you need to consider during the migration because you need to reduce the risk of something wrong happening with your new endpoint/microservice. There are at least two different strategies to consider: + Both microservices exist but do not have any connection between them. The consumers migrate from one place to another, assuming all the risk that something could be wrong with the new microservice. + The old microservice acts as a proxy between the consumers and the new microservice. This approach offers many advantages. You can use some library or tool that implement feature flags like FF4J, Togglz, or Unleash, which have clients to use with Java/Kotlin so you can redirect a percentage of the requests to the new microservices and check if everything works fine or no. If everything works fine, you can continue to increase the rate until you arrive to redirect all the requests to the new microservice. Still, if something terrible happens, you can send all the requests to the old implementation to fix the problem on the new one.
As a recommendation to choose which implementation of feature flags, it’s best to try to create an ADR (Architecture Design Record) to discuss the pros/cons of each implementation. * Define the early adopters: You need to define which of the consumers could be an excellent option to become the early adopters or the new microservice considering different aspects like the level of communication that you have with that team or if they are receiving the changes like something good and try to help you. You need to think about this particular point, like having allies that allow you to push these changes. * Deadline: You can’t maintain both microservices forever because it implies that your team needs to fix bugs or issues in both. Also, these microservices could not have the same language or frameworks, so your team takes more time to solve the problems. The deadline needs to consider the number of consumers and the type of changes between both microservices. Suppose the changes only imply changing the URL using the same request/response. In that case, you can declare that the deadline for the old one could be in one month, but if the changes are complex, like adding new parameters on the request or the response changing all the structure could be an excellent option to have a deadline in a few months. * Communicate your plan: The last part of the plan is to define the strategies to communicate the migration and follow the advances. As a suggestion, try not to use one particular way to inform all the consumers because it’s not the same as notifying all the consumers that are part of the same company that inform external consumers. Some approaches could be: + Send the information on the headers: You can add the headers “Deprecation” and “Sunset” on all your requests with the information about the deadline to migrate, which is an approach that many companies like Paypal, IBM, and Clearbit use following the RFC8594 which introduces these headers.
Deprecation: Tue, 31 Dec 2024 23:59:59 GMT``Sunset: Wed, 31 Dec 2025 23:59:59 GMT
The main problem with this approach is that your consumers need to have some mechanism to intercept and notify that some endpoint will disappear, which only sometimes happens. By the way, adding headers could be simple if you use frameworks like Spring Boot or Quarkus, adding some interceptors which add the headers in all the responses. + Send an email with the information: The idea is to send all the information about the deprecation process and the changes the other teams need to make. As a recommendation, the email includes questions about whether it is possible to do it until the deadline and if all the explanation is straightforward. This approach implies send and respond many emails, but it’s one of the best ways to do it. + Create a group on your internal communication tool: If you use tools like Teams or Slack could be an excellent option to create a channel with the people responsible for the different teams involved in the migration to respond to the question just once. * Track the advance: Create a document to represent the advance of the migration and which teams migrated to the new microservice. If there are no significant advances during a period, you can send an invite to have a meeting and discuss what happens.

These aspects are only the basic ones, but other ones depend on the context of the situation.

Start the process of deprecationAfter creating the plan with a specific deadline and having the new endpoint/microservice in the production environment, the next step is to start the deprecation process with the microservices that are part of your team because it’s an excellent way to detect problems with genuine requests. You could deploy and do a rollback in case something terrible happens without the need to notify another team. When you finish with the migration of all the microservices of your team and do not detect any problem for a period, you need to notify external consumers about the deprecation process.

This migration phase could take some time, weeks, or months, so the good idea is to monitor your APM or your logging tool periodically to check if the number of operations on the old microservice decreases each week. If nothing happens for a couple of weeks, try communicating with the consumers to understand what happens.

During this migration process, try to avoid including new features on the old microservice because this will imply that you will have them in the new one. An excellent strategy to force your consumers to migrate is to indicate that some new endpoints or features are only available on the new microservice. It’s not a unique way to motivate consumers to migrate, but you need to find some approach to seduce them to do it before the deadline.

At this phase, you must consider options in case some consumers will not migrate.

FINISH the process of deprecationWhen the deadline that you defined on the plan arrives, if everything occurs like your idea, you will not have any problems. You can remove the microservices in different environments but only in some cases; this situation happens, so you need to define what you will do with the consumers. Here are some alternatives you could consider possible options:

  • Communicate with the teams: You can meet with the teams that do not do the migration and try to obtain a tentative new deadline. If you adopt this approach, consider adding some mechanism to throw an exception when you receive a request for other microservices not declared in some whitelist. The main problem that could appear if you do not have a whitelist is that someone can assume that the microservice is active and try to use them, so you need to reduce the risk that only specific consumers for a while.

If you Spring Boot or Quarkus, a good option could be to create an interceptor that checks if the whitelist contains the consumer that requests to reject or accept it.Lastly, define a new deadline which is explícity declared on the microservice, and after that date, reject all the requests. * Remove the microservice: Another alternative is to remove the microservice in a non-productive environment. Hence, the entire platform continues working, but the consumers can’t continue using your microservice on the rest of the environment. It’s a way to force consumers to do their migration. This is especially useful when you have requests from unknown consumers. * Redirect all the requests: Another possibility, if the changes of the endpoint or microservice do not have modifications on the request/response, it’s to create specific rules of routing all the requests to the new microservice and stop the old one. This implies that you need time from some DevOps to do it; the best scenario for this approach is that you have an API Gateway like Spring Cloud Gateway writing the rules without assistance.

Before choosing any alternative, you need to consider the tradeoff because it’s not the same that deprecates a microservice that only exposes information about cities and countries instead of another one that processes all the company payments.

WHAT’S NEXT?There are tons of resources about the process of the deprecation of monoliths, but a few are related to microservices. The following is just a short list of resources that you can apply for both types of architectures:

  • Monolith to Microservices: Evolutionary Patterns to Transform Your Monolith by Sam Newman
  • Respectful REST APIs – ‘Sunset’ and ‘Deprecation’ HTTP Headers by Horatiu Dan
  • Evolution Patterns by Microservice API Patterns – This link will offer other strategies to use in the deprecation process.

Other resources that could be great reads to change the approach of connecting the microservices using a synchronic way like REST/SOAP to an architecture oriented to events.

  • Building Event-Driven Microservices: Leveraging Organizational Data at Scale by Adam Bellemare
  • Grokking Streaming Systems: Real-time event processing by Josh Fischer and Ning Wang

The following resources are connected with some topics that appear in the article:

  • Fundamentals of Software Architecture: An Engineering Approach by Mark Richards and Neal Ford – In this book, you will find a great explanation of the use of ADR

CONCLUSIONDepreciating an endpoint or microservice implies many things, most of which you could consider in the migration plan. Still, communication with other people is key to success. Try always to migrate with your microservices or with teams with a certain level of affinity to do a test; this will help you.

In the future, consider other alternatives that the consumers connect directly with your microservices; there are alternatives like using an API Gateway or events. Create an API Gateway that acts as a proxy between the consumers and your microservices. This approach will help you reduce the interaction complexity and wait for other teams to migrate. Using this approach, you can migrate to the gateway without the need to notify all the consumers.

The post Don’t remove the API appeared first on JVM Advent.

View Details

Eclipse Collections is an open source Java Collections framework. In this blog I am going to demonstrate five lesser known features of the framework. I have published similar blogs in Java Advent Calendars of 2018, 2019, 2020, and 2021. Please refer to the resources at the end of the blog for more information about the framework.

  1. toSortedList(Comparator): If you need to sort a RichIterable and convert it to a sorted List, you can use the toSortedList(Comparator) API on RichIterable. This API creates a sorted list where the elements are sorted using the comparator.
    It is important to remember the difference between sorted and ordered. Lists are by default ordered.
    Note: Since this is a to API, it means a new List will be created. @Testpublic void toSortedListComparator() { var list = Lists.mutable.with(3, 1, 2); var sortedList = list.toSortedList(Comparator.naturalOrder()); Assertions.assertEquals( Lists.mutable.with(1, 2, 3), sortedList, "Sorted List by Natural Order");}
  2. toSortedListBy(Function) : In addition to the API provided above, sometimes, you need to sort the list as a result of a transformation or a mapping function. In these cases toSortedListBy(Function) can be used.
    Note: Since this is a to API, it means a new List will be created. @Testpublic void toSortedListBy() { var list = Lists.mutable.with(-3, 2, -1); var sortedList = list.toSortedListBy(Math::abs); Assertions.assertEquals( Lists.mutable.with(-1, 2, -3), sortedList, "Sorted List by absolute value");}
  3. Fused API for collect + makeString(): Eclipse Collections provides the makeString() API to create a String representation of a collection. However, there are times when each element needs to be transformed before creating a String representation. Eclipse Collections provides an overloaded makeString() API which takes a Function, start , separator, and end. @Testpublic void collectMakeString() { var list = Lists.mutable.with(-3, 2, -1); var stringRep = list.makeString(Math::abs, "[", ":", "]"); Assertions.assertEquals( "[3:2:1]", stringRep, "String representation of absolute values");}
  4. containsBy(): The regular contains() API performs an .equals() check between a value and elements of a collection. However, there are times when you need to evaluate the presence of a value after applying a Function to the elements. You can use containsBy() for such a use case. Advantage of the containsBy() operation is that you will not have to first transform all the elements of the RichIterable and then perform contains() for a value. However, the disadvantage is that it is a O(n) operation. Please weigh the pros and cons w.r.t. your use case before using this API.
    Note: Since the function needs to be applied to each element, this is an O(n) operation. @Testpublic void containsBy() { var set = Sets.mutable.with(-1, -2, -3); // containsBy is a O(n) operation even for a Set. var containsBy = set.containsBy(Math::abs, 1); Assertions.assertTrue(containsBy); var list = Lists.mutable.with(-1, -2, -3); Assertions.assertFalse(list.containsBy(Math::abs, -1));}
  5. swap(): If you need to swap the indexes of two elements in a MutablePrimitiveList then you can use the swap() API. This API is currently only available on the MutablePrimitiveList hierarchy.
    Note: This is an in-place operation. @Testpublic void swap() { var intList = IntLists.mutable.with(1, 2, 3); // Swap the elements at index 0 and index 2 intList.swap(0, 2); var expected = IntLists.mutable.with(3, 2, 1); Assertions.assertEquals(expected, intList);}

Eclipse Collections Resources:
Eclipse Collections comes with it’s own implementations of List, Set and Map. It also has additional data structures like Multimap, Bag and an entire Primitive Collections hierarchy. Each of our collections have a fluent and rich API for commonly required iteration patterns.

  • Website
  • Source code on GitHub (Make sure to star the Repository)
  • Contribution Guide
  • Reference Guide
  • Hidden Treasures of Eclipse Collections 2020 Edition
  • Hidden Treasures of Eclipse Collections 2019 Edition
  • Hidden Treasures of Eclipse Collections 2018 Edition

The post HIDDEN TREASURES OF ECLIPSE COLLECTIONS 2022 EDITION appeared first on JVM Advent.

View Details

Key Takeaways:

  • Java is made of threads: many of the internals of JVM programs rely on threads from debugging to GC
  • The classical threads are expensive as they are just thin wrappers around OS threads
  • Various solutions appeared through the years from the executor framework, fork/join pool, reactive streams and the latest Project Loom and its virtual threads
  • Parallel usage of resources is very helpful, but misused can make your program expensive

Java has been around for over two decades, and it has been and continues to be a dominant force in the software development arena ever since. There are multiple reasons why it is doing so well, however, one of which is concurrency. Java started its journey by introducing an in-built threading model.

In this article, I will go over a bit of history, how it shaped our programming understanding and practice using Java, where we are now, and one particular problem with it.

This will be a bit long, but I’m sure you will enjoy it.

Let’s begin the journey!

Java is made of threads:From day zero, Java introduced threads. Threads are the basic units of execution in Java. This means that any Java code we want to run is executed by a thread. Threads are an independent unit of the execution environment on the Java platform.

From this, it’s easy to see that if a program has more threads, it has more places where code can be run. That means we can do more things simultaneously. That brings many benefits to the table. One particular benefit is that it improves the application’s throughput by utilizing all the resources available on the machine. By doing so, we can achieve more from a program.

Threads are in all layers of the Java Platform:

Not only does the thread execute code, but it also keeps track of the invocation of methods in its stacks. So when a Java program runs into trouble while executing, we get an exception. The exception contains the stack trace, from which we can figure out what went wrong. So we can tell that threads are a means of getting those stack traces.

Besides, we use threads if we need to debug our program through the IDE. We need threads if we need to profile our program or parts of it. Java garbage collectors run in a separate thread. All of these point out that concurrency, or “threads,” is an integral part of the programming platform.

However, threads are expensive:In modern Java web applications, throughput is achieved by using concurrent connections. Usually, a dedicated thread is given to every request from a client. Modern operating systems can handle millions of concurrent connections. This indicates that we will have more throughput if we have more concurrent connections.
The conclusion may seem legit; however, the reality is far from it. The reason is that we cannot create as many threads as we want to accomplish that.
Threads are limited and expensive. Creating a thread takes 2 MiB of memory outside of the heap. The other thing we must remember is that, traditionally, Java threads are just a thin wrapper around an operating system’s threads. And we can only create a few of them. Even though we can get a good amount of them, we cannot always guarantee the application’s overall performance. So the content switch has a cost associated with it.
You can test the following program and see how many threads you can create.

So far, we’ve discussed why Java threads are essential and some of their limitations. So, let’s dig a bit further.

What problem do we have now:Modern software application development requires high data scale, as we need to deal with too much data. We have high usage as well. That brings us to the cost associated with it. Cloud computing costs can quickly be accumulated if we are not careful.

We have already established that creating threads is not cheap, and they are limited in number, so we cannot afford to waste any of them. Instead, we need to use their full capacity; however, in reality, that’s not what happens. In the traditional programming model, it blocks the current threads when we call something that takes time to respond. For example, if we make a network call—it could be a microservice or a database call—the thread we are using to invoke the call gets blocked until we get the results. While waiting for the results, the thread does nothing, basically sits idle, and wastes valuable resources, resulting in cloud bills for no good reason.

So from the postulation mentioned above, we can conclude that blocking calls isn’t good for us.

Let’s see an example-

In the above code, we do make five method invocations. Let’s assume all of them require some time to process. For simplicity, let’s say they all take 200 milliseconds to process.

Since we are making all the calls one after another, we will require at least 200 * 5 = 1000 milliseconds to complete this method. Therefore, the thread that started all these invocations must wait for them to finish.

From the scenario, we see that the thread that invokes the calculateCreditForPerson() method is blocked most of the time since it’s waiting for the subsequent methods to be finished. While it’s blocked, it’s not doing anything. Basically, its resources are being wasted by not being able to do anything.

The question is, how can we improve it?

There have been several attempts to gradually improve the situation so that threads don’t get blocked in such a scenario. I will start from the very beginning of the history of Java.

Classical implementations: The method invocations inside the method of the above code are not all dependent on each other. So, for example, the second, third, and fourth invocations can be done in parallel to each other. If these three methods get executed in parallel, we can make some improvements. The invoker thread would then take less time, which means less blocking time. That’s a huge improvement.
We don’t have a solution for the blocking problem here, but the main thread responsible for invoking this method stays blocking far less time. It would have more time to work on something else using those time.

So how would we implement this classically?

We will end up with something like the above. We will create a new thread on an ad hoc basis and store the result in an AtomicReference.

This is fine, but we have no control over how many threads we want to create. If we keep creating threads on an ad hoc basis, we may end up creating too many, which would hurt the application. Furthermore, if we try to create too many, the application may throw java.lang.OutOfMemoryError Exceptions.

So we need to improve further.

Executor Framework: Java 5 brings executors along with Future and Callable/Runnable. It gives us control over how many threads we want to create and pool them.

With this, we can improve the above code as follows:

This is a considerable improvement in writing code, but we’re not there yet. The future’s mechanism is still quite complex. The get() call on it is still a blocking call. Although we are making an asynchronous call, in the end, we need a blocking call to get the value from the future.

The other thing is that it creates an opportunity for cache corruption. For example, if main threads submit tasks to the thread pool, the tasks would be executed by a thread from the pool. The main thread needs data, which is in another thread. These two threads may encounter different cores, potentially resulting in cache corruption. On top of that, the context switching from one core to another is also expensive.

Also, it needs the composability option that we like. So the code is much more imperative. Having imperative code is okay, but functional and declarative code is much more fun. So at least we could improve here; if we cannot do it in other places, that is alright.

So the next question would be, “What next from here?” How can we improve further?

Fork/Join Pool: Java introduced the fork/join pool. It’s an implementation of the ExecutorService introduced by Java 5, as well as the executor framework. It fixes many of the problems we had with the old executor framework, like corrupted caches. In addition, it works on the idea that tasks that have just been made are likely to have a closer cache. That means newly created tasks should run on the same CPU, and older tasks may run on another CPU. Also, compared to other thread pool implementations, each thread in a fork/join pool keeps its own queue. Besides, the ForkJoin pool is implemented using a work-stealing algorithm. So if a thread in the pool finishes, it can steal from another thread’s queue from the tail of it. All of these provide us with performances to be proud of.

Let’s bring compossibility to the mixtureJava 8 introduces CompletableFuture on top of the fork/join pool. It includes the composition feature that we all enjoy; with this, we can rewrite the above code as follows:

Reactive Java:Well, this is great. We have gotten everything we wanted. This is improved performance with composability. However, there are some other alternatives to this on the market. Reactive frameworks like RxJava, Akka, Eclipse Vert.x, Spring WebFlux, Slick, etc., also give us performance and composability benefits. Let’s see an example of WebFlux.

However, there are some drawbacks to these patterns as well. Here are a few examples:

  • The learning curve of such a framework is quite stiff. Some of the patterns may seem mind-bending to the beginning.
  • The cognitive load associated with these isn’t even an exaggeration. It hurts the code reading experience.
  • The debugging of any problem is quite difficult. Since we don’t know what parts of the certain code are running on which threads, the path to accomplishing a task can be anything. And that’s why even the thread dump isn’t quite helpful.
  • Also, it’s easy to overcomplicate things with these.

So what’s the solution?

Well, if we had the opportunity to go back to the imperative code we had at the beginning and the easy asynchronous functionality, that would be awesome. And that’s where Project looms comes into the picture.

Project Loom: Project loom allows us to create as many threads as we want on an ad hoc basis without paying the penalty we had earlier. We don’t even care how many threads you want to create; we can, in fact, create millions of them. And they are cheap.

On top of that, we can have our imperative and blocking codes. So we don’t need to worry about blocking code at all.

Java 19 brings virtual threads; with that, we can have as much blocking code as we want.

If we want to use virtual threads, we can use the following executors:

The existing code for executors that we wrote earlier remains the same. Simply pass our newVirtualThreadPerTaskExecutor executor service. That’s it.

The virtual threads run on top of the classical threads, which are also called platform threads. These platform threads are basically threads in the fork/join pool. So by running a virtual thread, we get all the benefits that the fork/join pool brings.

In short, what virtual threads do is, when they see a blocking call, yield themselves from the running platform threads. The platform thread then keeps executing other virtual threads. Blocking calls usually happens when we call sleep or network operations. So when all these are done, the virtual thread can be resumed back to the platform thread to do the rest of the jobs.
By doing these, we are not wasting any of the threads’ time by being idle, but rather they are always busy working on something. On the other hand, the virtual thread is a Java construct that can be paused and resumed later without consuming extra CPU.

That makes our programming easy.

ConclusionIn conclusion, the blocking call was an enemy. To tackle this, we have invented many things; however, in the end, alongside all the inventions, we have come up with a new paradigm, which is a virtual thread, and with this, we no longer need to treat blocking calls as our enemy.

Rather, we can proudly invoke any blocking call, as many of them as we want.

That’s the story of two cities where blocking operations are treated differently. However, we have lots of bridges in between that make our life easy in many ways.

The post A tale of two cities: how blocking calls are treated? appeared first on JVM Advent.

View Details

Reflections on Log4Shell by The Diabolical Developer Introduction Thanks to Log4Shell, the past two weeks have been some of the most intense days in my career! The industry response has been immense, with IT staff working non-stop across the globe to patch systems, improve detections, and to provide support. Within Microsoft, I witnessed a masterclass in […]

The post Reflections on Log4Shell appeared first on JVM Advent.

View Details

As some of you might know, I’m working at Sonatype. That gives me great access to the behind-the-scenes data from Maven Central. You didn’t know Sonatype were the stewards for Maven Central? Well now you know. The folk at Sonatype do quite a lot for the Java community but tend not to shout about it […]

The post Log4JShell: are you on the naughty or nice list? appeared first on JVM Advent.

View Details

Pretty much all modern web browsers have developer tools console which lets you type some JavaScript code and run it in the browser. In theory, this allows you to do any kind of automation and extend browser functionality at runtime. In practice, it’s not suitable for the purpose, and, to be fair, most users are […]

The post The Easy Way to Create IntelliJ Plugins appeared first on JVM Advent.

View Details

Cloud Native computing is all about working with stateless data and serverless systems. But we all live in a stateful world, in which data flows through systems inter-connected with one another through complex networks. So how can systems be able to manage and track the flow of data in a coherent fashion and in a […]

The post Different Approaches to building Stateful Microservices in the Cloud Native World appeared first on JVM Advent.

View Details

Many Java-based organizations adopt cloud native development practices with the goal of shipping features faster. The technologies and architectures may change when we move to the cloud, but the fact remains that we all still add the occasional bug to our code. The challenge here is that many of your existing local debugging tools and […]

The post Easily Debug Java Apps Running on Kubernetes with Telepresence and IntelliJ IDEA appeared first on JVM Advent.

View Details

Apache Maven is 20 years old. Looking at our detailed releases history: Maven 1 had 12 releases from 2002 to 2007, Maven 2 had 16 releases from 2005 to 2009, Maven 3 had 38 releases from 2009 to now, still counting. For users, there was a big breaking change from Maven 1 to Maven 2, […]

The post From Maven 3 to Maven 5 appeared first on JVM Advent.

View Details

A static website is ideal for a project, a product or personal blog. It runs on every HTTP server (no server side code execution). It’s fast. It’s stable. Hosting is cheap. Or even free for projects using Github Pages. However, writing a website in pure HTML and JavaScript is tedious. Client-side JavaScript frameworks only go so far. […]

The post Generate Your Static Website With Java and JBake appeared first on JVM Advent.

View Details

Eclipse Collections is an open source Java Collections framework. In this blog I am going to demonstrate five lesser known features of the framework. I have published similar blogs in Java Advent Calendars of 2018, 2019, and 2020. Please refer to the resources at the end of the blog for more information about the framework. selectWithIndex() and rejectWithIndex(): […]

The post Hidden Treasures of Eclipse Collections 2021 Edition appeared first on JVM Advent.

View Details

Thread is the heart of the java programming language. When we run a hello world java program, we run on the main thread. And then, we can definitely create threads easily as we need to compose our application code to be functional, responsive, and performant at the same time. Think about a web server; it […]

The post 5 things you probably didn’t know about java concurrency.  appeared first on JVM Advent.

View Details

When JetBrains first announced coroutines as Kotlin’s asynchronous programming solution, many developers were intrigued but doubtful whether or not this shiny new gem would be enough to solve all their asynchronous problems. At that time, in the Java world, the market standard was the reactive library RxJava (which also has equivalents in other languages – […]

The post Are Kotlin Coroutines Enough to Replace RxJava? appeared first on JVM Advent.

View Details

It’s a common misconception that once you’re using an object-relational mapping (ORMs) tool, you wouldn’t have to know about SQL any longer, as the ORM would fully abstract you from that. Nothing could be further from the truth though; while an ORM indeed will free you from the burden of writing many SQL statements from […]

The post Keep Your SQL in Check With Flight Recorder, JMC Agent and JfrUnit appeared first on JVM Advent.

View Details

One of the great things about the Java ecosystem is the plethora of available libraries to integrate with virtually any imaginable tool, and one of those libraries is JGit. What is JGit? JGit is, as you have probably guessed, a Java library that implements the Git version control system. You may be wondering why we […]

The post Using JGit to Analyse the Legacy of Individual Developers appeared first on JVM Advent.

View Details

May 12th 2021 It just may be that we’ll look back at this date as the start of something significant.  It may just be the date when the world finally decided that cyber-attacks had crossed the line from being a nuisance to being a real threat to the modern world. May 12 was when the […]

The post The White House and Java: How getting serious about security is going to affect developers appeared first on JVM Advent.

View Details

The festive season is that period of the year when they tempt you to indulge in those dear sweet, sugary treats. Personally, as an Italian, I do love me some panettone. And as much as I enjoy the bitter taste of Java coffee, I have been enjoying the sugar that has been introduced in the […]

The post Type You An Actor Runtime For Greater Good! (with Java 17, records, switch expressions and JBang) appeared first on JVM Advent.

View Details

There are skills that you need to be a developer. There are other skills that you need to take control of your career, be able to forge your own path and become a rockstar.

The post 5 skills to walk your own path and rock your developer career in 2022 appeared first on JVM Advent.

View Details

Java moves our world. Think of any industry or technology and you’ll see Java – from banking, health, commerce, gaming, insurance, education to Quantum Computing, Artificial Intelligence, Blockchain and many more. It is literally everywhere. As a trusted ecosystem, Java has adapted to changing developer and business needs and continues to be relevant and popular. […]

The post Navigating the Java Ecosystem appeared first on JVM Advent.

View Details

Your project has reached that state when it’s a good time to create a release. Everyone has an opinion on how releases should be made. Some developers rely on a set of scripts (at times arcane and outdated) to automate as much as they can; others have a todo list with a number of steps […]

The post Automating your release process appeared first on JVM Advent.

View Details

1 – How to Write Code I studied each new Java version, and I wrote code almost every day, but it wasn’t enough. As it turns out, there’s so much more to writing code than, well, writing code! We also have to estimate work, pair, and work with VCS. Estimating how long something will take […]

The post 5 Java Coding Skills I Didn’t Learn at University appeared first on JVM Advent.

View Details

When I was a kid, I could not contain my excitement about Christmas. Meeting my extended family, having a great dinner with lots of laughter and joy. There was a tradition for the kids to search for Christmas presents in the living room. Be it behind the couch, between the Christmas tree branches, or way […]

The post Finding your presents using CodeQL appeared first on JVM Advent.

View Details

Lean microservice infrastructures continue to replace classic 3-tier architectures in ​​enterprise software. Pushing enterprise developers who lived in the fully integrated world of application servers towards dealing with new methodologies and technologies in a cloud-native world. As a matter of fact, distributed architectures differ fundamentally from known, monolithic applications. And the complexity of the execution […]

The post You need more than containers. A short history of the mess we’re in. appeared first on JVM Advent.

View Details

JVM Crash JVM crash is one of the toughest problem professional Java developers face. In case of a JVM crash, the operating system creates a core dump file which is a memory snapshot of a running process. A core dump is created by the operating system when a fatal or unhandled error like signal or […]

The post Diagnosing a JVM Crash! appeared first on JVM Advent.

View Details

Quarkus has always been focused on developer experience, and Quarkus 2.0 has taken this to the next level with support for a feature we are calling continuous testing. From its inception Quarkus has supported live reload in development mode, where changes to Java files take effect immediately. For testing though our approach has been the […]

The post Continuous Testing with Quarkus appeared first on JVM Advent.

View Details

While building systems and products today, we often come to situation that our system depend on some 3rd party systems. In most case our system communicate with some API to retrieve some data, that is needed to serve customers. Challenge that we face is how to make sure our systems perform as expected in this […]

The post Improving quality by mocking APIs with WireMock appeared first on JVM Advent.

View Details

Builds require a few properties, chief among them reproducibility. I would consider speed to be low on the order of priorities. However, it’s also one of the most limiting factors to your release cycle: if your build takes T, you cannot release faster than each T. Hence, you’ll probably want to speed up your builds […]

The post Faster Maven builds appeared first on JVM Advent.