2024 DevOps Lifecycle: Share your expertise on CI/CD, deployment metrics, tech debt, and more for our Feb. Trend Report (+ enter a raffle!).
Kubernetes in the Enterprise: Join our Virtual Roundtable as we dive into Kubernetes over the past year, core usages, and emerging trends.
Also known as the build stage of the SDLC, coding focuses on the writing and programming of a system. The Zones in this category take a hands-on approach to equip developers with the knowledge about frameworks, tools, and languages that they can tailor to their own build needs.
A framework is a collection of code that is leveraged in the development process by providing ready-made components. Through the use of frameworks, architectural patterns and structures are created, which help speed up the development process. This Zone contains helpful resources for developers to learn about and further explore popular frameworks such as the Spring framework, Drupal, Angular, Eclipse, and more.
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.
JavaScript (JS) is an object-oriented programming language that allows engineers to produce and implement complex features within web browsers. JavaScript is popular because of its versatility and is preferred as the primary choice unless a specific function is needed. In this Zone, we provide resources that cover popular JS frameworks, server applications, supported data types, and other useful topics for a front-end engineer.
Programming languages allow us to communicate with computers, and they operate like sets of instructions. There are numerous types of languages, including procedural, functional, object-oriented, and more. Whether you’re looking to learn a new language or trying to find some tips or tricks, the resources in the Languages Zone will give you all the information you need and more.
Development and programming tools are used to build frameworks, and they can be used for creating, debugging, and maintaining programs — and much more. The resources in this Zone cover topics such as compilers, database management systems, code editors, and other software tools and can help ensure engineers are writing clean code.
Development at Scale
As organizations’ needs and requirements evolve, it’s critical for development to meet these demands at scale. The various realms in which mobile, web, and low-code applications are built continue to fluctuate. This Trend Report will further explore these development trends and how they relate to scalability within organizations, highlighting application challenges, code, and more.
Building Robust Real-Time Data Pipelines With Python, Apache Kafka, and the Cloud
In the previous articles, you learned about the virtual threads in Java 21 in terms of history, benefits, and pitfalls. In addition, you probably got inspired by how Quarkus can help you avoid the pitfalls but also understood how Quarkus has been integrating the virtual threads to Java libraries as many as possible continuously. In this article, you will learn how the virtual thread performs to handle concurrent applications in terms of response time, throughput, and resident state size (RSS) against traditional blocking services and reactive programming. Most developers including you and the IT Ops teams also wonder if the virtual thread could be worth replacing with existing business applications in production for high concurrency workloads. Performance Applications I’ve conducted the benchmark testing with the Todo application using Quarkus to implement 3 types of services such as imperative (blocking), reactive (non-blocking), and virtual thread. The Todo application implements the CRUD functionality with a relational database (e.g., PostgreSQL) by exposing REST APIs. Take a look at the following code snippets for each service and how Quarkus enables developers to implement the getAll() method to retrieve all data from the Todo entity (table) from the database. Find the solution code in this repository. Imperative (Blocking) Application In Quarkus applications, you can make methods and classes with @Blocking annotation or non-stream return type (e.g. String, List). Java @GET public List<Todo> getAll() { return Todo.listAll(Sort.by("order")); } Virtual Threads Application It’s quite simple to make a blocking application into a virtual thread application. As you see in the following code snippets, you just need to add a @RunOnVirtualThread annotation into the blocking service, getAll() method. Java @GET @RunOnVirtualThread public List<Todo> getAll() { return Todo.listAll(Sort.by("order")); } Reactive (Non-Blocking) Application Writing a reactive application should be a big challenge for Java developers when they need to understand the reactive programming model and the continuation and event stream handler implementation. Quarkus allows developers to implement both non-reactive and reactive applications in the same class because Quarkus is built on reactive engines such as Netty and Vert.x. To make an asynchronous reactive application in Quarkus, you can add a @NonBlocking annotation or set the return type with Uni or Multi in the SmallRye Mutiny project as below the getAll() method. Java @GET public Uni<List<Todo>> getAll() { return Panache.withTransaction(() -> Todo.findAll(Sort.by("order")).list()); } Benchmark scenario To make the test result more efficient and fair, we’ve followed the Techempower guidelines such as conducting multiple scenarios, running on bare metal, and containers on Kubernetes. Here is the same test scenario for the 3 applications (blocking, reactive, and virtual threads), as shown in Figure 1. Fetch all rows from a DB (quotes) Add one quote to the returned list Sort the list Return the list as JSON Figure 1: Performance test architecture Response Time and Throughput During the performance test, we’ve increased the concurrency level from 1200 to 4400 requests per second. As you expected, the virtual thread scaled better than worker threads (traditional blocking services) in terms of response time and throughput. More importantly, it didn’t outperform the reactive service all the time. When the concurrent level reached 3500 requests per second, the virtual threads went way slower and lower than the worker threads. Figure 2: Response time and throughput Resource Usage (CPU and RSS) When you design a concurrent application regardless of cloud deployment, you or your IT Ops team need to estimate the resource utilization and capacity along with high scalability. The CPU and RSS (resident set size) usage is a key metric to measure resource utilization. With that, when the concurrency level reached out to 2000 requests per second in CPU and Memory usage, the virtual threads turned rapidly higher than the worker threads. Figure 3: Resource usage (CPU and RSS) Memory Usage: Container Container runtimes (e.g., Kubernetes) are inevitable to run concurrent applications with high scalability, resiliency, and elasticity on the cloud. The virtual threads had lower memory usage inside the limited container environment than the worker thread. Figure 4: Memory usage - Container Conclusion You learned how the virtual threads performed in multiple environments in terms of response time, throughput, resource usage, and container runtimes. The virtual threads seem to be better than the blocking services on the worker threads all the time. But when you look at the performance metrics carefully, the measured performance went down than the blocking services at some concurrent levels. On the other hand, the reactive services on the event loops were always higher performed than both the virtual and worker threads all the time. Thus, the virtual thread can provide high enough performance and resource efficiency based on your concurrency goal. Of course, the virtual thread is still quite simple to develop concurrent applications without a steep learning curve as the reactive programming.
In this blog, you will learn how to monitor a Spring Boot application using Ostara. Ostara is a desktop application that monitors and manages your application. Enjoy! Introduction When an application runs in production (but also your other environments), it is wise to monitor its health. You want to make sure that everything is running without any problems, and the only way to know this is to measure the health of your application. When something goes wrong, you hopefully will be notified before your customer notices the problem, and maybe you can solve the problem before your customer notices anything. In a previous post, it was explained how to monitor your application using Spring Actuator, Prometheus, and Grafana. In this post, you will take a look at an alternative approach using Spring Actuator in combination with Ostara. The setup with Ostara is a bit easier; therefore, it looks like a valid alternative. The proof of the pudding is in the eating, so let’s try Ostara! The sources used in this blog are available on GitHub. Prerequisites The prerequisites needed for this blog are: Basic Spring Boot 3 knowledge; Basic Linux knowledge; Java 17 is used. Create an Application Under Test First, you need to create an application that you can monitor. Navigate to Spring Initializr and add the Spring Web and Spring Boot Actuator dependencies. Spring Web will be used to create two dummy Rest endpoints, and Spring Boot Actuator will be used to enable the monitor endpoints. See a previous post in order to get more acquainted with Spring Boot Actuator. The post is written for Spring Boot 2, but the contents are still applicable for Spring Boot 3. Add the git-commit-id-plugin to the pom file in order to be able to generate build information. Also, add the build-info goal to the executions of the spring-boot-maven-plugin in order to generate the information automatically during a build. See a previous post if you want to know more about the git-commit-id-plugin. XML <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>build-info</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>pl.project13.maven</groupId> <artifactId>git-commit-id-plugin</artifactId> <version>4.9.10</version> <executions> <execution> <id>get-the-git-infos</id> <goals> <goal>revision</goal> </goals> </execution> </executions> <configuration> <dotGitDirectory>${project.basedir}/.git</dotGitDirectory> <prefix>git</prefix> <verbose>false</verbose> <generateGitPropertiesFile>true</generateGitPropertiesFile> <generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename> <format>properties</format> <gitDescribe> <skip>false</skip> <always>false</always> <dirty>-dirty</dirty> </gitDescribe> </configuration> </plugin> </plugins> </build> Enable the full git information to the actuator endpoint in the application.properties. Properties files management.info.git.mode=full Add a Rest controller with two dummy endpoints. Java @RestController public class MetricsController { @GetMapping("/endPoint1") public String endPoint1() { return "Metrics for endPoint1"; } @GetMapping("/endPoint2") public String endPoint2() { return "Metrics for endPoint2"; } } Build the application. Shell $ mvn clean verify Run the application. Shell $ java -jar target/myostaraplanet-0.0.1-SNAPSHOT.jar Verify the endpoints. Shell $ curl http://localhost:8080/endPoint1 Metrics for endPoint1 $ curl http://localhost:8080/endPoint2 Metrics for endPoint2 Verify the actuator endpoint. Shell $ curl http://localhost:8080/actuator | python3 -mjson.tool ... { "_links": { "self": { "href": "http://localhost:8080/actuator", "templated": false }, "health": { "href": "http://localhost:8080/actuator/health", "templated": false }, "health-path": { "href": "http://localhost:8080/actuator/health/{*path}", "templated": true } } } Add Security The basics are in place now. However, it is not very secure. Let’s add authorization to the actuator endpoint. Beware that the setup in this paragraph is not intended for production usage. Add the Spring Security dependency to the pom. XML <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> Add the credentials and role to the application.properties file. Again, do not use this for production purposes. Properties files spring.security.user.name=admin spring.security.user.password=admin123 spring.security.user.roles=ADMIN Add a WebSecurity class, which adds the security layer to the actuator endpoint. Java @Configuration @EnableWebSecurity public class WebSecurity { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authz -> authz .requestMatchers("/actuator/**").hasRole("ADMIN") .anyRequest().permitAll()) .httpBasic(Customizer.withDefaults()); return http.build(); } } Build and start the application. Verify whether the actuator endpoint can be accessed using the credentials as specified. Shell $ curl http://localhost:8080/actuator -u "admin:admin123" | python3 -mjson.tool ... { "_links": { "self": { "href": "http://localhost:8080/actuator", "templated": false }, "health": { "href": "http://localhost:8080/actuator/health", "templated": false }, "health-path": { "href": "http://localhost:8080/actuator/health/{*path}", "templated": true } } } Install Ostara Navigate to the Ostara website and click the Download Ostara button. Choose the platform you are using (Linux 64bit, in my case), and the file Ostara-0.12.0.AppImage is downloaded. Double-click the file, and Ostara is started. That’s all! Monitor Application By default, only a limited set of actuator endpoints are enabled. Ostara will function with this limited set, but less information will be visible as a consequence. In order to see the full set of capabilities of Ostara, you enable all actuator endpoints. Again, beware of how much you expose in production. Properties files management.endpoints.web.exposure.include=* management.endpoint.health.show-details=always Before you continue using Ostara, you are advised to disable sending usage statistics and error information. Navigate to the settings (right top corner), choose Privacy, and disable the tracking options. In the left menu, choose Create Instance and fill in the fields as follows: Actuator URL Alias: MyFirstInstance Application Name: MyFirstApp Disable SSL Verification: Yes (for this demo, no SSL connection is used) Authentication Type: Basic Username and Password: the admin credentials Click the Test Connection button. This returns an unauthorized error, which appears to be a bug in Ostara because the credential information is correct. Ignore the error and click the Save button. Ostara can connect to the application, and the dashboard shows some basic status information. You can explore all the available information for yourself. Some of them are highlighted below. Info The Info page shows you the information which you made available with the help of the git-commit-id-plugin. App Properties The App Properties page shows you the application properties. However, as you can see in the below screenshot, all values are masked. This is the default Spring Boot 3 behavior. This behavior can be changed in application.properties of the Spring Boot Application. You can choose between always (not recommended), when-authorized or never. Properties files management.endpoint.configprops.show-values=when-authorized management.endpoint.env.show-values=when-authorized Build and start the application again. The values are visible. Metrics The Metrics page allows you to enable notifications for predefined or custom metrics. Open the http.server.requests metric and click the Add Metric Notification. Fill in the following in order to create a notification when EndPoint1 is invoked more than ten times, and click the Save button: Name: EndPoint 1 invoked > 10 times Type: Simple Tags: /endPoint1 Operation: Greater Than Value: 10 Invoke EndPoint1 more than ten times in a row. Wait for a minute, and the notification appears at the top of your main screen. Loggers The Loggers page shows you the available loggers, and you are able to change the desired log level. This is an interesting feature when you need to analyze a bug. Click the DEBUG button for the com.mydeveloperplanet.myostaraplanet.MetricsController. A message is shown that this operation is forbidden. The solution is to disable the csrf protection for the actuator endpoints. For more information about csrf attacks, see this blog. Add the following line to the WebSecurity class. Java public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authz -> authz .requestMatchers("/actuator/**").hasRole("ADMIN") .anyRequest().permitAll()) .csrf(csrf -> csrf .ignoringRequestMatchers("/actuator/**") ) .httpBasic(Customizer.withDefaults()); return http.build(); } Also, add some logging statements to the EndPoint1 code in order to verify the result. Java @RequestMapping("/endPoint1") public String endPoint1() { logger.debug("This is DEBUG message"); logger.trace("This is a TRACE message"); return "Metrics for endPoint1"; } Build and restart the application. Enable the DEBUG logging again for the MetricsController and invoke EndPoint1. The DEBUG statement is shown in the logs. Shell 2023-09-10T15:06:04.511+02:00 DEBUG 30167 --- [nio-8080-exec-8] c.m.myostaraplanet.MetricsController : This is DEBUG message Multiple Instances When you have multiple instances of your application, you can create another instance to monitor. Start another instance of the application on port 8081. Shell $ java -jar -Dserver.port=8081 target/myostaraplanet-0.0.1-SNAPSHOT.jar Hover over MyFirstApp and click the three dots menu. Choose Add Instance and fill in the following: Actuator URL Alias: MySecondInstance Clicking the Test Connection button is successful this time. Click the Save button. The second instance is added, and the application dashboard view shows the summary information. Conclusion Ostara is a good alternative for monitoring Spring Boot applications. The installation only requires you to download a file and start it. Ostara gives you a clear visual view, and the notifications notify you when something is wrong. It is also capable of starting thread profiling on your instance and downloading heap dumps. Compared to Grafana, Grafana has some more fancy graphs than Ostara. However, Ostara is not only a visualization tool; but you can also interact with your application and receive notifications when something goes wrong.
Python Flask is a popular framework for building web applications and APIs in Python. It provides developers with a quick and easy way to create RESTful APIs that can be used by other software applications. Flask is lightweight and requires minimal setup, making it a great choice for building small to medium-sized APIs. This makes Flask an ideal choice for developers looking to build robust and scalable APIs in Python. This example will review how to create a simple rest API Flask tutorial. Pre-Requisites Before we start, we must ensure that a couple of prerequisites are completed. To follow along and run this tutorial, you will need to: Install Python Have an IDE to edit your code Install Postman so that you can test the endpoint After all 3 of these prerequisites are completed, we can begin! Creating the Base Project To create the base project, the first thing we will do is create a folder named python-flask-api in your chosen directory. With the new folder created, we will open a terminal in the root of the folder so that commands can be executed to build and run our Python project. Once your terminal is pointed to the root directory of your project, run the following commands so you can initialize the Python Rest API Flask project and manage the dependencies. First, we will use pip to install Flask in the project directory. To do this, run the command below. Shell pip install Flask Writing the Code In our first line of code in the app.py file, we will import the modules for json, Flask, jsonify, and request. Python import json from flask import Flask, jsonify, request Next, we will create a new Flask application by adding the following code just below our import statements. Python app = Flask(__name__) Next, to give our API a little bit of data to work with, we will define an array of employee objects with an ID and name. Python employees = [ { 'id': 1, 'name': 'Ashley' }, { 'id': 2, 'name': 'Kate' }, { 'id': 3, 'name': 'Joe' }] To define our API endpoint, we will now add code to define a route for GET requests to the ‘/employees’ endpoint. This will return all employees (from our employees array defined above) in JSON format. Python @app.route('/employees', methods=['GET']) def get_employees(): return jsonify(employees) On top of our GET method, we will also define a route for POST, PUT, and DELETE methods as well. These functions can be used to create a new employee and update or delete the employee based on their given ID. Python @app.route('/employees', methods=['POST']) def create_employee(): global nextEmployeeId employee = json.loads(request.data) if not employee_is_valid(employee): return jsonify({ 'error': 'Invalid employee properties.' }), 400 employee['id'] = nextEmployeeId nextEmployeeId += 1 employees.append(employee) return '', 201, { 'location': f'/employees/{employee["id"]}' } @app.route('/employees/<int:id>', methods=['PUT']) def update_employee(id: int): employee = get_employee(id) if employee is None: return jsonify({ 'error': 'Employee does not exist.' }), 404 updated_employee = json.loads(request.data) if not employee_is_valid(updated_employee): return jsonify({ 'error': 'Invalid employee properties.' }), 400 employee.update(updated_employee) return jsonify(employee) @app.route('/employees/<int:id>', methods=['DELETE']) def delete_employee(id: int): global employees employee = get_employee(id) if employee is None: return jsonify({ 'error': 'Employee does not exist.' }), 404 employees = [e for e in employees if e['id'] != id] return jsonify(employee), 200 Once our code is complete, it should look like this: Python import json from flask import Flask, jsonify, request app = Flask(__name__) employees = [ { 'id': 1, 'name': 'Ashley' }, { 'id': 2, 'name': 'Kate' }, { 'id': 3, 'name': 'Joe' } ] nextEmployeeId = 4 3 @app.route('/employees', methods=['GET']) def get_employees(): return jsonify(employees) @app.route('/employees/<int:id>', methods=['GET']) def get_employee_by_id(id: int): employee = get_employee(id) if employee is None: return jsonify({ 'error': 'Employee does not exist'}), 404 return jsonify(employee) def get_employee(id): return next((e for e in employees if e['id'] == id), None) def employee_is_valid(employee): for key in employee.keys(): if key != 'name': return False return True @app.route('/employees', methods=['POST']) def create_employee(): global nextEmployeeId employee = json.loads(request.data) if not employee_is_valid(employee): return jsonify({ 'error': 'Invalid employee properties.' }), 400 employee['id'] = nextEmployeeId nextEmployeeId += 1 employees.append(employee) return '', 201, { 'location': f'/employees/{employee["id"]}' } @app.route('/employees/<int:id>', methods=['PUT']) def update_employee(id: int): employee = get_employee(id) if employee is None: return jsonify({ 'error': 'Employee does not exist.' }), 404 updated_employee = json.loads(request.data) if not employee_is_valid(updated_employee): return jsonify({ 'error': 'Invalid employee properties.' }), 400 employee.update(updated_employee) return jsonify(employee) @app.route('/employees/<int:id>', methods=['DELETE']) def delete_employee(id: int): global employees employee = get_employee(id) if employee is None: return jsonify({ 'error': 'Employee does not exist.' }), 404 employees = [e for e in employees if e['id'] != id] return jsonify(employee), 200 if __name__ == '__main__': app.run(port=5000) Lastly, we will add a line of code to run our Flask app. As you can see, we call the run method and get the Flask app running on port 5000. Python if __name__ == '__main__': app.run(port=5000) Running and Testing The Code With our code written and saved, we can start the app up. To run the app, in the terminal, we will execute the following pip command. Shell pip api.py Now, our API is up and running. You can send a test HTTP request through Postman. By sending a request to localhost:5000/employees. After the request is sent. you should see a 200 OK status code in the response along with an array of employees returned. For this test, no request body is needed for the incoming request. Wrapping Up With that, we’ve created a simple RESTful API Flask using Python. This code can then be expanded on as needed to build APIs for your applications.
In the ever-evolving software development landscape, staying up-to-date with the latest technologies is paramount to ensuring your applications' efficiency, security, and maintainability. As a stalwart in the world of programming languages, Java continues to transform to meet the demands of modern development practices. One such significant transition is the migration from Java 21 to Java 11. In this comprehensive article, we embark on a journey to explore the intricacies of migrating from the cutting-edge Java 21 to the robust and widely adopted Java 11. We will delve into this migration process, offering a step-by-step guide to demystify the complexities and facilitate a seamless transition for developers and organizations. Beyond the how-to aspect, we will unravel the benefits of embracing Java 11. From performance enhancements to long-term support, Java 11 brings many advantages that make the migration a strategic move for any Java-based project. Understanding these benefits is crucial for making informed decisions and optimizing your software development workflow. Moreover, we will dissect the reasons behind undertaking such a migration. Whether it's to leverage new features, adhere to industry standards, or ensure compatibility with a broader ecosystem, the motivations for moving to Java 11 are multifaceted. By gaining insights into these rationales, developers can align their objectives with the more general goals of their projects and organizations. This article is a comprehensive guide for developers, project managers, and decision-makers navigating the transition from Java 21 to Java 11. Join us as we explore the how-to, unlock the benefits, and uncover why this migration is a pivotal step in the ever-evolving landscape of Java development. The Case for Migrating to Java 21 In our journey towards understanding the imperative of migrating from Java 11 to Java 21, we dive deep into four key categories that underline the significance of this transition. Each facet is pivotal in its own right and collectively contributes to creating a compelling case for embracing the latest iteration of the Java programming language. 1. Security: Safeguarding Your Code Against CVE Vulnerabilities Security is a paramount concern in the ever-evolving landscape of software development. By upgrading to Java 21, developers can ensure their applications are shielded against potential vulnerabilities. CVE, or Common Vulnerabilities and Exposures, are standardized identifiers for known cybersecurity vulnerabilities. Upgrading to the latest Java version is a proactive measure to safeguard your codebase against potential threats, providing a more secure environment for your applications. 2. Framework Support: Aligning With Evolving Ecosystems Java has long been synonymous with powerful frameworks that streamline development processes. Frameworks like Spring, Quarkus, and Jakarta EE, pillars of the Java ecosystem, are actively moving towards Java 21. Consequently, continuing with Java 11 might leave your project without the crucial updates and support necessary for seamless integration with these frameworks. The necessity of staying aligned with evolving ecosystems propels the migration to Java 21, ensuring that your code remains compatible with the latest innovations and optimizations offered by these frameworks. 3. Java 21’s New Features: Enhancing Productivity With Innovation Java 21 brings many features that enhance developer productivity and code maintainability. Notable additions include the Record Pattern, which simplifies the creation of immutable classes, and the Sequence Collection, offering efficient and concise methods for working with sequences of elements. Embracing these features modernizes your codebase and equips developers with powerful tools to write cleaner, more efficient code. The journey to Java 21 is a quest for innovation and enhanced productivity in the ever-evolving software development landscape. 4. Java Performance: Unleashing the Power of Java 21 Java 21 doesn’t just bring new features; it also turbocharges the performance of your applications. As Per Minborg explores in the presentation, With Java 21, Your Code Runs Even Faster, But How is that Possible?, upgrading to Java 21 can significantly boost the speed of your code execution. Faster-running code enhances user experience and reduces throughput, potentially resulting in cost savings, especially in cloud environments. This session encourages developers to consider the tangible benefits of improved performance as a compelling reason to migrate to Java 21. As we conclude our exploration into the compelling reasons for migrating from Java 11 to Java 21, it becomes evident that this transition is not merely an upgrade but a strategic move toward a more secure, innovative, and performant future for your Java applications. By prioritizing security, we ensure our code is shielded against potential vulnerabilities, aligning with the best cybersecurity practices. The evolving landscape of frameworks necessitates our migration to Java 21, where ongoing support and optimizations await, unlocking new dimensions in software development. Java 21's innovative features empower developers to write cleaner, more efficient code, marking a paradigm shift in productivity. Additionally, the promise of enhanced performance, as explored in Per Minborg's insightful presentation, makes Java 21 a version upgrade and a tangible stride towards optimized code execution. In our next session, we embark on a practical journey—providing a step-by-step guide on navigating the migration process seamlessly. We will unravel the intricacies, address potential challenges, and equip you with the tools to ensure a smooth transition to Java 21. Join us as we dive into the practical aspects of migration, turning theory into actionable steps for a successful evolution in your Java development journey. Navigating the Migration – A Step-By-Step Guide Migrating from Java 11 to Java 21 may seem daunting, but fear not – we are here to guide you through a carefully curated, step-by-step process. The key to a successful migration is executing these steps, like playing the piano and taking gentle and deliberate baby steps to ensure precision and accuracy. In the world of migrations, it’s akin to dancing through the complexities – a meticulous choreography that allows you to backtrack if needed. 1. Upgrade Frameworks and Libraries: The first dance move involves upgrading your frameworks and libraries to versions compatible with Java 11. Take, for example, Spring Boot 2.7.x – make sure to upgrade to the latest version, such as 2.7.18. Don’t forget your dance partner, Lombok; upgrade it to version 1.18.30. This initial step sets the stage for compatibility, ensuring your codebase is ready for the subsequent moves. 2. Pipeline Inclusion for Java 21: Now, let’s choreograph your CI/CD pipeline. Add a step to ensure that your code is building seamlessly for both Java 11 and Java 21. Tests play a crucial role here, acting as the choreographer’s keen eye, preventing any unnoticed missteps. With this in place, you can gracefully advance to the next phase. 3. Compile and Execute With Java 21: The spotlight now turns to Java 21. Adjust your pipeline to compile and execute your code with Java 21, but without making any changes in the code itself. It’s like rehearsing a new routine – initially, stick to the familiar steps but perform them with a contemporary flair. GitOps acts as your backstage pass, providing visibility into every change making it easier to revert if needed. 4. Enable Java 21 Capabilities: As your code gracefully glides through the previous steps, it’s time to unlock the capabilities of Java 21. Adjust your compiler settings (-source and -target) to Java 21, whether you’re using Maven or Gradle. Now, your code runs efficiently and takes advantage of the innovative features Java 21 brings to the dance floor. 5. Continuous Maintenance and Library Updates: The dance doesn’t end with a flawless performance; it’s an ongoing routine. Regularly check for library updates and framework enhancements. Utilize tools like Dependabot to automate the process, ensuring your codebase stays synchronized with the latest and greatest. The era of waiting for years before tending to your garden is over – embrace a proactive approach, growing to your code weekly rather than annually. In conclusion, the migration from Java 11 to Java 21 is a choreographed dance executed precisely. By following this piano-piano approach, backed by GitOps and robust testing, you ensure a smooth transition and unlock the full potential of Java 21 for your applications. Keep dancing, keep evolving, and let your code shine on the modern stage of software development. Conclusion In this comprehensive exploration of migrating from Java 11 to Java 21, we've navigated through the compelling reasons, the strategic considerations, and the intricate dance steps required for a seamless transition. As we draw the curtain on this article, let's reflect on the key takeaways and the transformative journey ahead. The decision to migrate stems not just from a desire to stay current. Still, it is rooted in addressing security concerns, aligning with evolving frameworks, harnessing new Java features, and unlocking enhanced performance. It's a strategic move that transcends version numbers, paving the way for a more secure, innovative, and efficient future for your Java applications. The step-by-step guide serves as a roadmap, emphasizing the importance of a meticulous piano-piano approach. This methodical journey allows developers to tread carefully, ensuring that the codebase remains stable and resilient at every stage. GitOps is a reliable companion, offering visibility into every change and providing the safety net needed to revert if a misstep occurs. From upgrading frameworks and libraries to including Java 21 in your CI/CD pipeline and enabling its capabilities, each step is a deliberate move towards a more robust and modern codebase. It's a dance that embraces change and ensures that your applications are equipped to harness the full potential of Java 21. Moreover, the commitment to continuous maintenance and regular library updates is stressed as an ongoing responsibility. Tools like Dependabot are highlighted as allies in the journey, making it easier to keep your codebase synchronized with the ever-evolving landscape of Java development. As you embark on your migration journey, remember that this is more than a technical upgrade; it's an evolution. By adopting Java 21, you're not just updating your code; you're future-proofing your applications, ensuring they remain agile, secure, and ready to embrace the innovations. In the grand symphony of software development, migrating to Java 21 is like tuning your orchestra for a performance that resonates with the cadence of modernity. Embrace the evolution, dance through the steps, and let your code take center stage in the dynamic landscape of Java development.
Working in technology for over three decades, I have found myself in a position of getting up to speed on something new at least 100 times. For about half of my career, I have worked in a consulting role, which is where I faced the challenge of understanding a new project the most. Over time, I established a personal goal to be productive on a new project in half the time it took for the average team member. I often called this the time to first commit or TTFC. The problem with my approach to setting a TTFC record was the unexpected levels of stress that I endured in those time periods. Family members and friends always knew when I was in the early stages of a brand-new project. At the time, however, since I always wanted to provide my clients with the best value for the rate they agreed to pay for my services, there really wasn’t any other option. Recently, I discovered Unblocked … which provides the possibility to crush TTFCs I had set on past projects. About Unblocked Unblocked, which is still in beta, at the time of this writing, is focused on removing the mysteries in your code. The AI platform trains on all the information about your project and then allows you to ask questions and get answers about your project and codebase. It absorbs threads stored within instant messaging, pull requests, source code, and bugs/stories/tasks within project manager software. Even project information stored in content collaboration solutions can be consumed by Unblocked. Information from these various sources is then cataloged into a secured repository owned and maintained by Unblocked. From there, a simple user interface allows you to ask questions…and get answers fast… in a human-readable format. Use Case: The Service Owned By No One The idea of taking ownership of a service or solution that is owned by no one has become quite common as API adoption has skyrocketed. Services can be initialized to meet a shared need by contributors from various groups within the organization. This can be an effective approach to solve short-term problems, however, when there is no true service owner, the following long-term challenges can occur: Vulnerability mitigation – who is going to address vulnerabilities as they surface? Bug fixes and enhancements – who is going to fix or further extend the service? Tooling updates – who will handle larger scale migrations, like a change in CI/CD tooling? Supportability – who is responsible for answering general questions posed by service consumers? I ran into these exact issues recently, because my team inherited a service that was effectively owned by no one. In fact, there were features within the service that had very little documentation, except the source code itself. The challenge for our team was that a bug existed within the original source code and we weren’t sure what the service was supposed to be doing. Efforts to scan completed tickets in Jira or even Confluence pages would result in incomplete and incorrect information. I attempted to perform searches against the Slack instant messaging service, but it appeared that the chat history around these concepts had long since been removed as a part of corporate retention policies. Getting Started with Unblocked The Unblocked platform can be used to reduce an engineer’s TTFC by simply selecting the source code management system they wish to use: After selecting which source code repositories you wish to use, you have the opportunity to add integrations with Slack and Jira as shown below: Additional integrations can be configured from the Unblocked dashboard: Confluence Linear Notion Stack Overflow After setup, Unblocked begins the data ingestion and process phase. The amount of time required to complete this step is largely dependent on the amount of data that needs to be analyzed. At this point, one of the following client platforms can be prepared for use: Unblocked Client for macOS Unblocked IDE Plug-in for Visual Studio Code Unblocked IDE Plug-in for any JetBrains IDE (IntelliJ, PyCharm, and so on) There is also a web dashboard that can be accessed via a standard web browser. Where Unblocked Provides Value I decided to use the web dashboard. After completing the data ingestion and processing phase, I decided to see what would happen if I asked Unblocked “How does the front end communicate with the back end?” Below is how the interaction appeared: When I clicked the block-patterns.php file, I was taken directly to the file within the connected GitHub repository. Diving a little deeper, I wanted to understand what endpoints are available in the backend. This time I was provided the result from an answer that had been asked 11 days earlier. What is really nice is that the /docs URI was also provided, saving me more time in getting up to speed. I also wanted to understand what changes had been made to the backend recently. I was impressed by the response Unblocked provided: For this answer, there were five total references included in the response. Let’s take a look at several of these references. Clicking the first reference provided information from Github: The second reference provided the ability to download Markdown files from the Git source code management: The experience was quite impressive. By asking a few simple questions, I was able to make huge progress in understanding a service that is completely new to me in a matter of minutes. Conclusion The “service owned by no one” scenario is more common now than at any other point in my 30+ year career in technology. The stress of having issues to understand and fix – without any documentation or service owner expertise – does not promote a healthy and productive work environment. My readers may recall that I have been focused on the following mission statement, which I feel can apply to any IT professional: “Focus your time on delivering features/functionality that extends the value of your intellectual property. Leverage frameworks, products, and services for everything else.” - J. Vester Unblocked supports my personal mission statement by giving software engineers an opportunity to be productive quickly. The platform relies on a simple interface and AI-based process to do the hard work for you, allowing you to remain focused on meeting your current objectives. By asking a few simple questions, I was able to gain valuable information about the solutions connected to Unblocked. In a world where it can be difficult to find a subject matter expert, this is a game changer – especially from a TTFC perspective. While updating my IntelliJ IDEA client I realized there is even an Unblocked plug-in that I could have utilized just as easily too! The same good news applies to users of Visual Studio Code. This functionality allows engineers to pose questions to Unblocked without leaving their IDE. The best part is that Unblocked is currently in an open beta, which means it is 100% free to use. You can get started by clicking here. Take Unblocked for a test drive and see how it holds up for your use case. I am super interested in hearing about your results in the comments section. Have a really great day!
The AIDocumentLibraryChat project uses the Spring AI project with OpenAI to search in a document library for answers to questions. To do that, Retrieval Augmented Generation is used on the documents. Retrieval Augmented Generation The process looks like this: The process looks like this: Upload Document Store Document in Postgresql DB. Split Document to create Embeddings. Create Embeddings with a call to the OpenAI Embedding Model. Store the Document Embeddings in the Postgresql Vector DB. Search Documents: Create Search Prompt Create Embedding of the Search Prompt with a call to the OpenAI Embedding Model. Query the Postgresql Vector DB for documents with nearest Embedding distances. Query Postgresql DB for Document. Create Prompt with the Search Prompt and the Document text chunk. Request an answer from GPT Model and show the answer based on the search prompt and the Document text chunk. Document Upload The uploaded document is stored in the database to have the source document of the answer. The document text has to be split in chunks to create embeddings per chunk. The embeddings are created by an embedding model of OpenAI and are a vectors with more than 1500 dimensions to represent the text chunk. The embedding is stored in an AI document with the chunk text and the id of the source document in the vector database. Document Search The document search takes the search prompt and uses the Open AI embedding model to turn it in an embedding. The embedding is used to search in the vector database for the nearest neighbor vector. That means that the embeddings of search prompt and the text chunk that have the biggest similarities. The id in the AIDocument is used to read the document of the relational database. With the Search Prompt and the text chunk of the AIDocument, the Document Prompt created. Then, the OpenAI GPT model is called with the prompt to create an answer based on Search Prompt and the document context. That causes the model to create answers that are closely based on the documents provided and improves the accuracy. The answer of the GPT model is returned and displayed with a link of the document to provide the source of the answer. Architecture The architecture of the project is built around Spring Boot with Spring AI. The Angular UI provides the user interface to show the document list, upload the documents and provide the Search Prompt with the answer and the source document. It communicates with the Spring Boot backend via the rest interface. The Spring Boot backend provides the rest controllers for the frontend and uses Spring AI to communicate with the OpenAI models and the Postgresql Vector database. The documents are stored with Jpa in the Postgresql Relational database. The Postgresql database is used because it combines the relational database and the vector database in a Docker image. Implementation Frontend The frontend is based on lazy loaded standalone components build with Angular. The lazy loaded standalone components are configured in the app.config.ts: TypeScript export const appConfig: ApplicationConfig = { providers: [provideRouter(routes), provideAnimations(), provideHttpClient()] }; The configuration sets the routes and enables the the http client and the animations. The lazy loaded routes are defined in app.routes.ts: TypeScript export const routes: Routes = [ { path: "doclist", loadChildren: () => import("./doc-list").then((mod) => mod.DOCLIST), }, { path: "docsearch", loadChildren: () => import("./doc-search").then((mod) => mod.DOCSEARCH), }, { path: "**", redirectTo: "doclist" }, ]; In 'loadChildren' the 'import("...").then((mod) => mod.XXX)' loads the the route lazily from the provided path and sets the exported routes defined in the 'mod.XXX' constant. The lazy loaded route 'docsearch' has the index.ts to export the constant: TypeScript export * from "./doc-search.routes"; That exports the doc-search.routes.ts: TypeScript export const DOCSEARCH: Routes = [ { path: "", component: DocSearchComponent, }, { path: "**", redirectTo: "" }, ]; It defines the routing to the 'DocSearchComponent'. The fileupload can be found in the DocImportComponent with the template doc-import.component.html: HTML <h1 mat-dialog-title i18n="@@docimportImportFile">Import file</h1> <div mat-dialog-content> <p i18n="@@docimportFileToImport">File to import</p> @if(uploading) { <div class="upload-spinner"><mat-spinner></mat-spinner></div> } @else { <input type="file" (change)="onFileInputChange($event)"> } @if(!!file) { <div> <ul> <li>Name: {{file.name}</li> <li>Type: {{file.type}</li> <li>Size: {{file.size} bytes</li> </ul> </div> } </div> <div mat-dialog-actions> <button mat-button (click)="cancel()" i18n="@@cancel">Cancel</button> <button mat-flat-button color="primary" [disabled]="!file || uploading" (click)="upload()" i18n="@@docimportUpload">Upload</button> </div> The fileupload is done with the '<input type="file" (change)="onFileInputChange($event)">' tag. It provides the upload feature and calls the 'onFileInputChange(...)' method after each upload. The 'Upload' button calls the 'upload()' method to send the file to the server on click. The doc-import.component.ts has methods for the template: TypeScript @Component({ selector: 'app-docimport', standalone: true, imports: [CommonModule,MatFormFieldModule, MatDialogModule,MatButtonModule, MatInputModule, FormsModule, MatProgressSpinnerModule], templateUrl: './doc-import.component.html', styleUrls: ['./doc-import.component.scss'] }) export class DocImportComponent { protected file: File | null = null; protected uploading = false; private destroyRef = inject(DestroyRef); constructor(private dialogRef: MatDialogRef<DocImportComponent>, @Inject(MAT_DIALOG_DATA) public data: DocImportComponent, private documentService: DocumentService) { } protected onFileInputChange($event: Event): void { const files = !$event.target ? null : ($event.target as HTMLInputElement).files; this.file = !!files && files.length > 0 ? files[0] : null; } protected upload(): void { if(!!this.file) { const formData = new FormData(); formData.append('file', this.file as Blob, this.file.name as string); this.documentService.postDocumentForm(formData) .pipe(tap(() => {this.uploading = true;}), takeUntilDestroyed(this.destroyRef)) .subscribe(result => {this.uploading = false; this.dialogRef.close();}); } } protected cancel(): void { this.dialogRef.close(); } } This is the standalone component with its module imports and the injected 'DestroyRef'. The 'onFileInputChange(...)' method takes the event parameter and stores its 'files' property in the 'files' constant. Then it checks for the first file and stores it in the 'file' component property. The 'upload()' method checks for the 'file' property and creates the 'FormData()' for the file upload. The 'formData' constant has the datatype ('file'), the content ('this.file') and the filename ('this.file.name') appended. Then the 'documentService' is used to post the 'FormData()' object to the server. The 'takeUntilDestroyed(this.destroyRef)' function unsubscribes the Rxjs pipeline after the component is destroyed. That makes unsubscribing pipelines very convenient in Angular. Backend The backend is a Spring Boot application with the Spring AI framework. Spring AI manages the requests to the OpenAI models and the Vector Database Requests. Liquibase Database setup The database setup is done with Liquibase and the script can be found in the db.changelog-1.xml: XML <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd"> <changeSet id="1" author="angular2guy"> <sql>CREATE EXTENSION if not exists hstore;</sql> </changeSet> <changeSet id="2" author="angular2guy"> <sql>CREATE EXTENSION if not exists vector;</sql> </changeSet> <changeSet id="3" author="angular2guy"> <sql>CREATE EXTENSION if not exists "uuid-ossp";</sql> </changeSet> <changeSet author="angular2guy" id="4"> <createTable tableName="document"> <column name="id" type="bigint"> <constraints primaryKey="true"/> </column> <column name="document_name" type="varchar(255)"> <constraints notNullConstraintName="document_document_name_notnull" nullable="false"/> </column> <column name="document_type" type="varchar(25)"> <constraints notNullConstraintName="document_document_type_notnull" nullable="false"/> </column> <column name="document_content" type="blob"/> </createTable> </changeSet> <changeSet author="angular2guy" id="5"> <createSequence sequenceName="document_seq" incrementBy="50" startValue="1000" /> </changeSet> <changeSet id="6" author="angular2guy"> <createTable tableName="vector_store"> <column name="id" type="uuid" defaultValueComputed="uuid_generate_v4 ()"> <constraints primaryKey="true"/> </column> <column name="content" type="text"/> <column name="metadata" type="json"/> <column name="embedding" type="vector(1536)"> <constraints notNullConstraintName= "vectorstore_embedding_type_notnull" nullable="false"/> </column> </createTable> </changeSet> <changeSet id="7" author="angular2guy"> <sql>CREATE INDEX vectorstore_embedding_index ON vector_store USING HNSW (embedding vector_cosine_ops);</sql> </changeSet> </databaseChangeLog> In the changeset 4 the table for the Jpa document entity is created with the primary key 'id'. The content type/size is unknown and because of that set to 'blob'. I changeset 5 the sequence for the Jpa entity is created with the default properties of Hibernate 6 sequences that are used by Spring Boot 3.x. In changeset 6 the table 'vector_store' is created with a primary key 'id' of type 'uuid' that is created by the 'uuid-ossp' extension. The column 'content' is of type 'text'('clob' in other databases) to has a flexible size. The 'metadata' column stores the metadata in a 'json' type for the AIDocuments. The 'embedding' column stores the embedding vector with the number of OpenAI dimensions. In changeset 7 the index for the fast search of the 'embeddings' column is set. Due to limited parameters of the Liquibase '<createIndex ...>' '<sql>' is used directly to create it. Spring Boot / Spring AI implementation The DocumentController for the frontend looks like this: Java @RestController @RequestMapping("rest/document") public class DocumentController { private final DocumentMapper documentMapper; private final DocumentService documentService; public DocumentController(DocumentMapper documentMapper, DocumentService documentService) { this.documentMapper = documentMapper; this.documentService = documentService; } @PostMapping("/upload") public long handleDocumentUpload( @RequestParam("file") MultipartFile document) { var docSize = this.documentService .storeDocument(this.documentMapper.toEntity(document)); return docSize; } @GetMapping("/list") public List<DocumentDto> getDocumentList() { return this.documentService.getDocumentList().stream() .flatMap(myDocument ->Stream.of(this.documentMapper.toDto(myDocument))) .flatMap(myDocument -> { myDocument.setDocumentContent(null); return Stream.of(myDocument); }).toList(); } @GetMapping("/doc/{id}") public ResponseEntity<DocumentDto> getDocument( @PathVariable("id") Long id) { return ResponseEntity.ofNullable(this.documentService .getDocumentById(id).stream().map(this.documentMapper::toDto) .findFirst().orElse(null)); } @GetMapping("/content/{id}") public ResponseEntity<byte[]> getDocumentContent( @PathVariable("id") Long id) { var resultOpt = this.documentService.getDocumentById(id).stream() .map(this.documentMapper::toDto).findFirst(); var result = resultOpt.stream().map(this::toResultEntity) .findFirst().orElse(ResponseEntity.notFound().build()); return result; } private ResponseEntity<byte[]> toResultEntity(DocumentDto documentDto) { var contentType = switch (documentDto.getDocumentType()) { case DocumentType.PDF -> MediaType.APPLICATION_PDF; case DocumentType.HTML -> MediaType.TEXT_HTML; case DocumentType.TEXT -> MediaType.TEXT_PLAIN; case DocumentType.XML -> MediaType.APPLICATION_XML; default -> MediaType.ALL; }; return ResponseEntity.ok().contentType(contentType) .body(documentDto.getDocumentContent()); } @PostMapping("/search") public DocumentSearchDto postDocumentSearch(@RequestBody SearchDto searchDto) { var result = this.documentMapper .toDto(this.documentService.queryDocuments(searchDto)); return result; } } The 'handleDocumentUpload(...)' handles the uploaded file with the 'documentService' at the '/rest/document/upload' path. The 'getDocumentList()' handles the get requests for the document lists and removes the document content to save on the response size. The 'getDocumentContent(...)' handles the get requests for the document content. It loads the document with the 'documentService' and maps the 'DocumentType' to the 'MediaType'. Then it returns the content and the content type, and the browser opens the content based on the content type. The 'postDocumentSearch(...)' method puts the request content in the 'SearchDto' object and returns the AI generated result of the 'documentService.queryDocuments(...)' call. The method 'storeDocument(...)' of the DocumentService looks like this: Java public Long storeDocument(Document document) { var myDocument = this.documentRepository.save(document); Resource resource = new ByteArrayResource(document.getDocumentContent()); var tikaDocuments = new TikaDocumentReader(resource).get(); record TikaDocumentAndContent(org.springframework.ai.document.Document document, String content) { } var aiDocuments = tikaDocuments.stream() .flatMap(myDocument1 -> this.splitStringToTokenLimit( myDocument1.getContent(), CHUNK_TOKEN_LIMIT) .stream().map(myStr -> new TikaDocumentAndContent(myDocument1, myStr))) .map(myTikaRecord -> new org.springframework.ai.document.Document( myTikaRecord.content(), myTikaRecord.document().getMetadata())) .peek(myDocument1 -> myDocument1.getMetadata() .put(ID, myDocument.getId().toString())).toList(); LOGGER.info("Name: {}, size: {}, chunks: {}", document.getDocumentName(), document.getDocumentContent().length, aiDocuments.size()); this.documentVsRepository.add(aiDocuments); return Optional.ofNullable(myDocument.getDocumentContent()).stream() .map(myContent -> Integer.valueOf(myContent.length).longValue()) .findFirst().orElse(0L); } private List<String> splitStringToTokenLimit(String documentStr, int tokenLimit) { List<String> splitStrings = new ArrayList<>(); var tokens = new StringTokenizer(documentStr).countTokens(); var chunks = Math.ceilDiv(tokens, tokenLimit); if (chunks == 0) { return splitStrings; } var chunkSize = Math.ceilDiv(documentStr.length(), chunks); var myDocumentStr = new String(documentStr); while (!myDocumentStr.isBlank()) { splitStrings.add(myDocumentStr.length() > chunkSize ? myDocumentStr.substring(0, chunkSize) : myDocumentStr); myDocumentStr = myDocumentStr.length() > chunkSize ? myDocumentStr.substring(chunkSize) : ""; } return splitStrings; } The 'storeDocument(...)' method saves the document to the relational database. Then, the document is converted in a 'ByteArrayResource' and read with the 'TikaDocumentReader' of Spring AI to turn it in a AIDocument list. Then the AIDocument list is flatmapped to split the documents into chunks with the the 'splitToTokenLimit(...)' method that are turned in new AIDocuments with the 'id' of the stored document in the Metadata map. The 'id' in the Metadata enables loading the matching document entity for the AIDocuments. Then the embeddings for the AIDocuments are created implicitly with calls to the 'documentVsRepository.add(...)' method that calls the OpenAI Embedding model and stores the AIDocuments with the embeddings in the vector database. Then the result is returned. The method 'queryDocument(...)' looks like this: Java public AiResult queryDocuments(SearchDto searchDto) { var similarDocuments = this.documentVsRepository .retrieve(searchDto.getSearchString()); var mostSimilar = similarDocuments.stream() .sorted((myDocA, myDocB) -> ((Float) myDocA.getMetadata().get(DISTANCE)) .compareTo(((Float) myDocB.getMetadata().get(DISTANCE)))).findFirst(); var documentChunks = mostSimilar.stream().flatMap(mySimilar -> similarDocuments.stream().filter(mySimilar1 -> mySimilar1.getMetadata().get(ID).equals( mySimilar.getMetadata().get(ID)))).toList(); Message systemMessage = switch (searchDto.getSearchType()) { case SearchDto.SearchType.DOCUMENT -> this.getSystemMessage( documentChunks, (documentChunks.size() <= 0 ? 2000 : Math.floorDiv(2000, documentChunks.size()))); case SearchDto.SearchType.PARAGRAPH -> this.getSystemMessage(mostSimilar.stream().toList(), 2000); }; UserMessage userMessage = new UserMessage(searchDto.getSearchString()); Prompt prompt = new Prompt(List.of(systemMessage, userMessage)); LocalDateTime start = LocalDateTime.now(); AiResponse response = aiClient.generate(prompt); LOGGER.info("AI response time: {}ms", ZonedDateTime.of(LocalDateTime.now(), ZoneId.systemDefault()).toInstant().toEpochMilli() - ZonedDateTime.of(start, ZoneId.systemDefault()).toInstant() .toEpochMilli()); var documents = mostSimilar.stream().map(myGen -> myGen.getMetadata().get(ID)).filter(myId -> Optional.ofNullable(myId).stream().allMatch(myId1 -> (myId1 instanceof String))).map(myId -> Long.parseLong(((String) myId))) .map(this.documentRepository::findById) .filter(Optional::isPresent) .map(Optional::get).toList(); return new AiResult(searchDto.getSearchString(), response.getGenerations(), documents); } private Message getSystemMessage( List<org.springframework.ai.document.Document> similarDocuments, int tokenLimit) { String documents = similarDocuments.stream() .map(entry -> entry.getContent()) .filter(myStr -> myStr != null && !myStr.isBlank()) .map(myStr -> this.cutStringToTokenLimit(myStr, tokenLimit)) .collect(Collectors.joining("\n")); SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(this.systemPrompt); Message systemMessage = systemPromptTemplate .createMessage(Map.of("documents", documents)); return systemMessage; } private String cutStringToTokenLimit(String documentStr, int tokenLimit) { String cutString = new String(documentStr); while (tokenLimit < new StringTokenizer(cutString, " -.;,").countTokens()){ cutString = cutString.length() > 1000 ? cutString.substring(0, cutString.length() - 1000) : ""; } return cutString; } The method first loads the documents best matching the 'searchDto.getSearchString()' from the vector database. To do that the OpenAI Embedding model is called to turn the search string into an embedding and with that embedding the vector database is queried for the AIDocuments with the lowest distance(the distance between the vectors of the search embedding and the database embedding). Then the AIDocument with the lowest distance is stored in the 'mostSimilar' variable. Then all the AIDocuments of the document chunks are collected by matching the document entity id of their Metadata 'id's. The 'systemMessage' is created with the 'documentChunks' or the 'mostSimilar' content. The 'getSystemMessage(...)' method takes them and cuts the contentChunks to a size that the OpenAI GPT models can handle and returns the 'Message'. Then the 'systemMessage' and the 'userMessage' are turned into a 'prompt' that is send with 'aiClient.generate(prompt)' to the OpenAi GPT model. After that the AI answer is available and the document entity is loaded with the id of the metadata of the 'mostSimilar' AIDocument. The 'AiResult' is created with the search string, the GPT answer, the document entity and is returned. The vector database repository DocumentVsRepositoryBean with the Spring AI 'VectorStore' looks like this: Java @Repository public class DocumentVSRepositoryBean implements DocumentVsRepository { private final VectorStore vectorStore; public DocumentVSRepositoryBean(JdbcTemplate jdbcTemplate, EmbeddingClient embeddingClient) { this.vectorStore = new PgVectorStore(jdbcTemplate, embeddingClient); } public void add(List<Document> documents) { this.vectorStore.add(documents); } public List<Document> retrieve(String query, int k, double threshold) { return new VectorStoreRetriever(vectorStore, k, threshold).retrieve(query); } public List<Document> retrieve(String query) { return new VectorStoreRetriever(vectorStore).retrieve(query); } } The repository has the 'vectorStore' property that is used to access the vector database. It is created in the constructor with the injected parameters with the 'new PgVectorStore(...)' call. The PgVectorStore class is provided as the Postgresql Vector database extension. It has the 'embeddingClient' to use the OpenAI Embedding model and the 'jdbcTemplate' to access the database. The method 'add(...)' calls the OpenAI Embedding model and adds AIDocuments to the vector database. The methods 'retrieve(...)' query the vector database for embeddings with the lowest distances. Conclusion Angular made the creation of the front end easy. The standalone components with lazy loading have made the initial load small. The Angular Material components have helped a lot with the implementation and are easy to use. Spring Boot with Spring AI has made the use of Large Language Models easy. Spring AI provides the framework to hide the creation of embeddings and provides an easy-to-use interface to store the AIDocuments in a vector database(several are supported). The creation of the embedding for the search prompt to load the nearest AIDocuments is also done for you and the interface of the vector database is simple. The Spring AI prompt classes make the creation of the prompt for the OpenAI GPT models also easy. Calling the model is done with the injected 'aiClient,' and the results are returned. Spring AI is a very good Framework from the Spring Team. There have been no problems with the experimental version. With Spring AI, the Large Language Models are now easy to use on our own documents.
What Is a JSON Object? JSON stands for Javascript Object Notation and is a standard text-based format for representing structured data based on JavaScript object syntax. JSON defines only two data structures: objects and arrays. JSON object is a data type that consists of key/value pairs surrounded by curly braces. JSON array is a list of values. JSON objects are often used when data is sent from a server to a webpage or when data is stored in files or databases. JSON closely resembles Javascript object literal syntax but can be used independently from Javascript. Many programming environments support reading and generating JSON. Features of JSON Objects They are written in key/value pairs, separated by commas. A key is a string enclosed in double quotes, and a value must be a JSON data type as below string number object array boolean null They are surrounded by curly braces {}. Curly braces can also be used to nest objects within objects, creating a hierarchical structure. Arrays are enclosed in brackets [], and their values are separated by a comma (,). Each value in an array may be of a different type, including another array or an object. They are case-sensitive, meaning the keys and values must match exactly in spelling and capitalization. They don’t allow trailing commas. In simple terms, there should not be any text outside the curly braces or inside the double quotes that are not part of the key/value pairs. They don’t allow comments. JSON offers several benefits, making it a popular choice for representing structured data: Simplicity and readability: JSON is straightforward and easy to understand. Unlike more verbose formats like XML, JSON is relatively easy to read as-is. Its concise syntax allows for efficient data representation. Ease of parsing: JSON is simpler and faster to parse than XML. Flexibility: JSON supports various data types and object hierarchies, and relationships can be preserved during transmission and reassembled appropriately at the receiving end. Widespread usage: Most modern APIs accept JSON requests and issue JSON responses, making it universal for data exchange between systems Examples of JSON Objects Basic JSON object: {"name": "Natalie", "married": false, "age": 21, "city": "New York", "zip" : "10001", "awards": null} Nested JSON object: It is a data type that consists of a list of name/value pairs, where one or more of the values are another JSON object.{"person": {"name": "Natalie", "age": 21}, "address": {"street": "123 XYZ Street", "City": "New York", "State" : "NY", "zip": "10001"} Array of JSON object: [ { "name": "Natalie", "age": 21 }, { "name": "David", "age": 37 }, { "name": "Mark", "age": 43 } ] Parsing JSON Objects Parsing is the method of converting a JSON object into a native Javascript object. JSON.parse() method: The JSON.parse() method parses a string and returns a Javascript object. The string has to be in JSON format. Syntax: JSON.parse(string, function) Parameter Required/Optional Description String Required A string written in JSON format Reviver function Optional A function that takes a key and a value as parameters and returns a modified value or undefined to delete the property. The function is called for each item. Any nested objects are transformed before the parent. Example JSON var text = '{"name": "Natalie", "married": false, "age": 21, "city": "New York", "zip" : "10001", "awards": null}'; var obj = JSON.parse(text, function (key, value) { if (key === "name") { return value.toUpperCase(); } else { return value; } }); console.log(obj); Output JSON { name: 'NATALIE', married: false, age: 21, city: 'New York', zip: '10001', awards: null } JSON.stringify() Method This method converts Javascript objects into strings. When sending data to a web server the data has to be a string. JSON.stringify() also works with arrays. Syntax: JSON.stringify(obj, replacer, space) Parameter Required/Optional Description Obj Required The value to convert to a string Replacer Optional A function or an array used to transform the result. The replacer is called for each item. Space Optional A string to be used as white space (max 10 characters), or a number, from 0 to 10, to indicate how many space characters to use as white space. Example JSON var obj = {"name": "Natalie", "married": false, "age": 21, "city": "New York", "zip" : "10001", "awards": null}; var text = JSON.stringify(obj, function (key, value) { if (key === "name") { return value.toUpperCase(); } else { return value; } }); console.log(text); Output {"name":"NATALIE","married":false,"age":21,"city":"New York","zip":"10001","awards":null} JSON /*Insert the word SPACE for each white space:*/ var newText = JSON.stringify(obj, null, "space"); console.log(“Text with the word space “+ newText); Output JSON Text with the word space { space"name": "Natalie", space"married": false, space"age": 21, space"city": "New York", space"zip": "10001", space"awards": null } Navigation of JSON Objects The dot (.) or bracket ([]) notation can be used to navigate into its properties and access their values. JSON // Access the name using dot notation var obj = {"name": "Natalie", "married": false, "age": 21, "city": "New York", "zip" : "10001", "awards": null}; console.log(obj.name); Output: Natalie JSON // Access the city using dot notation console.log(obj["city"]; Output: New York JSON var obj_array = [ { "name": "Natalie", "age": 21 }, { "name": "David", "age": 37 }, { "name": "Mark", "age": 43 } ] JSON // Access the first member's name using dot and bracket notation console.log(obj_array[0].name); Output: Natalie JSON // Access the second member's age using dot and bracket notation console.log(obj_array[1][ "age"]); Output: 37 Object.keys() Method keys() method returns an array of a given object’s own enumerable string-keyed property names. The keys() method, being a static method, is called using the Object class name. Syntax: Object.keys(obj) Parameter Required/Optional Description obj Required The object whose enumerable properties are to be returned Object.values() Method values() method returns an array of a given object’s own enumerable string-keyed property values. The values() method, being a static method, is called using the Object class name. Syntax: Object.values(obj) Parameter Required/Optional Description obj Required The object whose enumerable properties are to be returned Object.entries() Method This method returns an array of key-value pairs of an object’s enumerable properties. The entries() method, being a static method, is called using the Object class name. Syntax: Object.entries(obj) Parameter Required/Optional Description obj Required The object whose enumerable properties are to be returned Example JSON var obj = {"name": "Natalie", "married": false, "age": 21, "city": "New York", "zip" : "10001", "awards": null}; var keys = Object.keys(obj); var values = Object.values(obj); var entries = Object.entries(obj); console.log("Array of keys :"); console.log(keys); console.log("Array of values :"); console.log(values); console.log("Array of entries :"); console.log(entries); Output JSON Array of keys : [ 'name', 'married', 'age', 'city', 'zip', 'awards' ] Array of values : [ 'Natalie', false, 21, 'New York', '10001', null ] Array of entries : [ [ 'name', 'Natalie' ], [ 'married', false ], [ 'age', 21 ], [ 'city', 'New York' ], [ 'zip', '10001' ], [ 'awards', null ] ] for loop A for loop repeats until a specified condition is evaluated to be false. Syntax: for (initialization; condition; expression) {code block to be executed} Parameter Required/Optional Description Initialization Required Executed one time before the execution of the code block Condition Required The condition for executing the code block Expression Required Executed every time after the code block has been executed Example JSON var obj = [ { "name": "Natalie", "age": 21, "married": true }, { "name": "David", "age": 37, "married": false }, { "name": "Mark", "age": 43, "married": true } ]; for(var i=0; i<obj.length; i++) { console.log("Name: " + obj[i]["name"]); //using bracket notation console.log("Married Status: " + obj[i].married); //using dot notation } Output JSON Output Name: Natalie Married Status: true Name: David Married Status: false Name: Mark Married Status: true for…in Loop The for...in statement iterates over all enumerable string non-symbol properties of an object including inherited enumerable properties. The code block inside the loop is executed once for each property. Syntax: for (item in object) {code block to be executed} Parameter Required/Optional Description item Required A variable to iterate over the properties. object Required The object to be iterated. Example JSON var obj = [ { "name": "Natalie", "age": 21, "married": true }, { "name": "David", "age": 37, "married": false }, { "name": "Mark", "age": 43, "married": true } ]; for(item in obj) { console.log("Name: " + obj[item]["name"]); //using bracket notation console.log("Married Status: " + obj[item].married); //using dot notation } Output JSON Name: Natalie Married Status: true Name: David Married Status: false Name: Mark Married Status: true for…of Loop A for..of loop operates on the values sourced from an iterable object one by one in sequential order. Syntax: array.forEach(variable of iterable object) {statement} Parameter Required/Optional Description Variable Required For every iteration, the value of the next property is assigned to the variable. A variable can be declared with const, let, or var. Iterable object Required The source of values on which the loop operates. Example JSON var obj = [ { "name": "Natalie", "age": 21, "married": true }, { "name": "David", "age": 37, "married": false }, { "name": "Mark", "age": 43, "married": true } ]; for(var item of obj) { console.log("Name: " + item["name"]); //using bracket notation console.log("Married Status: " + item.married); //using dot notation } Output JSON Name: Natalie Married Status: true Name: David Married Status: false Name: Mark Married Status: true forEach() Method The forEach() method calls a function for each element in an array. It must take at least one parameter which represents the elements of an array. Syntax: array.forEach(function(currentValue, index, array), thisValue) Parameter Required/Optional Description Function Required A function to run for each element of the array currentvalue Required The value of the current element index Optional Index of the current element Array Optional Array of the current element Thisvalue Optional A value passed to the function as this value. Default undefined. Example JSON var obj = [ { "name": "Natalie", "age": 21, "married": true }, { "name": "David", "age": 37, "married": false }, { "name": "Mark", "age": 43, "married": true } ]; obj.forEach((item, index, arr) => { console.log("Details of element: " +index); console.log("Name: "+arr[index]["name"]); console.log("Age: "+item.age); }); Output JSON Details of element: 0 Name: Natalie Age: 21 Details of element: 1 Name: David Age: 37 Details of element: 2 Name: Mark Age: 43 Conclusion JSON is commonly employed for transmitting data between servers and web applications. The JSON’s flexibility, ease of parsing, and simplicity allow software developers to work with structured data efficiently in various programming environments.
Over the past four years, developers have harnessed the power of Quarkus, experiencing its transformative capabilities in evolving Java microservices from local development to cloud deployments. As we stand on the brink of a new era, Quarkus 3 beckons with a promise of even more enhanced features, elevating developer experience, performance, scalability, and seamless cloud integration. In this enlightening journey, let’s delve into the heart of Quarkus 3's integration with virtual threads (Project Loom). You will learn how Quarkus enables you to simplify the creation of asynchronous concurrent applications, leveraging virtual threads for unparalleled scalability while ensuring efficient memory usage and peak performance. Journey of Java Threads You might have some experience with various types of Java threads if you have implemented Java applications for years. Let me remind you real quick how Java threads have been evolving over the last decades. Java threads have undergone significant advancements since their introduction in Java 1.0. The initial focus was on establishing fundamental concurrency mechanisms, including thread management, thread priorities, thread synchronization, and thread communication. As Java matured, it introduced atomic classes, concurrent collections, the ExecutorService framework, and the Lock and Condition interfaces, providing more sophisticated and efficient concurrency tools. Java 8 marked a turning point with the introduction of functional interfaces, lambda expressions, and the CompletableFuture API, enabling a more concise and expressive approach to asynchronous programming. Additionally, the Reactive Streams API standardized asynchronous stream processing and Project Loom introduced virtual threads, offering lightweight threads and improved concurrency support. Java 19 further enhanced concurrency features with structured concurrency constructs, such as Flow and WorkStealing, providing more structured and composable concurrency patterns. These advancements have significantly strengthened Java's concurrency capabilities, making it easier to develop scalable and performant concurrent applications. Java threads continue to evolve, with ongoing research and development focused on improving performance, scalability, and developer productivity in concurrent programming. Virtual threads, generally available (GA) in Java 21, are a revolutionary concurrency feature that addresses the limitations of traditional operating system (OS) threads. OS threads are heavyweight, limited in scalability, and complex to manage, posing challenges for developing scalable and performant concurrent applications. Virtual threads also offer several benefits, such as being a lightweight and efficient alternative, consuming less memory, reducing context-switching overhead, and supporting concurrent tasks. They simplify thread management, improve performance, and enhance scalability, paving the way for new concurrency paradigms and enabling more efficient serverless computing and microservices architectures. Virtual threads represent a significant advancement in Java concurrency, poised to shape the future of concurrent programming. Getting Started With Virtual Threads In general, you need to create a virtual thread using Thread.Builder directly in your Java project using JDK 21. For example, the following code snippet showcases how developers can create a new virtual thread and print a message to the console from the virtual thread. The Thread.ofVirtual() method creates a new virtual thread builder, and the name() method sets the name of the virtual thread to "virtual-thread". Then, the start() method starts the virtual thread and executes the provided Runnable lambda expression, which prints a message to the console. Lastly, the join() method waits for the virtual thread to finish executing before continuing. The System.out.println() statement in the main thread prints a message to the console after the virtual thread has finished executing. Java public class MyVirtualThread { public static void main(String[] args) throws InterruptedException { // Create a new virtual thread using Thread.Builder Thread thread = Thread .ofVirtual() .name("my-vt") .start(() -> { System.out.println("Hello from virtual thread!"); }); // Wait for the virtual thread to finish executing thread.join(); System.out.println("Main thread completed."); } } Alternatively, you can implement the ThreadFactory interface to start a new virtual thread in your Java project with JDK 21. The following code snippet showcases how developers can define a VirtualThreadFactory class that implements the ThreadFactory interface. The newThread() method of this class creates a new virtual thread using the Thread.ofVirtual() method. The name() method of the Builder object is used to set the name of the thread and the factory() method is used to set the ThreadFactory object. Java // Implement a ThreadFactory to start a new virtual thread public class VirtualThreadFactory implements ThreadFactory { private final String namePrefix; public VirtualThreadFactory(String namePrefix) { this.namePrefix = namePrefix; } @Override public Thread newThread(Runnable r) { return Thread.ofVirtual() .name(namePrefix + "-" + r.hashCode()) .factory(this) .build(); } } You might feel it will get more complex when you try to run your actual methods or classes on top of the virtual threads. Luckily, Quarkus enables you to skip the learning curve and execute the existing blocking services on the virtual threads quickly and efficiently. Let’s dive into it. Quarkus Way to the Virtual Thread You just need to keep reminding yourself of two things to run an application on virtual threads. Implement blocking services rather than reactive (or non-blocking) services based on JDK 21. Use @RunOnVirtualThread annotation on top of a method or a class that you want. Here is a code snippet of how Quarkus allows you to run the process() method on a virtual thread. Java @Path("/hello") public class GreetingResource { @GET @Produces(MediaType.TEXT_PLAIN) @RunOnVirtualThread public String hello() { Log.info(Thread.currentThread()); return "Quarkus 3: The Future of Java Microservices with Virtual Threads and Beyond"; } } You can start the Quarkus Dev mode (Live coding) to verify the above sample application. Then, invoke the REST endpoint using the curl command. Shell $ curl http://localhost:8080/hello The output should look like this. Shell Quarkus 3: The Future of Java Microservices with Virtual Threads and Beyond When you take a look at the terminal, you see that Quarkus dev mode is running. You can see that a virtual thread is created to run this application. Shell (quarkus-virtual-thread-0) VirtualThread[#123,quarkus-virtual-thread-0]/runnable@ForkJoinPool-1-worker-1 Try to invoke the endpoint a few more times, and the logs in the terminal should look like this. You learned how Quarkus integrates the virtual thread for Java developers to run blocking applications with a single @RunOnVirtualThread annotation. You should be aware that this annotation is not a silver bullet for all use cases. In the next article, I’ll introduce pitfalls, limitations, and performance test results against reactive applications.
Next.js, an open-source Javascript framework was created specifically for leveraging React to create user-friendly web applications and static websites. It was developed by Vercel and offers an integrated environment that makes server-side rendering simpler. Next.js was released on October 26, 2023, and during the Next.js Conf, Guillermo Rauch, the CEO, talked about the new features. One of the features, termed “Partial Prerendering,” was introduced in the preview to provide both quick initial and dynamic visuals without sacrificing the developer experience. Next.js 14 brings notable improvements to its TurboPack, the engine responsible for the efficient compilation, now it is faster. Furthermore, the stabilization of Server Actions and partial prerendering results in a better development experience and faster websites. Next.JS 14 Features Partial Pre-Rendering and SSR Pre-rendering is the practice of creating a web page’s HTML before a user requests it, either during the build or deployment time. Next.js offers two pre-rendering options for optimal speed: Static Generation and Server-side rendering (SSR). Static Generation works with data that is already available at build time which makes better performance over Server-side rendering. In Server-side rendering, data fetching and rendering are done at the request time. Still Server-side rendering is preferable compared to client-render apps. And if we use Next.js, we will have server rendering by default. Another significant feature, Partial pre-rendering, is announced for Next.js 14. Partial pre-rendering differs from pre-rendering in that it creates parts of the page dynamically during runtime in response to user interactions. By getting the static parts of the page as HTML and updating just the dynamic parts when needed, it is intended to deliver both fast initial page loads and dynamic visuals. If the generated HTML is static until the next build or c pre-rendering works well for pages with static or rarely changing content. Partial pre-rendering can help with faster initial page loads when a choice between static and dynamic rendering is required. Despite being an experimental feature and optional, it is crucial to integrate dynamic rendering with static generation. Turbo Mode The primary objective of Next.js 14 is to enhance speed and performance. Their Rust-based compiler Turbopack took the concern of the team and they made a remarkable improvement. Now local server startup is 53.3% faster and code updates with fast refresh speeds up to 94.7% quicker. We should note that Turbo is not yet fully finalized. Server Actions In addition to providing stability to Server Actions, Next.js 14 introduces mechanisms to improve the performance of web applications. This integration facilitates a smooth interaction between the client and server, empowering developers to incorporate essential functionalities such as error handling, caching, revalidating, and redirection—all within the context of the App Router model. Furthermore, for those utilizing TypeScript, this update ensures better type safety between the client and server components, contributing to a more robust and maintainable codebase. The FormData Web API offers a familiar paradigm for developers accustomed to server-centric frameworks. Image Optimization and Image Component Next.js 14 introduces an enhanced and flexible image optimization feature, streamlining the process of optimizing images automatically. Here’s a brief overview of how Next.js facilitates image optimization: Prioritizing Image Loading Next.js intelligently prioritizes image loading. Images within the viewport are loaded first, providing a faster initial page load, and images below are loaded asynchronously as the user scrolls down. Dynamic Sizing and Format Selection The Image component in Next.js dynamically selects the right size and format based on the user’s bandwidth and device resources. This ensures that users receive appropriately sized and optimized images. Responsive Image Resizing Next.js simplifies responsive image handling by automatically resizing images as needed. This responsive design approach ensures that images adapt to various screen sizes, further enhancing the overall user experience. Support for Next-Gen Formats (Webp): The image optimization in Next.js extends to supporting next-generation image formats like WebP. This format, known for its superior compression and quality, is automatically utilized by the Image component when applicable. Preventing Cumulative Layout Shifts: To enhance visual stability and prevent layout shifts, Next.js incorporates placeholders for images. These placeholders serve as temporary elements until the actual images are fully loaded, avoiding disruptions in the layout. Additionally, Next.js 14 enhances performance by efficiently handling font downloads. The framework optimizes the download process for fonts from sources such as Next/font/google. Automatic Code Splitting Automatic code splitting in Next.js 14 is a powerful technique that significantly contributes to optimizing web performance. Code-splitting results in breaking JS bundles into smaller, more manageable chunks. Users only download what’s necessary, leading to more efficient use of bandwidth. With less JS the performance on slower devices sees a notable improvement. Route-based splitting: By default, Next.js splits JavaScript into manageable chunks for each route. As users interact with different UI elements, the associated code chunks are sent, reducing the amount of code to be parsed and compiled at once. Component-based splitting: Developers can optimize even further on a component level. Large components can be split into separate chunks, allowing non-critical components or those rendering only on specific UI interactions to be lazily loaded as needed. These approaches collectively contribute to a more efficient, faster, and user-friendly web application experience, aligning with the continuous efforts to enhance performance in Next.js 14. Conclusion In conclusion, Next.js 14 introduces several groundbreaking features that contribute to speed and performance for web applications. The introduction of “Partial Prerendering” stands out among the new features. This innovation aims to deliver both quick initial and dynamic visuals without compromising the developer experience. The Rust-based compiler brings remarkable improvements, making local server startup 53.3% faster and code updates with fast refresh up to 94.7% quicker. Server Actions have been stabilized in this version, presenting an integration that allows developers to define server-side functions directly within React components. Image optimization is another highlight of Next.js 14, offering enhanced features like prioritized loading, dynamic sizing, and format selection. Automatic code splitting emerges as a powerful technique for optimizing web performance by breaking JS bundles into smaller, manageable chunks. Developers can leverage these features to create faster, more efficient, and user-friendly web applications, solidifying Next.js as a leading framework in the web development landscape.
Java Concurrent Mark and Sweep (CMS) algorithm operates by dividing the garbage collection process into multiple phases, concurrently marking and sweeping the memory regions without a significant pause. While its design brings benefits in terms of reduced pause times, it also introduces unique challenges that demand careful tuning and optimization. In this post, we will explore techniques to tune CMS GC specifically for enhanced performance. However, if you want to learn more basics, you may watch this Garbage Collection tuning talk delivered at the JAX London conference. How To Enable CMS GC You can enable the Concurrent Mark-Sweep (CMS) Garbage Collector in your Java application by adding the following JVM argument when launching your application: Shell -XX:+UseConcMarkSweepGC Note: The CMS GC algorithm has been deprecated starting from JDK 9, and it has been completely removed in JDK 14, as no credible contributors stepped up to take on the maintenance of CMS. If your application runs on JDK 9 or later, it’s advisable to explore alternative Garbage Collectors like the Garbage-First (G1), Shenandoah, ZGC for optimal memory management. When To Use CMS GC You can consider using CMS GC for your application if you have any one of the requirements: JDK 14 and below: CMS has been deprecated since JDK 9 and completely removed from JDK 14. Focus has shifted towards the Garbage-First (G1), Shenandoah, and ZGC Garbage Collectors. However, for applications running on earlier JDK versions or with specific use cases, you can consider using CMS. Low-latency requirements: When your application demands low and predictable pause times, such as in real-time systems, CMS can be a suitable choice. Its concurrent nature allows it to perform garbage collection without significantly halting the application. Frequent object creation and deletion environments: In situations where objects are created and become unreachable frequently, CMS can efficiently reclaim memory without causing prolonged interruptions, making it suitable for applications with dynamic memory patterns. JVMs with limited resources: In environments where system resources, especially memory, are constrained, CMS can be a pragmatic choice as it aims to balance memory management efficiency with minimal disruption. CMS GC Tuning Parameters In this section, let’s review important CMS GC tuning parameters that you can configure for your application. 1. -Xms and -Xmx -Xms sets the initial heap size when the Java Virtual Machine (JVM) starts, and -Xmx sets the maximum heap size that the JVM can use. Setting both values to be the same value ensures a fixed and non-resizable heap size. This configuration reduces hiccups associated with heap management, providing stability and predictable memory usage for your application. 2. -XX:CMSInitiatingOccupancyFraction Determines the heap occupancy percentage at which the CMS collector starts. This parameter helps in fine-tuning CMS initiation based on memory usage. For example, setting -XX:CMSInitiatingOccupancyFraction=75 means that CMS starts when the occupancy is 75%. The default value for this initiating occupancy threshold is approximately 92%, but the value is subject to change from release to release. 3. -XX:+UseCMSInitiatingOccupancyOnly CMS is restricted to handling only full garbage collections. This can be useful in scenarios where you want to control CMS initiation more precisely. When set, CMS won’t start until the occupancy fraction is reached and a full garbage collection is required. 4. -XX:MaxGCPauseMillis Sets the maximum acceptable pause time for CMS in milliseconds. This parameter helps in controlling the pause times during garbage collection. For instance, setting -XX:MaxGCPauseMillis=500 aims to limit pauses to 500 milliseconds. 5. -XX:+UseCMSCompactAtFullCollection and -XX:+CMSParallelRemarkEnabled Enables parallel threads for the CMS collector, improving overall efficiency during garbage collection. These options enhance the parallelism in different phases of the CMS collection process. 6. -XX:ParallelCMSThreads and -XX:MaxParallelCMSThreads Specifies the initial and maximum number of parallel threads for the CMS collector, influencing parallelism during the collection process. 7. -XX:+UseLargePages The -XX:+UseLargePages option is a powerful tweak for optimizing Java heap performance. When enabled, it allows the Java Virtual Machine (JVM) to use large pages, typically 2 MB or more in size, for heap memory allocation. Below are the potential benefits of using -XX:+UseLargePages: Reduced TLB misses: Large pages diminish Translation Lookaside Buffer (TLB) misses, streamlining memory access and enhancing overall speed. Improved TLB efficiency: The larger page size optimizes TLB efficiency, leading to more efficient memory translations. Enhanced speed: With fewer TLB misses, the application experiences improved memory access speed, which is particularly beneficial for memory-intensive operations. Note: While enabling large pages can deliver notable performance benefits, it’s essential to be aware that its effectiveness can vary depending on the application and system. Additionally, specific operating system-level configurations may be required. Advanced CMS GC Options As we explore the intricacies of Concurrent Mark-Sweep (CMS) Garbage Collection, let’s delve into advanced options that offer nuanced control over its behavior. These options go beyond the basics, providing additional tools for fine-tuning and optimizing CMS performance. 1. -XX:+UseCMSCompactAtFullCollection and -XX:+CMSParallelRemarkEnabled Enables parallel threads for the CMS collector, improving overall efficiency during garbage collection. These options enhance the parallelism in different phases of the CMS collection process. 2. -XX:+UseCMSCompactAtFullCollection Enabling this option instructs CMS to perform a compaction phase during a full garbage collection. This can help reduce fragmentation in the old generation, potentially improving memory utilization. However, note that compaction introduces additional overhead, and its impact should be carefully assessed based on your application’s characteristics. 3. -XX:CMSFullGCsBeforeCompaction This option determines the number of full garbage collections that should occur before initiating a compaction phase. Adjusting this parameter allows you to control when compaction occurs, balancing the benefits of reduced fragmentation with the associated costs. Experiment with different values to find the optimal setting for your application. 4. -XX:+CMSClassUnloadingEnabled This option allows the CMS collector to unload classes during garbage collection, potentially freeing up more memory. However, be cautious with this option, as it may have implications for certain application scenarios. 5. -XX:+CMSIncrementalMode and -XX:+CMSIncrementalPacing These options enable incremental mode for the CMS collector, allowing garbage collection to occur in smaller, incremental steps. This can help reduce pause times, but it’s essential to evaluate the impact on overall throughput and responsiveness. 6. -XX:CMSInitiatingPermOccupancyFraction If your application has a permanent generation (PermGen) space (common in Java 7 and earlier), this parameter determines the occupancy fraction at which CMS starts collecting the permanent generation. 7. -XX:CMSClassUnloadingMaxInterval Specifies the maximum interval (in milliseconds) between CMS class unloading cycles. If class unloading is crucial for your application, tuning this parameter can influence how frequently class unloading occurs. 8. -XX:+CMSParallelInitialMarkEnabled Enables parallel threads for the initial mark phase in the CMS collector. This can enhance the efficiency of the initial mark process, reducing the impact on pause times. 9. -XX:CMSIncrementalSafetyFactor Specifies the factor by which CMS increases its duty cycle length during incremental mode. Adjusting this factor can impact the balance between incremental garbage collection and application throughput. 10. -XX:CMSMaxAbortablePrecleanTime Sets the maximum time in milliseconds that CMS will spend in the abortable preclean phase. Adjusting this parameter may be necessary to optimize CMS performance, especially in scenarios where abortable precleaning takes longer than desired. 11. -XX:CMSWaitDuration Defines the amount of time CMS waits for a requested collection to complete before initiating a new collection. Adjusting this parameter can influence the CMS collector’s aggressiveness in responding to memory demands. 12. -XX:CMSParallelRemarkEnabled Enables parallel threads for the CMS collector during the remark phase. This can enhance the efficiency of the remark process, which is a crucial step in the CMS collection cycle. 13. -XX:CMSIncrementalDutyCycle Sets the percentage of time that CMS should be active during an incremental collection cycle. Adjusting this parameter influences the balance between incremental collection and application throughput. 14. -XX:CMSIncrementalOffset Specifies the percentage of the CMS duty cycle time that is devoted to the incremental update. This parameter can be fine-tuned to optimize the CMS incremental collection process. 15. -XX:CMSRescanMultiple Controls the number of cards (regions of the heap) that are rescanned during a CMS cycle. Adjusting this parameter can impact the efficiency of the CMS collector. 16. -XX:CMSWaitDuration Defines the amount of time CMS waits for a requested collection to complete before initiating a new collection. Adjusting this parameter can influence the CMS collector’s aggressiveness in responding to memory demands. Studying CMS GC Behavior Studying the performance characteristics of CMS GC is best achieved by analyzing the GC log. The GC log contains detailed information about garbage collection events, memory usage, and other relevant metrics. There are several tools available that can assist in analyzing the GC log, such as GCeasy, IBM GC & Memory visualizer, HP Jmeter, and Google Garbage Cat. By using these tools, you can visualize memory allocation patterns, identify potential bottlenecks, and assess the efficiency of garbage collection. This allows for informed decision-making when fine-tuning CMS GC for optimal performance. Conclusion In this post, we have explored essential CMS JVM arguments, ranging from basic configurations like enabling CMS to advanced options such as compaction and trigger ratios. Moreover, considerations for large pages and class unloading shed light on optimizing memory usage. We hope it will be of help to you.