Java is an object-oriented programming language that allows engineers to produce software for multiple platforms. Our resources in this Zone are designed to help engineers with Java program development, Java SDKs, compilers, interpreters, documentation generators, and other tools used to produce a complete application.
Previously we checked on ReentRantLock and its fairness. One of the things we can stumble upon is the creation of a Condition. By using Condition, we can create mechanisms that allow threads to wait for specific conditions to be met before proceeding with their execution. Java public interface Condition { void await() throws InterruptedException; void awaitUninterruptibly(); long awaitNanos(long nanosTimeout) throws InterruptedException; boolean await(long time, TimeUnit unit) throws InterruptedException; boolean awaitUntil(Date deadline) throws InterruptedException; void signal(); void signalAll(); } The closest we came to that so far is the wait Object Monitor method. A Condition is bound to a Lock and a thread cannot interact with a Condition and its methods if it does not have a hold on that Lock. Also, Condition uses the underlying lock mechanisms. For example, signal and signalAll will use the underlying queue of the threads that are maintained by the Lock, and will notify them to wake up. One of the obvious things to implement using Conditions is a BlockingQueue. Worker threads process data and publisher threads dispatch data. Data are published on a queue, worker threads will process data from the queue, and then they should wait if there is no data in the queue. For a worker thread, if the condition is met the flow is the following: Acquire the lock Check the condition Process data Release the lock If the condition is not met, the flow would slightly change to this: Acquire the lock Check the condition Wait until the condition is met Re-acquire the lock Process data Release the lock The publisher thread, whenever it adds a message, should notify the threads waiting on the condition. The workflow would be like this: Acquire the lock Publish data Notify the workers Release the lock Obviously, this functionality already exists through the BlockingQueue interface and the LinkedBlockingDeque and ArrayBlockingQueue implementations. We will proceed with an implementation for the sake of the example. Let’s see the message queue: Java package com.gkatzioura.concurrency.lock.condition; import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class MessageQueue<T> { private Queue<T> queue = new LinkedList<>(); private Lock lock = new ReentrantLock(); private Condition hasMessages = lock.newCondition(); public void publish(T message) { lock.lock(); try { queue.offer(message); hasMessages.signal(); } finally { lock.unlock(); } } public T receive() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { hasMessages.await(); } return queue.poll(); } finally { lock.unlock(); } } } Now let’s put it into action: Java MessageQueue<String> messageQueue = new MessageQueue<>(); @Test void testPublish() throws InterruptedException { Thread publisher = new Thread(() -> { for (int i = 0; i < 10; i++) { String message = "Sending message num: " + i; log.info("Sending [{}]", message); messageQueue.publish(message); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); Thread worker1 = new Thread(() -> { for (int i = 0; i < 5; i++) { try { String message = messageQueue.receive(); log.info("Received: [{}]", message); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); Thread worker2 = new Thread(() -> { for (int i = 0; i < 5; i++) { try { String message = messageQueue.receive(); log.info("Received: [{}]", message); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); publisher.start(); worker1.start(); worker2.start(); publisher.join(); worker1.join(); worker2.join(); } That’s it! Our workers processed the expected messages and waited when the queue was empty.
Integrating MicroStream and PostgreSQL, leveraging the new Jakarta EE specifications known as Jakarta Data, presents a powerful solution for developing ultrafast applications with SQL databases. MicroStream is a high-performance, in-memory object graph persistence library that enables efficient data storage and retrieval. At the same time, PostgreSQL is a widely used, robust SQL database system known for its reliability and scalability. Developers can achieve remarkable application performance and efficiency by combining these technologies' strengths and harnessing Jakarta Data's capabilities. This article will explore the integration between MicroStream and PostgreSQL, focusing on leveraging Jakarta Data to enhance the development process and create ultrafast applications. We will delve into the key features and benefits of MicroStream and PostgreSQL, highlighting their respective strengths and use cases. Furthermore, we will dive into the Jakarta Data specifications, which provide a standardized approach to working with data in Jakarta EE applications, and how they enable seamless integration between MicroStream and PostgreSQL. By the end of this article, you will have a comprehensive understanding of how to leverage MicroStream, PostgreSQL, and Jakarta Data to build high-performance applications that combine the benefits of in-memory storage and SQL databases. Facing the Java and SQL Integration Challenge The biggest challenge in integrating SQL databases with Java applications is the impedance mismatch between the object-oriented programming (OOP) paradigm used in Java and the relational database model used by SQL. This impedance mismatch refers to the fundamental differences in how data is structured and manipulated in these two paradigms, leading to the need for conversion and mapping between the object-oriented world and the relational database world. Java is known for its powerful OOP features, such as encapsulation, polymorphism, and inheritance, which enable developers to create modular, maintainable, and readable code. However, these concepts do not directly translate to the relational database model, where data is stored in tables with rows and columns. As a result, when working with SQL databases, developers often have to perform tedious and error-prone tasks of mapping Java objects to database tables and converting between their respective representations. This impedance mismatch not only hinders productivity but also consumes significant computational power. According to some estimates, up to 90% of computing power can be consumed by the conversion and mapping processes between Java objects and SQL databases. It impacts performance and increases the cost of cloud resources, making it a concern for organizations following FinOps practices. MicroStream addresses this challenge with its in-memory object graph persistence approach by eliminating the need for a separate SQL database and the associated mapping process. With MicroStream, Java objects can be stored directly in memory without the overhead of conversions to and from a relational database. It results in significant performance improvements and reduces the power consumption required for data mapping. By using MicroStream, developers can leverage the natural OOP capabilities of Java, such as encapsulation and polymorphism, without the need for extensive mapping and conversion. It leads to cleaner and more maintainable code and reduces the complexity and cost of managing a separate database system. In the context of a cloud environment, the reduction in power consumption provided by MicroStream translates to cost savings, aligning with the principles of the FinOps culture. Organizations can optimize their cloud infrastructure usage and reduce operational expenses by minimizing the resources needed for data mapping and conversion. Overall, MicroStream helps alleviate the impedance mismatch challenge between SQL databases and Java, enabling developers to build high-performance applications that take advantage of OOP's natural design and readability while reducing power consumption and costs associated with data mapping. While addressing the impedance mismatch between SQL databases and Java applications can bring several advantages, it is vital to consider the trade-offs involved. Here are some trade-offs associated with the impedance mismatch: Increased complexity: Working with an impedance mismatch adds complexity to the development process. Developers need to manage and maintain the mapping between the object-oriented model and the relational database model, which can introduce additional layers of code and increase the overall complexity of the application. Performance overhead: The conversion and mapping process between Java objects and SQL databases can introduce performance overhead. The need to transform data structures and execute database queries can impact the overall application performance, especially when dealing with large datasets or complex data models. Development time and effort: Addressing the impedance mismatch often requires writing additional code for mapping and conversion, which adds to the development time and effort. Developers need to implement and maintain the necessary logic to synchronize data between the object-oriented model and the relational database, which can increase the development effort and introduce potential sources of errors. Maintenance challenges: When an impedance mismatch exists, any changes to the object-oriented model or the database schema may require mapping and conversion logic updates. This can create maintenance challenges, as modifications to one side of the system may necessitate adjustments on the other side to ensure consistency and proper data handling. Learning curve: Dealing with the impedance mismatch typically requires understanding the intricacies of both the object-oriented paradigm and the relational database model. Developers must have a good grasp of SQL, database design, and mapping techniques. This may introduce a learning curve for those more accustomed to working solely in the object-oriented domain. It is essential to weigh these trade-offs against the benefits and specific requirements of the application. Different scenarios may prioritize various aspects, such as performance, development speed, or long-term maintenance. Alternative solutions like MicroStream can help mitigate these trade-offs by providing a direct object storage approach and reducing the complexity and performance overhead associated with the impedance mismatch. Enough for today's theory; let’s see this integration in practice. It will be a simple application using Java, Maven, and Java SE. The first step is to have an installed PostgreSQL. To make it easier, let’s use docker and run the following command: Shell docker run --rm=true --name postgres-instance -e POSTGRES_USER=micronaut \ -e POSTGRES_PASSWORD=micronaut -e POSTGRES_DB=airplane \ -p 5432:5432 postgres:14.1 Ultrafast With PostgreSQL and MicroStream In this example, let’s use an airplane sample where we’ll have several planes and models that we’ll filter by manufacturer. The first step of our project is about the Maven dependencies. Besides the CDI, we need to include the MicroStream integration following the MicroStream relational integration, and furthermore, the PostgreSQL driver. XML <dependency> <groupId>expert.os.integration</groupId> <artifactId>microstream-jakarta-data</artifactId> <version>${microstream.data.version}</version> </dependency> <dependency> <groupId>one.microstream</groupId> <artifactId>microstream-afs-sql</artifactId> <version>${microstream.version}</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.14</version> </dependency> The second step is to overwrite the configuration for using a relational database. First, create DataSource, and then we’ll inject it and then use it on the StorageManager. Java @ApplicationScoped class DataSourceSupplier implements Supplier<DataSource> { private static final String JDBC = "microstream.postgresql.jdbc"; private static final String USER = "microstream.postgresql.user"; private static final String PASSWORD = "microstream.postgresql.password"; @Override @Produces @ApplicationScoped public DataSource get() { Config config = ConfigProvider.getConfig(); PGSimpleDataSource dataSource = new PGSimpleDataSource(); dataSource.setUrl(config.getValue(JDBC, String.class)); dataSource.setUser(config.getValue(USER, String.class)); dataSource.setPassword(config.getValue(PASSWORD, String.class)); return dataSource; } } @Alternative @Priority(Interceptor.Priority.APPLICATION) @ApplicationScoped class SQLSupplier implements Supplier<StorageManager> { @Inject private DataSource dataSource; @Override @Produces @ApplicationScoped public StorageManager get() { SqlFileSystem fileSystem = SqlFileSystem.New( SqlConnector.Caching( SqlProviderPostgres.New(dataSource) ) ); return EmbeddedStorage.start(fileSystem.ensureDirectoryPath("microstream_storage")); } public void close(@Disposes StorageManager manager) { manager.close(); } } With the configuration ready, the next step is to create the entity and its repository. In our sample, we’ll make Airplane and Airport as entity and repository, respectively. Java @Repository public interface Airport extends CrudRepository<Airplane, String> { List<Airplane> findByModel(String model); } @Entity public class Airplane { @Id private String id; @Column("title") private String model; @Column("year") private Year year; @Column private String manufacturer; } The last step is executing the application, creating airplanes, and filtering by the manufacturer. Thanks to the Jakarta EE and MicroProfile specifications, the integration works with microservices and monolith. Java public static void main(String[] args) { try (SeContainer container = SeContainerInitializer.newInstance().initialize()) { Airplane airplane = Airplane.id("1").model("777").year(1994).manufacturer("Boing"); Airplane airplane2 = Airplane.id("2").model("767").year(1982).manufacturer("Boing"); Airplane airplane3 = Airplane.id("3").model("747-8").year(2010).manufacturer("Boing"); Airplane airplane4 = Airplane.id("4").model("E-175").year(2023).manufacturer("Embraer"); Airplane airplane5 = Airplane.id("5").model("A319").year(1995).manufacturer("Airbus"); Airport airport = container.select(Airport.class).get(); airport.saveAll(List.of(airplane, airplane2, airplane3, airplane4, airplane5)); var boings = airport.findByModel(airplane.getModel()); var all = airport.findAll().toList(); System.out.println("The boings: " + boings); System.out.println("The boing models avialables: " + boings.size()); System.out.println("The airport total: " + all.size()); } System.exit(0); } Conclusion In conclusion, the impedance mismatch between SQL databases and Java applications presents significant challenges in terms of complexity, performance, development effort, maintenance, and the learning curve. However, by understanding these trade-offs and exploring alternative solutions, such as MicroStream, developers can mitigate these challenges and achieve better outcomes. MicroStream offers a powerful approach to address the impedance mismatch by eliminating the need for a separate SQL database and reducing the complexity of mapping and conversion processes. With MicroStream, developers can leverage the natural benefits of object-oriented programming in Java without sacrificing performance or increasing computational overhead. By storing Java objects directly in memory, MicroStream enables efficient data storage and retrieval, resulting in improved application performance. It eliminates the need for complex mapping logic and reduces the development effort required to synchronize data between the object-oriented model and the relational database. Moreover, MicroStream aligns with the principles of FinOps culture by reducing power consumption, which translates into cost savings in cloud environments. By optimizing resource usage and minimizing the need for data mapping and conversion, MicroStream contributes to a more cost-effective and efficient application architecture. While trade-offs are associated with impedance mismatch, such as increased complexity and maintenance challenges, MicroStream offers a viable solution that balances these trade-offs and enables developers to build ultrafast applications with SQL databases. By leveraging the power of Jakarta Data specifications and MicroStream's in-memory object graph persistence, developers can achieve a harmonious integration between Java and SQL databases, enhancing application performance and reducing development complexities. In the rapidly evolving application development landscape, understanding the challenges and available solutions for impedance mismatch is crucial. With MicroStream, developers can embrace the advantages of object-oriented programming while seamlessly integrating with SQL databases, paving the way for efficient, scalable, and high-performance applications. Source: MicroStream Integration on GitHub
Data loss is one of the biggest problems developers face when building distributed systems. Whether due to network issues or code bugs, data loss can have serious consequences for enterprises. In this article, we'll look at how to build Kafka listeners with Spring Boot and how to use Kafka's acknowledgment mechanisms to prevent data loss and ensure the reliability of our systems. Apache Kafka Apache Kafka is a distributed message platform used to store and deliver messages. Once a message is written to Kafka, it will be kept there according to a retention policy. The consumer groups mechanism is used to read out messages. The offset for each consumer group is used to understand the stage of message processing and to keep track of the progress of each consumer group in reading messages from a partition. It allows each consumer group to independently read messages from a topic and resume reading from where it left off in case of failures or restarts. In a simplified way, this can be represented as follows: After successfully processing a message, a consumer sends an acknowledgment to Kafka, and the offset pointer for that consumer group is shifted. As mentioned earlier, other consumer groups store their offset values in the message broker, allowing messages to be read independently. When we talk about high-reliability systems that must guarantee no data loss, we must consider all possible scenarios. Apache Kafka, by design, already has the features to ensure reliability. We, as consumers of messages, must also provide proper reliability. But what can go wrong? The consumer receives the message and crashes before he can process it The consumer receives the message, processes it, and then crashes Any network problems This can happen for reasons beyond our control — temporary network unavailability, an incident on the instance, pod eviction in a K8s cluster, etc. Kafka allows guaranteeing message delivery using the acknowledgment mechanism — at least once delivery. It means that the message will be delivered at least once, but under certain circumstances, it can be delivered several times. All we need to do is to configure Apache Kafka correctly and be able to react to duplicate messages if needed. Let's try to implement this in practice. Run Apache Kafka To start the message broker, we also need the zookeeper. The easiest way to do this is with docker-compose. Create the file docker-compose.yml: YAML --- version: '3' services: zookeeper: image: confluentinc/cp-zookeeper:7.3.3 container_name: zookeeper environment: ZOOKEEPER_CLIENT_PORT: 2181 ZOOKEEPER_TICK_TIME: 2000 broker: image: confluentinc/cp-kafka:7.3.3 container_name: broker ports: - "9092:9092" depends_on: - zookeeper environment: KAFKA_BROKER_ID: 1 KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 Create a new topic: Shell docker exec broker \ kafka-topics --bootstrap-server broker:9092 \ --create \ --topic demo To produce messages, you can run the command: Shell docker exec -ti broker \ kafka-console-producer --bootstrap-server broker:9092 \ --topic demo Each line is a new message. When finished, press Ctrl+C: Shell >first >second >third >^C% Messages have been written and will be stored in Apache Kafka. Spring Boot Application Create a gradle project and add the necessary dependencies to build.gradle: Groovy plugins { id 'java' id 'org.springframework.boot' version '2.7.10' id 'io.spring.dependency-management' version '1.0.15.RELEASE' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.kafka:spring-kafka' compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok:1.18.26' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.kafka:spring-kafka-test' testCompileOnly 'org.projectlombok:lombok:1.18.26' testAnnotationProcessor 'org.projectlombok:lombok:1.18.26' } application.yml: YAML spring: kafka: consumer: bootstrap-servers: localhost:9092 group-id: demo-group auto-offset-reset: earliest key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer Let's write an event handler: Java @Component @Slf4j public class DemoListener { @KafkaListener(topics = "demo", groupId = "demo-group") void processKafkaEvents(ConsumerRecord<String, String> record) { log.info("Try to process message"); // Some code log.info("Processed value: " + record.value()); } } Execution Result: Shell Try to process message Processed value: first Try to process message Processed value: second Try to process message Processed value: third But what if an error happens during message processing? In that case, we need to handle it correctly. If this error is related to an invalid message, we can write to the log or place this message in a separate topic — DLT (dead letter topic) for further parsing of this message. And what if processing implies calling another microservice, but that microservice doesn't answer? In this case, we may need the retry mechanism. To implement it, we can configure DefaultErrorHandler: Java @Configuration @Slf4j public class KafkaConfiguration { @Bean public DefaultErrorHandler errorHandler() { BackOff fixedBackOff = new FixedBackOff(5000, 3); DefaultErrorHandler errorHandler = new DefaultErrorHandler((consumerRecord, exception) -> { log.error("Couldn't process message: {}; {}", consumerRecord.value().toString(), exception.toString()); }, fixedBackOff); errorHandler.addNotRetryableExceptions(NullPointerException.class); return errorHandler; } } Here we have specified that in case of an error, we will do retries (maximum three times) at intervals of five seconds. But if we have an NPE, we won't do iterations in that case but just write a message to the log and skip the message. But if we want more flexibility in error handling, we can do it manually: YAML spring: kafka: consumer: bootstrap-servers: localhost:9092 group-id: demo-group auto-offset-reset: earliest key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer properties: enable.auto.commit: false listener: ack-mode: MANUAL Here we set spring.kafka.consumer.properties.enable.auto.commit=false (if true, the consumer's offset will be periodically committed in the background. In that case property auto.commit.interval.ms (default 5000ms will be used) and spring.kafka.listener.ack-mode=MANUAL, which means we want to control this mechanism ourselves. Now we can control the sending of the acknowledgment ourselves: Java @KafkaListener(topics = "demo", groupId = "demo-group") void processKafkaEvents(ConsumerRecord<String, String> record, Acknowledgment acknowledgment) { log.info("Try to process message"); try { //Some code log.info("Processed value: " + record.value()); acknowledgment.acknowledge(); } catch (SocketTimeoutException e) { log.error("Error while processing message. Try again later"); acknowledgment.nack(Duration.ofSeconds(5)); } catch (Exception e) { log.error("Error while processing message: {}" + record.value()); acknowledgment.acknowledge(); } } The Acknowledgment object allows you to explicitly acknowledge or reject (nack) the message. By calling acknowledge(), you are telling Kafka that the message has been successfully processed and can be committed. By calling nack(), you are telling Kafka that the message should be re-queued for processing after a specified delay (i.e., in a case when another microservice isn't responding). Conclusion Data loss prevention is critical for consumer Kafka applications. In this article, we looked at some best practices for exception handling and data loss prevention with Spring Boot. By following these practices, you can ensure that your application is more resilient to failures and can gracefully recover from errors without data loss. By applying these strategies, you can build a robust and reliable Kafka consumer application.
In our last post, we introduced you to the Manifold project and how it offers a revolutionary set of language extensions for Java, including the ability to parse and process JSON files seamlessly in Java. Today, we will take a look at another exciting feature of the Manifold project: string templates. But before we get to that, some of the feedback I got from the previous post was that I was unclear about Manifold. Manifold is a combination of an IDE plugin and plugins to Maven or Gradle. Once used we can enhance the Java language (or environment) almost seamlessly in a fluid way. A frequent question was, "How is it different from something like Lombok?" There are many similarities and, in fact, if you understand Lombok then you are on your way to understanding Manifold. Lombok is a great solution for some problems in the Java language. It is a bandaid on the verbosity of Java and some of its odd limitations (I mean bandaid as a compliment, no hate mail). Manifold differs from Lombok in several critical ways: It’s modular: All the extensions built into Manifold are separate from one another. You can activate a particular feature or leave it out of the compiler toolchain. It’s bigger: Lombok has many features but Manifold's scope is fantastic and far more ambitious. It tries to do the “right thing": Lombok is odd. We declare private fields but then use getters and setters as if they aren’t private. Manifold uses properties (which we will discuss later) that more closely resemble what the Java language “should have offered." Manifold also has some drawbacks: It only works as a compiler toolchain and only in one way. Lombok can be compiled back to plain Java source code and removed. It only supports one IDE - IntelliJ. These partially relate to the age of Manifold which is a new project by comparison. But it also relates to the different focus. Manifold focuses on language functionality and a single working fluid result. JEP 430 String Interpolation One of the big features coming to JDK 21 is JEP 430, which is a string interpolation language change. It will allow writing code like this: String name = "Joan"; String info = STR."My name is \{name}"; In this case, info will have the value “My name is Joan”. This is just the tip of the iceberg in this JSR as the entire architecture is pluggable. I will discuss this in a future video but for now, the basic functionality we see here is pretty fantastic. Unfortunately, it will take years to use this in production. It will be in preview in JDK 21, then it will be approved. We will wait for an LTS, and then wait for the LTS to reach critical mass. In the meantime, can we use something as nice as this today? Maven Dependencies Before we dive into the code, I want to remind you that all the code for this and other videos in this series is available on GitHub (feel free to star it and follow). String templating has no dependencies. We still need to make changes to the pom file but we don’t need to add dependencies. I’m adding one dependency here for the advanced templates we will discuss soon. All that’s needed is the compiler plugin. That means that string templates are a compile-time feature and have no runtime impact! <dependencies> <dependency> <groupId>systems.manifold</groupId> <artifactId>manifold-templates-rt</artifactId> <version>${manifold.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <source>19</source> <target>19</target> <encoding>UTF-8</encoding> <compilerArgs> <!-- Configure manifold plugin --> <arg>-Xplugin:Manifold</arg> </compilerArgs> <!-- Add the processor path for the plugin --> <annotationProcessorPaths> <path> <groupId>systems.manifold</groupId> <artifactId>manifold-strings</artifactId> <version>${manifold.version}</version> </path> <path> <groupId>systems.manifold</groupId> <artifactId>manifold-templates</artifactId> <version>${manifold.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build> Manifold String Interpolation To begin, we can create a new variable that we can use to get external input. In the second line, we integrate that variable into the printout: String world = args.length > 0 ? args[0] : "world"; System.out.println("Hello $world! I can write \$world as the variable..."); The backslash syntax implicitly disables the templating behavior, just like in other string elements in Java. This will print “Hello world! I can write $world as the variable…”. There’s something that you can’t really see in the code, you need to look at a screenshot of the same code: It’s subtle, do you see it? Notice the $world expression: it is colored differently. It's no longer just a string but a variable embedded in a string. This means that we can control-click it and go to the variable declaration, rename it, or see find its usage. There's another way to escape a string, and we can use the @DisableStringLiteralTemplates annotation on a method or a class to disable this functionality in the respective block of code. This can be useful if we use the dollar sign frequently in a block of code: @DisableStringLiteralTemplates private static void noTemplate(String word) { System.out.println("Hello $world!"); } Templates The Manifold project allows us to create JSP-like templates without all of the baggage. We can define a base class to a template to create generic code for the templates and place common functionality in a single location. We can create a file called HelloTemplate.html.mtl in the resources/templates directory with the following content. Notice the params we define in the template file can be anything: <%@ params(String title, String body) %> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>${title}</title> </head> <body> ${body} </body> </html> This will seem very familiar to those of us with a JSP background. We can then use the file in the Java code like this: we can pass the parameters and they will replace the appropriate blocks in the HTML file. System.out.println(HelloTemplate.render("My Title", "My Body")); Notice the generated template it compiled to a class, similar to JSP. Unlike JSP this template isn’t a servlet and can be used in any context. A local application, a server, etc. The templating language is more lightweight and doesn’t depend on various server APIs. It is also less mature. The main value is in using such an API to generate arbitrary files like Java source files or configuration files. The templating capabilities are powerful yet simple. Just like we could in JSP, we can embed Java source code into the template e.g. we can include control flow and similar restrictions just like we could in JSP: <% if(body != null) {%> ${body} <% } %> Why Not: JSP, Velocity, Thymeleaf, or Freemarker? There are so many templating languages in Java already. Adding yet another one seems like a heavy burden of replication. I think all of those are great and this isn’t meant to replace them, at least not yet. Their focus is very much on web generation, they might not be ideal for more fluid use cases like code generation or web frameworks like Spark. Another big advantage is size and performance. All of these frameworks have many dependencies and a lot of runtime overhead. Even JSP performs the initial compilation in runtime by default. This templating support is compiled and Spartan, in a good way. It’s fast, simple, and deeply integrated into the application flow. Import We can import Java packages just like we can in every Java class using code like this: <%@ import com.debugagent.stringtemplates.* %> Once imported we can use any class within the code. Notice that this import statement must come above other lines in the code, just like a regular import statement. Include We can use include to simply include another template into the current template, allowing us to assemble sophisticated templates like headers and footers. If we want to generate a complex Java class, we can wrap the boilerplate in a generic template and include that in. We can conditionally include a template using an if statement and use a for loop to include multiple entries: <%@ include JavaCode("ClassName", classBody) %> Notice that we can include an entry with parameters and pass them along to the underlying template. We can pass hardcoded strings or variables along the include chain. A Lot More I skipped extends because of a documentation issue, which has since been fixed. It has a lot of potential. There’s layout functionality that has a lot of potential, but is missing parameter passing at the moment. But the main value is in the simplicity and total integration. When I define a dependency on a class and remove it from the code the error appears even in the template file. This doesn’t happen in Thymeleaf. Video Final Word In conclusion, with the Manifold project, we can write fluent text processing code today without waiting for a future JVM enhancement. The introduction of String templates can help Java developers generate files that aren't a web application, which is useful in several cases; e.g., where code generation is needed. Manifold allows us to create JSP-like templates without all of the baggage and generate any arbitrary file we want. With the inclusion of sophisticated options like layout, the sky's the limit. There’s a lot more to Manifold and we will dig deeper into it as we move forward.
In this post, you will learn how to create a Docker image for your GraalVM native image. By means of some hands-on experiments, you will learn that it is a bit trickier than what you are used to when creating Docker images. Enjoy! Introduction In a previous post, you learned how to create a GraalVM native image for a Spring Boot 3 application. Nowadays, applications are often distributed as Docker images, so it is interesting to verify how this is done for a GraalVM native image. A GraalVM native image does not need a JVM, so can you use a more minimalistic Docker base image for example? You will execute some experiments during this blog and will learn by doing. The sources used in this blog are available on GitHub. The information provided in the GraalVM documentation is a good starting point for learning. It is good reference material when reading this blog. As an example application, you will use the Spring Boot application from the previous post. The application contains one basic RestController which just returns a hello message. The RestController also includes some code in order to execute tests in combination with Reflection, but this part was added for the previous post. Java @RestController public class HelloController { @RequestMapping("/hello") public String hello() { // return "Hello GraalVM!" String helloMessage = "Default message"; try { Class<?> helloClass = Class.forName("com.mydeveloperplanet.mygraalvmplanet.Hello"); Method helloSetMessageMethod = helloClass.getMethod("setMessage", String.class); Method helloGetMessageMethod = helloClass.getMethod("getMessage"); Object helloInstance = helloClass.getConstructor().newInstance(); helloSetMessageMethod.invoke(helloInstance, "Hello GraalVM!"); helloMessage = (String) helloGetMessageMethod.invoke(helloInstance); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } return helloMessage; } } Build the application: Shell $ mvn clean verify Run the application from the root of the repository: Shell $ java -jar target/mygraalvmplanet-0.0.1-SNAPSHOT.jar Test the endpoint: Shell $ curl http://localhost:8080/hello Hello GraalVM! You are now ready for Dockerizing this application! Prerequisites Prerequisites for this blog are: Basic Linux knowledge, Ubuntu 22.04 is used during this post Basic Java and Spring Boot knowledge Basic GraalVM knowledge Basic Docker knowledge Basic SDKMAN knowledge Create Docker Image for Spring Boot Application In this section, you will create a Dockerfile for the Spring Boot application. This is a very basic Dockerfile and is not to be used in production code. See previous posts "Docker Best Practices" and "Spring Boot Docker Best Practices" for tips and tricks for production-ready Docker images. The Dockerfile you will be using is the following: Dockerfile FROM eclipse-temurin:17.0.5_8-jre-alpine COPY target/mygraalvmplanet-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"] You use a Docker base image containing a Java JRE, copy the JAR file into the image, and, in the end, you run the JAR file. Build the Docker image: Shell $ docker build . --tag mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT Verify the size of the image. It is 188MB in size. Shell $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE mydeveloperplanet/mygraalvmplanet 0.0.1-SNAPSHOT be12e1deda89 33 seconds ago 188MB Run the Docker image: Shell $ docker run --name mygraalvmplanet mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT ... 2023-02-26T09:20:48.033Z INFO 1 --- [ main] c.m.m.MyGraalVmPlanetApplication : Started MyGraalVmPlanetApplication in 2.389 seconds (process running for 2.981) As you can see, the application started in about 2 seconds. Test the endpoint again. First, find the IP Address of your Docker container. In the output below, the IP Address is 172.17.0.2, but it will probably be something else on your machine. Shell $ docker inspect mygraalvmplanet | grep IPAddress "SecondaryIPAddresses": null, "IPAddress": "172.17.0.2", "IPAddress": "172.17.0.2", Invoke the endpoint with the IP Address and verify that it works. Shell $ curl http://172.17.0.2:8080/hello Hello GraalVM! In order to continue, stop the container, remove it, and also remove the image. Do this after each experiment. This way, you can be sure that you start from a clean situation each time. Shell $ docker rm mygraalvmplanet $ docker rmi mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT Create Docker Image for GraalVM Native Image Let’s do the same for the GraalVM native image. First, switch to using GraalVM. Shell $ sdk use java 22.3.r17-nik Create the native image: Shell $ mvn -Pnative native:compile Create a similar Dockerfile (Dockerfile-native-image). This time, you use an Alpine Docker base image without a JVM. You do not need a JVM for running a GraalVM native image as it is an executable and not a JAR file. Dockerfile FROM alpine:3.17.1 COPY target/mygraalvmplanet mygraalvmplanet ENTRYPOINT ["/mygraalvmplanet"] Build the Docker image, this time with an extra --file argument because the file name deviates from the default. Shell $ docker build . --tag mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT --file Dockerfile-native-image Verify the size of the Docker image. It is now only 76.5MB instead of the 177MB earlier. Shell $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE mydeveloperplanet/mygraalvmplanet 0.0.1-SNAPSHOT 4f7c5c6a9b29 25 seconds ago 76.5MB Run the container and note that it does not start correctly. Shell $ docker run --name mygraalvmplanet mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT exec /mygraalvmplanet: no such file or directory What is wrong here? Why does this not work? It is a vague error, but the Alpine Linux Docker image uses musl as a standard C library whereas the GraalVM native image is compiled using an Ubuntu Linux distro, which uses glibc. Let’s change the Docker base image to Ubuntu. The Dockerfile is Dockerfile-native-image-ubuntu: Dockerfile FROM ubuntu:jammy COPY target/mygraalvmplanet mygraalvmplanet ENTRYPOINT ["/mygraalvmplanet"] Build the Docker image. Shell $ docker build . --tag mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT --file Dockerfile-native-image-ubuntu Verify the size of the Docker image, it is now 147MB. Shell $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE mydeveloperplanet/mygraalvmplanet 0.0.1-SNAPSHOT 1fa90b1bfc54 3 hours ago 147MB Run the container and it starts successfully in less than 200ms. Shell $ docker run --name mygraalvmplanet mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT ... 2023-02-26T12:48:26.140Z INFO 1 --- [ main] c.m.m.MyGraalVmPlanetApplication : Started MyGraalVmPlanetApplication in 0.131 seconds (process running for 0.197) Create Docker Image Based on Distroless Image The size of the Docker image build with the Ubuntu base image is 147MB. But, the Ubuntu image does contain a lot of tooling which is not needed. Can we reduce the size of the image by using a distroless image which is very small in size? Create a Dockerfile Dockerfile-native-image-distroless and use a distroless base image. Dockerfile FROM gcr.io/distroless/base COPY target/mygraalvmplanet mygraalvmplanet ENTRYPOINT ["/mygraalvmplanet"] Build the Docker image. Shell $ docker build . --tag mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT --file Dockerfile-native-image-distroless Verify the size of the Docker image, it is now 89.9MB. Shell $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE mydeveloperplanet/mygraalvmplanet 0.0.1-SNAPSHOT 6fd4d44fb622 9 seconds ago 89.9MB Run the container and see that it is failing to start. It appears that several necessary libraries are not present in the distroless image. Shell $ docker run --name mygraalvmplanet mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT /mygraalvmplanet: error while loading shared libraries: libz.so.1: cannot open shared object file: No such file or directory When Googling this error message, you will find threads that mention copying the required libraries from other images (e.g., the Ubuntu image), but you will encounter a next error and a next error. This is a difficult path to follow and costs some time. See, for example, this thread. A solution for using distroless images can be found here. Create Docker Image Based on Oracle Linux Another approach for creating Docker images is the one that can be found on the GraalVM GitHub page. Build the native image in a Docker container and use a multistage build to build the target image. The Dockerfile being used is copied from here and can be found in the repository as Dockerfile-oracle-linux. Create a new file Dockerfile-native-image-oracle-linux, copy the contents of Dockerfile-oracle-linux into it, and change the following: Update the Maven SHA and DOWNLOAD_URL. Change L36 in order to compile the native image as you used to do: mvn -Pnative native:compile Change L44 and L45 in order to copy and use the mygraalvmplanet native image. The resulting Dockerfile is the following: Dockerfile FROM ghcr.io/graalvm/native-image:ol8-java17-22 AS builder # Install tar and gzip to extract the Maven binaries RUN microdnf update \ && microdnf install --nodocs \ tar \ gzip \ && microdnf clean all \ && rm -rf /var/cache/yum # Install Maven # Source: # 1) https://github.com/carlossg/docker-maven/blob/925e49a1d0986070208e3c06a11c41f8f2cada82/openjdk-17/Dockerfile # 2) https://maven.apache.org/download.cgi ARG USER_HOME_DIR="/root" ARG SHA=1ea149f4e48bc7b34d554aef86f948eca7df4e7874e30caf449f3708e4f8487c71a5e5c072a05f17c60406176ebeeaf56b5f895090c7346f8238e2da06cf6ecd ARG MAVEN_DOWNLOAD_URL=https://dlcdn.apache.org/maven/maven-3/3.9.0/binaries/apache-maven-3.9.0-bin.tar.gz RUN mkdir -p /usr/share/maven /usr/share/maven/ref \ && curl -fsSL -o /tmp/apache-maven.tar.gz ${MAVEN_DOWNLOAD_URL} \ && echo "${SHA} /tmp/apache-maven.tar.gz" | sha512sum -c - \ && tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \ && rm -f /tmp/apache-maven.tar.gz \ && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn ENV MAVEN_HOME /usr/share/maven ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2" # Set the working directory to /home/app WORKDIR /build # Copy the source code into the image for building COPY . /build # Build RUN mvn -Pnative native:compile # The deployment Image FROM docker.io/oraclelinux:8-slim EXPOSE 8080 # Copy the native executable into the containers COPY --from=builder /build/target/mygraalvmplanet . ENTRYPOINT ["/mygraalvmplanet"] Build the Docker image. Relax, this will take quite some time. Shell $ docker build . --tag mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT -f Dockerfile-native-image-oracle-linux This image size is 177MB. Shell $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE mydeveloperplanet/mygraalvmplanet 0.0.1-SNAPSHOT 57e0fda006f0 9 seconds ago 177MB Run the container and it starts in 55ms. Shell $ docker run --name mygraalvmplanet mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT ... 2023-02-26T13:13:50.188Z INFO 1 --- [ main] c.m.m.MyGraalVmPlanetApplication : Started MyGraalVmPlanetApplication in 0.055 seconds (process running for 0.061) So, this works just fine. This is the way to go when creating Docker images for your GraalVM native image: Prepare a Docker image based on your target base image; Install the necessary tooling, in the case of this application, GraalVM and Maven; Use a multistage Docker build in order to create the target image. Conclusion Creating a Docker image for your GraalVM native image is possible, but you need to be aware of what you are doing. Using a multistage build is the best option. Dependent on whether you need to shrink the size of the image by using a distroless image, you need to prepare the image to build the native image yourself.
One of the more notable aspects of ChatGPT is its engine, which not only powers the web-based chatbot but can also be integrated into your Java applications. Whether you prefer reading or watching, let’s review how to start using the OpenAI GPT engine in your Java projects in a scalable way, by sending prompts to the engine only when necessary: Budget Journey App Imagine you want to visit a city and have a specific budget in mind. How should you spend the money and make your trip memorable? This is an excellent question to delegate to the OpenAI engine. Let’s help users get the most out of their trips by building a simple Java application called BudgetJourney. The app can suggest multiple points of interest within a city, tailored to fit specific budget constraints. The architecture of the BudgetJourney app looks as follows: The users open a BudgetJourney web UI that runs on Vaadin. Vaadin connects to a Spring Boot backend when users want to get recommendations for a specific city and budget. Spring Boot connects to a YugabyteDB database instance to check if there are already any suggestions for the requested city and budget. If the data is already in the database, the response is sent back to the user. Otherwise, Spring Boot connects to the OpenAI APIs to get recommendations from the neural network. The response is stored in YugabyteDB for future reference and sent back to the user. Now, let’s see how the app communicates with the Open AI engine (step 4) and how using the database (step 3) makes the solution scalable and cost-effective. OpenAI Java Library The OpenAI engine can be queried via the HTTP API. You need to create an account, get your token (i.e., API key) and use that token while sending requests to one of the OpenAI models. A model in the context of OpenAI is a computational construct trained on a large dataset to recognize patterns, make predictions, or perform specific tasks based on input data. Presently, the service supports several models that can understand and generate natural language, code, images, or convert audio into text. Our BudgetJourney app uses the GPT-3.5 model which understands and generates natural language or code. The app asks the model to suggest several points of interest within a city while considering budget constraints. The model then returns the suggestions in a JSON format. The open-source OpenAI Java library implements the GPT-3.5 HTTP APIs, making it easy to communicate with the service via well-defined Java abstractions. Here’s how you get started with the library: Add the latest OpenAI Java artifact to your pom.xml file. XML <dependency> <groupId>com.theokanning.openai-gpt3-java</groupId> <artifactId>service</artifactId> <version>${version}</version> </dependency> Create an instance of the OpenAiService class by providing your token and a timeout for requests between the app and OpenAI engine. Java OpenAiService openAiService = new OpenAiService( apiKey, Duration.ofSeconds(apiTimeout)); Easy! Next, let’s see how you can work with the GPT-3.5 model via the OpenAiService instance. Sending Prompts to GPT-3.5 Model You communicate with the OpenAI models by sending text prompts that tell what you expect a model to do. The model behaves best when your instructions are clear and include examples. To build a prompt for the GPT-3.5 model, you use the ChatCompletionRequest API of the OpenAI Java library: Java ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest .builder() .model(“gpt-3.5-turbo”) .temperature(0.8) .messages( List.of( new ChatMessage("system", SYSTEM_TASK_MESSAGE), new ChatMessage("user", String.format("I want to visit %s and have a budget of %d dollars", city, budget)))) .build(); model(“gpt-3.5-turbo”) is an optimized version of the GPT-3.5 model. temperature(...) controls how much randomness and creativity to expect in a model’s response. For instance, higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more deterministic. messages(...) are the actual instructions or prompts to the model. There are “system” messages that instruct the model to behave a certain way, “assistant” messages that store previous responses, and “user” messages that carry user requests with asks. The SYSTEM_TASK_MESSAGE of the BudgetJourney app looks as follows: You are an API server that responds in a JSON format. Don't say anything else. Respond only with the JSON. The user will provide you with a city name and available budget. While considering that budget, you must suggest a list of places to visit. Allocate 30% of the budget to restaurants and bars. Allocate another 30% to shows, amusement parks, and other sightseeing. Dedicate the remainder of the budget to shopping. Remember, the user must spend 90-100% of the budget. Respond in a JSON format, including an array named 'places'. Each item of the array is another JSON object that includes 'place_name' as a text, 'place_short_info' as a text, and 'place_visit_cost' as a number. Don't add anything else after you respond with the JSON. Although wordy and in need of optimization, this system message conveys the desired action: to suggest multiple points of interest with maximal budget utilization and to provide the response in JSON format, which is essential for the rest of the application. Once you created the prompt (ChatCompletionRequest) providing both the system and user messages as well as other parameters, you can send it via the OpenAiService instance: Java OpenAiService openAiService = … //created earlier StringBuilder builder = new StringBuilder(); openAiService.createChatCompletion(chatCompletionRequest) .getChoices().forEach(choice -> { builder.append(choice.getMessage().getContent()); }); String jsonResponse = builder.toString(); The jsonResponse object is then further processed by the rest of the application logic which prepares a list of points of interest and displays them with the help of Vaadin. For example, suppose a user is visiting Tokyo and wants to spend up to $900 in the city. The model will strictly follow our instructions from the system message and respond with the following JSON: JSON { "places": [ { "place_name": "Tsukiji Fish Market", "place_short_info": "Famous fish market where you can eat fresh sushi", "place_visit_cost": 50 }, { "place_name": "Meiji Shrine", "place_short_info": "Beautiful Shinto shrine in the heart of Tokyo", "place_visit_cost": 0 }, { "place_name": "Shibuya Crossing", "place_short_info": "Iconic pedestrian crossing with bright lights and giant video screens", "place_visit_cost": 0 }, { "place_name": "Tokyo Skytree", "place_short_info": "Tallest tower in the world, offering stunning views of Tokyo", "place_visit_cost": 30 }, { "place_name": "Robot Restaurant", "place_short_info": "Unique blend of futuristic robots, dancers, and neon lights", "place_visit_cost": 80 }, // More places ]} This JSON is then converted into a list of different points of interest. It is then shown to the user: NOTE: The GPT-3.5 model was trained on the Sep 2021 data set. Therefore, it can’t provide 100% accurate and relevant trip recommendations. However, this inaccuracy can be improved with the help of OpenAI plugins that give models access to real-time data. For instance, once the Expedia plugin for OpenAI becomes publicly available as an API, this will let you improve this BudgetJourney app further. Scaling With a Database As you can see, it’s straightforward to integrate the neural network into your Java applications and communicate with it in a way similar to other 3rd party APIs. You can also tune the API behavior, such as adding a desired output format. But, this is still a 3rd party API that charges you for every request. The more prompts you send and the longer they are, the more you pay. Nothing comes for free. Plus, it takes time for the model to process your prompts. For instance, it can take 10-30 seconds before the BudgetJourney app receives a complete list of recommendations from OpenAI. This might be overkill, especially if different users send similar prompts. To make OpenAI GPT applications scalable, it’s worth storing the model responses in a database. That database allows you to: Reduce the volume of requests to the OpenAI API and, therefore, the associated costs. Serve user requests with low latency by returning previously processed (or preloaded) recommendations from the database. The BudgetJourney app uses the YugabyteDB database due to its ability to scale globally and store the model responses close to the user locations. With the geo-partitioned deployment mode, you can have a single database cluster with the data automatically pinned to and served from various geographies with low latency. A custom geo-partitioning column (the “region” column in the picture above) lets the database decide on a target row location. For instance, the database nodes from Europe already store recommendations for a trip to Miami on a $1500 budget. Next, suppose a user from Europe wants to go to Miami and spend that amount. In that case, the application can respond within a few milliseconds by getting the recommendations straight from the database nodes in the same geography. The BudgetJourney app uses the following JPA repository to get recommendations from the YugabyteDB cluster: Java @Repository public interface CityTripRepository extends JpaRepository<CityTrip, Integer> { @Query("SELECT pointsOfInterest FROM CityTrip WHERE cityName=?1 and budget=?2 and region=?3") String findPointsOfInterest(String cityName, Integer budget, String region); } With an Entity class looking as follows: Java @Entity public class CityTrip { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "landmark_generator") @SequenceGenerator(name = "landmark_generator", sequenceName = "landmark_sequence", allocationSize = 5) int id; @NotEmpty String cityName; @NotNull Integer budget; @NotEmpty @Column(columnDefinition = "text") String pointsOfInterest; @NotEmpty String region; //The rest of the logic } So, all you need to do is to make a call to the database first, then revert to the OpenAI API if relevant suggestions are not yet available in the database. As your application increases in popularity, more and more local recommendations will be available, making this approach even more cost-effective over time. Wrapping Up A ChatGPT web-based chatbot is an excellent way to demonstrate the OpenAI engine’s capabilities. Explore the engine’s powerful models and start building new types of Java applications. Just make sure you do it in a scalable way!
I had a hard time solving the org.hibernate.type.descriptor.java.EnumJavaTypeDescriptor.fromOrdinal(EnumJavaTypeDescriptor.java:76)error that I got while reading the value from the database. Background While using the Spring Boot application, I saved some values in the database as an INT that I had to map back with the enum UserRole. The challenge: I used id and name in the enum, but instead of starting the id value from 0, I started the ID value from 1. This is the point the story started to challenge itself. The actual problem is instead of mapping the id of the enum while populating the object back to the Java object. It started reading the ordinal value. And I was feeling clueless even after debugging. Every blog that I was reading was suggesting to change the id to 0 so that the underlying exception (shown below) can be avoided. Java ERROR 13992 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2] with root cause java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2 ... My POJO mappings were: User POJO Java @Entity @Table(name = "USER") @Getter @Setter @NoArgsConstructor(force = true) public class User { @Id @Column(name = "ID") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "ROLE") @NotNull(message = "Role is required") @JsonAlias("roleId") private UserRole roleId; @Column(name = "NAME") @NotNull(message = "User Name is required") private Sting name; } UserRole POJO Java public enum UserRole { ADMIN(1, "Admin"), USER(2, "User"); private final int id; private final String name; private UserRole(final int id, final String name) { this.id = id; this.name = name; } public int getId() { return id; } public String getName() { return name; } public static UserRole valueOf(final Integer id) { if (id == null) { return null; } for (UserRole type : UserRole.values()) { if (type.id == id) { return type; } } return null; } } Solution When such a situation arises where you have to manipulate the DB values as per your enum, you can use the: javax.persistence.Converter Here's How! I added the following code to the User POJO: Java @Convert(converter = UserRole.UserRoleConverter.class) @Column(name = "ROLE") @NotNull(message = "Role is required") @JsonAlias("roleId") private UserRole roleId; And the below code to the UserRole POJO: Java @Converter(autoApply = true) public static class UserRoleConverter implements AttributeConverter<UserRole, Integer> { @Override public Integer convertToDatabaseColumn(UserRole attribute) { if (attribute == null) { throw new BadRequestException("Please provide a valid User Role."); } return attribute.getId(); } @Override public UserRole convertToEntityAttribute(Integer dbData) { return UserRole.valueOf(dbData); } } This converter maps the value fetched from the DB to the enum value by internally calling the valueOf() method. I was facing this error, and this approach solved the problem for me. I am pretty sure it will solve your problem too. Do let me know in the comments if you find the solution useful. :)
Native Image technology is gaining traction among developers whose primary goal is to accelerate startup time of applications. In this article, we will learn how to turn Java applications into native images and then containerize them for further deployment in the cloud. We will use: Spring Boot 3.0 with baked-in support for Native Image as the framework for our Java application; Liberica Native Image Kit (NIK) as a native-image compiler; Alpaquita Stream as a base image. Building Native Images from Spring Boot Apps Installing Liberica NIK It would be best to utilize a powerful computer with several gigabytes of RAM to work with native images. Opt for a cloud service provided by Amazon or a workstation so as not to overload the laptop. We will be using Linux bash commands further on because bash is a perfect way of accessing the code remotely. macOS commands are similar. As for Windows, you can use any alternative, for instance, bash included in the Git package for Windows. Download Liberica Native Image Kit for your system. Choose a Full version for our purposes. Unpack tar.gz with: tar -xzvf ./bellsoft-liberica.tar.gz Now, put the compiler to $PATH with: GRAALVM_HOME=/home/user/opt/bellsoft-liberica export PATH=$GRAALVM_HOME/bin:$PATH Check that Liberica NIK is installed: java -version openjdk version "17.0.5" 2022-10-18 LTS OpenJDK Runtime Environment GraalVM 22.3.0 (build 17.0.5+8-LTS) OpenJDK 64-Bit Server VM GraalVM 22.3.0 (build 17.0.5+8-LTS, mixed mode, sharing) native-image --version GraalVM 22.3.0 Java 17 CE (Java Version 17.0.5+8-LTS) If you get the error "java: No such file or directory" on Linux, you installed the binary for Alpine Linux, not Linux. Check the binary carefully. Creating a Spring Boot Project The easiest way to create a new Spring Boot project is to generate one with Spring Initializr. Select Java 17, Maven, JAR, and Spring SNAPSHOT-version (3.0.5 at the time of writing this article), then fill in the fields for project metadata. We don’t need any dependencies.Add the following code to you main class:System.out.println("Hello from Native Image!"); Spring has a separate plugin for native compilation, which utilizes multiple context dependent parameters under the hood. Let’s add the required configuration to our pom.xml file: XML <profiles> <profile> <id>native</id> <build> <plugins> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> <executions> <execution> <id>build-native</id> <goals> <goal>compile-no-fork</goal> </goals> <phase>package</phase> </execution> </executions> </plugin> </plugins> </build> </profile> </profiles> Let’s build the project with the following command: ./mvnw clean package -Pnative The resulting native image is in the target directory. Write a Dockerfile We need to write a Dockerfile to generate a Docker image container. Put the following file into the application folder: Dockerfile FROM bellsoft/alpaquita-linux-base:stream-musl COPY target/native-image-demo . CMD ["./native-image-demo"] Where we: Create an image with Alpaquita Linux base image (the native image doesn’t need a JVM to execute); Copy the app into the new image; Run the program inside the container. We can also skip the step with Liberica NIK installation and we build a native image straight in a container, which is useful when the development and deployment architectures are different. For that purpose, create another folder and put there your application and the following Dockerfile: Dockerfile FROM bellsoft/liberica-native-image-kit-container:jdk-17-nik-22.3-stream-musl as builder WORKDIR /home/myapp ADD native-image-demo /home/myapp/native-image-demo RUN cd native-image-demo && ./mvnw clean package -Pnative FROM bellsoft/alpaquita-linux-base:stream-musl WORKDIR /home/myapp COPY --from=builder /home/myapp/native-image-demo/target/native-image-demo . CMD ["./native-image-demo"] Where we: Specify the base image for Native Image generation; Point to the directory where the image will execute inside Docker; Copy the program to the directory; Build a native image; Create another image with Alpaquita Linux base image (the native image doesn’t need a JVM to execute); Specify the executable directory; Copy the app into the new image; Run the program inside the container. Build a Native Image Container To generate a native image and containerize it, run: docker build . Note that if you use Apple M1, you may experience troubles with building a native image inside a container. Check that the image was create with the following command: Dockerfile docker images REPOSITORY TAG IMAGE ID CREATED SIZE <none> <none> 8ebc2a97ef8e 18 seconds ago 45.2MB Tag the newly created image: docker tag 8ebc2a97ef8e nik-example Now you can run the image with: docker run -it --rm 8ebc2a97ef8e Hello from Native Image! Conclusion Native image containerization is as simple as creating Docker container images of standard Java apps. Much trickier is to migrate a Java application to Native Image. We used a simple program that didn’t require any manual configuration. But dynamic Java features (Reflection, JNI, Serialization, etc.) are not supported by GraalVM, so you have to make the native-image tool aware of them.
We want to turn our Google Photos clone from single-threaded to multithreaded, to generate thumbnails much faster than before - but there are a couple of challenges along the way. We'll learn that using Java's Parallel File Streams seems to be a buggy endeavor, so we'll fall back on good, old ExecutorServices. But how many threads should we use to generate thumbnails? How can threads get in conflict with each other? How do we make our program fail-safe for threading issues? Find out in this episode! What’s in the Video 00:00 Intro We start off with a quick recap. In the previous episodes, we built a tiny application that can take a folder full of images, and turn those images into thumbnails - by spawning external ImageMagick processes. We did that sequentially, spawning the next process, as soon as a thumbnail conversion process has finished. But we surely can make this faster and also utilize our system resources (CPU/IO) more by doing the thumbnail conversion multithreaded, spawning multiple ImageMagick processes at the very same time. We'll try and figure out how to do that in this episode. 00:24 Java’s Parallel Streams The first idea would be to use Java's built-in parallel streams feature, as we are reading in the files as a stream anyway. Interestingly enough the API lets you do this just fine, and it even works flawlessly on my machine, but as soon as we deploy our application to a different server, it stops working. Why is that? We'll need to do a bit of benchmarking and fumbling around, to notice that parallel file streams, in JDKs < 19, aren't really supported. So, depending on the Java version, you'll get different behavior. Hence, we cannot work with parallel streams for now. 03:32 Java’s ExecutorService Given that parallel streams are not an option, we will resort back to using a good old ExecutorService. An ExecutorService lets us define how many threads we want to open, and then work off n-tasks in parallel. Figuring out the API is not that difficult, but the real question is: How many threads specifically should we open up simultaneously? We'll cover that question in detail during this segment. 06:12 Performance Benchmarking After having implemented multithreading, we also need to make sure to benchmark our changes. Will we get a 2x/3x speed improvement? Or maybe even a speed reduction? During this segment, we'll run and time our application locally, as well as on my NAS, and see how different hardware configurations might affect the final result. 08:10 File Storage and Hashing Last but not least, we'll have to figure out how to store our thumbnails. So far, we created thumbnails with the same filename as the original image and put all the files into the same directory. That doesn't work for a huge amount of files, with potential file clashes and multithreading conflicts. Hence, we will start hashing our files with the BLAKE3 algorithm, store the files under that hash, and also use a directory layout similar to what Git uses internally to store its files. 16:52 Up Next We did a ton of multithreading work in this episode. Up next it is time to add a database to our application and store the information about all converted thumbnails there. Stay tuned!
In this article, we’re going to compare some essential metrics of web applications using two different Java stacks: Spring Boot and Eclipse MicroProfile. More precisely, we’ll implement the same web application in Spring Boot 3.0.2 and Eclipse MicroProfile 4.2. These releases are the most recent at the time of this writing. Since there are several implementations of Eclipse MicroProfile, we’ll be using one of the most famous: Quarkus. At the time of this writing, the most recent Quarkus release is 2.16.2. This mention is important regarding Eclipse MicroProfile because, as opposed to Spring Boot, which isn’t based on any specification and, consequently, the question of the implementation doesn’t exist, Eclipse MicroProfile has largely been adopted by many editors who provide different implementations, among which Quarkus, Wildfly, Open Liberty and Payara are from the most evangelical. In this article, we will implement the same web application using two different technologies, Spring Boot and Quarkus, such that to compare their respective two essential metrics: RSS (Resident Set Size) and TFR (Time to First Request). The Use Case The use case that we’ve chosen for the web application to be implemented is a quite standard one: the one of a microservice responsible to manage press releases. A press release is an official statement delivered to members of the news media for the purpose of providing information, creating an official statement, or making a public announcement. In our simplified case, a press release consists in a set of data like a unique name describing its subject, an author, and a publisher. The microservice used to manage press releases is very straightforward. As with any microservice, it exposes a REST API allowing for CRUD press releases. All the required layers, like domain, model, entities, DTOs, mapping, persistence, and service, are present as well. Our point here is not to discuss the microservices structure and modus operandi but to propose a common use case to be implemented in the two similar technologies, Spring Boot and Quarkus, to be able to compare their respective performances through the mentioned metrics. Resident Set Size (RSS) RSS is the amount of RAM occupied by a process and consists of the sum of the following JVM spaces: Heap space Class metadata Thread stacks Compiled code Garbage collection RSS is a very accurate metric, and comparing applications based on it is a very reliable way to measure their associated performances and footprints. Time to First Request (TFR) There is a common concern about measuring and comparing applications' startup times. However, logging it, which is how this is generally done, isn’t enough. The time you’re seeing in your log file as being the application startup time isn’t accurate because it represents the time your application or web server started, but not the one required that your application starts to receive requests. Application and web servers, or servlet containers, might start in a couple of milliseconds, but this doesn’t mean your application can process requests. These platforms often delay work through the process and may give a false, lazy initialization indication about the TFR. Hence, to accurately determine the TFR, in this report, we’re using Clément Escofier’s script time.js, found here in the GitHub repository, which illustrates the excellent book Reactive Systems in Java by Clément Escoffier and Ken Finnigan. Spring Boot Implementation To compare the metrics presented above for the two implementations, you need to clone and run the two projects. Here are the steps required to experience the Spring Boot implementation: Shell $ git clone https://github.com/nicolasduminil/Comparing-Resident-Size- Set-Between-Spring-Boot-and-Quarkus.git metrics $ cd metrics $ git checkout spring-boot $ mvn package $ java -jar target/metrics.jar Here you start by cloning the GIT repository, and once this operation is finished, you go into the project’s root directory and do a Maven build. Then you start the Spring Boot application by running the über JAR created by the spring-boot-maven-plugin. Now you can test the application via its exposed Swagger UI interface by going here. Please take a moment to use the feature that tries it out that Swagger UI offers. The order of operations is as follows: First, the POST endpoint is to create a press release. Please use the editor to modify the JSON payload proposed by default. While doing this, you should leave the field pressReleaseId having a value of “0” as this is the primary key that will be generated by the insert operation. Below, you can see an example of how to customize this payload: JSON { "pressReleaseId": 0, "name": "AWS Lambda", "author": "Nicolas DUMINIL", "publisher": "ENI" } Next, a GET /all is followed by a GET /id to check that the previous operation has successfully created a press release. A PUT to modify the current press release A DELETE /id to clean-up Note: Since the ID is automatically generated by a sequence, as explained, the first record will have the value of “1.” You can use this value in GET /id and DELETE /id requests. Notice that the press release name must be unique. Now, once you have experienced your microservice, let’s see its associated RSS. Proceed as follows: Shell $ ps aux | grep metrics nicolas 31598 3.5 1.8 13035944 598940 pts/1 Sl+ 19:03 0:21 java -jar target/metrics.jar nicolas 31771 0.0 0.0 9040 660 pts/2 S+ 19:13 0:00 grep --color=auto metrics $ ps -o pid,rss,command -p 31598 PID RSS COMMAND 31598 639380 java -jar target/metrics.ja Here, we get the PID of our microservice by looking up its name, and once we have it, we can display its associated RSS. Notice that the command ps -o above will display the PID, the RSS, and the starting command associated with the process, which PID is passed as the -p argument. And as you may see, the RSS for our process is 624 MB (639380 KB). If you’re hesitating about how to calculate this value, you can use the following command: Shell $ echo 639380/1024 | bc 624 As for the TFR, all you need to do is to run the script time.js, as follows: Shell node time.js "java -jar target/metrics.jar" "http://localhost:8080/" 173 ms To resume, our Spring Boot microservice has a RSS of 624 MB and a TFR of 173 ms. Quarkus Implementation We need to perform these same operations to experience our Quarkus microservice. Here are the required operations: Shell $ git checkout quarkus $ mvn package quarkus:dev Once our Quarkus microservice has started, you may use the Swager UI interface here. And if you’re too tired to use the graphical interface, then you may use the curl scripts provided in the repository ( post.sh, get.sh, etc.) as shown below: Shell java -jar target/quarkus-ap/quarkus-run.jar & ./post.sh ./get.sh ./get-1.sh 1 ./update.sh ... Now, let’s see how we do concerning our RSS and TFR: Shell $ ps aux | grep quarkus-run nicolas 24776 20.2 0.6 13808088 205004 pts/3 Sl+ 16:27 0:04 java -jar target/quarkus-app/quarkus-run.jar nicolas 24840 0.0 0.0 9040 728 pts/5 S+ 16:28 0:00 grep --color=auto quarkus-run $ ps -o pid,rss,command -p 24776 PID RSS COMMAND 24776 175480 java -jar target/quarkus-app/quarkus-run.jar $ echo 175480/1024 | bc 168 $ node time.js "java -jar target/quarkus-app/quarkus-run.jar" "http://localhost:8081/q/swagger-ui" 121 ms As you can see, our Quarkus microservice uses an RSS of 168MB, i.e., almost 500MB less than the 624MB with Spring Boot. Also, the TFR is slightly inferior (121ms vs. 173ms). Conclusion Our exercise has compared the RSS and TFR metrics for the two microservices executed with the HotSpot JVM (Oracle JDK 17). Spring Boot and Quarkus support the compilation into native executables through GraalVM. It would have been interesting to compare these same metrics of the native replica of the two microservices, and if we didn’t do it here, that’s because Spring Boot heavily relies on Java introspection and, consequently, it’s significantly more difficult to generate Spring Boot native microservices than Quarkus ones. But stay tuned; it will come soon. The source code may be found here. The GIT repository has a master branch and two specific ones, labeled spring-boot and, respectively, quarkus. Enjoy!
Nicolas Fränkel
Head of Developer Advocacy,
Api7
Shai Almog
OSS Hacker, Developer Advocate and Entrepreneur,
Codename One
Marco Behler
Ram Lakshmanan
yCrash - Chief Architect