Quantcast
Channel: Baeldung
Viewing all 4561 articles
Browse latest View live

Convert String XML Fragment to Document Node in Java

$
0
0

1. Introduction

XML processing is a common requirement in Java, especially when dealing with data interchange, configuration files, or web services. Besides, converting a string that contains an XML fragment into a Document node allows us to manipulate the XML structure using DOM (Document Object Model) APIs.

This tutorial explores different methods to convert a string containing an XML fragment into a Document node using Java.

2. Converting String XML Fragment to Document Node

To manipulate an XML string in Java, we must first parse it into a Document object. Besides, the DocumentBuilder class from the javax.xml.parsers package allows us to do this efficiently.

Consider the following XML string fragment:

String xmlString = "<child>Example</child>";

To convert this string into a Document, we utilize the DocumentBuilder to parse the string:

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new InputSource(new StringReader(xmlString)));

In this code snippet, we create an instance of DocumentBuilderFactory and initialize a DocumentBuilder. The XML string is wrapped inside an InputSource using StringReader. This enables the builder to parse the string and produce a Document object.

The parsed document now contains the XML structure from the string, and we can manipulate it as needed. To verify the content of the parsed document, we can access its root element and its child node:

Element rootElement = document.getDocumentElement();
assertNotNull(document);
assertEquals("root", rootElement.getNodeName());

First, we ensure that the parsed Document isn’t null and assert that the root element’s name is root. Next, we verify that the child element is read correctly:

var childElements = rootElement.getElementsByTagName("child");
assertNotNull(childElements);
assertEquals(1, childElements.getLength());
assertEquals("Example", childElements.item(0).getTextContent());

In addition to verifying the root element, we can also assert that the child node is read correctly:

assertNotNull(rootElement.getElementsByTagName("child"));
assertEquals(1, rootElement.getElementsByTagName("child").getLength());
assertEquals("Example", rootElement.getElementsByTagName("child").item(0).getTextContent());

This ensures that the document’s root node equals child and its text content equals Example.

3. Inserting the Document Node into an Existing Document

Once we’ve parsed the new XML fragment into a Document, we can add it to an existing XML structure. To achieve this, we first import the node into the existing document before appending it to the desired location.

Let’s begin with an existing XML document:

Document existingDocument = builder.newDocument();
Element rootElement = existingDocument.createElement("existingRoot");
existingDocument.appendChild(rootElement);

This creates a new XML document with a root element called existingRoot. Now, we parse the XML string fragment that we want to add to this document:

String xmlString = "<child>Example</child>";
Document newDocument = builder.parse(new InputSource(new StringReader(xmlString)));

In this case, the XML string <child>Example</child> is converted into a DOM structure that contains a child node. To add this node to the existing document, we must import the node first:

Element newNode = (Element) existingDocument.importNode(newDocument.getDocumentElement(), true);

The importNode() method copies the node from the parsed newDocument into the existingDocument. This step is necessary because we can’t directly append a node from one document to another without importing it first. Moreover, the true flag indicates that the entire subtree (all child elements) will be imported.

Finally, we append the imported node to the root element of the existing document:

existingDocument.getDocumentElement().appendChild(newNode);

This operation adds the child node to the root existingRoot node. To ensure the node has been successfully appended, we can validate the structure by checking the number of child nodes in the root element:

assertEquals(1, existingDocument.getDocumentElement().getChildNodes().getLength());
assertEquals("child", existingDocument.getDocumentElement().getChildNodes().item(0).getNodeName());

Here, we verify that the root element contains exactly one child node and that the name of this child node is a child.

4. Handling Invalid XML Strings

When working with XML, we may encounter situations where the input string is not well-formed or invalid. In such cases, handling exceptions during the parsing process is important. Consider the following invalid XML string:

String invalidXmlString = "<child>Example</child";

Here, the XML string is missing the closing bracket for the root element, making it invalid. To handle this, we attempt to parse the invalid XML string using DocumentBuilder and ensure that the appropriate exception is thrown. Moreover, we start by initializing a DocumentBuilderFactory and DocumentBuilder:

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();

Next, we attempt to parse the invalid XML string and use assertThrows to verify that a SAXParseException is thrown:

assertThrows(SAXParseException.class, () -> {
    builder.parse(new InputSource(new StringReader(invalidXmlString)));
});

In this code, the assertThrows method checks if a SAXParseException is thrown during the parsing attempt. Furthermore, this ensures that our code properly detects and handles the invalid XML string.

By validating input in this way, we can ensure that only well-formed XML strings are processed, improving the reliability of our XML parsing logic.

5. Conclusion

In conclusion, converting string XML fragments to Document nodes is an important part of working with XML in Java. By leveraging Java’s DOM API, we can dynamically parse, manipulate, and integrate XML content.

As usual, we can find the full source code and examples over on GitHub.

       

Understanding findAny() and anyMatch() in Streams

$
0
0

1. Overview

In this tutorial, we’ll explore two key methods in Java 8 Streams: findAny() and anyMatch(). Both methods serve different purposes, and understanding their differences is essential for writing effective Stream operations. After understanding these methods individually, we’ll compare findAny() and anyMatch() directly. This comparison will help clarify when to use each method based on the desired outcome.

We’ll briefly touch on related methods like findFirst(), count(), and allMatch(). These methods complement findAny() and anyMatch() in various scenarios, and we’ll get a quick overview of how they fit into the broader Stream API.

2. Understanding findAny()

We can use the findAny() method to retrieve any element from a Stream. It’s particularly useful when the order of elements doesn’t matter, as it’s designed to return an element without any guarantees about which one. This makes it a great choice for parallel streams, where performance can be prioritized over maintaining the order of elements.

The method returns an Optional, which means it can either hold a value or be empty if the Stream contains no elements. This is useful because it forces us to handle the possibility of an empty result, encouraging safer code practices.

Let’s take a look at a simple example to see findAny() in action:

@Test
public void whenFilterStreamUsingFindAny_thenOK() {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    Integer result = numbers.stream()
      .filter(n -> n % 2 == 0)
      .findAny()
      .orElse(null);
    assertNotNull(result);
    assertTrue(Arrays.asList(2, 4, 6, 8, 10).contains(result));
}

In a sequential Stream like the one above, findAny() behaves similarly to findFirst(). It returns the first match it finds. However, the real advantage of findAny() comes when working with parallel streams, where it can quickly grab any matching element, potentially improving performance when order isn’t important:

@Test
public void whenParallelStreamUsingFindAny_thenOK() {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    Integer result = numbers.parallelStream()
      .filter(n -> n % 2 == 0)
      .findAny()
      .orElse(null);
    assertNotNull(result); 
    assertTrue(Arrays.asList(2, 4, 6, 8, 10).contains(result));
}

By using a parallel Stream, findAny() can leverage parallelism to improve performance, especially for larger datasets.

3. Understanding anyMatch()

The anyMatch() method is used to check if any element in a Stream matches a given predicate. anyMatch() returns a boolean, true if any elements satisfy the condition, or false if none do. It’s ideal when we only need to check if any element meets a condition without retrieving the actual element.

anyMatch() is particularly efficient because it can short-circuit the Stream operation. As soon as it finds a matching element, it stops processing the rest of the Stream. If no elements match, the method evaluates the entire Stream. This behavior makes it useful when working with large datasets, as it doesn’t need to evaluate every element unnecessarily.

Let’s take a look at a simple example to see anyMatch() in action:

@Test
public void whenFilterStreamUsingAnyMatch_thenOK() {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    boolean result = numbers.stream()
      .anyMatch(n -> n % 2 == 0);
    assertTrue(result);
}

This method is great when we want a fast check to see if any element in a Stream satisfies a condition, without the need to process the entire Stream unless necessary.

4. Comparison Between findAny() and anyMatch()

While both findAny() and anyMatch() are useful when working with streams, they serve different purposes and return different types of results.

The next table provides a side-by-side comparison of both methods:

Feature findAny() anyMatch()
Return Type Optional<T> boolean
Use Case When we need to fetch an element without caring about the order When we need to verify if at least one element matches
Short-circuiting Stops processing as soon as it finds a matching element Stops processing as soon as it finds a match
Parallel Stream Efficient in parallel streams. Returns any element quickly Works efficiently in parallel streams by checking conditions
Null Safety Returns an Optional to handle the absence of elements Returns false if no matching elements are found

In addition to findAny() and anyMatch(), Stream offers other useful methods like findFirst(), count(), and allMatch() that serve different purposes when working with collections.

The findFirst() method returns the first element of the Stream, whether it’s processed sequentially or in parallel. It’s especially useful when element order matters, as it guarantees the first element in encounter order is returned:

@Test
public void whenFilterStreamUsingFindFirst_thenOK() {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    Integer result = numbers.stream()
      .filter(n -> n % 2 == 0)
      .findFirst()
      .orElse(null);
    assertNotNull(result);
    assertEquals(2, result);
}

The count() method returns the total number of elements in a Stream:

@Test
public void whenCountingElementsInStream_thenOK() {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    long count = numbers.stream()
      .filter(n -> n % 2 == 0)
      .count();
    assertEquals(5, count);
}

The allMatch() method checks if all elements in the Stream satisfy the provided predicate. If even one element fails the condition, it returns false. This method short-circuits as soon as it encounters an element that doesn’t match:

@Test
public void whenCheckingAllMatch_thenOK() {
    List<Integer> numbers = Arrays.asList(2, 4, 6, 8, 10);
    boolean allEven = numbers.stream()
      .allMatch(n -> n % 2 == 0);
    assertTrue(allEven);
}

Each of these methods has a specific purpose in Stream processing, allowing us to retrieve, count, or validate elements based on conditions.

6. Conclusion

In this article, we explored the differences between findAny() and anyMatch() and how they serve different purposes when working with streams.

While findAny() helps us retrieve an element from the Stream, anyMatch() is ideal for checking if any elements meet a condition. We also touched on related methods like findFirst(), count(), and allMatch(), which offer additional flexibility when processing Streams.

As always, the source code is available over on GitHub.

       

Using CompletableFuture With Feign Client in Spring Boot

$
0
0

1. Introduction

Calling external web dependencies while maintaining low latency is a critical task when working with distributed systems.

In this tutorial, we’ll use OpenFeign and CompletableFuture to parallelize multiple HTTP requests, handle errors, and set network and thread timeouts.

2. Setting up a Demo Application

To illustrate the usage of parallel requests, we’ll create a capability that allows customers to purchase items on a website. Firstly, the service makes one request to get the available payment methods based on the country where the customer lives. Secondly, it makes a request to generate a report to the customer about the purchase. The purchase report doesn’t include information on the payment method.

So, let’s first add the dependency to work with spring-cloud-starter-openfeign:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

3. Creating the External Dependency Clients

Now, let’s create two clients pointing to localhost:8083 using the @FeignClient annotation:

@FeignClient(name = "paymentMethodClient", url = "http://localhost:8083")
public interface PaymentMethodClient {
    @RequestMapping(method = RequestMethod.GET, value = "/payment_methods")
    String getAvailablePaymentMethods(@RequestParam(name = "site_id") String siteId);
}

Our first client name is paymentMethodClient. It calls GET /payment_methods to get the available payment methods using a site_id request parameter representing the customer country.

Let’s see our second client:

@FeignClient(name = "reportClient", url = "http://localhost:8083")
public interface ReportClient {
    @RequestMapping(method = RequestMethod.POST, value = "/reports")
    void sendReport(@RequestBody String reportRequest);
}

We named it reportClient and it calls POST /reports to generate the purchase report.

4. Creating the Parallel Request Executor

Calling the two clients in sequence would suffice to accomplish the demo application requirements. In that case, the total response time of this API would be at least the sum of the two requests’ response times.

Noticeably, the report doesn’t contain information about the payment method, so the two requests are independent. Thus, we could parallelize the work to reduce the total response time of our API to approximately the same response time of the slowest request.

In the next sections, we’ll see how to create a parallel executor of HTTP calls and handle external errors.

4.1. Creating the Parallel Executor

Therefore, let’s create the service that parallelizes the two requests using CompletableFutures:

@Service
public class PurchaseService {
    private final PaymentMethodClient paymentMethodClient;
    private final ReportClient reportClient;
    // all-arg constructor
    public String executePurchase(String siteId) throws ExecutionException, InterruptedException {
        CompletableFuture<String> paymentMethodsFuture = CompletableFuture.supplyAsync(() -> 
          paymentMethodClient.getAvailablePaymentMethods(siteId));
        CompletableFuture.runAsync(() -> reportClient.sendReport("Purchase Order Report"));
        return String.format("Purchase executed with payment method %s", paymentMethodsFuture.get());
    }
}

The executePurchase() method first posts a parallel task to get the available payment methods using supplyAsync(). Then, we submit another parallel task to generate the report using runAsync(). Finally, we retrieve the payment method result using get() and return the complete result.

The choice of supplyAsync() and runAsync() for the two tasks is due to the different nature of the two methods. The supplyAsync() method returns the result from the GET call. On the other hand, runAsync() doesn’t return anything and thus, it’s better suited for generating the report.

Another difference is that runAsync() fires up a new thread immediately as soon as we invoke the code without any task scheduling by the thread pool. In contrast, supplyAsync() tasks might be scheduled or delayed depending on whether there are other tasks scheduled by the thread pool.

To validate our code, let’s use an integration test using WireMock:

@BeforeEach
public void startWireMockServer() {
    wireMockServer = new WireMockServer(8083);
    configureFor("localhost", 8083);
    wireMockServer.start();
    stubFor(post(urlEqualTo("/reports"))
      .willReturn(aResponse().withStatus(HttpStatus.OK.value())));
}
@AfterEach
public void stopWireMockServer() {
    wireMockServer.stop();
}
@Test
void givenRestCalls_whenBothReturnsOk_thenReturnCorrectResult() throws ExecutionException, InterruptedException {
    stubFor(get(urlEqualTo("/payment_methods?site_id=BR"))
      .willReturn(aResponse().withStatus(HttpStatus.OK.value()).withBody("credit_card")));
    String result = purchaseService.executePurchase("BR");
    assertNotNull(result);
    assertEquals("Purchase executed with payment method credit_card", result);
}

In the test above, we first configure a WireMockServer to start up at localhost:8083 and to shut down when done using the @BeforeEach and @AfterEach annotations.

Then, in the test scenario method, we used two stubs that respond with a 200 HTTP status when we call both feign clients. Finally, we assert the correct result from the parallel executor using assertEquals().

4.2. Handling External API Errors Using exceptionally()

What if the GET /payment_methods request fails with a 404 HTTP status, suggesting that there are no available payment methods for that country? It’s useful to do something in scenarios like these, like, for example, returning a default value.

To handle errors in CompletableFuture, let’s add the following exceptionally() block to our paymentMethodsFuture:

