DZone Research Report: A look at our developer audience, their tech stacks, and topics and tools they're exploring.
Getting Started With Large Language Models: A guide for both novices and seasoned practitioners to unlock the power of language models.
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.
Mastering Exception Handling in Java CompletableFuture: Insights and Examples
IntelliJ and Java Spring Microservices: Productivity Tips With GitHub Copilot
Z Garbage Collector (ZGC) is an innovative garbage collection algorithm introduced by Oracle in JDK 11. Its principal aim is to minimize application pause times on the Java Virtual Machine (JVM), making it particularly suitable for modern applications that necessitate low latency and high-throughput performance. ZGC adopts a generational approach to garbage collection, segmenting the heap into two generations: the Young Generation and the Old Generation (also referred to as the Mature Generation). The Young Generation is further divided into the Eden space and two survivor spaces. The Old Generation is where long-lived objects are eventually relocated. Key Characteristics of ZGC Low Latency Focus: The primary emphasis of ZGC centers on ensuring consistently short pause times. This objective is achieved through the reduction of stop-the-world (STW) pauses, positioning it as an excellent choice for applications that require nearly instantaneous responsiveness. Scalability: ZGC is meticulously engineered to handle large memory heaps efficiently. It exhibits the capability to seamlessly manage memory heaps spanning from a few gigabytes to several terabytes, making it a compelling option for memory-intensive applications. Concurrent Phases Integration: ZGC incorporates concurrent phases for vital tasks such as marking, relocating objects, and processing references. This means that a significant portion of garbage collection activities occurs concurrently with application threads, effectively curtailing STW pauses. Predictable and Consistent Performance: ZGC is purposefully designed to deliver stable and predictable performance. It strives to maintain GC pause times within a predefined limit, a critical requirement for applications with stringent latency demands. Support for Compressed Oops: ZGC harmoniously integrates with Compressed Oops (Ordinary Object Pointers), allowing it to work efficiently with 32-bit references even on 64-bit platforms. This compatibility contributes to efficient memory usage. Now, let's explore practical examples to gain a better understanding of how to effectively utilize ZGC. Utilizing ZGC To utilize ZGC in your Java application, you must ensure that you are running at least JDK 11, as ZGC was introduced in this version. If you are using a later JDK version, ZGC should also be available for use. Activating ZGC To enable ZGC for your Java application, you can utilize the following command line option: Java java -XX:+UseZGC YourApp Monitoring ZGC You can closely monitor ZGC's performance and behavior through various tools and options. For instance, you can enable GC logging by incorporating the following flags: Java java -XX:+UseZGC -Xlog:gc* YourApp This will furnish comprehensive insights into ZGC's behavior, encompassing pause times, memory utilization, and more. Real-World Example Example 1 Let's contemplate a straightforward Java application that simulates a multi-threaded web server tasked with handling incoming requests. We will employ ZGC to manage memory and ensure minimal response times. Java import java.util.ArrayList; import java.util.List; public class WebServer { private List<String> requestQueue = new ArrayList<>(); public synchronized void handleRequest(String request) { requestQueue.add(request); } public synchronized String getNextRequest() { if (!requestQueue.isEmpty()) { return requestQueue.remove(0); } return null; } public static void main(String[] args) { WebServer webServer = new WebServer(); // Simulate incoming requests for (int i = 0; i < 1000; i++) { String request = "Request " + i; webServer.handleRequest(request); } // Simulate request processing while (true) { String request = webServer.getNextRequest(); if (request != null) { // Process the request System.out.println("Processing request: " + request); // Simulate some work try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } } In this example, we have created a basic web server that manages incoming requests. We employ synchronized methods to ensure thread safety when accessing the request queue. To run this application with ZGC, you can utilize the following command: Java java -XX:+UseZGC WebServer With ZGC enabled, the garbage collector works in the background to manage memory while the web server continues to process requests. ZGC's low-latency characteristics guarantee that the application remains responsive even during garbage collection. Example 2: Reducing Pause Times in a Data-Intensive Application Consider a data-intensive Java application that processes large datasets in memory. Using traditional garbage collectors, the application experiences significant pause times, leading to delays in data processing. By switching to ZGC, the application can continue processing data while garbage collection occurs concurrently, thereby reducing pause times and improving overall throughput. Java public class DataProcessor { public static void main(String[] args) { // Configure the application to use ZGC System.setProperty("java.vm.options", "-XX:+UseZGC"); // Simulate data processing processData(); } private static void processData() { // Data processing logic } } In this example, setting the JVM option -XX:+UseZGC configures the application to use ZGC, which can lead to shorter pause times during data processing. Example 3: Managing Large Heaps in a Web Application Imagine a high-traffic web application that requires a large heap to manage user sessions and data caching. With traditional garbage collectors, managing such a large heap could result in long pause times, affecting user experience. By adopting ZGC, the application can manage the large heap more efficiently, with minimal impact on response times. Java public class WebApplication { public static void main(String[] args) { // Configure the application to use ZGC System.setProperty("java.vm.options", "-XX:+UseZGC -Xmx10g"); // Start web server and handle requests startServer(); } private static void startServer() { // Web server logic } } Here, the -Xmx10g option is used to specify a large heap size, and ZGC is enabled to ensure that garbage collection does not significantly impact the application's responsiveness. Customizing ZGC ZGC offers several options for tailoring its behavior to better suit your application's needs. Some commonly used customization options include: -Xmx: Configuring the maximum heap size. -Xms: Establishing the initial heap size. -XX:MaxGCPauseMillis: Setting the target maximum pause time for ZGC. -XX:ConcGCThreads: Defining the number of threads allocated for concurrent phases. These options provide flexibility in configuring ZGC to optimize for latency, throughput, or a balanced approach, depending on your application's requirements. Choosing ZGC Wisely ZGC proves to be a valuable choice for applications necessitating low-latency characteristics while maintaining minimal pause times. Some common scenarios where ZGC shines include: Real-time Applications: Applications demanding near-real-time responsiveness, such as financial trading systems and gaming servers. Big Data Applications: Applications dealing with substantial datasets that need to minimize the impact of garbage collection on processing times. Microservices: Microservices architectures often impose strict latency requirements, and ZGC can effectively meet these demands. However, it is essential to recognize that ZGC may not be the optimal solution for all scenarios. In situations where maximizing throughput is critical, alternative garbage collectors like G1 or Parallel GC may prove more suitable. Advantages of ZGC ZGC offers several advantages over traditional garbage collectors: Low Pause Times: ZGC is designed to achieve pause times of less than ten milliseconds, even for heaps larger than a terabyte. Scalability: ZGC can efficiently manage large heaps, making it suitable for applications that require substantial memory. Predictable Performance: By minimizing pause times, ZGC provides more predictable performance, which is crucial for real-time and latency-sensitive applications. Conclusion In conclusion, Java's Z Garbage Collector (ZGC) stands out as a valuable addition to the array of garbage collection algorithms available in the Java ecosystem. It is tailored to provide efficient memory management while minimizing disruptions to application execution, making it an excellent choice for contemporary applications that require low latency and consistent performance. Throughout this article, we have delved into ZGC's fundamental attributes, learned how to activate and monitor it, and examined a real-world example of its integration into a multi-threaded web server application. We have also discussed customization options and identified scenarios where ZGC excels. As Java continues to evolve, ZGC remains a powerful tool for developers aiming to optimize their application's performance while adhering to strict latency requirements. Its ability to strike a balance between low latency and efficient memory management positions it as a valuable asset in the Java developer's toolkit.
In a previous blog, we set up a Debezium server reading events from a PostgreSQL database. Then we streamed those changes to a Redis instance through a Redis stream. We might get the impression that to run Debezium we need to have two extra components running in our infrastructure: A standalone Debezium server instance. A software component with streaming capabilities and various integrations, such as Redis or Kafka. This is not always the case since Debezium can run in embedded mode. By running in embedded mode you use Debezium to read directly from a database’s transaction log. It is up to you how you are gonna handle the entries retrieved. The process of reading the entries from the transaction log can reside on any Java application thus there is no need for a standalone deployment. Apart from the number of components reduced, the other benefit is that we can alter the entries as we read them from the database and take action in our application. Sometimes we might just need a subset of the capabilities offered. Let’s use the same PostgreSQL configurations we used previously. Properties files listen_addresses = '*' port = 5432 max_connections = 20 shared_buffers = 128MB temp_buffers = 8MB work_mem = 4MB wal_level = logical max_wal_senders = 3 Also, we shall create an initialization script for the table we want to focus on. PLSQL #!/bin/bash set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL create schema test_schema; create table test_schema.employee( id SERIAL PRIMARY KEY, firstname TEXT NOT NULL, lastname TEXT NOT NULL, email TEXT not null, age INT NOT NULL, salary real, unique(email) ); EOSQL Our Docker Compose file will look like this. Dockerfile version: '3.1' services: postgres: image: postgres restart: always environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres volumes: - ./postgresql.conf:/etc/postgresql/postgresql.conf - ./init:/docker-entrypoint-initdb.d command: - "-c" - "config_file=/etc/postgresql/postgresql.conf" ports: - 5432:5432 The configuration files we created are mounted onto the PostgreSQL Docker container. Docker Compose V2 is out there with many good features. Provided we run docker compose up, a Postgresql server with a schema and a table will be up and running. Also, that server will have logical decoding enabled and Debezium shall be able to track changes on that table through the transaction log. We have everything needed to proceed with building our application. First, let’s add the dependencies needed: XML <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <version.debezium>2.3.1.Final</version.debezium> <logback-core.version>1.4.12</logback-core.version> </properties> <dependencies> <dependency> <groupId>io.debezium</groupId> <artifactId>debezium-api</artifactId> <version>${version.debezium}</version> </dependency> <dependency> <groupId>io.debezium</groupId> <artifactId>debezium-embedded</artifactId> <version>${version.debezium}</version> </dependency> <dependency> <groupId>io.debezium</groupId> <artifactId>debezium-connector-postgres</artifactId> <version>${version.debezium}</version> </dependency> <dependency> <groupId>io.debezium</groupId> <artifactId>debezium-storage-jdbc</artifactId> <version>${version.debezium}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback-core.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>${logback-core.version}</version> </dependency> </dependencies> We also need to create the Debezium embedded properties: Properties files name=embedded-debezium-connector connector.class=io.debezium.connector.postgresql.PostgresConnector offset.storage=org.apache.kafka.connect.storage.FileOffsetBackingStore offset.flush.interval.ms=60000 database.hostname=127.0.0.1 database.port=5432 database.user=postgres database.password=postgres database.dbname=postgres database.server.name==embedded-debezium debezium.source.plugin.name=pgoutput plugin.name=pgoutput database.server.id=1234 topic.prefix=embedded-debezium schema.include.list=test_schema table.include.list=test_schema.employee Apart from establishing the connection towards the PostgreSQL Database we also decided to store the offset in a file. By using the offset in Debezium we keep track of the progress we make in processing the events. On each change that happens on the table, test_schema.employee we shall receive an event. Once we receive that event our codebase should handle it. To handle the events we need to create a DebeziumEngine.ChangeConsumer. The ChangeConsumer will consume the events emitted. Java package com.egkatzioura; import io.debezium.engine.DebeziumEngine; import io.debezium.engine.RecordChangeEvent; import org.apache.kafka.connect.source.SourceRecord; import java.util.List; public class CustomChangeConsumer implements DebeziumEngine.ChangeConsumer<RecordChangeEvent<SourceRecord>> { @Override public void handleBatch(List<RecordChangeEvent<SourceRecord>> records, DebeziumEngine.RecordCommitter<RecordChangeEvent<SourceRecord>> committer) throws InterruptedException { for(RecordChangeEvent<SourceRecord> record: records) { System.out.println(record.record().toString()); } } } Every incoming event will be printed on the console. Now we can add our main class where we set up the engine. Java package com.egkatzioura; import io.debezium.embedded.Connect; import io.debezium.engine.DebeziumEngine; import io.debezium.engine.format.ChangeEventFormat; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Properties; public class Application { public static void main(String[] args) throws IOException { Properties properties = new Properties(); try(final InputStream stream = Application.class.getClassLoader().getResourceAsStream("embedded_debezium.properties")) { properties.load(stream); } properties.put("offset.storage.file.filename",new File("offset.dat").getAbsolutePath()); var engine = DebeziumEngine.create(ChangeEventFormat.of(Connect.class)) .using(properties) .notifying(new CustomChangeConsumer()) .build(); engine.run(); } } Provided our application is running as well as the PostgreSQL database we configured previously, we can start inserting data. SQL docker exec -it debezium-embedded-postgres-1 psql postgres postgres psql (15.3 (Debian 15.3-1.pgdg120+1)) Type "help" for help. postgres=# insert into test_schema.employee (firstname,lastname,email,age,salary) values ('John','Doe 1','john1@doe.com',18,1234.23); Also, we can see the change in the console. Shell SourceRecord{sourcePartition={server=embedded-debezium}, sourceOffset={last_snapshot_record=true, lsn=22518160, txId=743, ts_usec=1705916606794160, snapshot=true} ConnectRecord{topic='embedded-debezium.test_schema.employee', kafkaPartition=null, key=Struct{id=1}, keySchema=Schema{embedded-debezium.test_schema.employee.Key:STRUCT}, value=Struct{after=Struct{id=1,firstname=John,lastname=Doe 1,email=john1@doe.com,age=18,salary=1234.23},source=Struct{version=2.3.1.Final,connector=postgresql,name=embedded-debezium,ts_ms=1705916606794,snapshot=last,db=postgres,sequence=[null,"22518160"],schema=test_schema,table=employee,txId=743,lsn=22518160},op=r,ts_ms=1705916606890}, valueSchema=Schema{embedded-debezium.test_schema.employee.Envelope:STRUCT}, timestamp=null, headers=ConnectHeaders(headers=)} We did it. We managed to run Debezium through a Java application without the need for a standalone Debezium server running or a streaming component. You can find the code on GitHub.
Base64 encoding was originally conceived more than 30 years ago (named in 1992). Back then, the Simple Mail Transfer Protocol (SMTP) forced developers to find a way to encode e-mail attachments in ASCII characters so SMTP servers wouldn't interfere with them. All these years later, Base64 encoding is still widely used for the same purpose: to replace binary data in systems where only ASCII characters are accepted. E-mail file attachments remain the most common example of where we use Base64 encoding, but it’s not the only use case. Whether we’re stashing images or other documents in HTML, CSS, or JavaScript, or including them in JSON objects (e.g., as a payload to certain API endpoints), Base64 simply offers a convenient, accessible solution when our recipient systems say “no” to binary. The Base64 encoding process starts with a binary encoded file — or encoding a file's content in binary if it isn't in binary already. The binary content is subsequently broken into groups of 6 bits, with each group represented by an ASCII character. There are precisely 64 ASCII characters available for this encoding process — hence the name Base64 — and those characters range from A-Z (capitalized), a-z (lower case), 0 – 9, +, and /. The result of this process is a string of characters; the phrase “hello world”, for example, ends up looking like “aGVsbG8gd29ybGQ=”. The “=” sign at the end is used as padding to ensure the length of the encoded data is a multiple of 4. The only significant challenge with Base64 encoding in today’s intensely content-saturated digital world is the toll it takes on file size. When we Base64 encode content, we end up with around 1 additional byte of information for every 3 bytes in our original content, increasing the original file size by about 33%. In context, that means an 800kb image file we’re encoding instantly jumps to over 1mb, eating up additional costly resources and creating an increasingly cumbersome situation when we share Base64 encoded content at scale. When our work necessitates a Base64 content conversion, we have a few options at our disposal. First and foremost, many modern programming languages now have built-in classes designed to handle Base64 encoding and decoding locally. Since Java 8 was initially released in 2014, for example, we’ve been able to use java.util.Base64 to handle conversions to and from Base64 with minimal hassle. Similar options exist in Python and C# languages, among others. Depending on the needs of our project, however, we might benefit from making our necessary conversions to and from Base64 encoding with a low-code API solution. This can help take some hands-on coding work off our own plate, offload some of the processing burden from our servers, and in some contexts, deliver more consistent results. In the remainder of this article, I’ll demonstrate a few free APIs we can leverage to streamline our workflows for 1) identifying when content is Base64 encoded and 2) encoding or decoding Base64 content. Demonstration Using the ready-to-run Java code examples provided further down the page, we can take advantage of three separate free-to-use APIs designed to help build out our Base64 detection and conversion workflow. These three APIs serve the following functions (respectively): Detect if a text string is Base64 encoded Base64 decode content (convert Base64 string to binary content) Base64 encode content (convert either binary or file data to a Base64 text string) A few different text encoding options exist out there, so we can use the first API as a consistent way of identifying (or validating) Base64 encoding when it comes our way. Once we’re sure that we’re dealing with Base64 content, we can use the second API to decode that content back to binary. When it comes time to package our own content for email attachments or relevant systems that require ASCII characters, we can use the third API to convert binary OR file data content directly to Base64. As a quick reminder, if we’re using a file data string, the API will first binary encode that content before Base64 encoding it, so we don’t have to worry about that part. To authorize our API calls, we’ll need a free-tier API key, which will allow us a limit of 800 API calls per month (with no additional commitments — our total will simply reset the following month if/when we reach it). Before we call the functions for any of the above APIs, our first step is to install the SDK. In our Maven POM file, let’s add a reference to the repository (Jitpack is used to dynamically compile the library): XML <repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository> </repositories> Next, let’s add a reference to the dependency: XML <dependencies> <dependency> <groupId>com.github.Cloudmersive</groupId> <artifactId>Cloudmersive.APIClient.Java</artifactId> <version>v4.25</version> </dependency> </dependencies> Now we can implement ready-to-run code to call each independent API. Let’s start with the base64 detection API. We can use the following code to structure our API call: Java // Import classes: //import com.cloudmersive.client.invoker.ApiClient; //import com.cloudmersive.client.invoker.ApiException; //import com.cloudmersive.client.invoker.Configuration; //import com.cloudmersive.client.invoker.auth.*; //import com.cloudmersive.client.EditTextApi; ApiClient defaultClient = Configuration.getDefaultApiClient(); // Configure API key authorization: Apikey ApiKeyAuth Apikey = (ApiKeyAuth) defaultClient.getAuthentication("Apikey"); Apikey.setApiKey("YOUR API KEY"); // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null) //Apikey.setApiKeyPrefix("Token"); EditTextApi apiInstance = new EditTextApi(); Base64DetectRequest request = new Base64DetectRequest(); // Base64DetectRequest | Input request try { Base64DetectResponse result = apiInstance.editTextBase64Detect(request); System.out.println(result); } catch (ApiException e) { System.err.println("Exception when calling EditTextApi#editTextBase64Detect"); e.printStackTrace(); } Next, let’s move on to our base64 decoding API. We can use the following code to structure our API call: Java // Import classes: //import com.cloudmersive.client.invoker.ApiClient; //import com.cloudmersive.client.invoker.ApiException; //import com.cloudmersive.client.invoker.Configuration; //import com.cloudmersive.client.invoker.auth.*; //import com.cloudmersive.client.EditTextApi; ApiClient defaultClient = Configuration.getDefaultApiClient(); // Configure API key authorization: Apikey ApiKeyAuth Apikey = (ApiKeyAuth) defaultClient.getAuthentication("Apikey"); Apikey.setApiKey("YOUR API KEY"); // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null) //Apikey.setApiKeyPrefix("Token"); EditTextApi apiInstance = new EditTextApi(); Base64DecodeRequest request = new Base64DecodeRequest(); // Base64DecodeRequest | Input request try { Base64DecodeResponse result = apiInstance.editTextBase64Decode(request); System.out.println(result); } catch (ApiException e) { System.err.println("Exception when calling EditTextApi#editTextBase64Decode"); e.printStackTrace(); } Finally, let’s implement our base64 encoding option (as a reminder, we can use binary OR file data content for this one). We can use the following code to structure our API call: Java // Import classes: //import com.cloudmersive.client.invoker.ApiClient; //import com.cloudmersive.client.invoker.ApiException; //import com.cloudmersive.client.invoker.Configuration; //import com.cloudmersive.client.invoker.auth.*; //import com.cloudmersive.client.EditTextApi; ApiClient defaultClient = Configuration.getDefaultApiClient(); // Configure API key authorization: Apikey ApiKeyAuth Apikey = (ApiKeyAuth) defaultClient.getAuthentication("Apikey"); Apikey.setApiKey("YOUR API KEY"); // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null) //Apikey.setApiKeyPrefix("Token"); EditTextApi apiInstance = new EditTextApi(); Base64EncodeRequest request = new Base64EncodeRequest(); // Base64EncodeRequest | Input request try { Base64EncodeResponse result = apiInstance.editTextBase64Encode(request); System.out.println(result); } catch (ApiException e) { System.err.println("Exception when calling EditTextApi#editTextBase64Encode"); e.printStackTrace(); } Now we have a few additional options for identifying, decoding, and/or encoding base64 content in our Java applications.
Java's automatic memory management is one of its most notable features, providing developers with the convenience of not having to manually manage memory allocation and deallocation. However, there may be cases where a developer wants to create a custom Java automatic memory management system to address specific requirements or constraints. In this guide, we will provide a granular step-by-step process for designing and implementing a custom Java automatic memory management system. Step 1: Understand Java's Memory Model Before creating a custom memory management system, it is crucial to understand Java's memory model, which consists of the heap and the stack. The heap stores objects, while the stack holds local variables and method call information. Your custom memory management system should be designed to work within this memory model. Step 2: Design a Custom Memory Allocator A custom memory allocator is responsible for reserving memory for new objects. When designing your memory allocator, consider the following: Allocation strategies: Choose between fixed-size blocks, variable-size blocks, or a combination of both. Memory alignment: Ensure that memory is correctly aligned based on the underlying hardware and JVM requirements. Fragmentation: Consider strategies to minimize fragmentation, such as allocating objects of similar sizes together or using a segregated free list. Step 3: Implement Reference Tracking To manage object lifecycles, you need a mechanism to track object references. You can implement reference tracking using reference counting or a tracing mechanism. In reference counting, each object maintains a counter of the number of references to it, whereas in tracing, the memory manager periodically scans the memory to identify live objects. Step 4: Choose a Garbage Collection Algorithm Select a garbage collection algorithm that suits your application's requirements. Some common algorithms include: Mark and Sweep: Marks live objects and then sweeps dead objects to reclaim memory. Mark and Compact: Similar to mark and sweep, but also compacts live objects to reduce fragmentation. Copying: Divides the heap into two areas and moves live objects from one area to the other, leaving behind a contiguous block of free memory. Step 5: Implement Root Object Identification Identify root objects that serve as the starting points for tracing live objects. Root objects typically include global variables, thread stacks, and other application-specific roots. Maintain a set of root objects for your custom memory management system. Step 6: Implement a Marking Algorithm Design and implement a marking algorithm that identifies live objects by traversing object references starting from the root objects. Common algorithms for marking include depth-first search (DFS) and breadth-first search (BFS). Step 7: Implement a Sweeping Algorithm Design and implement a sweeping algorithm that reclaims memory occupied by dead objects (those not marked as live). This can be done by iterating through the entire memory space and freeing unmarked objects or maintaining a list of dead objects during the marking phase and releasing them afterward. Step 8: Implement Compaction (Optional) If your memory model is prone to fragmentation, you may need to implement a compaction algorithm that defragments memory by moving live objects closer together and creating a contiguous block of free memory. Step 9: Integrate With Your Application Integrate your custom memory management system with your Java application by replacing the default memory management system and ensuring that object references are properly managed throughout the application code. Step 10: Monitor and Optimize Monitor the performance and behavior of your custom memory management system to identify any issues or areas for improvement. Fine-tune its parameters, such as heap size, allocation strategies, and collection frequency, to optimize its performance for your specific application requirements. Example Here's an example of a basic mark and sweep garbage collector in Java: Java import java.util.ArrayList; import java.util.List; class CustomObject { boolean marked = false; List<CustomObject> references = new ArrayList<>(); } class MemoryManager { List<CustomObject> heap = new ArrayList<>(); List<CustomObject> roots = new ArrayList<>(); CustomObject allocateObject() { CustomObject obj = new CustomObject(); heap.add(obj); return obj; } void addRoot(CustomObject obj) { roots.add(obj); } void removeRoot(CustomObject obj) { roots.remove(obj); } void mark(CustomObject obj) { if (!obj.marked) { obj.marked = true; for (CustomObject ref : obj.references) { mark(ref); } } } void sweep() { List<CustomObject> newHeap = new ArrayList<>(); for (CustomObject obj : heap) { if (obj.marked) { obj.marked = false; newHeap.add(obj); } } heap = newHeap; } void collectGarbage() { // Mark phase for (CustomObject root : roots) { mark(root); } // Sweep phase sweep(); } } Conclusion In conclusion, implementing a custom automatic memory management system in Java is a complex and advanced task that requires a deep understanding of the JVM internals. The provided example demonstrates a simplified mark and sweep garbage collector for a hypothetical language or runtime environment, which serves as a starting point for understanding the principles of garbage collection.
AngularAndSpringWithMaps is a Sprint Boot project that shows company properties on a Bing map and can be run on the JDK or as a GraalVM native image. ReactAndGo is a Golang project that shows the cheapest gas stations in your post code area and is compiled in a binary. Both languages are garbage collected, and the AngularAndSpringWithMaps project uses the G1 collector. The complexity of both projects can be compared. Both serve as a frontend, provide rest data endpoints for the frontend, and implement services for the logic with repositories for the database access. How to build the GraalVM native image for the AngularAndSpringWithMaps project is explained in this article. What To Compare On the performance side, Golang and Java on the JVM or as a native image are fast and efficient enough for the vast majority of use cases. Further performance fine-tuning needs good profiling and specific improvements, and often, the improvements are related to the database. The two interesting aspects are: Memory requirements Startup time(can include warmup) The memory requirements are important because the available memory limit on the Kubernetes node or deployment server is mostly reached earlier than the CPU limit. If you use less memory, you can deploy more Docker images or Spring Boot applications on the resource. The startup time is important if you have periods with little load and periods with high load for your application. The shorter the startup time is the more aggressive you can scale the amount of deployed applications/images up or down. Memory Requirements 420 MB AngularAndSpringWithMaps JVM 21 280 MB AngularAndSpringWithMaps GraalVM native image 128-260 MB ReactAndGo binary The GraalVM native image uses significantly less memory than the JVM jar. That makes the native image more resource-efficient. The native image binary is 240 MB in size, which means 40 MB of working memory. The ReactAndGo binary is 29 MB in size and uses 128-260 MB of memory depending on the size of the updates it has to process. That means if the use case would need only 40 MB of working memory like the GraalVM native image, 70 MB would be enough to run it. That makes the Go binary much more resource-efficient. Startup Time 4300ms AngularAndSpringWithMaps JVM 21 220ms AngularAndSpringWithMaps GraalVM native image 100ms ReactAndGo binary The GraalVM native image startup time is impressive and enables the scale-to-zero configurations that start the application on demand and scale down to zero without load. The JVM start time requires one running instance as a minimum. The ReactAndGo binary startup time is the fastest and enables scale to zero. Conclusion The GraalVM native image and the Go binary are the most efficient in this comparison. Due to their lower memory requirements can, the CPU resources be used more efficiently. The fast startup times enable scale to zero configurations that can save money in on-demand environments. The winner is the Go project. The result is that if efficient use of hardware resources is the most important to you, Go is the best. If your developers are most familiar with Java then the use of GraalVM native image can improve the efficient use of hardware resources. Creating GraalVM native images needs more effort and developer time. Some of the effort can be automated, and with some of the effort, that would be hard. Then the question becomes: Is the extra developer time worth the saved hardware resources?
In this post, you will learn how you can integrate Large Language Model (LLM) capabilities into your Java application. More specifically, how you can integrate with LocalAI from your Java application. Enjoy! Introduction In a previous post, it was shown how you could run a Large Language Model (LLM) similar to OpenAI by means of LocalAI. The Rest API of OpenAI was used in order to interact with LocalAI. Integrating these capabilities within your Java application can be cumbersome. However, since the introduction of LangChain4j, this has become much easier to do. LangChain4j offers you a simplification in order to integrate with LLMs. It is based on the Python library LangChain. It is therefore also advised to read the documentation and concepts of LangChain since the documentation of LangChain4j is rather short. Many examples are provided though in the LangChain4j examples repository. Especially, the examples in the other-examples directory have been used as inspiration for this blog. The real trigger for writing this blog was the talk I attended about LangChain4j at Devoxx Belgium. This was the most interesting talk I attended at Devoxx: do watch it if you can make time for it. It takes only 50 minutes. The sources used in this blog can be found on GitHub. Prerequisites The prerequisites for this blog are: Basic knowledge about what a Large Language Model is Basic Java knowledge (Java 21 is used) You need LocalAI if you want to run the examples (see the previous blog linked in the introduction on how you can make use of LocalAI). Version 2.2.0 is used for this blog. LangChain4j Examples In this section, some of the capabilities of LangChain4j are shown by means of examples. Some of the examples used in the previous post are now implemented using LangChain4j instead of using curl. How Are You? As a first simple example, you ask the model how it is feeling. In order to make use of LangChain4j in combination with LocalAI, you add the langchain4j-local-ai dependency to the pom file. XML <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-local-ai</artifactId> <version>0.24.0</version> </dependency> In order to integrate with LocalAI, you create a ChatLanguageModel specifying the following items: The URL where the LocalAI instance is accessible The name of the model you want to use in LocalAI The temperature: A high temperature allows the model to respond in a more creative way. Next, you ask the model to generate an answer to your question and you print the answer. Java ChatLanguageModel model = LocalAiChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.9) .build(); String answer = model.generate("How are you?"); System.out.println(answer); Start LocalAI and run the example above. The response is as expected. Shell I'm doing well, thank you. How about yourself? Before continuing, note something about the difference between LanguageModel and ChatLanguageModel. Both classes are available in LangChain4j, so which one to choose? A chat model is a variation of a language model. If you need a "text in, text out" functionality, you can choose LanguageModel. If you also want to be able to use "chat messages" as input and output, you should use ChatLanguageModel. In the example above, you could just have used LanguageModel and it would behave similarly. Facts About Famous Soccer Player Let’s verify whether it also returns facts about the famous Dutch soccer player Johan Cruijff. You use the same code as before, only now you set the temperature to zero because no creative answer is required. Java ChatLanguageModel model = LocalAiChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.0) .build(); String answer = model.generate("who is Johan Cruijff?"); System.out.println(answer); Run the example, the response is as expected. Shell Johan Cruyff was a Dutch professional football player and coach. He played as a forward for Ajax, Barcelona, and the Netherlands national team. He is widely regarded as one of the greatest players of all time and was known for his creativity, skill, and ability to score goals from any position on the field. Stream the Response Sometimes, the answer will take some time. In the OpenAPI specification, you can set the stream parameter to true in order to retrieve the response character by character. This way, you can display the response already to the user before awaiting the complete response. This functionality is also available with LangChain4j but requires the use of a StreamingResponseHandler. The onNext method receives every character one by one. The complete response is gathered in the answerBuilder and futureAnswer. Running this example prints every single character one by one, and at the end, the complete response is printed. Java StreamingChatLanguageModel model = LocalAiStreamingChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.0) .build(); StringBuilder answerBuilder = new StringBuilder(); CompletableFuture<String> futureAnswer = new CompletableFuture<>(); model.generate("who is Johan Cruijff?", new StreamingResponseHandler<AiMessage>() { @Override public void onNext(String token) { answerBuilder.append(token); System.out.println(token); } @Override public void onComplete(Response<AiMessage> response) { futureAnswer.complete(answerBuilder.toString()); } @Override public void onError(Throwable error) { futureAnswer.completeExceptionally(error); } }); String answer = futureAnswer.get(90, SECONDS); System.out.println(answer); Run the example. The response is as expected. Shell J o h a n ... s t y l e . Johan Cruijff was a Dutch professional football player and coach who played as a forward. ... Other Languages You can instruct the model by means of a system message how it should behave. For example, you can instruct it to answer always in a different language; Dutch, in this case. This example shows clearly the difference between LanguageModel and ChatLanguageModel. You have to use ChatLanguageModel in this case because you need to interact by means of chat messages with the model. Create a SystemMessage to instruct the model. Create a UserMessage for your question. Add them to a list and send the list of messages to the model. Also, note that the response is an AiMessage. The messages are explained as follows: UserMessage: A ChatMessage coming from a human/user AiMessage: A ChatMessage coming from an AI/assistant SystemMessage: A ChatMessage coming from the system Java ChatLanguageModel model = LocalAiChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.0) .build(); SystemMessage responseInDutch = new SystemMessage("You are a helpful assistant. Antwoord altijd in het Nederlands."); UserMessage question = new UserMessage("who is Johan Cruijff?"); var chatMessages = new ArrayList<ChatMessage>(); chatMessages.add(responseInDutch); chatMessages.add(question); Response<AiMessage> response = model.generate(chatMessages); System.out.println(response.content()); Run the example, the response is as expected. Shell AiMessage { text = "Johan Cruijff was een Nederlands voetballer en trainer. Hij speelde als aanvaller en is vooral bekend van zijn tijd bij Ajax en het Nederlands elftal. Hij overleed in 1996 op 68-jarige leeftijd." toolExecutionRequest = null } Chat With Documents A fantastic use case is to use an LLM in order to chat with your own documents. You can provide the LLM with your documents and ask questions about it. For example, when you ask the LLM for which football clubs Johan Cruijff played ("For which football teams did Johan Cruijff play and also give the periods, answer briefly"), you receive the following answer. Shell Johan Cruijff played for Ajax Amsterdam (1954-1973), Barcelona (1973-1978) and the Netherlands national team (1966-1977). This answer is quite ok, but it is not complete, as not all football clubs are mentioned and the period for Ajax includes also his youth period. The correct answer should be: Years Team 1964-1973 Ajax 1973-1978 Barcelona 1979 Los Angeles Aztecs 1980 Washington Diplomats 1981 Levante 1981 Washington Diplomats 1981-1983 Ajax 1983-1984 Feyenoord Apparently, the LLM does not have all relevant information and that is not a surprise. The LLM has some basic knowledge, it runs locally and has its limitations. But what if you could provide the LLM with extra information in order that it can give an adequate answer? Let’s see how this works. First, you need to add some extra dependencies to the pom file: XML <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-embeddings</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId> <version>${langchain4j.version}</version> </dependency> Save the Wikipedia text of Johan Cruijff to a PDF file and store it in src/main/resources/example-files/Johan_Cruyff.pdf. The source code to add this document to the LLM consists of the following parts: The text needs to be embedded; i.e., the text needs to be converted to numbers. An embedding model is needed for that, for simplicity you use the AllMiniLmL6V2EmbeddingModel. The embeddings need to be stored in an embedding store. Often a vector database is used for this purpose, but in this case, you can use an in-memory embedding store. The document needs to be split into chunks. For simplicity, you split the document into chunks of 500 characters. All of this comes together in the EmbeddingStoreIngestor. Add the PDF to the ingestor. Create the ChatLanguageModel just like you did before. With a ConversationalRetrievalChain, you connect the language model with the embedding store and model. And finally, you execute your question. Java EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel(); EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>(); EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() .documentSplitter(DocumentSplitters.recursive(500, 0)) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); Document johanCruiffInfo = loadDocument(toPath("example-files/Johan_Cruyff.pdf")); ingestor.ingest(johanCruiffInfo); ChatLanguageModel model = LocalAiChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.0) .build(); ConversationalRetrievalChain chain = ConversationalRetrievalChain.builder() .chatLanguageModel(model) .retriever(EmbeddingStoreRetriever.from(embeddingStore, embeddingModel)) .build(); String answer = chain.execute("Give all football teams Johan Cruijff played for in his senior career"); System.out.println(answer); When you execute this code, an exception is thrown. Shell Exception in thread "main" java.lang.RuntimeException: java.lang.RuntimeException: java.io.InterruptedIOException: timeout at dev.langchain4j.internal.RetryUtils.withRetry(RetryUtils.java:29) at dev.langchain4j.model.localai.LocalAiChatModel.generate(LocalAiChatModel.java:98) at dev.langchain4j.model.localai.LocalAiChatModel.generate(LocalAiChatModel.java:65) at dev.langchain4j.chain.ConversationalRetrievalChain.execute(ConversationalRetrievalChain.java:65) at com.mydeveloperplanet.mylangchain4jplanet.ChatWithDocuments.main(ChatWithDocuments.java:55) Caused by: java.lang.RuntimeException: java.io.InterruptedIOException: timeout at dev.ai4j.openai4j.SyncRequestExecutor.execute(SyncRequestExecutor.java:31) at dev.ai4j.openai4j.RequestExecutor.execute(RequestExecutor.java:59) at dev.langchain4j.model.localai.LocalAiChatModel.lambda$generate$0(LocalAiChatModel.java:98) at dev.langchain4j.internal.RetryUtils.withRetry(RetryUtils.java:26) ... 4 more Caused by: java.io.InterruptedIOException: timeout at okhttp3.internal.connection.RealCall.timeoutExit(RealCall.kt:398) at okhttp3.internal.connection.RealCall.callDone(RealCall.kt:360) at okhttp3.internal.connection.RealCall.noMoreExchanges$okhttp(RealCall.kt:325) at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:209) at okhttp3.internal.connection.RealCall.execute(RealCall.kt:154) at retrofit2.OkHttpCall.execute(OkHttpCall.java:204) at dev.ai4j.openai4j.SyncRequestExecutor.execute(SyncRequestExecutor.java:23) ... 7 more Caused by: java.net.SocketTimeoutException: timeout at okio.SocketAsyncTimeout.newTimeoutException(JvmOkio.kt:147) at okio.AsyncTimeout.access$newTimeoutException(AsyncTimeout.kt:158) at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:337) at okio.RealBufferedSource.indexOf(RealBufferedSource.kt:427) at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.kt:320) at okhttp3.internal.http1.HeadersReader.readLine(HeadersReader.kt:29) at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.kt:178) at okhttp3.internal.connection.Exchange.readResponseHeaders(Exchange.kt:106) at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.kt:79) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:34) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at dev.ai4j.openai4j.ResponseLoggingInterceptor.intercept(ResponseLoggingInterceptor.java:21) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at dev.ai4j.openai4j.RequestLoggingInterceptor.intercept(RequestLoggingInterceptor.java:31) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at dev.ai4j.openai4j.AuthorizationHeaderInjector.intercept(AuthorizationHeaderInjector.java:25) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201) ... 10 more Caused by: java.net.SocketException: Socket closed at java.base/sun.nio.ch.NioSocketImpl.endRead(NioSocketImpl.java:243) at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:323) at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:346) at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:796) at java.base/java.net.Socket$SocketInputStream.read(Socket.java:1099) at okio.InputStreamSource.read(JvmOkio.kt:94) at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:125) ... 32 more This can be solved by setting the timeout of the language model to a higher value. Java ChatLanguageModel model = LocalAiChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.0) .timeout(Duration.ofMinutes(5)) .build(); Run the code again, and the following answer is received, which is correct. Shell Johan Cruijff played for the following football teams in his senior career: - Ajax (1964-1973) - Barcelona (1973-1978) - Los Angeles Aztecs (1979) - Washington Diplomats (1980-1981) - Levante (1981) - Ajax (1981-1983) - Feyenoord (1983-1984) - Netherlands national team (1966-1977) Using a 1.x version of LocalAI gave this response, which was worse. Shell Johan Cruyff played for the following football teams: - Ajax (1964-1973) - Barcelona (1973-1978) - Los Angeles Aztecs (1979) The following steps were used to solve this problem. When you take a closer look at the PDF file, you notice that the information about the football teams is listed in a table next to the regular text. Remember that splitting the document was done by creating chunks of 500 characters. So, maybe this splitting is not executed well enough for the LLM. Copy the football teams in a separate text document. Plain Text Years Team Apps (Gls) 1964–1973 Ajax 245 (193) 1973–1978 Barcelona 143 (48) 1979 Los Angeles Aztecs 22 (14) 1980 Washington Diplomats 24 (10) 1981 Levante 10 (2) 1981 Washington Diplomats 5 (2) 1981–1983 Ajax 36 (14) 1983–1984 Feyenoord 33 (11) Add both documents to the ingestor. Java Document johanCruiffInfo = loadDocument(toPath("example-files/Johan_Cruyff.pdf")); Document clubs = loadDocument(toPath("example-files/Johan_Cruyff_clubs.txt")); ingestor.ingest(johanCruiffInfo, clubs); Run this code and this time, the answer was correct and complete. Shell Johan Cruijff played for the following football teams in his senior career: - Ajax (1964-1973) - Barcelona (1973-1978) - Los Angeles Aztecs (1979) - Washington Diplomats (1980-1981) - Levante (1981) - Ajax (1981-1983) - Feyenoord (1983-1984) - Netherlands national team (1966-1977) It is therefore important that the sources you provide to an LLM are split wisely. Besides that, the used technologies improve in a rapid way. Even while writing this blog, some problems were solved in a couple of weeks. Updating to a more recent version of LocalAI for example, solved one way or the other the problem with parsing the single PDF. Conclusion In this post, you learned how to integrate an LLM from within your Java application using LangChain4j. You also learned how to chat with documents, which is a fantastic use case! It is also important to regularly update to newer versions as the development of these AI technologies improves continuously.
Last year, I wrote the article, "Upgrade Guide To Spring Boot 3.0 for Spring Data JPA and Querydsl," for the Spring Boot 3.0.x upgrade. Now, we have Spring Boot 3.2. Let's see two issues you might deal with when upgrading to Spring Boot 3.2.2. The technologies used in the SAT project are: Spring Boot 3.2.2 and Spring Framework 6.1.3 Hibernate + JPA model generator 6.4.1. Final Spring Data JPA 3.2.2 Querydsl 5.0.0. Changes All the changes in Spring Boot 3.2 are described in Spring Boot 3.2 Release Notes and What's New in Version 6.1 for Spring Framework 6.1. The latest changes in Spring Boot 3.2.2 can be found on GitHub. Issues Found A different treatment of Hibernate dependencies due to the changed hibernate-jpamodelgen behavior for annotation processors Unpaged class was redesigned. Let's start with the Hibernate dependencies first. Integrating Static Metamodel Generation The biggest change comes from the hibernate-jpamodelgen dependency, which is generating a static metamodel. In Hibernate 6.3, the treatment of dependencies was changed in order to mitigate transitive dependencies. Spring Boot 3.2.0 bumped up the hibernate-jpamodelgen dependency to the 6.3 version (see Dependency Upgrades). Unfortunately, the new version causes compilation errors (see below). Note: Spring Boot 3.2.2 used here already uses Hibernate 6.4 with the same behavior. Compilation Error With this change, the compilation of our project (Maven build) with Spring Boot 3.2.2 fails on the error like this: Plain Text [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.049 s [INFO] Finished at: 2024-01-05T08:43:10+01:00 [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project sat-jpa: Compilation failure: Compilation failure: [ERROR] on the class path. A future release of javac may disable annotation processing [ERROR] unless at least one processor is specified by name (-processor), or a search [ERROR] path is specified (--processor-path, --processor-module-path), or annotation [ERROR] processing is enabled explicitly (-proc:only, -proc:full). [ERROR] Use -Xlint:-options to suppress this message. [ERROR] Use -proc:none to disable annotation processing. [ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityRepository.java:[3,41] error: cannot find symbol [ERROR] symbol: class City_ [ERROR] location: package com.github.aha.sat.jpa.city [ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityRepository.java:[3] error: static import only from classes and interfaces ... [ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[4] error: static import only from classes and interfaces [ERROR] java.lang.NoClassDefFoundError: net/bytebuddy/matcher/ElementMatcher [ERROR] at org.hibernate.jpamodelgen.validation.ProcessorSessionFactory.<clinit>(ProcessorSessionFactory.java:69) [ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMeta.handleNamedQuery(AnnotationMeta.java:104) [ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMeta.handleNamedQueryRepeatableAnnotation(AnnotationMeta.java:78) [ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMeta.checkNamedQueries(AnnotationMeta.java:57) [ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMetaEntity.init(AnnotationMetaEntity.java:297) [ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMetaEntity.create(AnnotationMetaEntity.java:135) [ERROR] at org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor.handleRootElementAnnotationMirrors(JPAMetaModelEntityProcessor.java:360) [ERROR] at org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor.processClasses(JPAMetaModelEntityProcessor.java:203) [ERROR] at org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor.process(JPAMetaModelEntityProcessor.java:174) [ERROR] at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:1021) [ER... [ERROR] at org.codehaus.plexus.classworlds.launcher.Launcher.main(Launcher.java:348) [ERROR] Caused by: java.lang.ClassNotFoundException: net.bytebuddy.matcher.ElementMatcher [ERROR] at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:445) [ERROR] at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:593) [ERROR] at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526) [ERROR] ... 51 more [ERROR] -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException This is caused by the changed approach in the static metamodel generation announced in the Hibernate migration guide (see Integrating Static Metamodel Generation and the original issue HHH-17362). Their explanation for such change is this: "... in previous versions of Hibernate ORM you were leaking dependencies of hibernate-jpamodelgen into your compile classpath unknowingly. With Hibernate ORM 6.3, you may now experience a compilation error during annotation processing related to missing Antlr classes." Dependency Changes As you can see below in the screenshots, Hibernate dependencies were really changed. Spring Boot 3.1.6: Spring Boot 3.2.2: Explanation As stated in the migration guide, we need to change our pom.xml from a simple Maven dependency to the annotation processor paths of the Maven compiler plugin (see documentation). Solution We can remove the Maven dependencies hibernate-jpamodelgen and querydsl-apt (in our case) as recommended in the last article. Instead, pom.xml has to define the static metamodel generators via maven-compiler-plugin like this: XML <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-jpamodelgen</artifactId> <version>${hibernate.version}</version> </path> <path> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${querydsl.version}</version> <classifier>jakarta</classifier> </path> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> See the related changes in SAT project on GitHub. As we are forced to use this approach due to hibernate-jpamodelgen, we need to apply it to all dependencies tight to annotation processing (querydsl-apt or lombok). For example, when lombok is not used this way, we get the compilation error like this: Plain Text [INFO] ------------------------------------------------------------- [ERROR] COMPILATION ERROR : [INFO] ------------------------------------------------------------- [ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityService.java:[15,30] error: variable repository not initialized in the default constructor [INFO] 1 error [INFO] ------------------------------------------------------------- [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4.535 s [INFO] Finished at: 2024-01-08T08:40:29+01:00 [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project sat-jpa: Compilation failure [ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityService.java:[15,30] error: variable repository not initialized in the default constructor The same applies to querydsl-apt. In this case, we can see the compilation error like this: Plain Text [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 5.211 s [INFO] Finished at: 2024-01-11T08:39:18+01:00 [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project sat-jpa: Compilation failure: Compilation failure: [ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryRepository.java:[3,44] error: cannot find symbol [ERROR] symbol: class QCountry [ERROR] location: package com.github.aha.sat.jpa.country [ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryRepository.java:[3] error: static import only from classes and interfaces [ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[3,41] error: cannot find symbol [ERROR] symbol: class QCity [ERROR] location: package com.github.aha.sat.jpa.city [ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[3] error: static import only from classes and interfaces [ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[4,44] error: cannot find symbol [ERROR] symbol: class QCountry [ERROR] location: package com.github.aha.sat.jpa.country [ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[4] error: static import only from classes and interfaces [ERROR] -> [Help 1] The reason is obvious. We need to apply all the annotation processors at the same time. Otherwise, some pieces of code can be missing, and we get the compilation error. Unpaged Redesigned The second minor issue is related to a change in Unpaged class. A serialization of PageImpl by the Jackson library was impacted by changing Unpaged from enum to class (see spring-projects/spring-data-commons#2987). Spring Boot 3.1.6: Java public interface Pageable { static Pageable unpaged() { return Unpaged.INSTANCE; } ... } enum Unpaged implements Pageable { INSTANCE; ... } Spring Boot 3.2.2: Java public interface Pageable { static Pageable unpaged() { return unpaged(Sort.unsorted()); } static Pageable unpaged(Sort sort) { return Unpaged.sorted(sort); } ... } final class Unpaged implements Pageable { private static final Pageable UNSORTED = new Unpaged(Sort.unsorted()); ... } When new PageImpl<City>(cities) is used (as we were used to using it), then this error is thrown: Plain Text 2024-01-11T08:47:56.446+01:00 WARN 5168 --- [sat-elk] [ main] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: (was java.lang.UnsupportedOperationException)] MockHttpServletRequest: HTTP Method = GET Request URI = /api/cities/country/Spain Parameters = {} Headers = [] Body = null Session Attrs = {} Handler: Type = com.github.aha.sat.elk.city.CityController Method = com.github.aha.sat.elk.city.CityController#searchByCountry(String, Pageable) Async: Async started = false Async result = null Resolved Exception: Type = org.springframework.http.converter.HttpMessageNotWritableException The workaround is to use the constructor with all attributes as: Java new PageImpl<City>(cities, ofSize(PAGE_SIZE), cities.size()) Instead of: Java new PageImpl<City>(cities) Note: It should be fixed in Spring Boot 3.3 (see this issue comment). Conclusion This article has covered both found issues when upgrading to the latest version of Spring Boot 3.2.2 (at the time of writing this article). The article started with the handling of the annotation processors due to the changed Hibernate dependency management. Next, the change in Unpaged class and workaround for using PageImpl was explained. All of the changes (with some other changes) can be seen in PR #64. The complete source code demonstrated above is available in my GitHub repository.
Managing JSON data in the world of Java development can be a challenging task. However, GSON, a powerful library developed by Google, can simplify the conversion between Java objects and JSON strings. This article will guide you through the basics of GSON, using practical examples, and show how Object-Oriented Programming (OOP) principles play a crucial role in this process. What Is GSON? GSON is a Java library that simplifies the process of converting Java objects to JSON and vice versa. It stands for "Google's JSON" and provides developers with a seamless integration between their Java objects and JSON data. This means manual parsing and formatting are not required, making working with JSON data easier and more efficient. Getting Started To utilize the GSON library in your project, you need to add it to your project's dependencies. GSON is a popular Java library for serializing and deserializing Java objects to JSON and vice versa. It provides a simple and efficient way to convert JSON strings to Java objects and vice versa. If you're using Maven, you can easily include GSON by adding the following dependency to your project's pom.xml file: XML <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.9</version> </dependency> Once you've added this dependency, you can use GSON in your code. Serialization: Java Object to JSON Consider a simple `Person` class: Java public class Person { private String name; private int age; // getters and setters } To convert objects to JSON, we can use the following code to serialize them: Java import com.google.gson.Gson; public class SerializationExample { public static void main(String[] args) { Person person = new Person(); person.setName("John"); person.setAge(20); Gson gson = new Gson(); String json = gson.toJson(person); System.out.println(json); } } The output will look like this: Shell {"name":"John","age":30} Deserialization: JSON to Java Object In GSON, a reverse process allows you to convert JSON back into an object. This can be useful if you have previously converted an object into a JSON format and now need to retrieve the original object. The process involves using the GSON library to deserialize the JSON string and convert it into an object. This can be done using the fromJson() method, which takes in the JSON string and the object's class to be created. Once the JSON string has been deserialized, a new object will be created with the same properties as the original object: Java import com.google.gson.Gson; public class DeserializationExample { public static void main(String[] args) { String jsonString = "{\"name\":\"Jane\",\"age\":25,\"studentId\":\"S67890\"}"; Gson gson = new Gson(); Student student = gson.fromJson(jsonString, Student.class); System.out.println("Name: " + student.getName()); System.out.println("Age: " + student.getAge()); System.out.println("Student ID: " + student.getStudentId()); } } The above code code converts the JSON string back into a `Student` object. GSON Annotations GSON provides various annotations to customize the serialization and deserialization processes: @SerializedName Allows you to specify a custom name for the JSON key. For example: Java public class Person { @SerializedName("full_name") private String name; private int age; // getters and setters } In this example, the `@SerializedName` annotation changes the JSON key to "full_name" instead of "name." @Expose Controls field inclusion and exclusion during serialization and deserialization. For example: Java import com.google.gson.annotations.Expose; public class Person { @Expose private String name; @Expose(serialize = false) private int age; // getters and setters } The `age` field will be excluded during serialization due to `serialize = false.` @Since and @Until Specify version information for fields. For example: Java mport com.google.gson.annotations.Since; import com.google.gson.annotations.Until; public class Product { @Since(1.0) private String name; @Until(2.0) private double price; // getters and setters } In this case, the `name` field is included in versions 1.0 and above, while the `price` field is included until version 2.0. Object-Oriented Programming in Gson Object-oriented programming (OOP) is a programming paradigm that revolves around the concept of "objects." In this paradigm, objects are the basic building blocks of software development. An object is an instance of a class, which is a blueprint that defines the structure and behavior of objects. The four main principles of OOP are encapsulation, inheritance, polymorphism, and abstraction. Encapsulation is the practice of hiding the implementation details of an object from the outside world. Inheritance is the ability of objects to inherit properties and methods from their parent class. Polymorphism is the ability of objects to take on multiple forms, allowing different objects to be treated as if they were the same. Abstraction is the process of focusing on essential features of an object while ignoring its non-essential details. In addition to these principles, object-oriented programming concepts can be applied to objects in the serialization and deserialization process. Serialization is the process of transforming an object into a format that can be easily stored or transmitted. Deserialization is the process of transforming a serialized object back into its original form. When working with GSON, the principles of OOP can be used to ensure that serialized and deserialized objects are consistent with their original form. Let's dive into polymorphism and inheritance in GSON: Inheritance With GSON in Java Inheritance is a fundamental concept in Object-Oriented Programming. It allows a subclass or child class to inherit attributes and behaviors from a superclass or parent class. When working with GSON in Java, it is essential to understand how inheritance can be managed during serialization and deserialization. For instance, suppose we have a base class called Vehicle and two subclasses, Car and Motorcycle. In that case, we need to explore how GSON handles the serialization and deserialization of these classes: Java class Vehicle { private String type; // Constructors, getters, setters, and other methods omitted for brevity @Override public String toString() { return "Vehicle{" + "type='" + type + '\'' + '}'; } } class Car extends Vehicle { private int numberOfDoors; // Constructors, getters, setters, and other methods omitted for brevity @Override public String toString() { return "Car{" + "type='" + getType() + '\'' + ", numberOfDoors=" + numberOfDoors + '}'; } } class Motorcycle extends Vehicle { private boolean hasSidecar; // Constructors, getters, setters, and other methods omitted for brevity @Override public String toString() { return "Motorcycle{" + "type='" + getType() + '\'' + ", hasSidecar=" + hasSidecar + '}'; } } public class InheritanceWithGsonExample { public static void main(String[] args) { // Creating instances of Car and Motorcycle Car car = new Car(); car.setType("Car"); car.setNumberOfDoors(4); Motorcycle motorcycle = new Motorcycle(); motorcycle.setType("Motorcycle"); motorcycle.setHasSidecar(true); // Using Gson for serialization Gson gson = new Gson(); String carJson = gson.toJson(car); String motorcycleJson = gson.toJson(motorcycle); System.out.println("Car JSON: " + carJson); System.out.println("Motorcycle JSON: " + motorcycleJson); // Using Gson for deserialization Car deserializedCar = gson.fromJson(carJson, Car.class); Motorcycle deserializedMotorcycle = gson.fromJson(motorcycleJson, Motorcycle.class); System.out.println("Deserialized Car: " + deserializedCar); System.out.println("Deserialized Motorcycle: " + deserializedMotorcycle); } } The code above demonstrates a class hierarchy with inheritance and serialization/deserialization using GSON. The Vehicle class is the base class with a common attribute called "type". The Car and Motorcycle classes are subclasses of Vehicles that inherit the "type" attribute and have additional attributes specific to each type of vehicle. The InheritanceWithGsonExample class showcases the serialization and deserialization of Car and Motorcycle objects using Gson. During serialization, GSON automatically includes fields from the superclass, and during deserialization, it correctly reconstructs the class hierarchy. As a result, the output JSON will contain both the attributes from the subclass and its superclass. Polymorphism With GSON in Java Polymorphism is a crucial concept in Object-Oriented Programming (OOP). It enables objects of different types to be treated as if they were objects of a shared type. GSON utilizes the `@JsonSubTypes` annotation to support polymorphism and the `RuntimeTypeAdapterFactory` class. To better understand this concept, let's consider an example with an interface called `Shape,` which includes two implementing classes, `Circle` and `Rectangle`: Java import com.google.gson.Gson; import com.google.gson.GsonBuilder; interface Shape { double calculateArea(); } class Circle implements Shape { private double radius; // Constructors, getters, setters, and other methods omitted for brevity @Override public double calculateArea() { return Math.PI * Math.pow(radius, 2); } } class Rectangle implements Shape { private double length; private double width; // Constructors, getters, setters, and other methods omitted for brevity @Override public double calculateArea() { return length * width; } } public class PolymorphismWithGsonExample { public static void main(String[] args) { // Creating instances of Circle and Rectangle Circle circle = new Circle(); circle.setRadius(5); Rectangle rectangle = new Rectangle(); rectangle.setLength(4); rectangle.setWidth(6); // Using Gson with RuntimeTypeAdapterFactory for polymorphism Gson gson = new GsonBuilder() .registerTypeAdapterFactory(RuntimeTypeAdapterFactory .of(Shape.class, "type") .registerSubtype(Circle.class, "circle") .registerSubtype(Rectangle.class, "rectangle")) .create(); // Serialization String circleJson = gson.toJson(circle, Shape.class); String rectangleJson = gson.toJson(rectangle, Shape.class); System.out.println("Circle JSON: " + circleJson); System.out.println("Rectangle JSON: " + rectangleJson); // Deserialization Shape deserializedCircle = gson.fromJson(circleJson, Shape.class); Shape deserializedRectangle = gson.fromJson(rectangleJson, Shape.class); System.out.println("Deserialized Circle Area: " + deserializedCircle.calculateArea()); System.out.println("Deserialized Rectangle Area: " + deserializedRectangle.calculateArea()); } } The provided code showcases the implementation of the Shape Interface, which serves as a common type for various shapes, featuring a method named calculateArea(). The code also includes the Circle Class and Rectangle Class, which implement the Shape interface and provide their specific implementations of the calculateArea() method. Additionally, the PolymorphismWithGsonExample Class demonstrates how to serialize and deserialize Circle and Rectangle objects using GSON with a RuntimeTypeAdapterFactory. The RuntimeTypeAdapterFactory allows GSON to include type information in the JSON representation, ensuring that objects of different types that implement the common Shape interface can be deserialized correctly. Conclusion GSON is a popular Java library that provides easy-to-use APIs to serialize and deserialize Java objects to and from JSON (JavaScript Object Notation) format. One of the key features of GSON is its ability to handle inheritance and polymorphism seamlessly in Java. In object-oriented programming, inheritance is a mechanism that allows a new class to be based on an existing class, inheriting its properties and methods. Polymorphism, however, will enable objects of different types to be treated as if they were of the same type based on their common interface or superclass. When working with object-oriented code that involves class hierarchies and interface implementations, GSON can be a powerful tool. It can automatically handle the serialization and deserialization of objects with a common superclass or interface. This means you don't need to write any custom code to handle the serialization and deserialization of objects with different types that share common properties. For example, suppose you have a class hierarchy that consists of a base class and several derived classes. Each derived class has additional properties and methods that are specific to that class. With GSON, you can serialize and deserialize objects of any of these classes, and GSON will automatically handle the details of the inheritance and polymorphism for you. In conclusion, GSON is a valuable tool for working with object-oriented code that involves inheritance and polymorphism. It can save time and effort when serializing and deserializing objects with different types that share common properties.
In the world of software development, effectively managing resource consumption and ensuring fair usage of services are vital considerations for building scalable and robust applications. Throttling, the practice of controlling the rate at which certain operations are performed, emerges as a crucial mechanism for achieving these objectives. In this article, we'll delve into various ways to implement throttling in Java, presenting diverse strategies with practical examples. Disclaimer: In this article, I focus on uncomplicated single-threaded illustrations to address fundamental scenarios. Understanding Throttling Throttling involves regulating the frequency at which certain actions are allowed to occur. This is particularly important in scenarios where the system needs protection from abuse, demands resource management, or requires fair access to shared services. Common use cases for throttling include rate-limiting API requests, managing data updates, and controlling access to critical resources. Simple Blocking Rate Limiter With thread.sleep() - Not Use in Production! A straightforward approach to implement throttling is by using the Thread.sleep() method to introduce delays between consecutive operations. While this method is simple, it may not be suitable for high-performance scenarios due to its blocking nature. Java public class SimpleRateLimiter { private long lastExecutionTime = 0; private long intervalInMillis; public SimpleRateLimiter(long requestsPerSecond) { this.intervalInMillis = 1000 / requestsPerSecond; } public void throttle() throws InterruptedException { long currentTime = System.currentTimeMillis(); long elapsedTime = currentTime - lastExecutionTime; if (elapsedTime < intervalInMillis) { Thread.sleep(intervalInMillis - elapsedTime); } lastExecutionTime = System.currentTimeMillis(); // Perform the throttled operation System.out.println("Throttled operation executed at: " + lastExecutionTime); } } In this example, the SimpleRateLimiter class allows a specified number of operations per second. If the time elapsed between operations is less than the configured interval, it introduces a sleep duration to achieve the desired rate. Basic Throttling with wait Let's start with a simple example that we use wait to throttle the execution of a method. The goal is to allow the method to be invoked only after a certain cooldown period has elapsed. Java public class BasicThrottling { private final Object lock = new Object(); private long lastExecutionTime = 0; private final long cooldownMillis = 5000; // 5 seconds cooldown public void throttledOperation() throws InterruptedException { synchronized (lock) { long currentTime = System.currentTimeMillis(); long elapsedTime = currentTime - lastExecutionTime; if (elapsedTime < cooldownMillis) { lock.wait(cooldownMillis - elapsedTime); } lastExecutionTime = System.currentTimeMillis(); // Perform the throttled operation System.out.println("Throttled operation executed at: " + lastExecutionTime); } } } In this example, the throttledOperation method uses the wait method to make the thread wait until the cooldown period elapses. Dynamic Throttling With Wait and Notify Let's enhance the previous example to introduce dynamic throttling, where the cooldown period can be adjusted dynamically. Production must have an opportunity to make a change in flight. Java public class DynamicThrottling { private final Object lock = new Object(); private long lastExecutionTime = 0; private long cooldownMillis = 5000; // Initial cooldown: 5 seconds public void throttledOperation() throws InterruptedException { synchronized (lock) { long currentTime = System.currentTimeMillis(); long elapsedTime = currentTime - lastExecutionTime; if (elapsedTime < cooldownMillis) { lock.wait(cooldownMillis - elapsedTime); } lastExecutionTime = System.currentTimeMillis(); // Perform the throttled operation System.out.println("Throttled operation executed at: " + lastExecutionTime); } } public void setCooldown(long cooldownMillis) { synchronized (lock) { this.cooldownMillis = cooldownMillis; lock.notify(); // Notify waiting threads that cooldown has changed } } public static void main(String[] args) { DynamicThrottling throttling = new DynamicThrottling(); for (int i = 0; i < 10; i++) { try { throttling.throttledOperation(); // Adjust cooldown dynamically throttling.setCooldown((i + 1) * 1000); // Cooldown increases each iteration } catch (InterruptedException e) { e.printStackTrace(); } } } } In this example, we introduce the setCooldown method to dynamically adjust the cooldown period. The method uses notify to wake up any waiting threads, allowing them to check the new cooldown period. Using Java's Semaphore Java's Semaphore class can be employed as a powerful tool for throttling. A semaphore maintains a set of permits, where each acquire operation consumes a permit, and each release operation adds one. Java public class SemaphoreRateLimiter { private final Semaphore semaphore; public SemaphoreRateLimiter(int permits) { this.semaphore = new Semaphore(permits); } public boolean throttle() { if (semaphore.tryAcquire()) { // Perform the throttled operation System.out.println("Throttled operation executed. Permits left: " + semaphore.availablePermits()); return true; } else { System.out.println("Request throttled. Try again later."); return false; } } public static void main(String[] args) { SemaphoreRateLimiter rateLimiter = new SemaphoreRateLimiter(5); // Allow 5 operations concurrently for (int i = 0; i < 10; i++) { rateLimiter.throttle(); } } } In this example, the SemaphoreRateLimiter class uses a Semaphore with a specified number of permits. The throttle method attempts to acquire a permit and allows the operation if successful. Multiple Examples From Box There are multiple simple solutions provided by frameworks like Spring or Redis. Spring AOP for Method Throttling Using Spring's Aspect-Oriented Programming (AOP) capabilities, we can create a method-level throttling mechanism. This approach allows us to intercept method invocations and apply throttling logic. Java @Aspect @Component public class ThrottleAspect { private Map<String, Long> lastInvocationMap = new HashMap<>(); @Pointcut("@annotation(throttle)") public void throttledOperation(Throttle throttle) {} @Around("throttledOperation(throttle)") public Object throttleOperation(ProceedingJoinPoint joinPoint, Throttle throttle) throws Throwable { String key = joinPoint.getSignature().toLongString(); if (!lastInvocationMap.containsKey(key) || System.currentTimeMillis() - lastInvocationMap.get(key) > throttle.value()) { lastInvocationMap.put(key, System.currentTimeMillis()); return joinPoint.proceed(); } else { throw new ThrottleException("Request throttled. Try again later."); } } } In this example, we define a custom @Throttle annotation and an AOP aspect (ThrottleAspect) to intercept methods annotated with @Throttle. The ThrottleAspect checks the time elapsed since the last invocation and allows or blocks the method accordingly. Using Guava RateLimiter Google's Guava library provides a RateLimiter class that simplifies throttling implementation. It allows defining a rate at which operations are permitted. Let's see how we can use RateLimiter for method throttling: Java import com.google.common.util.concurrent.RateLimiter; @Component public class ThrottledService { private final RateLimiter rateLimiter = RateLimiter.create(5.0); // Allow 5 operations per second @Throttle public void throttledOperation() { if (rateLimiter.tryAcquire()) { // Perform the throttled operation System.out.println("Throttled operation executed."); } else { throw new ThrottleException("Request throttled. Try again later."); } } } In this example, we use Guava's RateLimiter to control the rate of execution of the throttledOperation method. The tryAcquire method is used to check if an operation is allowed based on the defined rate. Redis as a Throttling Mechanism Using an external data store like Redis, we can implement a distributed throttling mechanism. This approach is particularly useful in a microservices environment where multiple instances need to coordinate throttling. Java @Component public class RedisThrottleService { @Autowired private RedisTemplate<String, String> redisTemplate; @Value("${throttle.key.prefix}") private String keyPrefix; @Value("${throttle.max.operations}") private int maxOperations; @Value("${throttle.duration.seconds}") private int durationSeconds; public void performThrottledOperation(String userId) { String key = keyPrefix + userId; Long currentCount = redisTemplate.opsForValue().increment(key); if (currentCount != null && currentCount > maxOperations) { throw new ThrottleException("Request throttled. Try again later."); } if (currentCount == 1) { // Set expiration for the key redisTemplate.expire(key, durationSeconds, TimeUnit.SECONDS); } // Perform the throttled operation System.out.println("Throttled operation executed for user: " + userId); } } In this example, we use Redis to store and manage the count of operations for each user. The performThrottledOperation method increments the count and checks whether the allowed limit has been reached. Conclusion Throttling plays a pivotal role in maintaining the stability and scalability of applications. In this article, we explored diverse ways to implement throttling in Java, ranging from simple techniques using Thread.sleep() and Semaphore to apply solutions from the box. The choice of throttling strategy depends on factors such as the nature of the application, performance requirements, and the desired level of control. When implementing throttling, it's essential to strike a balance between preventing abuse and ensuring a responsive and fair user experience. As you integrate throttling mechanisms into your applications, consider monitoring and adjusting parameters based on real-world usage patterns. Several inquiries may arise when deciding on a throttling implementation, such as how to handle situations where a task exceeds the allotted period. In the upcoming article, I plan to explore robust Java implementations that address various scenarios comprehensively.
As one of the most important aspects of modern business applications and services, the security of the Java enterprise-grade applications didn't wait for the Jakarta EE 10 outbreak. Starting from the first releases of J2EE in early Y2K, security was the crux of enterprise software architecture. It evolved little by little with the gradual development of specifications, but the JSR-375 as we know it today appeared a couple of years ago with Jakarta EE 8, under the name of Java EE Security API 1.0. The current release of the Jakarta EE 10 comes with a major update of Java EE Security API under its new name: Jakarta Security 3.0. The Jakarta Security specifications are organized around a new terminology defined by the following new concepts: Authentication mechanisms: Invoked by callers to obtain their credentials and to validate them against the existing ones in identity stores Caller: Principal (user or service) originating a call to the API Identity store: Software component that controls access to the API through credentials, roles groups, and permissions The Jakarta Security interacts with other 2 important specifications, as follows: Jakarta Authorization (formerly JSR-115: JACC - Java Authorization Contracts for Containers) Jakarta Authentication (formerly JASPIC - Java Authentication SPI for Containers) The concept of authorization mechanism, as defined by the Jakarta Security specifications, designates controllers that interact with a caller and a container environment to obtain credentials, validate them, and pass an authenticated identity (such as users of group names) to the container. In order to validate the credentials, the authorization mechanisms use identity stores. The specifications define built-in identity stores for files, RDBMS (Relational Data Base Management System) and LDAP (Lightweight Directory Access Protocol) servers, in addition to fully customized ones. In this blog, we'll look at how to secure Java web applications using Jakarta Security built-in RDBMS and LDAP-based identity stores. We chose Payara as the Jakarta EE platform to illustrate this, but the process should be the same, whatever the Jakarta EE-compliant implementation might be. A Common Use Case The project that serves to exemplify our storyline can be found here. It is structured as a maven project having a separate module for each of the demonstrated built-in identity stores, as follows: An aggregator POM called jsr-375 A WAR artifact called servlet-with-ldap-identity-store, demonstrating the LDAP built-in identity store A WAR artifact called servlet-with-jdbc-identity-store, demonstrating the LDAP built-in identity store An infrastructure project called platform, which relies on testcontainers in order to run two instances of the Payara Platform, a Server and a Micro, each one having deployed to it the two WARs referenced above The Infrastructure As explained above, our sample application is deployed on the Payara Server as well as on the Payara Micro. In order to do this, we're running two Docker containers: one for the Payara Server instance and one for the Payara Micro one. We need to orchestrate these containers; hence, we'll be using the docker-compose utility. Here is an excerpt of the associated YAML file: YAML version: '3.6' services: payara-micro: container_name: payara-micro image: payara/micro:latest ports: - 28080:8080 - 26900:6900 expose: - 8080 - 6900 volumes: - ../../../../servlet-with-ldap-identity-store/target/servlet-with-ldap-identity-store.war:/opt/payara/deployments/servlet-with-ldap-identity-store.war - ../../../../servlet-with-jdbc-identity-store/target/servlet-with-jdbc-identity-store.war:/opt/payara/deployments/servlet-with-jdbc-identity-store.war payara-full: container_name: payara-full image: payara/server-full:latest ports: - 18080:8080 - 18081:8081 - 14848:4848 - 19009:9009 expose: - 8080 - 8081 - 4848 - 9009 volumes: - ../../../../servlet-with-ldap-identity-store/target/servlet-with-ldap-identity-store.war:/opt/payara/deployments/servlet-with-ldap-identity-store.war - ../../../../servlet-with-jdbc-identity-store/target/servlet-with-jdbc-identity-store.war:/opt/payara/deployments/servlet-with-jdbc-identity-store.war - ./scripts/init.sql:/opt/payara/init.sql As we can see in the docker-compose.yaml file above, the following services are started as Docker containers: A service named payara-micro listening for HTTP connexions on the TCP port 28080 A service named payara-full listening for HTTP connexions on the TCP port 18080 Note that the two Payara services are mounting WARs to the container's deployment directory. This has the effect of deploying the given WARs. Note also that the service payara-full - which runs the Payara Server and, consequently, hosts the H2 database instance - also mounts the SQL script init.sql, which will be run in order to create and initialize the H2 schema required for the use of our identity store. Accordingly, it is the H2 database instance hosted by the Payara Server that will be used by both payara-full and payara-micro services. In order to run the docker-compose commands to start these services we're using the docker-compose-maven-plugin. Here is an excerpt of the associated POM: XML ... <plugin> <groupId>com.dkanejs.maven.plugins</groupId> <artifactId>docker-compose-maven-plugin</artifactId> <inherited>false</inherited> <executions> <execution> <id>up</id> <phase>install</phase> <goals> <goal>up</goal> </goals> <configuration> <composeFile>${project.basedir}/src/main/resources/docker-compose.yml</composeFile> <detachedMode>true</detachedMode> <removeOrphans>true</removeOrphans> </configuration> </execution> <execution> <id>down</id> <phase>clean</phase> <goals> <goal>down</goal> </goals> <configuration> <composeFile>${project.basedir}/src/main/resources/docker-compose.yml</composeFile> <removeVolumes>true</removeVolumes> <removeOrphans>true</removeOrphans> </configuration> </execution> </executions> </plugin> ... Here we bind the up operation to the install phase and the down one to the clean phase. This way we'll get the containers running by executing mvn install and we'll stop and remove them with mvn clean. The RDBMS Identity Store The module servlet-with-jdbc-identity-store, which is the one that interests us here, is organized around the following classes: JdbcIdentityStoreConfig: This is the configuration class. JdbcIdentitySoreServlet: This is a servlet demonstrating the database identity stored-based authentication. JdbcSetup: This class is setting up the identity store required schema. Let's have a more detailed view of each of these classes. The Class JdbcIdentityStoreConfig This class defines the configuration of our RDBMS identity store. The idea behind the RDBMS identity store is that the principal-related information is stored in a relational database. In our example, this database is the H2 instance that comes with the Payara Platform. This is an in-memory database, used here for the sake of simplicity. Of course, such a design shouldn't be reproduced in production where more production-ready databases, like Oracle, PostgreSQL, or MySQL should be used. In any case, the H2 schema is created and initialized by the JdbcSetup class, as it will be explained in a moment. The listing below shows an excerpt of the code: Java @ApplicationScoped @BasicAuthenticationMechanismDefinition(realmName="admin-realm") @DatabaseIdentityStoreDefinition( dataSourceLookup = "${'java:global/H2'}", callerQuery = "select password from caller where name = ?", groupsQuery = "select group_name from caller_groups where caller_name = ?" ) public class JdbcIdentityStoreConfig{} As we can see, our class is a CDI (Context and Dependency Injection) bean, having the application scope. The annotation @DatabaseIdentityStoreDefinition is the new Jakarta EE one, defining the database identity store mechanism. The argument named dataSourceLookup declares a JNDI (Java Name and Directory Interface) lookup name which will bring the associated data source definition. Once this data source reference is found, we'll execute the two defined SQL queries, callerQuery and groupsQuery, in order to find the caller credentials; i.e., its identifier and password, as well as its group membership. The notion of caller here is somehow equivalent to the one of user: while being a less human connotation, as it could also be a service. Hence, the use of the pronoun "it." But the most interesting thing to be noticed is the fact that we're using here the HTTP basic authentication mechanism, defined by the @BasicAuthenticationMechanismDefinition annotation. This means that at the application startup, we'll be presented with a login screen and challenged to authenticate with a username and password. This information will be further transmitted to the database identity store mechanism which will compare them with the ones stored in the database. This way we're composing two JSR-375 security features, the HTTP basic authentication associated with the database Identity Store. I'm leaving to the sovereign appraisal of the reader this facility which saves several dozens of lines of code. The Class JdbcIdentityStoreServlet This class is a servlet to which access is authorized to any caller having the role of admin-role. This is defined through the Jakarta EE annotation @ServletSecurity. Another specific Jakarta EE annotation is @DeclareRoles which allows for the enumeration of all the possible roles that the application should be aware of. The Class JdbcSetup This class is responsible for the creation and initialization of the data model required by the database identity store mechanism. In its @PostConstruct method, it creates two database tables named caller and, respectively, caller_groups. Then, these tables are initialized with caller names, passwords, and group names. The caller named admin is then attached to the groups admin-role and user-role while the caller named user is only a member of the group user-roles. It should be noted that the password is stored in the database as hashed. The Jakarta Security specifications define the interface Pbkdf2PasswordHash having a default implementation based on the PBKDF2WithHmacSHA256 algorithm. This implementation can be simply injected, as you can see, in the associated source code. Here we are using the default implementation which is largely satisfactory for our example. Other more secure hash algorithms may be used as well, and, in this case, the Pbkdf2PasswordHash default implementation may be initialized by passing to it a map containing the algorithm name as well as parameters like the salt, the number of iterations, etc. The Jakarta EE documentation presents all these details in extenso. Another thing to mention is the fact that using Java JDBC (Java Data Base Connectivity) code to initialize the database in a runtime singleton @PostConstruct method is probably not the most elegant way to deal with SQL. The in-memory H2 database used here accepts on its JDBC connection string the argument named "run script from," allowing us to define the script to be run in order to initialize the database. Accordingly, instead of doing that in Java JDBC code and having to provide a dedicated EJB (Enterprise JavaBeans) for this purpose, we could have had an initialization SQL script run automatically at the deployment time. Additionally, in order to deal with the password hashing, we could have dealt with the HASH function that H2 provides in its more recent releases. However, the Payara Platform comes with an older release of the H2 database, which doesn't support this feature. Accordingly, to save ourselves the burden of having to upgrade the H2 database release that comes with the Payara Platform, we finally preferred this simpler alternative. Running the Example In order to run our example, proceed as follows: Execute the command mvn clean install. This command will stop the Docker containers, if they are running, and starts new instances. It also will run the integration tests that should succeed. The integration test already tested the service in a Docker container started with testcontainers. But you can now test it on more production-ready containers, like the one managed by the platform Maven module. You can run commands like the below ones to test on Payara Server and, respectively, on Payara Micro: Shell curl http://localhost:18080/servlet-with-jdbc-identity-store/secured -u "admin:passadmin" Shell curl http://localhost:28080/servlet-with-jdbc-identity-store/secured -u "admin:passadmin" The LDAP Identity Store Using relational databases to store security principal-related information is a quite common practice; however, these databases aren't exactly the right tool for such use cases. More often than not, organizations use Microsoft ActiveDirectory to store users, groups, and roles-related information together with their associated credentials and other information. While we could have used in our example ActiveDirectory or any other similar LDAP implementation (for example, Apache DS), such an infrastructure would have been too heavy and complex. Hence, in order to avoid that, we preferred to use an in-memory LDAP server. There are several open-source LDAP in-memory implementations, among which one of the most suitable is UnboundID LDAP SDK for Java. In order to use it, all we need is a dedicated Maven plugin, as shown below: XML <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> </dependency> We also need to define our schema in an LDIF (LDAP Data Interchange Format) file that will be loaded into the in-memory directory. For example, we define two principals named admin and, respectively, user. The admin principal has the roles admin-role and user-role, while the user principal has only the user-role one. Here is the required LDIF notation: Plain Text ... dn: uid=admin,ou=caller,dc=payara,dc=fish objectclass: top objectclass: uidObject objectclass: person uid: admin cn: Administrator sn: Admin userPassword: passadmin dn: uid=user,ou=caller,dc=payara,dc=fish objectclass: top objectclass: uidObject objectclass: person uid: user cn: User sn: User userPassword: passuser ... dn: cn=admin-role,ou=group,dc=payara,dc=fish objectclass: top objectclass: groupOfNames cn: admin-role member: uid=admin,ou=caller,dc=payara,dc=fish dn: cn=user-role,ou=group,dc=payara,dc=fish objectclass: top objectclass: groupOfNames cn: user-role member: uid=admin,ou=caller,dc=payara,dc=fish member: uid=user,ou=caller,dc=payara,dc=fish ... The module servlet-with-ldap-identity-store, which is the one that interests us here, is organized around the following classes: LdapIdentityStoreConfig: This is the configuration class. LdapIdentitySoreServlet: This is a servlet demonstrating the database identity store-based authentication. LdapSetup: This class is setting up the identity store required schema. Let's have a more detailed view of each of these classes. The Class LdapIdentityStoreConfig This class defines the configuration of our LDAP-based identity store. Here is a code excerpt: Java @ApplicationScoped @BasicAuthenticationMechanismDefinition(realmName="admin-realm") @LdapIdentityStoreDefinition( url = "ldap://localhost:33389", callerBaseDn = "ou=caller,dc=payara,dc=fish", groupSearchBase = "ou=group,dc=payara,dc=fish") public class LdapIdentityStoreConfig{} As already mentioned, we're using the HTTP basic authentication. This is quite convenient as the browser will display a login screen allowing you to type in the user name and the associated password. Furthermore, these credentials will be used in order to authenticate against the ones stored in our LDAP service, listening for connections on the container's 33389 TCP port. The callerBaseDN argument defines, as its name implies the distinguished name of the caller, while the groupSearchBase one defines the LDAP query required in order to find the groups to which a user belongs. The Class LdapIdentityStoreServlet Our servlet is a protected one, authorized solely for principals having the role admin-role. Java @WebServlet("/secured") @DeclareRoles({ "admin-role", "user-role" }) @ServletSecurity(@HttpConstraint(rolesAllowed = "admin-role")) public class LdapIdentityStoreServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.getWriter().write("This is a secured servlet \n"); Principal principal = request.getUserPrincipal(); String user = principal == null ? null : principal.getName(); response.getWriter().write("User name: " + user + "\n"); response.getWriter().write("\thas role \"admin-role\": " + request.isUserInRole("admin-role") + "\n"); response.getWriter().write("\thas role \"user-role\": " + request.isUserInRole("user-role") + "\n"); } } We're using the @WebServlet annotation in order to declare our class as a servlet. The @ServletSecurity annotation means here that only users having the role admin-role are allowed. The Class LdapSetup Last but not least, the class LdapSetup instantiates and initializes the in-memory LDAP service: Java @Startup @Singleton public class LdapSetup { private InMemoryDirectoryServer directoryServer; @PostConstruct public void init() { try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=fish"); config.setListenerConfigs( new InMemoryListenerConfig("myListener", null, 33389, null, null, null)); directoryServer = new InMemoryDirectoryServer(config); directoryServer.importFromLDIF(true, new LDIFReader(this.getClass().getResourceAsStream("/users.ldif"))); directoryServer.startListening(); } catch (LDAPException e) { throw new IllegalStateException(e); } } @PreDestroy public void destroy() { directoryServer.shutDown(true); } } This is a Startup (runs automatically at the start-up) CDI bean, having the Singleton scope. It instantiates the in-memory directory server by loading the LDIF file shown above and starts listening for LDAP requests on the TCP port number 33389 of the localhost. Testing An integration test is provided to be executed with the failsafe Maven plugin. During Maven's integration test phase, this integration test uses testcontainers to create a Docker container running a Payara Micro image and deploy to it our WAR. Here is an excerpt from the integration test with testcontainers: @Container private static GenericContainer payara = new GenericContainer("payara/micro:latest") .withExposedPorts(8080) .withCopyFileToContainer(MountableFile.forHostPath( Paths.get("target/servlet-with-ldap-identity-store.war") .toAbsolutePath(), 0777), "/opt/payara/deployments/test.war") .waitingFor(Wait.forLogMessage(".* Payara Micro .* ready in .*\\s", 1)) .withCommand( "--noCluster --deploy /opt/payara/deployments/test.war --contextRoot /test"); Here we create a Docker container running the image payara/micro:latest and exposing the TCP port 8080. We also copy to the image the WAR that we just built during Maven's package phase, and, finally, we start the container. Since Payara Micro might need a couple of seconds in order to start, we need to wait until it has fully booted. There are several ways to wait for the server boot to complete but here we use the one consisting of scanning the log file until a message containing "Payara Micro is ready" is displayed. Last but not least, testing the deployed servlet is easy using the REST assured library, as shown below: Java @Test public void testGetSecuredPageShouldSucceed() throws IOException { given() .contentType(ContentType.TEXT) .auth().basic("admin", "passadmin") .when() .get(uri) .then() .assertThat().statusCode(200) .and() .body(containsString("admin-role")) .and() .body(containsString("admin-role")); } Running In order to run the applications proceed as follows: Execute the command mvn clean install. This command will stop the Docker containers, if they are running, and starts new instances. It also will run the integration test that should succeed. The integration test already tested the service in a Docker container started with testcontainers. But you can now test it on more production-ready containers, like the one managed by the platform Maven module. To test on the Payara Server, you can run commands like this: Shell curl http://localhost:18080/servlet-with-ldap-identity-store/secured -u "admin:passadmin" Run the below commands to test on Payara Micro. Shell curl http://localhost:28080/servlet-with-ldap-identity-store/secured -u "admin:passadmin" 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