CompletableFuture <String> paymentMethodsFuture = CompletableFuture.supplyAsync(() -> paymentMethodClient.getAvailablePaymentMethods(siteId))
  .exceptionally(ex -> {
      if (ex.getCause() instanceof FeignException && 
             ((FeignException) ex.getCause()).status() == 404) {
          return "cash";
      });

Now, if we get a 404, we return the default payment method named cash:

@Test
void givenRestCalls_whenPurchaseReturns404_thenReturnDefault() throws ExecutionException, InterruptedException {
    stubFor(get(urlEqualTo("/payment_methods?site_id=BR"))
        .willReturn(aResponse().withStatus(HttpStatus.NOT_FOUND.value())));
    String result = purchaseService.executePurchase("BR");
    assertNotNull(result);
    assertEquals("Purchase executed with payment method cash", result);
}

5. Adding Timeouts for Parallel Tasks and Network Requests

When calling external dependencies, we can’t be sure how long the request will take to run. Hence, if a request takes too long, at some point, we should give up on that request. With this in mind, we can add two types: a FeignClient and a CompletableFuture timeout.

5.1. Adding Network Timeouts to Feign Clients

This type of timeout works for single requests over the wire. Hence, it cuts the connection with the external dependency for one request at the network level.

We can configure timeouts for FeignClient using Spring Boot autoconfiguration:

feign.client.config.paymentMethodClient.readTimeout: 200
feign.client.config.paymentMethodClient.connectTimeout: 100

In the above application.properties file, we set read and connect timeouts for PaymentMethodClientThe numeric values are measured in milliseconds.

The connect timeout tells the feign client to cut the TCP handshake connection attempt after the threshold value. Similarly, the read timeout interrupts the request when the connection is made properly, but the protocol can’t read the data from the socket.

Then, we can handle that type of error inside the exceptionally() block in our parallel executor:

if (ex.getCause() instanceof RetryableException) {
    // handle TCP timeout
    throw new RuntimeException("TCP call network timeout!");
}

And to verify the correct behavior, we can add another test scenario:

@Test
void givenRestCalls_whenPurchaseRequestWebTimeout_thenReturnDefault() {
    stubFor(get(urlEqualTo("/payment_methods?site_id=BR"))
      .willReturn(aResponse().withFixedDelay(250)));
    Throwable error = assertThrows(ExecutionException.class, () -> purchaseService.executePurchase("BR"));
    assertEquals("java.lang.RuntimeException: REST call network timeout!", error.getMessage());
}

Here, we’ve used the withFixedDelay() method with 250 milliseconds to simulate a TCP timeout.

5.2. Adding Thread Timeouts

On the other hand, thread timeouts stop the entire CompletableFuture content, not only a single request attempt. For instance, for feign client retries, the times from the original request and the retry attempts also count when evaluating the timeout threshold.

To configure thread timeouts, we can slightly modify our payment method CompletableFuture:

CompletableFuture<String> paymentMethodsFuture = CompletableFuture.supplyAsync(() -> paymentMethodClient.getAvailablePaymentMethods(siteId))
  .orTimeout(400, TimeUnit.MILLISECONDS)
  .exceptionally(ex -> {
       // exception handlers
   });

Then, we can handle threat timeout errors inside the exceptionally() block:

if (ex instanceof TimeoutException) {
    // handle thread timeout
    throw new RuntimeException("Thread timeout!", ex);
}

Hence, we can verify it works properly:

@Test
void givenRestCalls_whenPurchaseCompletableFutureTimeout_thenThrowNewException() {
    stubFor(get(urlEqualTo("/payment_methods?site_id=BR"))
        .willReturn(aResponse().withFixedDelay(450)));
    Throwable error = assertThrows(ExecutionException.class, () -> purchaseService.executePurchase("BR"));
    assertEquals("java.lang.RuntimeException: Thread timeout!", error.getMessage());
}

We’ve added a longer delay to /payments_method so it passes the network timeout threshold, but fails on the thread timeout.

6. Conclusion

In this article, we learned how to execute two external dependency requests in parallel using CompletableFuture and FeignClient.

We also saw how to add network and thread timeouts to interrupt the program execution after a time threshold.

Finally, we handled 404 API and timeout errors gracefully using CompletableFuture.exceptionally().

As always, the source code is available over on GitHub.

       

Difference Between Mockito Core and Mockito Inline

$
0
0

1. Overview

Mockito is one of the most popular frameworks for creating mock objects in Java, and it offers Mockito Core and Mockito Inline as the two main libraries for unit testing with different features and use cases.

To learn more about testing with Mockito, check out our comprehensive Mockito series.

2. Mockito Core

One of the foundational libraries of Mockito is Mockito Core.  It provides the basic functionality for creating mocks, stubs, and spies. This library is sufficient for most common use cases, but has some limitations, particularly when dealing with final classes and static methods.

See the mocking example of Mockito Core here.

3. Mockito Inline

Mockito Inline is an extension of Mockito Core including additional capabilities for mocking final classes, final fields, static methods, and constructors. This library is useful when you need to mock or stub these types of methods or classes. In the latest version of Mockito Core, Mockito Inline became the default mock maker since Mockito Core version 5.0.0.

Now, let’s try it on our code. First, we need to add mockito-core in our pom.xml dependencies:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>

3.1. Mocking Final Class

Let’s try to mock our final class, we need to make a new class with the name FinalClass:

public final class FinalClass {
    public String greet() {
        return "Hello, World!";
    }
}

Let’s take a look at these code implementations of mocking a final class:

@Test
void testFinalClassMock() {
    FinalClass finalClass = mock(FinalClass.class);
    when(finalClass.greet()).thenReturn("Mocked Greeting");
    assertEquals("Mocked Greeting", finalClass.greet());
}

In the code example above, we mock our finalClass then we stub our method named greet() to return a String value of “Mocked Greeting” instead of the original value, which is “Hello, World!”.

3.2. Mocking Final Field

Let’s try to mock our final field, we need to make a new class with the name ClassWithFinalField:

public class ClassWithFinalField {
    public final String finalField = "Original Value";
    public String getFinalField() {
        return finalField;
    }
}

Let’s take a look at these code implementations of mocking a final class:

@Test
void testFinalFieldMock() {
    ClassWithFinalField instance = mock(ClassWithFinalField.class);
    when(instance.getFinalField()).thenReturn("Mocked Value");
    assertEquals("Mocked Value", instance.getFinalField());
}

In the code example above, we mock our instance then we stub our method name getFinalField() to return a String value of “Mocked Value” instead of the original value, which is “Original Value”. 

3.3. Mocking Static Method

Let’s try to mock our static method, we need to make a new class with the name ClassWithStaticMethod:

public class ClassWithStaticMethod {
    public static String staticMethod() {
        return "Original Static Value";
    }
}

Let’s take a look at these code implementations of mocking a static method:

@Test
void testStaticMethodMock() {
    try (MockedStatic<ClassWithStaticMethod> mocked = mockStatic(ClassWithStaticMethod.class)) {
        mocked.when(ClassWithStaticMethod::staticMethod).thenReturn("Mocked Static Value");
        assertEquals("Mocked Static Value", ClassWithStaticMethod.staticMethod());
    }
}

In the code example above, we mock our class name ClassWithStaticMethod then we stub our method name staticMethod() to return a String value of “Mocked Static Value” instead of the original value, which is “Original Static Value”.

3.4. Mocking Constructor

Let’s try to mock our constructor, we need to make a new class with the name ClassWithConstructor:

public class ClassWithConstructor {
    private String name;
    public ClassWithConstructor(String name) {
        this.name = name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name;
    }
}

Let’s take a look at these code implementations of mocking a constructor:

@Test
void testConstructorMock() {
    try (MockedConstruction<ClassWithConstructor> mocked = mockConstruction(ClassWithConstructor.class,
            (mock, context) -> when(mock.getName()).thenReturn("Mocked Name"))) {
        ClassWithConstructor myClass = new ClassWithConstructor("test");
        assertEquals("Mocked Name", myClass.getName());
    }
}

In the code example above, we mock our class name ClassWithConstructor then we mock our field called name to have a String value of “Mocked Name” instead of the value set in the constructor, which is “test”.

4. Summary

Let’s summarize what we’ve learned so far:

Element to Mock Mockito Function
 Final Classes
 mock(FinalClass.class)
 Final Fields
 mock(ClassWithFinalField.class)
 Static Methods
 mockStatic(ClassWithStaticMethod.class)
 Constructor
 mockConstruction(ClassWithConstructor.class)

 

5. Conclusion

In this tutorial, we can compare the differences between Mockito Core and Mockito Inline. Simply put, Mockito Core can mock, stub, and spy common cases in our Java code and can’t do anything related to final classes, final methods, static methods, and constructors. On the other hand, Mockito Inline can do what Mockito Core can’t, which is mock and stub final classes, final fields, static methods, and constructors.

The code examples are available over on GitHub.

       

Java Weekly, Issue 560

$
0
0

1. Spring and Java

>> JDK 23 is Released!  [jdk.java.net]
Java 23 is out!

Including features like markdown support for docs, generational ZGC, more enhanced switch, module imports, and a lot more goodness. Very cool 🙂

>> Introduction to Htmx for Spring Boot Developers [blog.jetbrains.com]

A practical guide for busy backend folks to develop their frontend bits with HTMX and Spring Boot

Also worth reading:

Webinars and presentations:

Time to upgrade:

2. Pick of the Week

>> Your company needs Junior devs [softwaredoug.com]

       

How Does a Random Seed Work in Java?

$
0
0
start here featured

1. Overview

Randomness is a fascinating concept with applications in various fields such as cryptography, gaming, simulations, and machine learning. In computer systems, true randomness is elusive.

In Java, randomness is often generated using pseudorandom number generators (PRNGs). These generators aren’t truly random but rely on algorithms that produce sequences of numbers that appear random but are determined by a starting point, known as the seed.

In this tutorial, we’ll explore how these random seeds work in Java and uncover their role in random number generation. We’ll also discuss how different Java classes utilize these seeds to produce predictable sequences of random values and the implications of these mechanisms for various applications.

2. How Does the Random Class Work?

To generate a random number in Java, we use the Random class, which produces numbers that appear random. However, what we get is a pseudorandom number, meaning that while the sequence seems random, a deterministic algorithm generates it based on an initial input – the seed.

Many implementations of Random in programming languages, including Java, use the Linear Congruential Generator (LCG) algorithm. This algorithm generates a sequence of numbers based on a simple mathematical formula:

Xn+1 = (aXn + C) % m

Where Xn is the current value, Xn+1 is the next value, a is the multiplier, c is the increment, and m is the modulus. The initial value X0 is the seed.

The choice of a,c, and m can significantly impact the quality of the random numbers produced. Taking the remainder of mod m is similar to figuring out where a ball will end up on a rotating wheel with numbered sections.

For example, let’s take a sequence obtained when m=10 and X0 = a = c = 7 is

7,6,9,0,7,6,9,0,...

As seen in the above example, the sequence is not always random for all values of a, m, c, and X0.

3. The Role of the Seed

A seed is an initial input that starts the PRNG‘s process. The seed acts as a key that unlocks a specific sequence of numbers from a vast, predetermined set. Using the same seed will always produce the same sequence of numbers. For example, initializing a Random object with a seed of 35 and asking it to generate 12 random numbers will result in the same sequence each time we run the code:

public void givenNumber_whenUsingSameSeeds_thenGenerateNumbers() {
    Random random1 = new Random(35);
    Random random2 = new Random(35);
    int[] numbersFromRandom1 = new int[12];
    int[] numbersFromRandom2 = new int[12];
    for(int i = 0 ; i < 12; i++) {
        numbersFromRandom1[i] = random1.nextInt();
        numbersFromRandom2[i] = random2.nextInt();
    }

assertArrayEquals(numbersFromRandom1, numbersFromRandom2); }

This property is crucial in situations where we need predictable results for testing or debugging, simulations, and cryptography, but it also allows for randomness when desired.

4. The Default Seed in Java

We can create a Random class object without specifying a seed, and Java will use the current system time as the seed. Internally, the Random class calls its constructor that takes a long seed parameter, but it computes this seed based on the system time.

This approach offers a degree of randomness, but it’s not perfect. The system time is relatively predictable, and it’s possible for two Random objects to be created at nearly the same time to have similar seeds, leading to correlated random sequences.

We can use System.nanoTime() to obtain a more precise and less predictable seed. However, even this approach has limitations. For a truly unpredicted number, we need to use a cryptographic random number generator (CSPRNG) or a hardware-based random number generator (HRNG).

Let’s take a look at how we can use System.nanoTime() as a seed:

public void whenUsingSystemTimeAsSeed_thenGenerateNumbers() {
    long seed = System.nanoTime();
    Random random = new Random(seed);
 
    for(int i = 0; i < 10; i++) {
        int randomNumber = random.nextInt(100);
        assertTrue(randomNumber >= 0 && randomNumber < 100);
    }
}

5. Beyond the Random Class

We can use the Random class to generate random numbers in Java easily. However, there are other options available. Some are better suited for applications that need high-quality or cryptographically secure random numbers.

5.1. SecureRandom

Standard JDK implementations of java.util.Random uses a Linear Congruential Generator (LCG) algorithm to provide random numbers. The problem with this algorithm is that it’s not cryptographically strong. In other words, the generated values are much more predictable, therefore attackers could use it to compromise our system.

To overcome this issue, we should use java.security.SecureRandom in any security decisions.

5.2. ThreadLocalRandom

The Random class doesn’t perform well in a multi-threaded environment. In a simplified way, the reason for the poor performance of Random in a multi-threaded environment is contention – given that multiple threads share the same Random instance.

To address that limitation, Java introduced the java.util.concurrent.ThreadLocalRandom class in JDK 7 – for generating random numbers in a multi-threaded environment.

6. Conclusion

In this article, we see that seeds play a key role in controlling the behavior of the Random class. We also observe how using the same seed consistently produces the same sequence of random numbers, resulting in identical outputs.

When we understand the role of random seeds and the algorithms behind them, we can make informed choices about generating random numbers in our Java applications. This helps us ensure they meet our specific needs for quality, reproducibility, and security.

As always, the complete source code is available over on GitHub.

       

How to Convert String to Date Using MapStruct in Java?

$
0
0

1. Introduction

MapStruct is a powerful library in Java that simplifies the process of mapping between different object models at compile time. It uses annotations to automatically generate type-safe mapper implementations, making it efficient and easy to maintain.

MapStruct is often used in applications that require object-to-object mapping, such as transferring data between layers or converting a DTO to an entity. A common use case is converting a String to a Date object, a process that can be tricky due to the numerous date formats and parsing requirements. In this article, we’ll explore various methods to achieve this conversion using MapStruct.

2. Dependencies

To start using MapStruct, we must include the necessary dependencies in our project. For the Maven project, we  add the following dependency to our pom.xml:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>

Additionally, we configure the maven-compiler-plugin to include the MapStruct processor:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.13.0</version>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>1.5.5.Final</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

With these dependencies set up, we can begin using MapStruct in our project.

3. Basic Mapping With @Mapping Annotation

MapStruct provides the @Mapping annotation to define how properties in one type should be mapped to another. By default, MapStruct doesn’t support a direct conversion between String and Date. However, we can utilize the format attribute of the @Mapping annotation to handle the conversion.

Let’s see a basic example where we map a UserDto to a User entity:

public class UserDto {
    private String name;
    // Date in String format
    private String birthDate; 
    // getters and setters
}
public class User {
    private String name;
    private Date birthDate;
    // getters and setters
}
@Mapper
public interface UserMapper {
    @Mapping(source = "birthDate", target = "birthDate", dateFormat = "yyyy-MM-dd")
    User toUser(UserDto userDto);
}

In this example, the @Mapping annotation specifies that the birthDate field from UserDto should be mapped to the birthDate field in User. The dateFormat attribute is used to define the format of the date string, allowing MapStruct to handle the conversion automatically.

Let’s verify this conversion:

@Test
public void whenMappingUserDtoToUser_thenMapsBirthDateCorrectly() throws ParseException {
    UserDto userDto = new UserDto();
    userDto.setName("John Doe");
    userDto.setBirthDate("2024-08-01");
    User user = userMapper.toUser(userDto);
    assertNotNull(user);
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    Date expectedDate = dateFormat.parse("2024-08-01");
    Assertions.assertEquals(expectedDate, user.getBirthDate());
}

In this test case, we confirm the accurate mapping of birthDate from a UserDto to a User using the UserMapper, confirming the expected date conversion.

4. Implementing Custom Conversion Methods

Sometimes, we might need to implement custom conversion methods for more complex scenarios. These methods can be defined directly within the mapper interface. By using the expression attribute of @Mapping annotation we can direct MapStruct to utilize our custom conversion methods.

Here’s how we can define a custom method to convert String to Date:

@Mapper
public interface UserMapper {
    @Mapping(target = "birthDate", expression = "java(mapStringToDate(userDto.getBirthDate()))")
    User toUserCustom(UserDto userDto) throws ParseException;
    default Date mapStringToDate(String date) throws ParseException {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        return dateFormat.parse(date);
    }
}

In this example, the mapStringToDate() method converts a String to a Date using SimpleDateFormat. We use an explicit expression in the @Mapping annotation to tell MapStruct to call this method during mapping.

5. Reuse General Custom Methods

Suppose we have multiple mappers in our project that require the same type of conversion. In that case, it’s more efficient to define the custom mapping methods in a separate utility class and reuse them across different mappers.

First, we’ll create a utility class with the conversion methods:

public class DateMapper {
    public Date mapStringToDate(String date) throws ParseException {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        return dateFormat.parse(date);
    }
}

Then, let’s use this utility class in our mapper:

@Mapper(uses = DateMapper.class)
public interface UserConversionMapper {
    @Mapping(source = "birthDate", target = "birthDate")
    User toUser(UserDto userDto);
}

By specifying uses = DateMapper.class in the @Mapper annotation, we tell MapStruct to use the methods in DateMapper for conversions. This approach promotes code reuse and keeps our mapping logic organized and maintainable.

6. Conclusion

MapStruct is a powerful tool for object-to-object mapping in Java. By leveraging custom methods, we can easily handle type conversions that are not supported out-of-the-box, such as converting String to Date.

By following the steps outlined in this article, we can efficiently implement and reuse custom conversion methods in our projects. Additionally, using the dateFormat attribute in the @Mapping annotation can simplify the process for straightforward date conversions.

These techniques allow us to harness the full potential of MapStruct for various mapping scenarios in our Java applications.

As always, the source code is available over on GitHub.

       

Error Handling in Micronaut

$
0
0

1. Overview

Error handling is one of the main concerns when developing systems. On a code level, error handling handles exceptions thrown by the code we write. On a service level, by errors, we mean all the non-successful responses we return.

It’s a good practice in large systems, to handle similar errors in a consistent way. For example, in a service with two controllers, we want the authentication error responses to be similar, so that we can debug issues easier. Taking a step back, we probably want the same error response from all services of a system, for simplicity. We can implement this approach by using global exception handlers.

In this tutorial, we’ll focus on the error handling in Micronaut. Similar to most of the Java frameworks, Micronaut provides a mechanism to handle errors commonly. We’ll discuss this mechanism and we’ll demonstrate it in examples.

2. Error Handling in Micronaut

In coding, the only thing we can take for granted is that errors will happen. No matter how good code we write and well-defined tests and test coverage we have, we can’t avoid errors. So, how we’re handling them in our system should be one of our main concerns. Error handling in Micronaut comes easier, by using some of the framework features like status handlers and exception handlers.

If we’re familiar with error handling in Spring, then it’s easy to onboard on Micronaut ways. Micronaut provides handlers to tackle exceptions thrown, but also handlers that deal with specific response statuses. In error status handling, we can set a local scope or a global one. Exception handling is on a global scope only.

One thing worth mentioning is that, if we take advantage of Micronaut environments capabilities, we can set different global error handlers for different active environments. If we have, for example, an error handler that publishes an event message, we can make use of active environments and skip the message publishing functionality on the local environment.

3. Error Handling in Micronaut Using the @Error Annotation

In Micronaut, we can define error handlers using the @Error annotation. This annotation is defined on a method level and it should be inside @Controller annotated classes. It has some functionalities similar to other controller methods, like that it can use request binding annotation on parameters to access request headers, the request body, etc.

By using the @Error annotation for error handling in Micronaut, we can either handle exceptions or response status codes. This is something different from other popular Java frameworks, which only provide handlers per exception.

One feature of the error handlers is that we can set a scope for them. We can have one handler that handles 404 responses for the whole service, by setting the scope to global. If we don’t set a scope, then the handler only handles the specified errors thrown in the same controller.

3.1. Using the @Error Annotation to Handle Response Error Codes

The @Error annotation provides a way for us to handle errors per error response status. This way, we can define a common way to handle all HttpStatus.NOT_FOUND responses, for example. The error statuses we can handle should be one defined in the io.micronaut.http.HttpStatus enum:

@Controller("/notfound")
public class NotFoundController {
    @Error(status = HttpStatus.NOT_FOUND, global = true)
    public HttpResponse<JsonError> notFound(HttpRequest<?> request) {
        JsonError error = new JsonError("Page Not Found")
          .link(Link.SELF, Link.of(request.getUri()));
        return HttpResponse.<JsonError> notFound().body(error);
    }
}
public class CustomException extends RuntimeException {
    public CustomException(String message) {
        super(message);
    }
}

In this controller, we define a method annotated with @Error that handles the HttpStatus.NOT_FOUND responses. The scope is set to global, so all 404 errors should go through this method. After handling, all such errors should return a status code of 404, with a modified body that contains the error message “Page Not Found” and a link.

Notice that even though we use the @Controller annotation, this controller doesn’t specify any HttpMethod, so it doesn’t work as a conventional controller exactly, but it has some implementation similarities, as we mentioned earlier.

Now let’s assume we have an endpoint that gives a NOT_FOUND error response:

@Get("/not-found-error")
public HttpResponse<String> endpoint1() {
    return HttpResponse.notFound();
}

The “/not-found-error” endpoint should always return 404. If we hit this endpoint, the NOT_FOUND error handler should be triggered:

@Test
public void whenRequestThatThrows404_thenResponseIsHandled(
    RequestSpecification spec
) {
    spec.given()
      .basePath(ERRONEOUS_ENDPOINTS_PATH)
      .when()
      .get("/not-found-error")
      .then()
      .statusCode(404)
      .body(Matchers.containsString("\"message\":\"Page Not Found\",\"_links\":"));
}

This Micronaut test makes a GET request to the “/not-found-error” endpoint and gets back the expected 404 status code. However, by asserting the response body, we can verify that the response came through the handler since the error message is the one we added to the handler.

One thing to clear up is that, if we change the base path and path to point to NotFoundController, because there is no GET defined in this controller, only the error, then the server is the one that throws a 404 and the handler still handles it.

3.2. Using @Error Annotation to Handle Exceptions

In a web service, if an exception is not caught and handled anywhere, then the controller returns an internal server error by default. Error handling in Micronaut offers the @Error annotation for such cases.

Let’s create an endpoint that throws an exception and a handler that handles those specific exceptions:

@Error(exception = UnsupportedOperationException.class)
public HttpResponse<JsonError> unsupportedOperationExceptions(HttpRequest<?> request) {
    log.info("Unsupported Operation Exception handled");
    JsonError error = new JsonError("Unsupported Operation")
      .link(Link.SELF, Link.of(request.getUri()));
    return HttpResponse.<JsonError> notFound().body(error);
}
@Get("/unsupported-operation")
public HttpResponse<String> endpoint5() {
    throw new UnsupportedOperationException();
}

The “/unsupported-operation” endpoint only throws an UnsupportedOperationException exception. The unsupportedOperationExceptions method uses the @Error annotation to handle these exceptions. It returns a 404 error code since this resource is not supported and a response body with the message “Unsupported Operation”. Note that the scope in this example is local since we don’t set it to global.

If we hit this endpoint, we should see the handler handling it and giving back the response as defined in the unsupportedOperationExceptions method:

@Test
public void whenRequestThatThrowsLocalUnsupportedOperationException_thenResponseIsHandled(
    RequestSpecification spec
) {
    spec.given()
      .basePath(ERRONEOUS_ENDPOINTS_PATH)
      .when()
      .get("/unsupported-operation")
      .then()
      .statusCode(404)
      .body(containsString("\"message\":\"Unsupported Operation\""));
}
@Test
public void whenRequestThatThrowsExceptionInOtherController_thenResponseIsNotHandled(
    RequestSpecification spec
) {
    spec.given()
      .basePath(PROBES_ENDPOINTS_PATH)
      .when()
      .get("/readiness")
      .then()
      .statusCode(500)
      .body(containsString("\"message\":\"Internal Server Error\""));
}

In the first example, we request the “/unsupported-operation” endpoint, which throws the UnsupportedOperationException exception. Since the local handler is in the same controller, then we get the response we expect from the handler, with the modified response error message “Unsupported Operation”.

In the second example, we request the “/readiness” endpoint, from a different controller, which also throws an UnsupportedOperationException exception. Because this endpoint is defined on a different controller, the local handler is not going to handle the exception, so the response we get is the default with error code 500.

4. Error Handling in Micronaut Using the ExceptionHandler Interface

Micronaut also offers the option to implement an ExceptionHandler interface, to handle specific exceptions in a global scope. This approach requires one class per exception, which means that by default they have to be on a global scope.

Micronaut provides some default exception handlers, for example:

  • jakarta.validation.ConstraintViolationException
  • com.fasterxml.jackson.core.JsonProcessingException
  • UnsupportedMediaException
  • and more

These handlers can of course be overridden on our service, if needed.

One thing to consider is the exception hierarchy. When we create a handler for a specific exception A, an exception B that extends A will also fall under the same handler, unless we implement one more handler for this specific exception B. More detail on that is in the following sections.

4.1. Handling an Exception

As described earlier, we can use the ExceptionHandler interface to handle a specific type of exception globally:

@Slf4j
@Produces
@Singleton
@Requires(classes = { CustomException.class, ExceptionHandler.class })
public class CustomExceptionHandler implements ExceptionHandler<CustomException, HttpResponse<String>> {
    @Override
    public HttpResponse<String> handle(HttpRequest request, CustomException exception) {
        log.info("handling CustomException: [{}]", exception.getMessage());
        return HttpResponse.ok("Custom Exception was handled");
    }
}

In this class, we implement the interface, which uses generics to define which exception we’ll be handling. In this case, it is the CustomException we defined earlier. The class needs to be annotated with @Requires and include the exception class, but also the interface. The handle method takes as parameters the request that triggered the exception and also the exception object. Then, we simply add our custom message in the response body, giving back a 200 response status code.

Now let’s assume we have an endpoint that throws a CustomException:

@Get("/custom-error")
public HttpResponse<String> endpoint3(@Nullable @Header("skip-error") String isErrorSkipped) {
    if (isErrorSkipped == null) {
        throw new CustomException("something else went wrong");
    }
    return HttpResponse.ok("Endpoint 3");
}

The “/custom-error” endpoint accepts an isErrorSkipped header, to enable/disable the exception thrown. If we don’t include the header, then the exception is thrown:

@Test
public void whenRequestThatThrowsCustomException_thenResponseIsHandled(
    RequestSpecification spec
) {
    spec.given()
      .basePath(ERRONEOUS_ENDPOINTS_PATH)
      .when()
      .get("/custom-error")
      .then()
      .statusCode(200)
      .body(is("Custom Exception was handled"));
}

In this test, we request the “/custom-error” endpoint, without including the header. So, a CustomException exception is thrown. Then, we verify that the handler has handled this exception, by asserting on the response code and response body that we expect from the handler.

4.2. Handling Exceptions Based on Hierarchy

In the case of exceptions that are not explicitly handled, if they extend an exception that has a handler, they are implicitly handled by the same handler. Let’s assume we have a CustomeChildException that extends our CustomException:

public class CustomChildException extends CustomException {
    public CustomChildException(String message) {
        super(message);
    }
}

And there’s an endpoint that throws this exception:

@Get("/custom-child-error")
public HttpResponse<String> endpoint4(@Nullable @Header("skip-error") String isErrorSkipped) {
    log.info("endpoint4");
    if (isErrorSkipped == null) {
        throw new CustomChildException("something else went wrong");
    }
    return HttpResponse.ok("Endpoint 4");
}

The “/custom-child-error” endpoint accepts an isErrorSkipped header, to enable/disable the exception thrown. If we don’t include the header, then the exception is thrown:

@Test
public void whenRequestThatThrowsCustomChildException_thenResponseIsHandled(
    RequestSpecification spec
) {
    spec.given()
      .basePath(ERRONEOUS_ENDPOINTS_PATH)
      .when()
      .get("/custom-child-error")
      .then()
      .statusCode(200)
      .body(is("Custom Exception was handled"));
}

This test hits the “/custom-child-error” endpoint and triggers the CustomChildException exception. From the response, we can verify that the handler has handled this child exception too, by asserting on the response code and response body that we expect from the handler.

5. Conclusion

In this article, we went through the error handling in Micronaut. There are different ways to handle errors, by handling exceptions or by handling error response status codes. We also saw how we can apply our handlers on different scopes, local and global. Last, we demonstrated all options discussed, with some code examples, and used Micronaut tests to verify the outcomes.

As always, all the source code is available over on GitHub.

       

Why Is 2 * (i * i) Faster Than 2 * i * i in Java?

$
0
0

1. Overview

When optimizing code, even small differences in expression syntax can impact performance. One such example is the difference between 2 * (i * i) and 2 * i * i in Java. At first glance, these two expressions might seem identical, but subtle differences in how they’re evaluated can lead to performance discrepancies.

In this tutorial, we’ll explore why 2 * (i * i) is generally faster than 2 * i * i and dive into the underlying reasons. Let’s begin.

2. Understanding the Expression

Let’s break down the two expressions. In this expression, the multiplication of i * i happens first, followed by multiplying the result by 2:

2 * (i * i)

In this expression, the evaluation proceeds from left to right:

2 * i * i

First, 2 * i is calculated, and then the result is multiplied by i.

3. Performance Comparison

Even though both expressions theoretically yield the same result, the order of operations can influence performance.

3.1. Compiler Optimization

Java compilers like the Just-In-Time (JIT) compiler in the JVM are sophisticated and can optimize code at runtime. However, compilers rely heavily on the clarity of the code to make optimizations:

  • 2 * (i * i): The parentheses clearly define the order of operations, making it easier for the compiler to optimize the multiplication.
  • 2 * i * i: The less explicit order of operations may result in less efficient optimization. The compiler might not optimize the code as efficiently as with 2 * (i * i).

In essence, 2 * (i * i) provides the compiler with a clear indication of how to perform the calculations, which can lead to a better-optimized bytecode.

3.2. Integer Overflow Considerations

Integer overflow occurs when a calculation produces a value larger than the maximum value that an int can store (2^31 – 1 for a 32-bit integer). While neither expression inherently causes overflow more than the other, the way the calculations are structured can affect how overflow is handled:

  • 2 * i * i: If i is large, the intermediate result of 2 * i might approach the overflow threshold. This could lead to potential issues during the final multiplication by i.
  • 2 * (i * i): In this expression, we better understand whether the multiplication by 2 will cause overflow after squaring i. Therefore, this makes the expression slightly safer in scenarios involving large values.

3.3. CPU-Level Execution

At the CPU level, instructions are executed in specific orders that vary based on how the operations are grouped:

  • 2 * (i * i): The CPU might optimize this operation better. Since the squaring (i * i) is more straightforward.
  • 2 * i * i: The CPU might require additional cycles to handle the intermediate result of 2 * i, especially if this result is large.

In most real-world scenarios, the performance difference between 2 * (i * i) and 2 * i * i is likely minimal for smaller i values. However, the difference can become significant when i is large or when this operation is performed repeatedly in a performance-critical code section.

4. Performance Testing Using JMH

Now, to prove the theory, let’s play with actual data. To be more precise, we’ll present the JMH (Java Microbenchmark Harness) test results of the most common collection operations.

First, we’ll present the main parameters of our benchmark tests:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
public class MultiplicationBenchmark {
}

Then we’ll set the warm-up iterations number to 3.

Now, it’s time to add the benchmark tests for the following small and large values:

private int smallValue = 255;
private int largeValue = 2187657;
@Benchmark
public int testSmallValueWithParentheses() {
    return 2 * (smallValue * smallValue);
}
@Benchmark
public int testSmallValueWithoutParentheses() {
    return 2 * smallValue * smallValue;
}
@Benchmark
public int testLargeValueWithParentheses() {
    return 2 * (largeValue * largeValue);
}
@Benchmark
public int testLargeValueWithoutParentheses() {
    return 2 * largeValue * largeValue;
}

Here are the test results for our calculations with and without parentheses:

Benchmark                                             Mode  Cnt  Score   Error  Units
MultiplicationBenchmark.largeValueWithParentheses     avgt    5  1.066 ± 0.168  ns/op
MultiplicationBenchmark.largeValueWithoutParentheses  avgt    5  1.283 ± 0.392  ns/op
MultiplicationBenchmark.smallValueWithParentheses     avgt    5  1.173 ± 0.218  ns/op
MultiplicationBenchmark.smallValueWithoutParentheses  avgt    5  1.222 ± 0.287  ns/op

The results show the average time (in nanoseconds per operation, ns/op) taken for different multiplication scenarios based on the presence or absence of parentheses.

Here’s a breakdown:

MultiplicationBenchmark.largeValueWithParentheses (1.066 ± 0.168 ns/op):

  • This represents multiplying large values with parentheses.
  • The average time taken is 1.066 nanoseconds, with a margin of error of ±0.168.

MultiplicationBenchmark.largeValueWithoutParentheses (1.283 ± 0.392 ns/op):

  • This represents multiplying large values without parentheses.
  • The average time is 1.283 nanoseconds, with a margin of error of ±0.392.

MultiplicationBenchmark.smallValueWithParentheses (1.173 ± 0.218 ns/op):

  • This represents multiplying small values with parentheses.
  • The average time is 1.173 nanoseconds, with a margin of error of ±0.218.

MultiplicationBenchmark.smallValueWithoutParentheses (1.222 ± 0.287 ns/op):

  • This represents multiplying small values without parentheses.
  • The average time is 1.222 nanoseconds, with a margin of error of ±0.287.

Faster Approach: The multiplication involving large values with parentheses is the fastest (1.066 ns/op).
Slower Approach: Large values without parentheses take the most time (1.283 ns/op).

5. Conclusion

In this article, we saw that while both 2 * (i * i) and 2 * i * i yield the same result, 2 * (i * i) is often faster. Parentheses offer a slight speed advantage in large and small value multiplications, though the difference is minor and within the margin of error for smaller values.

This suggests that parentheses might result in slightly more optimized multiplication, but the performance difference is marginal. It provides more predictable intermediate results, better compiler optimization opportunities, reduced risk of overflow, and more efficient CPU execution. When writing performance-critical code, we should consider not just correctness but also how the code will execute. Small changes, like the placement of parentheses, can significantly impact performance, showing the importance of understanding the mechanics of both the language and the hardware.

As always, the source code of all these examples is available over on GitHub.

       

Processing JDBC ResultSet With Stream API

$
0
0

1. Overview

Iterating through the ResultSet is a common approach to retrieving data from a JDBC query. However, in some cases, we may prefer to work with a stream of records instead.

In this article, we’ll explore a few approaches to processing a ResultSet using the Stream API.

2. Using Spliterators

We’ll start with a pure JDK approach, using Spliterators to create a stream.

First, let’s define a model for our entity:

public record CityRecord(String city, String country) {
}

In our CityRecord we store the information about the city and its country.

Next, let’s create a repository that interacts with the database and returns a stream of our CityRecord instances:

public class JDBCStreamAPIRepository {
    private static final String QUERY = "SELECT name, country FROM cities";
    private final Logger logger = LoggerFactory.getLogger(JDBCStreamAPIRepository.class);
    public Stream<CityRecord> getCitiesStreamUsingSpliterator(Connection connection)
            throws SQLException {
        PreparedStatement preparedStatement = connection.prepareStatement(QUERY);
        connection.setAutoCommit(false);
        preparedStatement.setFetchSize(5000);
        ResultSet resultSet = preparedStatement.executeQuery();
        return StreamSupport.stream(new Spliterators.AbstractSpliterator<CityRecord>(
          Long.MAX_VALUE, Spliterator.ORDERED) {
            @Override
            public boolean tryAdvance(Consumer<? super CityRecord> action) {
                try {
                    if(!resultSet.next()) return false;
                    action.accept(createCityRecord(resultSet));
                    return true;
                } catch(SQLException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }, false);
    }
    private CityRecord createCityRecord(ResultSet resultSet) throws SQLException {
        return new CityRecord(resultSet.getString(1), resultSet.getString(2));
    }
}

We’ve created a PreparedStatement to retrieve all the items from the cities table, specifying the fetch size to control memory consumption. We used an AbstractSpliterator to generate a stream, where new records are produced as long as the ResultSet has more values. Additionally, we mapped each row to a CityRecord using the createCityRecord method.

Finally, let’s write a test for our repository:

public class JDBCResultSetWithStreamAPIUnitTest {
    private static Connection connection = null;
    private static final String JDBC_URL = "jdbc:h2:mem:testDatabase";
    private static final String USERNAME = "dbUser";
    private static final String PASSWORD = "dbPassword";
    JDBCStreamAPIRepository jdbcStreamAPIRepository = new JDBCStreamAPIRepository();
    @BeforeEach
    void setup() throws Exception {
        connection = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);
        initialDataSetup();
    }
    private void initialDataSetup() throws SQLException {
        Statement statement = connection.createStatement();
        String ddlQuery = "CREATE TABLE cities (name VARCHAR(50), country VARCHAR(50))";
        statement.execute(ddlQuery);
        List<String> sqlQueryList = Arrays.asList(
          "INSERT INTO cities VALUES ('London', 'United Kingdom')",
          "INSERT INTO cities VALUES ('Sydney', 'Australia')",
          "INSERT INTO cities VALUES ('Bucharest', 'Romania')"
        );
        for (String query : sqlQueryList) {
            statement.execute(query);
        }
    }
    @Test
    void givenJDBCStreamAPIRepository_whenGetCitiesStreamUsingSpliterator_thenExpectedRecordsShouldBeReturned() throws SQLException {
        Stream<CityRecord> cityRecords = jdbcStreamAPIRepository
          .getCitiesStreamUsingSpliterator(connection);
        List<CityRecord> cities = cityRecords.toList();
        assertThat(cities)
          .containsExactly(
            new CityRecord("London", "United Kingdom"),
            new CityRecord("Sydney", "Australia"),
            new CityRecord("Bucharest", "Romania"));
    }

We establish a connection to the H2 database and, before the test, prepare the cities table with a few entries. Finally, we verify that our repository returns all the expected items from the table as a stream.

3. Using JOOQ

JOOQ is a popular library for working with relational databases. It already provides methods to retrieve a stream of records from a ResultSet.

Let’s begin by adding the necessary dependencies:

<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq</artifactId>
    <version>3.19.11</version>
</dependency>

Next, let’s add a new method to our JDBCStreamAPIRepository:

public Stream<CityRecord> getCitiesStreamUsingJOOQ(Connection connection)
        throws SQLException {
    PreparedStatement preparedStatement = connection.prepareStatement(QUERY);
    connection.setAutoCommit(false);
    preparedStatement.setFetchSize(5000);
    ResultSet resultSet = preparedStatement.executeQuery();
    return DSL.using(connection)
      .fetchStream(resultSet)
      .map(r -> new CityRecord(r.get("NAME", String.class),
        r.get("COUNTRY", String.class)))];
}

We used the fetchStream() method from the ResultQuery class to build a stream of records from the ResultSet. Additionally, we map JOOQ records to CityRecord instances before returning them from the method.

Let’s call our new method and verify that it behaves correctly:

@Test
void givenJDBCStreamAPIRepository_whenGetCitiesStreamUsingJOOQ_thenExpectedRecordsShouldBeReturned() throws SQLException {
    Stream<CityRecord> cityRecords = jdbcStreamAPIRepository
      .getCitiesStreamUsingJOOQ(connection);
    List<CityRecord> cities = cityRecords.toList();
    assertThat(cities)
      .containsExactly(
        new CityRecord("London", "United Kingdom"),
        new CityRecord("Sydney", "Australia"),
        new CityRecord("Bucharest", "Romania"));
}

As expected, we retrieved all the city records from the database in the stream.

4. Using jdbc-stream

Alternatively, we can create a stream from the ResultSet using a lightweight library called jdbc-stream.

Let’s add its dependencies:

<dependency>
    <groupId>com.github.juliomarcopineda</groupId>
    <artifactId>jdbc-stream</artifactId>
    <version>0.1.1</version>
</dependency>

Now, let’s add a new method to our JDBCStreamAPIRepository:

public Stream<CityRecord> getCitiesStreamUsingJdbcStream(Connection connection)
        throws SQLException {
    PreparedStatement preparedStatement = connection.prepareStatement(QUERY);
    connection.setAutoCommit(false);
    preparedStatement.setFetchSize(5000);
    ResultSet resultSet = preparedStatement.executeQuery();
    return JdbcStream.stream(resultSet)
      .map(r -> {
          try {
              return createCityRecord(resultSet);
          } catch (SQLException e) {
              throw new RuntimeException(e);
          }
      });
}

We’ve used JdbcStream to build a stream from our ResultSet. Under the hood, it uses Spliterators and builds the stream with the same logic as our own implementation.

Now, we’ll check how our new repository method works:

@Test
void givenJDBCStreamAPIRepository_whenGetCitiesStreamUsingJdbcStream_thenExpectedRecordsShouldBeReturned() throws SQLException {
    Stream<CityRecord> cityRecords = jdbcStreamAPIRepository
            .getCitiesStreamUsingJdbcStream(connection);
    List<CityRecord> cities = cityRecords.toList();
    assertThat(cities)
      .containsExactly(
        new CityRecord("London", "United Kingdom"),
        new CityRecord("Sydney", "Australia"),
        new CityRecord("Bucharest", "Romania"));
}

We’ve obtained all the expected items using the jdbc-stream library.

5. Close Resources

When working with JDBC, we must close all the resources we use to avoid connection leaks. The common practice is to use the try-with-resources syntax around Connection, PreparedStatement, and ResultSet. However, this approach isn’t suitable when using streams. If we return a stream from a repository method, all our resources will already be closed, and any operations on the stream won’t be able to access them.

To avoid this issue, we need to close all our resources using the stream’s onClose() method. Additionally, we must ensure that the stream is closed after we finish working with it.

Let’s modify our repository method to include the resource-closing logic:

public Stream<CityRecord> getCitiesStreamUsingJdbcStream(Connection connection)
        throws SQLException {
    PreparedStatement preparedStatement = connection.prepareStatement(QUERY);
    connection.setAutoCommit(false);
    preparedStatement.setFetchSize(5000);
    ResultSet resultSet = preparedStatement.executeQuery();
    return JdbcStream.stream(resultSet)
      .map(r -> {
          try {
              return createCityRecord(resultSet);
          } catch (SQLException e) {
              throw new RuntimeException(e);
          }
      })
      .onClose(() -> closeResources(connection, resultSet, preparedStatement));
}
private void closeResources(Connection connection, ResultSet resultSet, PreparedStatement preparedStatement) {
    try {
        resultSet.close();
        preparedStatement.close();
        connection.close();
        logger.info("Resources closed");
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
}

We’ve added the closeResources() method and attached it to the onClose() stream handler.

Now, let’s modify our client code to ensure that the stream is closed after use:

@Test
void givenJDBCStreamAPIRepository_whenGetCitiesStreamUsingJdbcStream_thenExpectedRecordsShouldBeReturned() throws SQLException {
    Stream<CityRecord> cityRecords = jdbcStreamAPIRepository
            .getCitiesStreamUsingJdbcStream(connection);
    List<CityRecord> cities = cityRecords.toList();
    cityRecords.close();
    assertThat(cities)
      .containsExactly(
        new CityRecord("London", "United Kingdom"),
        new CityRecord("Sydney", "Australia"),
        new CityRecord("Bucharest", "Romania"));
}

Here, we close the stream after all the items have been processed. Additionally, we can observe a log message indicating that all resources have been closed:

[main] INFO com.baeldung.resultset.streams.JDBCStreamAPIRepository -- Resources closed

6. Conclusion

In this article, we explored several options for manipulating ResultSets using the Stream API. This approach is particularly useful when dealing with large datasets that cannot be loaded into memory all at once. Additionally, if we follow a functional style in our applications, a streaming repository will align well with our logic.

As usual, the full source code can be found over on GitHub.

       

Calling getClass() From a Static Context

$
0
0

1. Overview

In Java, understanding how we call methods in static and non-static contexts is crucial, especially when working with methods like getClass().

One common problem we might encounter is trying to call the getClass() method from a static context, which will result in a compilation error.

In this tutorial, let’s explore why this happens and how we can handle it correctly.

2. Introduction to the Problem

The getClass() method is inherited from the Object class and returns the runtime Class of the object it is called upon. When we use getClass(), Java provides us with an instance of the Class object that represents the object’s runtime type.

Next, let’s see an example:

class Player {
 
    private String name;
    private int age;
 
    public Player(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public Class<?> currentClass() {
        return getClass();
    }
}

In this example, Player is a pretty straightforward class. The currentClass() method is an instance method, which allows us to obtain the Class<Player> object from a Player instance:

Player kai = new Player("Kai", 25);
assertSame(Player.class, kai.currentClass());

Sometimes, we want to get the current Class object from a static method. So, we may come up with this approach:

class Player {
    // ... unrelated code omitted
    public static Class<?> getClassInStatic() {
        return getClass();
    }
}

However, this code doesn’t compile:

java: non-static method getClass() cannot be referenced from a static context

Next, let’s understand why this happens and explore different ways to fix this problem.

3. Why Can’t We Call getClass() in a Static Context?

To figure out the cause of this problem, let’s quickly understand static and non-static contexts in Java.

A static method or variable belongs to the class instead of any particular class instance. This means that static members can be accessed without creating an instance of the class.

On the other hand, non-static members, such as instance methods and variables, are tied to individual instances of a class. We cannot access them without creating an object of the class.

Now, let’s examine why the compiler error occurred.

First, getClass() is an instance method:

public class Object {
    // ... unrelated code omitted
    public final native Class<?> getClass();
}

Our getClassInStatic() is a static method that we call without a Player instance. However, in this method, we invoked getClass(). The compiler raises an error because getClass() needs an instance of the class to determine the runtime type, but a static method doesn’t have any instance associated with it.

Next, let’s see how to resolve the issue.

4. Using the Class Literal

The most straightforward approach to retrieve the current Class object in a static context is to use the class literal (ClassName.class):

class Player {
    // ... unrelated code omitted
    public static Class<?> currentClassByClassLiteral() {
        return Player.class;
    }
}

As we can see, in the currentClassByClassLiteral() method, we return the Class object for the Player class using its class literal Player.class. 

Since currentClassByClassLiteral() is a static method, it doesn’t rely on an instance. We can call it from the class:

assertSame(Player.class, Player.currentClassByClassLiteral());

As the test shows, using class literals is pretty straightforward for obtaining the class type in a static context. However, we must know the class name at compile time. It won’t help if we need to determine the class type dynamically at runtime.

Next, let’s see how to get the Class object in a static context dynamically.

5. Using MethodHandles

MethodHandle was introduced in Java 7 and is primarily used in the context of method handles and dynamic invocation.

The MethodHandles.Lookup class provides various reflection-like capabilities, and the lookupClass() method returns the Class object for which the lookup() method was performed.

Next, let’s create a static method in the Player class to return the Class object using this approach:

class Player {
    // ... unrelated codes omitted
    public static Class<?> currentClassByMethodHandles() {
        return MethodHandles.lookup().lookupClass();
    }
}

When we call this method, we get the expected Player.class object:

assertSame(Player.class, Player.currentClassByMethodHandles());

The lookupClass() method dynamically returns the calling class where the lookup() is performed. In other words, the lookupClass() method returns the class where the MethodHandles.Lookup instance was created. This can be useful when writing dynamic code that needs to inspect the current class reflectively at runtime.

Further, it’s worth noting that since it involves a dynamic lookup and reflection-like mechanisms, it is generally slower than the class literal approach.

6. Conclusion

Calling getClass() from a static context leads to a compilation error in Java. In this article, we’ve discussed the root cause of this error.

Additionally, we’ve explored two solutions to avoid this issue and obtain the expected Class object from a static context. Using class literals is straightforward and efficient. It’s the best choice if we have the class information at compile time. However, if we need to deal with a dynamic invocation at runtime, the MethodHandles approach is a helpful alternative.

As always, the complete source code for the examples is available over on GitHub.

       

Implicitly Declared Classes and Instance Main Methods in Java

$
0
0

1. Overview

In recent versions of Java, the Oracle team has been focusing on making the Java language more accessible for newbie programmers. As part of that, the Oracle team has introduced implicit classes and instance main methods as a preview feature, which will eliminate the need to use constructs that typical newbie programmers are not familiar with.

In Java 23, the Oracle team has a few proposed enhancements for implicit classes and instance main methods, which will further simplify the learning curve for newbie programmers.

In this article, we’ll briefly discuss implicit classes, instance main methods, and the new proposed enhancements in JDK 23.

2. Before JDK 23

Implicit classes (or unnamed classes) and instance main methods were initially introduced in Java 21 as preview features.

Traditionally, Java requires developers to explicitly define classes to encapsulate the member variables and methods. This can be cumbersome, especially if we’re writing small programs with straightforward logic.

Developers must also declare the main method with the static keyword and method arguments, which serve as the entry point for applications. These constructs often confuse beginner programmers and limit the visibility of member variables.

With the introduction of implicit classes, developers can now write code without class declaration. The compiler will automatically generate the class behind the scenes so that developers can focus on the code’s core logic.

The instance main methods feature lets developers define the main method within a class instance. This means that developers can now access instance variables and methods directly, which will allow us to write more complex code within the main method.

Also, it doesn’t require the methods to be static or to have a String[] parameter, which makes it easy for beginners to get started.

3. Enhancements in Java 23

New enhancements were proposed in Java 23 to make the language more welcoming for beginners. Let’s briefly understand them in this section.

3.1. Interacting With the Console

Many beginner programmers often want to write programs that involve interacting with the console, like writing text on the console and taking console input, for example. However, to accomplish this, they need to write complex sentences that are mysterious for beginners.

For example, code that’s as simple as taking input from the user and displaying it requires us to write the following code:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Test {
    public static void main(String[] args) {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            System.out.print("Please enter your name: ");
            String name = reader.readLine();
            System.out.println("Hello " + name);
        } catch (IOException ioe) {
            System.err.println("An error occurred while reading input: " + ioe.getMessage());
        }
    }
}

The above code looks mysterious to beginners and can lead to many questions like What is BufferedReader and InputStreamReader? What is try, catch? What is an IOException?

With the proposed enhancement, the following methods are readily available for us to use in the body of every implicit class:

public static void println(Object object);
public static void print(Object obj);
public static String readln(String prompt);

To achieve this, Java introduces a new top-level class in the java.io package named IO. It declares the above three static methods for textual I/O with the console and nothing else. Every implicitly declared class automatically imports these static methods, as if the below declaration were included:

import static java.io.IO.*;

The new class java.io.IO is a preview API in JDK 23.

With this, we can write the above code more concisely:

void main() {
    String name = readln("Please enter your name: ");
    print("Hello "+name);
}

To run the above code, we need to compile and run the program by enabling the preview flag. For that, go to the directory where your source file is residing, and then execute two commands:

javac --source 23 --enable-preview .\Test.java
java --source 23 --enable-preview Test

3.2. Automatic Import of the java.base Module

To write any meaningful code in Java, we need to import other packages into our code. For beginners, however, these import statements look strange and can be another source of confusion.

The new proposal is to further simplify the code by making all public top-level classes and interfaces of the packages exported by the java.base module available for use in the body of every implicit class.

This will allow the programmers to use these classes/interfaces without having to have explicit import statements. These classes would be imported implicitly on demand. Popular APIs in commonly used packages such as java.io, java.math, and java.util are thus immediately usable, without any fuss.

Earlier, the Oracle team proposed a new import declaration in the format “import module M,” which imports, on demand, all of the public top-level classes and interfaces of the packages exported by module M.

Implicitly declared classes implicitly import the java.base module, as if the declaration import module java.base appears at the start of every source file containing an implicit class.

4. Conclusion

We’ve seen how implicit classes and instance main methods can significantly improve the beginner programmer experience while also adding value to experienced programmers who can now write cleaner and more readable code.

And, as always, the source code for the examples can be found over on GitHub.

       

Building a RAG App Using MongoDB and Spring AI

$
0
0

1. Overview

The use of AI technologies is becoming a key skill in modern development. In this article, we’ll build a RAG Wiki application that can answer questions based on stored documents.

We’ll use Spring AI to integrate our application with the MongoDB Vector database and the LLM.

2. RAG Applications

We use Retrieval-Augmented Generation (RAG) applications when natural language generation needs to rely on contextual data. A key component of RAG applications is the vector database, which plays a crucial role in effectively managing and retrieving this data:

Document population and prompting flows in RAG applications

We use an embedding model to process source documents. The embedding model converts the text from our documents into high-dimensional vectors. These vectors capture the semantic meaning of the content, allowing us to compare and retrieve similar content based on context rather than just keyword matching. Then we store the documents in the vector store.

Once we’ve saved the documents, we can send prompts based on them in the following way:

  • First, we use the embedding model to process the question, converting it into a vector that captures its semantic meaning.
  • Next, we perform a similarity search, comparing the question’s vector with the vectors of documents stored in the vector store.
  • From the most relevant documents, we build a context for the question.
  • Finally, we send both the question and its context to the LLM, which constructs a response relevant to the query and enriched by the context provided.

In this tutorial, we’ll use MongoDB Atlas Search as our vector store. It provides vector search capabilities that cover our needs in this project. To set the local instance of the MongoDB Atlas Search for test purposes, we’ll use the mongodb-atlas-local docker container. Let’s create a docker-compose.yml file:

version: '3.1'
services:
  my-mongodb:
    image: mongodb/mongodb-atlas-local:7.0.9
    container_name: my-mongodb
    environment:
      - MONGODB_INITDB_ROOT_USERNAME=wikiuser
      - MONGODB_INITDB_ROOT_PASSWORD=password
    ports:
      - 27017:27017

4. Dependencies and Configuration

Let’s begin by adding the necessary dependencies. Since our application will offer an HTTP API, we’ll include the spring-boot-starter-web dependency:

<dependency>
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-web</artifactId>
    <version>LATEST_VERSION</version>
</dependency>

Additionally, we’ll be using the open AI API client to connect to LLM, so let’s add its dependency as well:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>LATEST_VERSION</version>
</dependency>

Finally, we’ll add the MongoDB Atlas Store dependency:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mongodb-atlas-store-spring-boot-starter</artifactId>
    <version>LATEST_VERSION</version>
</dependency>

Now, let’s add the configuration properties for our application:

spring:
  data:
    mongodb:
      uri: mongodb://wikiuser:password@localhost:27017/admin
      database: wiki
  ai:
    vectorstore:
      mongodb:
        collection-name: vector_store
        initialize-schema: true
        path-name: embedding
        indexName: vector_index
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-3.5-turbo

We’ve specified the MongoDB URL and database, and we’ve also configured our vector store by setting the collection name, embedding field name, and vector index name. Thanks to the initialize-schema property, all of these artifacts will be automatically created by the Spring AI framework.

Finally, we’ve added the open AI API key and the model version.

5. Save Documents to the Vector Store

Now, we’re adding the process for saving data to our vector store. Our application will be responsible for providing answers to user questions based on existing documents – essentially functioning as a kind of wiki.

Let’s add a model that will store the content of the files along with the file path:

public class WikiDocument {
    private String filePath;
    private String content;
    // standard getters and setters
}

Next step, we’ll add the WikiDocumentsRepository. In this repository, we’re encapsulating all the persistence logic:

import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
@Component
public class WikiDocumentsRepository {
    private final VectorStore vectorStore;
    public WikiDocumentsRepository(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }
    public void saveWikiDocument(WikiDocument wikiDocument) {
        Map<String, Object> metadata = new HashMap<>();
        metadata.put("filePath", wikiDocument.getFilePath());
        Document document = new Document(wikiDocument.getContent(), metadata);
        List<Document> documents = new TokenTextSplitter().apply(List.of(document));
        vectorStore.add(documents);
    }
}

Here, we’ve injected the VectorStore interface bean, which will be implemented by MongoDBAtlasVectorStore provided by the spring-ai-mongodb-atlas-store-spring-boot-starter. In the saveWikiDocument method, we create a Document instance and populate it with the content and metadata.

Then we use TokenTextSplitter to break the document into smaller chunks and save them in our vector store. Now let’s create a WikiDocumentsServiceImpl:

@Service
public class WikiDocumentsServiceImpl {
    private final WikiDocumentsRepository wikiDocumentsRepository;
    // constructors
    public void saveWikiDocument(String filePath) {
        try {
            String content = Files.readString(Path.of(filePath));
            WikiDocument wikiDocument = new WikiDocument();
            wikiDocument.setFilePath(filePath);
            wikiDocument.setContent(content);
            wikiDocumentsRepository.saveWikiDocument(wikiDocument);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

At the service layer, we retrieve the file contents, create WikiDocument instances, and send them to the repository for persistence.

In the controller, we’ll simply pass the file path to the service layer and return a 201 status code if the document is saved successfully:

@RestController
@RequestMapping("wiki")
public class WikiDocumentsController {
    private final WikiDocumentsServiceImpl wikiDocumentsService;
    // constructors
    @PostMapping
    public ResponseEntity<Void> saveDocument(@RequestParam String filePath) {
        wikiDocumentsService.saveWikiDocument(filePath);
        return ResponseEntity.status(201).build();
    }
}

Now, let’s start our application and see how our flow works. Let’s add the Spring Boot test dependency, which will allow us to set up a test web context:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>LATEST_VERSION</version>
</dependency>

Now, we’ll bootstrap the test application instance and call the POST endpoint for two documents:

@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
@SpringBootTest
class RAGMongoDBApplicationManualTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    void givenMongoDBVectorStore_whenCallingPostDocumentEndpoint_thenExpectedResponseCodeShouldBeReturned() throws Exception {
        mockMvc.perform(post("/wiki?filePath={filePath}",
          "src/test/resources/documentation/owl-documentation.md"))
          .andExpect(status().isCreated());
        mockMvc.perform(post("/wiki?filePath={filePath}",
          "src/test/resources/documentation/rag-documentation.md"))
          .andExpect(status().isCreated());
    }
}

Both calls should return a 201 status code, so the documents were added. We can use MongoDB Compass to confirm that the documents were successfully saved to the vector store:
View of the stored document in MongoDB Compass

As we can see – both documents were saved. We can see the original content as well as an embedding array.

Let’s add the similarity search functionality. We’ll include a findSimilarDocuments method in our repository:

@Component
public class WikiDocumentsRepository {
    private final VectorStore vectorStore;
    public List<WikiDocument> findSimilarDocuments(String searchText) {
        return vectorStore
          .similaritySearch(SearchRequest
            .query(searchText)
            .withSimilarityThreshold(0.87)
            .withTopK(10))
          .stream()
          .map(document -> {
              WikiDocument wikiDocument = new WikiDocument();
              wikiDocument.setFilePath((String) document.getMetadata().get("filePath"));
              wikiDocument.setContent(document.getContent());
              return wikiDocument;
          })
          .toList();
    }
}

We’ve called the similaritySearch method from the VectorStore. In addition to the search text, we’ve specified a result limit and a similarity threshold. The similarity threshold parameter allows us to control how closely the document content should match our search text.

In the service layer, we’ll proxy the call to the repository:

public List<WikiDocument> findSimilarDocuments(String searchText) {
    return wikiDocumentsRepository.findSimilarDocuments(searchText);
}

In the controller, let’s add a GET endpoint that receives the search text as a parameter and passes it to the service:

@RestController
@RequestMapping("/wiki")
public class WikiDocumentsController {
    @GetMapping
    public List<WikiDocument> get(@RequestParam("searchText") String searchText) {
        return wikiDocumentsService.findSimilarDocuments(searchText);
    }
}

Now let’s call our new endpoint and see how the similarity search works:

@Test
void givenMongoDBVectorStoreWithDocuments_whenMakingSimilaritySearch_thenExpectedDocumentShouldBePresent() throws Exception {
    String responseContent = mockMvc.perform(get("/wiki?searchText={searchText}", "RAG Application"))
      .andExpect(status().isOk())
      .andReturn()
      .getResponse()
      .getContentAsString();
    assertThat(responseContent)
      .contains("RAG AI Application is responsible for storing the documentation");
}

We called the endpoint with a search text that wasn’t an exact match in the document. However, we still retrieved the document with similar content and confirmed that it contained the text we stored in our rag-documentation.md file.

7. Prompt Endpoint

Let’s start building the prompt flow, which is the core functionality of our application. We’ll begin with the AdvisorConfiguration:

@Configuration
public class AdvisorConfiguration {
    @Bean
    public QuestionAnswerAdvisor questionAnswerAdvisor(VectorStore vectorStore) {
        return new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults());
    }
}

We’ve created a QuestionAnswerAdvisor bean, responsible for constructing the prompt request, including the initial question. Additionally, it will attach the vector store’s similarity search response as context for the question. Now, let’s add the search endpoint to our API:

@RestController
@RequestMapping("/wiki")
public class WikiDocumentsController {
    private final WikiDocumentsServiceImpl wikiDocumentsService;
    private final ChatClient chatClient;
    private final QuestionAnswerAdvisor questionAnswerAdvisor;
    public WikiDocumentsController(WikiDocumentsServiceImpl wikiDocumentsService,
                                   @Qualifier("openAiChatModel") ChatModel chatModel,
                                   QuestionAnswerAdvisor questionAnswerAdvisor) {
        this.wikiDocumentsService = wikiDocumentsService;
        this.questionAnswerAdvisor = questionAnswerAdvisor;
        this.chatClient = ChatClient.builder(chatModel).build();
    }
    @GetMapping("/search")
    public String getWikiAnswer(@RequestParam("question") String question) {
        return chatClient.prompt()
          .user(question)
          .advisors(questionAnswerAdvisor)
          .call()
          .content();
    }
}

Here, we’ve constructed a prompt request by adding the user’s input to the prompt and attaching our QuestionAnswerAdvisor.

Finally, let’s call our endpoint and see what it tells us about RAG applications:

@Test
void givenMongoDBVectorStoreWithDocumentsAndLLMClient_whenAskQuestionAboutRAG_thenExpectedResponseShouldBeReturned() throws Exception {
    String responseContent = mockMvc.perform(get("/wiki/search?question={question}", "Explain the RAG Applications"))
      .andExpect(status().isOk())
      .andReturn()
      .getResponse()
      .getContentAsString();
    logger.atInfo().log(responseContent);
    assertThat(responseContent).isNotEmpty();
}

We sent the question “Explain the RAG applications” to our endpoint and logged the API response:

b.s.r.m.RAGMongoDBApplicationManualTest : Based on the context provided, the RAG AI Application is a tool 
used for storing documentation and enabling users to search for specific information efficiently...

As we can see, the endpoint returned information about RAG applications based on the documentation files we previously saved in the vector database.

Now let’s try to ask something that we surely don’t have in our knowledge base:

@Test
void givenMongoDBVectorStoreWithDocumentsAndLLMClient_whenAskUnknownQuestion_thenExpectedResponseShouldBeReturned() throws Exception {
    String responseContent = mockMvc.perform(get("/wiki/search?question={question}", "Explain the Economic theory"))
      .andExpect(status().isOk())
      .andReturn()
      .getResponse()
      .getContentAsString();
    logger.atInfo().log(responseContent);
    assertThat(responseContent).isNotEmpty();
}

Now we’ve asked about economic theory, and here is the response:

b.s.r.m.RAGMongoDBApplicationManualTest : I'm sorry, but the economic theory is not directly related to the information provided about owls and the RAG AI Application.
If you have a specific question about economic theory, please feel free to ask.

This time, our application didn’t find any related documents and didn’t use any other sources to provide an answer.

8. Conclusion

In this article, we successfully implemented an RAG application using the Spring AI framework, which is an excellent tool for integrating various AI technologies. Additionally, MongoDB proves to be a strong choice for handling vector storage.

With this powerful combination, we can build modern AI-based applications for various purposes, including chatbots, automated wiki systems, and search engines.

As always, the code is available over on GitHub.

       

Introduction to the Hilla Framework

$
0
0

1. Overview

Hilla is a full-stack web framework for Java. Hilla lets us build a full-stack application by adding React views to a Spring Boot application and calling backend Java services from TypeScript through type-safe RPC.

It uses the Vaadin UI component set and is compatible with Vaadin Flow. Both are part of the Vaadin platform. In this tutorial, we’ll discuss the basics of Hilla development.

2. Creating a Hilla Project

We can create a Hilla project by adding the Vaadin dependency on Spring Initializr or downloading a customized starter on Vaadin Start.

Alternatively, we can add Hilla to an existing Spring Boot project, by adding the following bill of materials (BOM) in the project’s Maven pom.xml. We initialize the vaadin.version property with the latest version of vaadin-bom:

<properties>
    <vaadin.version>24.4.10</vaadin.version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-bom</artifactId>
            <version>${vaadin.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Then, we add the following dependency for the Vaadin platform, which includes Hilla:

<dependency>
    <groupId>com.vaadin</groupId>
    <artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>

To complete the setup, let’s create a theme. The theme configuration file ensures a consistent look and feel for all views and includes CSS utility classes. We add a src/main/frontend/themes/hilla/theme.json file with the following content:

{
    "lumoImports" : [ "typography", "color", "sizing", "spacing", "utility" ]
}

We then load the theme by updating our Spring Boot application to extend AppShellConfigurator and adding a @Theme(“hilla”) annotation:

@Theme("hilla") // needs to match theme folder name
@SpringBootApplication
public class DemoApplication implements AppShellConfigurator {
    // application code omitted
}

3. Starting the Application

Hilla includes React views and backend Java sources in the same project as one unified full-stack project. We can define views by creating React components in the src/main/frontend/views folder.

Let’s start by adding a view by creating a new file, src/main/frontend/views/@index.tsx (creating folders as needed) with the following content:

export default function Index() {
    return (
        <h1>Hello world</h1>
    );
}

Now, let’s start the application by running mvn spring-boot:run or running the Spring Boot application class in our IDE.

The first startup takes some time as Hilla downloads both Maven and npm dependencies and starts a Vite dev server. Subsequent starts are quicker.

With the build running, we can open up our browser to localhost:8080 to see the ‘Hello world‘ greeting:

A browser window showing a "Hello world"-text

4. Calling Server Methods With @BrowserCallable

A unique aspect of Hilla applications is how we call the server from the client. Unlike traditional Spring Boot apps with React frontends, we don’t create two separate applications that communicate over a generic REST API. Instead, we have one full-stack application that uses RPC to call methods on service classes written in Java. It follows a backend-for-frontend (BFF) architecture.

Let’s look at how we can access a backend service from the browser. We’ll use the Contact class throughout our example:

@Entity
public class Contact {
    @Id
    @GeneratedValue
    private Long id;
    @Size(min = 2)
    private String name;
    @Email
    private String email;
    private String phone;
    // Constructor, getters, setters
}

We’ll also use a Spring Data JPA repository for accessing and persisting data:

public interface ContactRepository extends 
    JpaRepository<Contact, Long>, 
    JpaSpecificationExecutor<Contact> {
}

We can make a Java service callable from a TypeScript view by annotating it with @BrowserCallable. Hilla services are protected by Spring Security. By default, access is denied to all services. We can add an @AnonymousAllowed annotation to allow any user to call the service:

@BrowserCallable
@AnonymousAllowed
public class ContactService {
    private final ContactRepository contactRepository;
    public ContactService(ContactRepository contactRepository) {
        this.contactRepository = contactRepository;
    }
    public List<Contact> findAll() {
        return contactRepository.findAll();
    }
    public Contact findById(Long id) {
        return contactRepository.findById(id).orElseThrow();
    }
    public Contact save(Contact contact) {
        return contactRepository.save(contact);
    }
}

Java and TypeScript handle nullability differently. In Java, all non-primitive types can be null, whereas TypeScript requires us to explicitly define variables or fields as nullable. Hilla’s TypeScript generator is in a strict mode by default, ensuring Java and TypeScript types match exactly.

The downside of this strictness is that the TypeScript code can become clumsy as we need to introduce null checks in several places. If we follow good API design practices, avoiding null return values for collections, we can add a package-info.java file with a @NonnullApi annotation in our service package to simplify the TypeScript types:

@NonNullApi
package com.example.application;
import org.springframework.lang.NonNullApi;

We can now call the service with the same signature from React. Let’s update @index.tsx to find all contacts and display them in a Grid:

export default function Contacts() {
    const contacts = useSignal<Contact[]>([]);
    async function fetchContacts() {
        contacts.value = await ContactService.findAll();
    }
    useEffect(() => {
        fetchContacts();
    }, []);
    return (
        <div className="p-m flex flex-col gap-m">
            <h2>Contacts</h2>
            <Grid items={contacts.value}>
                <GridSortColumn path="name">
                    {({item}) => <NavLink to={`contacts/${item.id}/edit`}>{item.name}</NavLink>}
                </GridSortColumn>
                <GridSortColumn path="email"/>
                <GridSortColumn path="phone"/>
            </Grid>
        </div>
    );
}

Let’s define an async function, fetchContacts(), that awaits ContactService.findAll() and then sets the contacts signal value to the received contacts. We import the Contact type and ContactService from the Frontend/generated folder, which is where Hilla generates client-side code. Hilla uses signals that are based on Preact signals:

A browser window displaying a list of contacts in a data grid

5. Configuring Views and Layouts

Hilla uses file-based routing, which means that views are mapped to routes based on their filename and folder structure. The root folder for all views is src/main/frontend/views. In the following sections, we’ll walk through how to configure views and layouts.

5.1. View Naming Conventions

Views are mapped to paths by their name and the following conventions:

  • @index.tsx — the index for a directory
  • @layout.tsx — the layout for a directory
  • view-name.tsx — mapped to view-name
  • {parameter} — folder that captures a parameter
  • {parameter}.tsx — view that captures a parameter
  • {{parameter}}.tsx — view that captures an optional parameter
  • {…wildcard}.tsx — matches any character

5.2. View Configuration

We can configure a view by exporting a constant named config of type ViewConfig. Here, we can define things like the view’s title, icon, and access control:

export const config: ViewConfig = {
    title: "Contact List",
    menu: {
        order: 1,
    }
}
export default function Index() {
    // Code omitted
}

5.3. Defining Layouts

We can define parent layouts for any directory. A @layout.tsx in the root of the views folder wraps all content in the application, whereas a @layout.tsx in a contacts folder only wraps any views in that directory or its subdirectories.

Let’s create a new @layout.tsx file in the src/main/frontend/views directory:

export default function MainLayout() {
    return (
        <div className="p-m h-full flex flex-col box-border">
            <header className="flex gap-m pb-m">
                <h1 className="text-l m-0">
                    My Hilla App
                </h1>
                {createMenuItems().map(({to, title}) => (
                    <NavLink to={to} key={to}>
                        {title}
                    </NavLink>
                ))}
            </header>
            <Suspense>
                <Outlet/>
            </Suspense>
        </div>
    );
}

The createMenuItems() helper returns an array of the discovered routes and creates links for each.

Let’s open the browser again and verify that we can see the new menu above our view:

A browser window showing a navigation menu on top of the contacts view

6. Building Forms and Validating Input

Next, let’s implement the edit view to edit Contacts. We’ll use the Hilla useForm() hook to bind input fields to fields on the Contact object and validate that all validation constraints defined on it pass.

First, we create a new file views/contacts/{id}/edit.tsx with the following content:

export default function ContactEditor() {
    const {id} = useParams();
    const navigate = useNavigate();
    const {field, model, submit, read} = useForm(ContactModel, {
        onSubmit: async contact => {
            await ContactService.save(contact);
            navigate('/');
        }
    })
    async function loadUser(id: number) {
        read(await ContactService.findById(id))
    }
    useEffect(() => {
        if (id) {
            loadUser(parseInt(id))
        }
    }, [id]);
    return (
        <div className="flex flex-col items-start gap-m">
            <TextField label="Name" {...field(model.name)}/>
            <TextField label="Email" {...field(model.email)}/>
            <TextField label="Phone" {...field(model.phone)}/>
            <div className="flex gap-s">
                <Button onClick={submit} theme="primary">Save</Button>
                <Button onClick={() => navigate('/')} theme="tertiary">Cancel</Button>
            </div>
        </div>
    );
}

Then, let’s use the useParams() hook to access the id parameter from the URL.

Next, we pass ContactModel to useForm() and configure it to submit to our Java service. Let’s also destructure the field, model, submit, and read properties from the return value into variables. Finally, we read the currently selected Contact into the form with a useEffect that fetches the Contact by id from our backend.

We create input fields for each property on Contact and use the field method to bind them to the appropriate fields on the Contact object. This synchronizes the value and validation rules defined on the Java object with the UI.

The Save button calls the form’s submit() method:

A browser window showing a form editing a contact, the email field is showing a validation error.

7. Automatic CRUD Operations With AutoCrud

Because Hilla is a full-stack framework, it can help us automate some common tasks like creating listings and editing entities. Let’s update our service to take advantage of these features:

@BrowserCallable
@AnonymousAllowed
public class ContactService extends CrudRepositoryService<Contact, Long, ContactRepository> {
    public List<Contact> findAll() {
        return getRepository().findAll();
    }
    public Contact findById(Long id) {
        return getRepository().findById(id).orElseThrow();
    }
}

Extending CrudRepositoryService creates a service that provides all the basic CRUD operations Hilla needs to generate data grids, forms, or CRUD views based on a given entity. In this case, we also added the same findAll() and findById() methods we had in our service earlier to avoid breaking the existing views.

We can now create a new view that displays a CRUD editor based on the new service. Let’s define a new file named frontend/views/auto-crud.tsx with the following content:

export default function AutoCrudView() {
    return <AutoCrud service={ContactService} model={ContactModel} className="h-full"/>
}

We only need to return a single component, AutoCrud, and pass in the service and model to get the view for listing, editing, and deleting contacts:

A browser window showing a list of contacts on the left. The selected contact in the list is displayed in a form on the right.If we only need to list items without editing them, we can use the AutoGrid component instead. Likewise, if we need to edit an item but don’t want to display a list, we can use the AutoForm component. Both work in the same way as AutoCrud above.

8. Building for Production

To build Hilla for production, we use the production Maven profile. The production build creates an optimized frontend JavaScript bundle and turns off development-time debugging. The profile is included automatically in projects created on Spring Initializr and Vaadin Start. We can also add it manually if we have a custom project:

<profile>
    <!-- Production mode is activated using -Pproduction -->
    <id>production</id>
    <dependencies>
        <!-- Exclude development dependencies from production -->
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-core</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.vaadin</groupId>
                    <artifactId>vaadin-dev</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>com.vaadin</groupId>
                <artifactId>vaadin-maven-plugin</artifactId>
                <version>${vaadin.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>build-frontend</goal>
                        </goals>
                        <phase>compile</phase>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</profile>

We can build the project using the production profile:

./mvnw package -Pproduction

9. Conclusion

In this article, we learned the basics of Hilla development for building full-stack web applications that combine a Spring Boot backend with a React frontend with type-safe communication.

Hilla let’s us add React views to a Spring Boot project. The views are mapped to routes based on name and folder structure. We can call Spring Service classes from TypeScript, retaining full type information by annotating the Service class with @BrowserCallable. Hilla uses Java Bean Validation annotations for input validation on the client, and again to verify the correctness of data received in the Java service method.

As always, the code can be found over on GitHub.

       

Leveraging Quarkus and LangChain4j

$
0
0

1. Overview

In this tutorial, we’ll learn about LangChain, a framework for developing applications powered by large language models (LLMs). More specifically, we’ll use langchain4j, a Java framework that simplifies the integration with LangChain and allows developers to integrate LLMs into their applications.

This framework is very popular for building Retrieval-Augmented Generation (RAG). In this article, we’ll understand all those terms and see how to leverage Quarkus to build such an application.

2. Large Language Models (LLMs)

Large language models (LLMs) are AI systems trained with massive amounts of data. Their goal is to generate human-like output. GPT-4 from OpenAi is probably one of the most well-known LLMs used nowadays. However, LLMs can answer questions and perform various natural language processing tasks.

Such models are the backbone for power chatbots, content generation, document analyses, video and image generation, and more.

2.1. LangChain

LangChain is a popular open-source framework that helps developers build LLM-powered applications. Initially developed for Python, it simplifies the process of creating multi-step workflows using LLMs such as:

  • Integration with different LLMs: LangChain does not serve its own LLMs but rather provides a standard interface for interacting with many different LLMs, such as GPT-4, Lama-3, and more
  • Integrate external tools: LangChain enables integration with other tools that are very important for such applications, like vector databases, web browsers, and other APIs
  • Manage memory: Applications that require conversational flows need history or context persistence. LangChain has memory capabilities, allowing models to “remember” information across interactions

LangChain is one of the go-to frameworks for Python developers building AI applications. However, for Java developers, the LangChain4j framework offers a Java-like adaptation of this robust framework.

2.2. LangChain4j

LangChain4j is a Java library inspired by LangChain, designed to help build AI-powered applications using LLMs. The project creators aim to fill the gap between Java frameworks and the numerous Python and Javascript options for AI systems.

LangChain4j gives Java developers access to the same tools and flexibility that LangChain provides to Python developers, enabling the development of applications such as chatbots, summarization engines, or intelligent search systems.

Powerful applications can be quickly built with other frameworks, such as Quarkus.

2.3. Quarkus

Quarkus is a Java framework designed for cloud-native applications. Its ability to significantly reduce memory usage and startup time makes it highly suitable for microservices, serverless architectures, and Kubernetes environments.

Incorporating Quarkus into your AI applications ensures that your systems are scalable, efficient, and ready for production at an enterprise level. Moreover, Quarkus offers an easy-to-use and low learning curve to integrate LangChain4j into our Quarkus applications.

3. ChatBot Application

In order to demonstrate the power of Quarkus integration with LangChain4j, we’ll create a simple chatbot application to answer questions about Quarkus. For this, let’s use GPT-4. This will help us since it is trained with a lot of data from the internet, and therefore, we will not need to train our own model.

The application will only respond to questions regarding Quarkus and will remember the previous conversation in the particular chat.

3.1. Dependencies

Now that we have introduced the main concepts and tools, let’s build our first application using LangChain4j and Quarkus. First things first, let’s create our Quarkus application using Rest and Redis and then add the following dependencies:

<dependency>
    <groupId>io.quarkiverse.langchain4j</groupId>
    <artifactId>quarkus-langchain4j-openai</artifactId>
    <version>0.18.0.CR1</version>
</dependency>
<dependency>
    <groupId>io.quarkiverse.langchain4j</groupId>
    <artifactId>quarkus-langchain4j-memory-store-redis</artifactId>
    <version>0.18.0.CR1</version>
</dependency>

The first step is to add the latest quarkus-langchain4j-openai and quarkus-langchain4j-memory-store-redis dependencies to our pom.xml

The first dependency will introduce the LangChain4J version, which is compatible with Quarkus. Moreover, it’ll give us out-of-the-box components that configure LangChain4J to use OpenAI LLM models as the backbone of our service. There are many LLMs to choose from, but for the sake of simplicity, let’s use the current default, which is GPT-4o-mini. In order to use such a service, we need to create an ApiKey in OpenAI. Once this is done, we can proceed. We’ll talk about the second dependency later.

3.2. Setup

Now, let’s set up our application to allow it to communicate with GPT-4 and Redis so we can start implementing our chatbot. For that, we need to configure some application properties in Quarkus, as it has out-of-the-box components to do so. We only need to add the following properties to the application.properties file:

quarkus.langchain4j.openai.api-key=${OPEN_AI_KEY}
quarkus.langchain4j.openai.organization-id=${OPEN_AI_ORG}
quarkus.redis.hosts=${REDIS_HOST}

Quarkus has many other valuable configurations for this application. Please refer to Quarkus documentation; however, this will be enough for our chatbot.

Optionally, we can also use environment variables to set such properties as:

QUARKUS_REDIS_HOSTS: redis://localhost:6379
QUARKUS_LANGCHAIN4J_OPENAI_API_KEY: changeme
QUARKUS_LANGCHAIN4J_OPENAI_ORGANIZATION_ID: changeme

3.3. Components

LangChain4j offers a rich set of attractions to help us implement complex flows, such as document retrieval. Nonetheless, in this article, we’ll focus on a simple conversation flow. Having said that, let’s create our simple chatbot:

@Singleton
@RegisterAiService
public interface ChatBot {
    @SystemMessage("""
    During the whole chat please behave like a Quarkus specialist and only answer directly related to Quarkus,
    its documentation, features and components. Nothing else that has no direct relation to Quarkus.
    """)
    @UserMessage("""
    From the best of your knowledge answer the question below regarding Quarkus. Please favor information from the following sources:
    - https://docs.quarkiverse.io/
    - https://quarkus.io/
    
    And their subpages. Then, answer:
    
    ---
    {question}
    ---
    """)
    String chat(@MemoryId UUID memoryId, String question);
}

In the ChatBot class, we register a service that will use the LLM during the conversation. To do that, Quarkus offers the @RegisterAiService annotation, which abstracts the setup to integrate LangChain4J and GTP-4. Next, we apply some Prompt Engineering to structure how we want the Chatbot to behave.

In short, Prompt Engineering is a technique for shaping instructions that will help the LLMs adapt their behavior during the interpretation and processing of user requests.

So, using such prompts, we implemented the desired behavior for our bot. LangChain4J uses messages to do that, whereas:

  • SystemMessage are internal instructions that influence the model responses but are hidden from the end-user;
  • UserMessage are messages sent by the end-user. However, LangChain4J helps us by allowing us to apply templates on top of such messages to enhance the prompting.

Let’s discuss about @MemoryId next.

3.4. Memory

Despite being very good at generating text and providing relevant answers to questions, LLMs by themselves are not enough to implement chatbots. A critical aspect that LLMs cannot do themselves is to remember the context or data from previous messages. That is why we need memory capabilities.

LangChain4J offers a subset of abstract ChatMemoryStore and ChatMemory. These abstractions allow different implementations to store and manage the message list that represents a chat. Additionally, an identifier is needed to store and retrieve the chat memory, and @MemoryId is used to mark such ID.

Quarkus offers an out-of-the-box Redis-based implementation for chat memory, which is why we added the second dependency and the Redis setup.

3.5. API

Finally, the piece missing from our sample chatbot application is an interface to enable users to communicate with the system. Let’s create an API so users can send their questions to our chatbot.

@Path("/chat")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ChatAPI {
    
    private ChatBot chatBot;
    public ChatAPI(ChatBot chatBot) {
        this.chatBot = chatBot;
    }
    @POST
    public Answer mesage(@QueryParam("q") String question, @QueryParam("id") UUID chatId) {
        chatId = chatId == null ? UUID.randomUUID() : chatId;
        String message = chatBot.chat(chatId, question);
        return new Answer(message, chatId);
    }
}

The API receives a question and, optionally, a chat ID. If the ID is not provided, we create it. This allows users to control whether they want to continue an existing chat or create one from scratch. Then, it sends both parameters to our ChatBot class, which integrates the conversation. Optionally, we could implement an endpoint to retrieve all messages from a chat.

Running the command below will send our first question to our chatbot.

# Let's ask: Does quarkus support redis?
curl --location --request POST 'http://localhost:8080/chat?q=Does%20quarkus%20support%20redis%3F'

So we get:

{
    "message": "Yes, Quarkus supports Redis through the Quarkus Redis client extension...",
    "chatId": "d3414b32-454e-4577-af81-8fb7460f13cd"
}

Note that since this was our first question, no ID was provided, so a new one was created. Now, we can use this id to keep track of our conversation history. Next, let’s test if the Chatbot is keeping our history as context for our conversation.

# Replace the id param with the value we got from the last call. 
# So we ask: q=What was my last Quarkus question?&id=d3414b32-454e-4577-af81-8fb7460f13cd
curl --location --request POST 'http://localhost:8080/chat?q=What%20was%20my%20last%20Quarkus%20question%3F&id=d3414b32-454e-4577-af81-8fb7460f13cd'

Excellent. As expected, our chatbot replied correctly.

{
    "message": "Your last Quarkus question was asking whether Quarkus supports Redis.",
    "chatId": "d3414b32-454e-4577-af81-8fb7460f13cd"
}

That means our simple chatbot application is ready only after creating a couple of classes and setting some properties.

4. Conclusion

Using Quarkus and LangChain4J simplifies the process of building a Quarkus-based chatbot that interacts with OpenAI’s language models and retains conversational memory. This powerful combination of Quarkus and LangChain4j enables the development of AI-powered applications with minimal overhead while still providing rich capabilities.

From this setup, we can continue to extend the chatbot’s capabilities by adding more domain-specific knowledge and improving its ability to answer complex questions. Quarkus and Langchain4j still provide many other tools for this.

In this article, we saw how much productivity and efficiency that combination of Quarkus and LangChain4J brings to the table for the development of AI systems using Java. Moreover, we presented the concepts involved in the development of such applications.

As usual, all code samples used in this article are available over on GitHub.

       

Introduction to TeaVM

$
0
0

1. Overview

TeaVM is a powerful tool for translating Java bytecode into JavaScript, enabling Java applications to run directly in the browser. This allows us to maintain a Java-based codebase while targeting web environments.

In this tutorial, we’ll explore how to bridge the gap between Java and JavaScript using TeaVM’s ability to interact with the DOM and call Java methods from JavaScript.

2. Main Uses of TeaVM

When we have complex Java-based logic, it’s not practical to rewrite the same functionality for the web. TeaVM allows us to avoid redundancy by efficiently compiling our Java code into JavaScript, optimizing the size and performance of the final script.

Either way, let’s remember that our Java code must fit within the confines of the Java Class Library (JCL) emulation. For example, Swing and JavaFX aren’t supported.

2.1. Single-Page Application (SPA)

TeaVM can be the right solution for creating a Single-Page Application (SPA) from scratch almost entirely in Java and CSS, minimizing the need to write JavaScript and HTML code. This is because TeaVM has these web-specific features:

  • Call Java methods directly from JavaScript, as we’ll see in the upcoming examples
  • Create native Java methods implemented in JavaScript using the @JSBody annotation
  • Manipulate web elements with Java using the JSO library

However, TeaVM doesn’t completely replace the need for JavaScript or HTML, especially with regard to layout and some browser-specific logic. In addition, TeaVM doesn’t handle CSS, which we have to write manually or manage through external frameworks.

2.2. Existing Website

TeaVM is also great for adding features written in Java to an existing website developed with a traditional CMS like WordPress or Joomla. For example, we can create a PHP plugin for our CMS that exposes functionality to TeaVM via a REST API and include a JavaScript compiled by TeaVM on a page of our website.

Such a script, taking advantage of our REST API, can convert JSON to Java objects and vice versa, and execute our business logic written in Java. It can also modify the DOM to create a user interface or integrate into an existing one.

This use case makes sense when the client-side business logic is so complex that we are more comfortable, skilled, and familiar with Java code than with JavaScript.

2.3. Other Cases

In addition, TeaVM has WebAssembly support under active development, with a new version in the works that will allow our JavaScript-targeted applications to be ported to WebAssembly with minimal changes. However, this feature is still in an experimental phase and not yet ready.

TeaVM can also transpile Java to C. We can use this functionality as an ultra-lightweight subset of GraalVM native images. Even if this functionality is stable and ready to use, we won’t go into it because it’s beyond the main use of TeaVM.

3. Maven Setup and TeaVM Configuration

First, the current 0.10.x version of TeaVM supports Java bytecode up to JDK 21 and requires at least Java 11 to run its Java-to-JavaScript compiler. Of course, these requirements may change in later versions.

We’ll use Maven and keep the pom.xml minimal by configuring TeaVM programmatically within a Java class. This approach allows us to dynamically change the configuration based on the parameters passed to the main(…) method via Maven’s -Dexec.args option. This helps to generate different JavaScript outputs from the same project for different purposes, all while sharing the same codebase.

If we prefer an alternative method, or if Maven isn’t in use, the official TeaVM Getting Started guide provides further instructions.

3.1. pom.xml

After checking what the latest version of TeaVM is in the Maven repository, let’s add the dependencies:

<dependency>
    <groupId>org.teavm</groupId>
    <artifactId>teavm-core</artifactId>
    <version>0.10.2</version>
</dependency>
<dependency>
    <groupId>org.teavm</groupId>
    <artifactId>teavm-classlib</artifactId>
    <version>0.10.2</version>
</dependency>
<dependency>
    <groupId>org.teavm</groupId>
    <artifactId>teavm-tooling</artifactId>
    <version>0.10.2</version>
</dependency>

Adding these three dependencies automatically adds other TeaVM transitive dependencies. The teavm-core includes libraries such as teavm-interop, teavm-metaprogramming-api, relocated ASM libraries (for bytecode manipulation), HPPC, and Rhino (JavaScript engine). In addition, teavm-classlib brings in teavm-jso, commons-io, jzlib and joda-time. The teavm-tooling dependency also includes a relocated version of commons-io.

These transitive dependencies provide essential functionality without the need to add them manually.

3.2. TeaVMRunner.java

This class configures TeaVM, we need to specify it using Maven’s -Dexec.mainClass option:

public class TeaVMRunner {
    public static void main(String[] args) throws Exception {
        TeaVMTool tool = new TeaVMTool();
        tool.setTargetDirectory(new File("target/teavm"));
        tool.setTargetFileName("calculator.js");
        tool.setMainClass("com.baeldung.teavm.Calculator");
        tool.setTargetType(TeaVMTargetType.JAVASCRIPT);
        tool.setOptimizationLevel(TeaVMOptimizationLevel.ADVANCED);
        tool.setDebugInformationGenerated(false);
        tool.setIncremental(false);
        tool.setObfuscated(true);
        tool.generate();
    }
}

Let’s take a closer look:

  • setTargetDirectory(…) → Output directory where TeaVM will place the generated files
  • setTargetFileName(…) → Name of the generated JavaScript file
  • setMainClass(…) → Fully qualified name of the main class of the application
  • setTargetType(…) → JavaScript, WebAssembly, or C
  • setOptimizationLevel(…) → The ADVANCED level produces the smallest JavaScript files, even smaller than the FULL level
  • setDebugInformationGenerated(…) → Only useful for creating a debug information file for the TeaVM Eclipse plugin
  • setIncremental(…) → If enabled, compiles faster, but reduces optimizations, so it’s not recommended for production
  • setObfuscated(…) → It reduces the size of the generated JavaScript by two or three times when enabled, so it should be preferred in most cases

There are also other options documented in the Tooling section of the official guide, e.g. for generating source maps.

The most important thing to note is that using setMainClass(…) creates a JavaScript main() function with global scope that executes the translated version of the Java main(String[]). We’ll see an example of this later.

3.3. HTML Page

While simple, this is a complete example of how to include the Javascript file generated by TeaVM. We’ve left out meta tags to optimize for mobile devices, indexing, and other requirements unrelated to TeaVM:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>TeaVM Example</title>
    <script type="text/javascript" src="calculator.js"></script>
    <script type="text/javascript">
        document.addEventListener('DOMContentLoaded', function() {
            // Call JS functions here
        });
    </script>
</head>
<body>
<h1>TeaVM Example</h1>
<div id="calculator-container"></div>
</body>
</html>

In this example, calculator.js is the file we specified earlier in setTargetFileName(…). We’ll see what to put in place of the “Call JS functions here” comment in a moment. Finally, <div id=”calculator-container”></div> is a placeholder that we’ll use to create a sample calculator.

4. Calculator Example

Let’s take a simple addition example.

4.1. Calling a Java Method From Javascript

This is the class we previously specified in setMainClass(…):

public class Calculator {
    public static void main(String[] args) {
    }
    @JSExport
    public static int sum(int a, int b) {
        return a + b;
    }
}

The @JSExport annotation in TeaVM is used to make Java methods, fields, or classes accessible to JavaScript, allowing them to be called directly from JavaScript code. So, after compiling the code, let’s call the sum(…) function in our example HTML page:

[...]
document.addEventListener('DOMContentLoaded', function() {
    // Call JS functions here
    let result = sum(51, 72);
    console.log("Sum result: " + result);
[...]

This is the output of the browser’s JavaScript console:

Sum result: 123

The result is as expected.

4.2. Java-Based DOM Manipulation

Now let’s implement the main(…) function. Let’s notice that document.getElementById(“calculator-container”) goes to select the <div id=”calculator-container”></div> tag that we previously inserted in the example HTML file:

public static void main(String[] args) {
    HTMLDocument document = HTMLDocument.current();
    HTMLElement container = document.getElementById("calculator-container");
    // Create input fields
    HTMLInputElement input1 = (HTMLInputElement) document.createElement("input");
    input1.setType("number");
    container.appendChild(input1);
    HTMLInputElement input2 = (HTMLInputElement) document.createElement("input");
    input2.setType("number");
    container.appendChild(input2);
    // Create a button
    HTMLButtonElement button = (HTMLButtonElement) document.createElement("button");
    button.appendChild(document.createTextNode("Calculate Sum"));
    container.appendChild(button);
    // Create a div to display the result
    HTMLElement resultDiv = document.createElement("div");
    container.appendChild(resultDiv);
    // Add click event listener to the button
    button.addEventListener("click", (evt) -> {
        try {
            long num1 = Long.parseLong(input1.getValue());
            long num2 = Long.parseLong(input2.getValue());
            long sum = num1 + num2;
            resultDiv.setTextContent("Result: " + sum);
        } catch (NumberFormatException e) {
            resultDiv.setTextContent("Please enter valid integer numbers.");
        }
    });
}

The code is self-explanatory. In a nutshell, it dynamically creates input fields, a button, and a result display area within the web page, all using Java. The button listens for a click event, retrieves the numbers entered in the input fields, calculates their sum, and displays the result. If invalid input is provided, an error message is shown instead.

Let’s remember to call the main() function:

document.addEventListener('DOMContentLoaded', function() {
    // Call JS functions here
    main();
});

Here is the result:

TeaVM Example Calculator

Starting with a simple example like this, we can modify the DOM as we like to create any layout, even with the help of CSS.

5. Conclusion

In this article, we explored how TeaVM facilitates the translation of Java bytecode into JavaScript, allowing Java applications to run directly in web browsers. We covered key features such as how to call Java methods from JavaScript, perform Java-based DOM manipulation, and implement simple web applications without having to write extensive JavaScript code. Through a practical calculator example, we demonstrated the ease of bridging Java and web development using TeaVM.

As always, the full source code is available over on GitHub.

       

Create Avro Schema With List of Objects

$
0
0

1. Overview

Apache Avro is a data serialization framework that provides powerful data structures and a lightweight, fast, binary data format.

In this tutorial, we’ll explore how to create an Avro schema which, when transformed into an object, contains a list of other objects.

2. The Objective

Let’s assume we want to develop an Avro schema that represents a parent-child relationship. Therefore, we’ll need a Parent class that contains a list of Child objects.

Here’s how this might look like in Java code:

public class Child {
    String name;
}
public class Parent {
    List<Child> children;
}

Our goal is to create an Avro schema that translates its structure into these objects for us.

Before we take a look at the solution, let’s quickly go over some Avro basics:

  • Avro schemas are defined using JSON
  • The type field refers to the data type (e.g., record, array, string)
  • The fields array defines the structure of a record

3. Creating the Avro Schema

To properly illustrate the Parent-Child relationship in Avro, we’ll need to use a combination of record and array types.

Here’s how the schema looks like:

{
    "namespace": "com.baeldung.apache.avro.generated",
    "type": "record",
    "name": "Parent",
    "fields": [
        {
            "name": "children",
            "type": {
                "type": "array",
                "items": {
                    "type": "record",
                    "name": "Child",
                    "fields": [
                        {"name": "name", "type": "string"}
                    ]
                }
            }
        }
    ]
}

We’ve begun by defining a record of type Parent. Inside the Parent record, we’ve defined a children field. This field is an array type, which lets us store multiple Child objects. The items property of the array type details the structure of each element part of the array. In our case, this is a Child record. As we can see, the Child record has a single property, name, of type string.

4. Using the Schema in Java

Once we’ve defined our Avro schema, we’ll use it to generate the Java classes. We’ll do this, of course, with the Avro Maven plugin. Here’s the configuration in the parent pom file:

<plugin>
    <groupId>org.apache.avro</groupId>
    <artifactId>avro-maven-plugin</artifactId>
    <version>1.11.3</version>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>schema</goal>
            </goals>
            <configuration>
                <sourceDirectory>src/main/java/com/baeldung/apache/avro/schemas</sourceDirectory>
                <outputDirectory>src/main/java</outputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>

In order for Avro to generate our classes, we’ll need to run the Maven generate sources command (mvn clean generate-sources) or go to the Plugins section of the Maven tool window and run the avro:schema goal of the avro plugin:

 

This way, Avro creates Java classes based on the provided schema, in the provided namespace. The namespace property also adds the package name at the top of the generated class.

5. Working With Generated Classes

The newly created classes provide the setup methods, to get and set the children list. Here’s what this looks like:

@Test
public void whenAvroSchemaWithListOfObjectsIsUsed_thenObjectsAreSuccessfullyCreatedAndSerialized() throws IOException {
    Parent parent = new Parent();
    List<Child> children = new ArrayList();
    Child child1 = new Child();
    child1.setName("Alice");
    children.add(child1);
    Child child2 = new Child();
    child2.setName("Bob");
    children.add(child2);
    parent.setChildren(children);
    SerializationDeserializationLogic.serializeParent(parent);
    Parent deserializedParent = SerializationDeserializationLogic.deserializeParent();
    assertEquals("Alice", deserializedParent.getChildren().get(0).getName());
}

We see from the test above that we’ve created a new Parent. We can do it this way, or use the builder() available. This article on Avro default values illustrates how to use the builder() pattern.

Then, we create two Child objects which we add to the Parent’s children property. Finally, we serialize and deserialize the object and compare one of the names.

6. Conclusion

In this article, we looked at how to create an Avro schema that contains a list of objects. Furthermore, we’ve detailed how to define a Parent record with a list property of Child records. This is a way we can represent complex data structures in Avro. Additionally, this is particularly useful when we need to work with collections of objects or hierarchical data.

Finally, Avro schemas are flexible and we can configure them to set up even more complex data structures. We can combine different types and nested structures to replicate our data models.

As always, the code is available over on GitHub.

       

Java Weekly, Issue 561

$
0
0

1. Spring and Java

>> A beginner’s guide to Spring Data Envers [vladmihalcea.com]

Auditing made easy: tracking entity changes with almost no changes required on the application part with Spring Data Envers. Good stuff.

>> The Arrival of Java 23! [inside.java]

From last week – general availability of JDK 23.

Very cool additions like primitive support for switch, ZGC improvement, better Javadocs, and a host more to play with.

Also worth reading:

Webinars and presentations:

Time to upgrade:

2. Pick of the Week

JetBrains is now graciously contributing to Baeldung Pro with a 6-month free license of IntelliJ Ultimate:

>> Baeldung Pro with IntelliJ Ultimate [baeldung.com]

       

What’s the Difference Between interface and @interface in Java?

$
0
0

1. Overview

Let’s dive deep to see what interface and @interface are and their applications. An interface is a contract for a class that implements it. In the most common form, it is a group of related methods with empty bodies.

On the other hand, an @interface allows you to add metadata to your code. The compiler, tools, or framework uses this metadata to influence class behavior or processing.

2. interface

An interface acts like a contract for its implementing class. It specifies a behavior that its implementing classes must implement without dictating how. It suggests that any class implementing the interface must provide concrete implementations for all its methods.

public interface Animal {
    String eat();
    String sleep();
}
public class Dog implements Animal {
    @Override
    public String eat() {
        return "Dog is eating";
    }
    @Override
    public String sleep() {
        return "Dog is sleeping";
    }
}

All interface methods are implicitly public and abstract (except default and static methods), and all fields are public, static, and final. We can achieve abstraction, multiple inheritances, and loose coupling in Java using interfaces.

Abstraction: The interface reveals only the essential information needed to invoke the method, whereas the complexities of the implementation remain concealed.

Multiple Inheritance: A class can implement several interfaces, thereby avoiding the diamond problem that can arise in languages that allow multiple inheritance from classes.

Loose Coupling: Interfaces provide a distinct separation between the functionality and the implementation details. It enables a class to alter its internal processes without affecting its users, as we define the method and the signature separately.

3. @interface

In Java, we use @interface to declare an annotation type. Annotations provide a way to add metadata to Java code elements like classes, methods, and fields. Consequently, tools and libraries can leverage this metadata to gather information during the compilation process or the runtime for code processing.

Let’s create an @Review custom annotation, which we can use to track who reviewed a piece of code and when:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Review {
    String reviewer();
    String date() default "";
}

Notice that when defining the Review annotation using @interface, we define it as a regular interface. We have method names and return types that use primitive data types or even arrays. However, we cannot use complex objects for return types.

Also, we need to provide values for all the above-defined properties while using our Review annotation. There is a way to define a default value at the time of declaration. We can use it if we don’t always need the value provided while using the annotation.

Also, one more thing to notice is the @Retention and the @Target annotations above the @interface definition. The  @Retention(RetentionPolicy.SOURCE) annotation makes our annotation available in the source code. The @Target({ElementType.METHOD}) annotation specifies that the Review annotation will be applied only to the methods in a Java class.

Now, let’s use the @Review annotation in the service method:

@Review(reviewer = "Natasha", date = "2024-08-24")
public String service() {
    return "Some logic here";
}

4. Comparison

Aspect interface @interface
Purpose Used to define a contract that classes can implement. Used to define a custom annotation.
Contains Method signatures without implementations, default methods, static methods, and constants. Annotation methods that provide metadata.

@Retention: Specifies retention of annotations.
@Target: Specifies the program elements to which an annotation type is applicable.

Usage Implemented by classes to provide specific behavior. Used to annotate code elements (classes, methods, fields, etc.) to provide metadata.
Use Cases To achieve abstraction, multiple inheritance in Java and decoupling methods from its implementations. To define custom annotation that can provide metadata for frameworks that support code documentation, configuration, code generation, and validations.

5. Conclusion

Understanding the distinction between interface and @interface is crucial as they play different roles in Java programming. An interface is about defining types and contracts. @interface is about providing metadata to the compiler or the runtime.

The complete source code for the above examples is available over on GitHub.

       

Iterate over a Guava Multimap

$
0
0

1. Overview

In this article, we will look at one of Map implementations from Google Guava library – Multimap. It is a collection that maps keys to values, similar to java.util.Map, but in which each key may be associated with multiple values.

2. Dependency

First, let’s add the Guava dependency to our pom.xml:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.3.0-jre</version>
</dependency>

The latest version can be found here.

3. Multimap Implementation

In the case of Guava Multimap, if we add two values for the same key, the second value will not override the first value. Instead, we will have two values in the resulting map. Let’s look at a test case:

String key = "a-key";
Multimap<String, String> map = ArrayListMultimap.create();
map.put(key, "firstValue");
map.put(key, "secondValue");
assertEquals(2, map.size());

Printing the map‘s content will output:

{a-key=[firstValue, secondValue]}

When we get values by key “a-key” we get Collection<String> that contains “firstValue” and “secondValue” as a result:

Collection<String> values = map.get(key);

Printing values will output:

[firstValue, secondValue]

4. Iterate Over a Multimap

Typically, the Multimap class provides several built-in methods that we can use to iterate over its content. So, let’s go down the rabbit hole and take a close look at each option.

Before we dive into the nitty-gritty details, let’s create and populate a Multimap with data:

Multimap<String, String> multiMap = ArrayListMultimap.create();
multiMap.putAll("key1", List.of("value1", "value11", "value111"));
multiMap.putAll("key2", List.of("value2", "value22", "value222"));
multiMap.putAll("key3", List.of("value3", "value33", "value333"));

4.1. Using entries()

Let’s start with the easiest solution, which involves using the entries() method. As the name implies, it returns a view collection of all key-value pairs contained in the given Multimap.

So, let’s see it in action:

static void iterateUsingEntries(Multimap<String, String> multiMap) {
    multiMap.entries()
      .forEach(entry -> LOGGER.info("{} => {}", entry.getKey(), entry.getValue()));
}

Executing the method will output:

key1 => value1
key1 => value11
key1 => value111
key2 => value2
key2 => value22
key2 => value222
key3 => value3
key3 => value33
key3 => value333

Here, we used the forEach() method to iterate through the extracted entries. Each entry denotes a Map.Entry instance and holds the key-value pair information.

4.2. Using asMap()

Alternatively, we can use the asMap() method to achieve the same outcome. This method returns a Map view of the specified Multimap:

static void iterateUsingAsMap(Multimap<String, String> multiMap) {
    multiMap.asMap()
      .entrySet()
      .forEach(entry -> LOGGER.info("{} => {}", entry.getKey(), entry.getValue()));
}

Here’s the output:

key1 => [value1, value11, value111]
key2 => [value2, value22, value222]
key3 => [value3, value33, value333]

As we can see, we used the entrySet() method to extract the Set of entries from the returned Map. Then, we iterated through them using the forEach() method.

4.3. Using keySet()

The keySet() method is another option to consider if we want to iterate only through the keys. It returns all the distinct keys contained in a particular Multimap as a Set:

static void iterateUsingKeySet(Multimap<String, String> multiMap) {
    multiMap.keySet()
      .forEach(LOGGER::info);
}

The above method will print:

key1
key2
key3

As we see above, we used a method reference instead of a lambda expression to display the extracted keys.

4.4. Using keys()

Similarly, we can use the keys() method to achieve the same objective. This method returns a view collection containing the key from each key-value pair:

static void iterateUsingKeys(Multimap<String, String> multiMap) {
    multiMap.keys()
      .forEach(LOGGER::info);
}

Unlike keySet(), which returns distinct keys, this method displays a key for each value. With that being said, it prints each key three times:

key1
key1
key1
key2
key2
key2
key3
key3
key3

In a nutshell, keys() returns a Multiset instance unlike keySet() which returns a Set. The key difference is that Multiset accepts duplicate elements.

4.5. Using values()

Lastly, Multimap provides the values() method to get a collection containing the values of the specified Multimap:

static void iterateUsingValues(Multimap<String, String> multiMap) {
    multiMap.values()
      .forEach(LOGGER::info);
}

The method will log the values of each key:

value1
value11
value111
value2
value22
value222
value3
value33
value333

We should note that updating the returned collection updates the underlying Multimap too, and vice versa. However, it’s not possible to add new elements to the collection.

5. Compared to the Standard Map

Standard map from java.util package doesn’t give us the ability to assign multiple values to the same key. Let’s consider a simple case when we put() two values into a Map using the same key:

String key = "a-key";
Map<String, String> map = new LinkedHashMap<>();
map.put(key, "firstValue");
map.put(key, "secondValue");
assertEquals(1, map.size());

The resulting map has only one element (“secondValue”), because of a second put() operation that overrides the first value. Should we want to achieve the same behavior as with Guava’s Multimap, we would need to create a Map that has a List<String> as a value type:

String key = "a-key";
Map<String, List<String>> map = new LinkedHashMap<>();
List<String> values = map.get(key);
if(values == null) {
    values = new LinkedList<>();
    values.add("firstValue");
    values.add("secondValue");
 }
map.put(key, values);
assertEquals(1, map.size());

Obviously, it is not very convenient to use and if we have such need in our code then Guava’s Multimap could be a better choice than java.util.Map.

One thing to notice here is that, although we have a list that has two elements in it, size() method returns 1. In Multimap, size() returns an actual number of values stored in a Map, but keySet().size() returns the number of distinct keys.

6. Pros of Multimap

Multimaps are commonly used in places where a Map<K, Collection<V>> would otherwise have appeared. The differences include:

  • There is no need to populate an empty collection before adding an entry with put()
  • The get() method never returns null, only an empty collection (we do not need to check against null like in Map<String, Collection<V>> test case)
  • A key is contained in the Multimap if and only if it maps to at least one value. Any operation that causes a key to has zero associated values, has the effect of removing that key from the Multimap (in Map<String, Collection<V>>, even if we remove all values from the collection, we still keep an empty Collection as a value, and this is unnecessary memory overhead)
  • The total entry values count is available as size()
  • Multimap comes with multiple ready-to-use methods that make the iteration logic easy and practical

7. Conclusion

In this article, we learned how and when to use Guava Multimap. We compared it to standard java.util.Map and saw pros of Guava Multimap. Along the way, we explored different ways of iterating over a given Multimap.

All these examples and code snippets can be found available over on GitHub.

       
Viewing all 4561 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>