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.
We want to turn our Google Photos clone from single-threaded to multithreaded, to generate thumbnails much faster than before - but there are a couple of challenges along the way. We'll learn that using Java's Parallel File Streams seems to be a buggy endeavor, so we'll fall back on good, old ExecutorServices. But how many threads should we use to generate thumbnails? How can threads get in conflict with each other? How do we make our program fail-safe for threading issues? Find out in this episode! What’s in the Video 00:00 Intro We start off with a quick recap. In the previous episodes, we built a tiny application that can take a folder full of images, and turn those images into thumbnails - by spawning external ImageMagick processes. We did that sequentially, spawning the next process, as soon as a thumbnail conversion process has finished. But we surely can make this faster and also utilize our system resources (CPU/IO) more by doing the thumbnail conversion multithreaded, spawning multiple ImageMagick processes at the very same time. We'll try and figure out how to do that in this episode. 00:24 Java’s Parallel Streams The first idea would be to use Java's built-in parallel streams feature, as we are reading in the files as a stream anyway. Interestingly enough the API lets you do this just fine, and it even works flawlessly on my machine, but as soon as we deploy our application to a different server, it stops working. Why is that? We'll need to do a bit of benchmarking and fumbling around, to notice that parallel file streams, in JDKs < 19, aren't really supported. So, depending on the Java version, you'll get different behavior. Hence, we cannot work with parallel streams for now. 03:32 Java’s ExecutorService Given that parallel streams are not an option, we will resort back to using a good old ExecutorService. An ExecutorService lets us define how many threads we want to open, and then work off n-tasks in parallel. Figuring out the API is not that difficult, but the real question is: How many threads specifically should we open up simultaneously? We'll cover that question in detail during this segment. 06:12 Performance Benchmarking After having implemented multithreading, we also need to make sure to benchmark our changes. Will we get a 2x/3x speed improvement? Or maybe even a speed reduction? During this segment, we'll run and time our application locally, as well as on my NAS, and see how different hardware configurations might affect the final result. 08:10 File Storage and Hashing Last but not least, we'll have to figure out how to store our thumbnails. So far, we created thumbnails with the same filename as the original image and put all the files into the same directory. That doesn't work for a huge amount of files, with potential file clashes and multithreading conflicts. Hence, we will start hashing our files with the BLAKE3 algorithm, store the files under that hash, and also use a directory layout similar to what Git uses internally to store its files. 16:52 Up Next We did a ton of multithreading work in this episode. Up next it is time to add a database to our application and store the information about all converted thumbnails there. Stay tuned!
The old engineering adage “Don’t touch it, it works” is terrible. Don’t listen to it. It might be OK at a small scale, but as time goes by, the bit rot spreads through your code and servers polluting everything. Large swaths of your system become “no-man's-land.” As you’re developing a new system, you must always “touch it” and make sure we hire engineers who aren’t afraid to do so. Yes, I get it. I said that sentence frequently in the past. I understand the motivation. Management doesn’t care about the bit rot in our future. They care about the here and now. Why are you wasting time on this feature? It’s working. Don’t you have enough on your plate already? Are you the Marie Kondo of coding? Does this code not spark joy? It’s more like a bad apple in a barrel. Bad code and forbidden zones tend to grow and metastasize. A living project needs to be fully accessible by the current team. It can keep working without that, but that makes every future step painful. When we have a flexible team with a relatively small and familiar code base, touching everything isn’t challenging. It’s easy in that case. The Legacy Project The hard part is touching code in legacy projects. As a consultant, I had to do that often. How do you enter a project with a million lines of code and start refactoring? The nice thing is that we’re all alike. The engineers that built the project were trained with similar books and similar thought processes. Once you understand their logic you can understand why they did something. But a large part of the difficulty is in the tooling. Projects that were built 20 years ago used tools that are no longer available. The code might no longer compile on a modern IDE. Our immediate reflex would be to try to use an old IDE and old tooling. That might be a mistake. Old tools keep the stale bit rot. This is an opportunity. Revisit the project and update the tools. A few years ago I did some work for an older C++ codebase. I didn’t understand the code base, but the original developers built it in an older version of Visual Studio. Getting it to work on my Mac with LLVM and VS Code helped me visualize the moving pieces more clearly. Once I had a debugger up and running, fixing the bugs and weird issues became trivial. I can’t say I fully understood that codebase. But the process of porting and updating the tools exposed me to many nuances and issues. When You Can’t The flip side of that were cases where an existing legacy system is a customer requirement. I had to implement integrations with legacy systems that were external black boxes. We didn’t need to touch their code, but we needed to interface with these systems and rely on their behaviors. This is a very challenging situation. Our solution in those cases was to create a mock of the system so we can simulate and test various scenarios. In one such situation, we wrote an app that sent requests and saved responses from such a “black box” to create a simple recorder. We then used the recordings as the basis for tests in our implementation. This might not be an option since sometimes, the black box is directly wired to production (directly to the stock market in one case). My rules for dealing with such a black box are: A single isolated module handles all the connections — that way we can build uniform workarounds for failures. We can use a physically isolated microservice which is ideal for this specific case. Expose results using asynchronous calls — this prevents deadlocks and overloading a legacy system. We can use a queue to map causes of failure and error handling is simpler since a failure just won’t invoke the result callback. We need to code defensively. Use circuit breakers, logging and general observability tooling. Expect failure in every corner since this will be the most contentious part of the project. Once we wrap that legacy we need to trigger alerts on the failures. Some failures might not bubble up to the user interface and might trigger retries that succeed. This can be a serious problem. E.g. in a case of a stock market purchase command that fails a trader might press retry which will issue a new successful command. But the original command might retry implicitly in the legacy system and we can end up with two purchases. Such mistakes can be very costly and originate from that black box. Without reviewing the legacy code fully and understanding it, we can make no guarantee. What we can do is respond promptly and accurately to failures of this type. Debuggability is important in these situations hence the importance of observability and isolation in such a black box. Confidence Through Observability In the past, we used to watch the server logs whenever we pushed a new release. Waiting for user complaints to pour in. Thanks to observability we’re the first to know about a problem in our production. Observability flipped the script. Unfortunately, there’s a wide chasm between knowing about a problem and understanding it, fixing it and noticing it. If we look at the observability console, we might notice an anomaly that highlights a problem but it might not trigger an alert even though a regression occurs. A good example of that would be a miscalculation. A change to the application logic can report wrong results and this is very unlikely to show in the observability data. In theory, tests should have found that issue but tests are very good at verifying that things we predicted didn’t happen. They don’t check against unexpected bugs. E.g. We might allocate a field size for financial calculations and it worked great for our developers based in the USA. However, a customer in Japan working in Yen might have a far larger number and experience a regression because of that limit. We can debug such issues with developer observability tools but when we deeply integrate legacy systems, we must apply the fail-fast principles deeply, that way the observability layer will know of the problem. We need to assert expectations and check for conditions not in the test, but in the production code. Here an actual error will be better than a stealthy bug. A lot of focus has been given in languages to the non-null capabilities of languages. But the concepts pioneered in languages like Eiffel of design by contract have gone out of fashion. This is understandable, it’s hard and awkward to write that sort of code. Checked exceptions are often the most hated feature of the Java language. Imagine having to write all the constraints you expect for every input. Not to mention dependencies on the environmental state. This isn’t tenable and enforcing this check-in runtime would be even more expensive. However, this is something we can consciously do in entry points to our module or microservice. The fail-fast principle is essential when integrating with legacy systems because of the unpredictable nature of the result. Summary In the 90s I used to take a bus to my job. Every day as I walked to the office I would pass by a bank machine and every time it would reboot as I came close. This was probably part of their cycling policy, banks have a culture of rebooting machines on a schedule to avoid potential issues. One morning I went by the machine and it didn’t reboot. I did what every good programmer/hacker would do; I pulled out my card and tried to use it. It instantly rebooted and wouldn’t take my card, but the fact that my instinct was to “try” is good. Even if it isn’t the smartest thing in the world, we need to keep code accessible and fresh. Legacy code isn’t a haunted house and we shouldn’t be afraid.
In this article, we’re going to compare some essential metrics of web applications using two different Java stacks: Spring Boot and Eclipse MicroProfile. More precisely, we’ll implement the same web application in Spring Boot 3.0.2 and Eclipse MicroProfile 4.2. These releases are the most recent at the time of this writing. Since there are several implementations of Eclipse MicroProfile, we’ll be using one of the most famous: Quarkus. At the time of this writing, the most recent Quarkus release is 2.16.2. This mention is important regarding Eclipse MicroProfile because, as opposed to Spring Boot, which isn’t based on any specification and, consequently, the question of the implementation doesn’t exist, Eclipse MicroProfile has largely been adopted by many editors who provide different implementations, among which Quarkus, Wildfly, Open Liberty and Payara are from the most evangelical. In this article, we will implement the same web application using two different technologies, Spring Boot and Quarkus, such that to compare their respective two essential metrics: RSS (Resident Set Size) and TFR (Time to First Request). The Use Case The use case that we’ve chosen for the web application to be implemented is a quite standard one: the one of a microservice responsible to manage press releases. A press release is an official statement delivered to members of the news media for the purpose of providing information, creating an official statement, or making a public announcement. In our simplified case, a press release consists in a set of data like a unique name describing its subject, an author, and a publisher. The microservice used to manage press releases is very straightforward. As with any microservice, it exposes a REST API allowing for CRUD press releases. All the required layers, like domain, model, entities, DTOs, mapping, persistence, and service, are present as well. Our point here is not to discuss the microservices structure and modus operandi but to propose a common use case to be implemented in the two similar technologies, Spring Boot and Quarkus, to be able to compare their respective performances through the mentioned metrics. Resident Set Size (RSS) RSS is the amount of RAM occupied by a process and consists of the sum of the following JVM spaces: Heap space Class metadata Thread stacks Compiled code Garbage collection RSS is a very accurate metric, and comparing applications based on it is a very reliable way to measure their associated performances and footprints. Time to First Request (TFR) There is a common concern about measuring and comparing applications' startup times. However, logging it, which is how this is generally done, isn’t enough. The time you’re seeing in your log file as being the application startup time isn’t accurate because it represents the time your application or web server started, but not the one required that your application starts to receive requests. Application and web servers, or servlet containers, might start in a couple of milliseconds, but this doesn’t mean your application can process requests. These platforms often delay work through the process and may give a false, lazy initialization indication about the TFR. Hence, to accurately determine the TFR, in this report, we’re using Clément Escofier’s script time.js, found here in the GitHub repository, which illustrates the excellent book Reactive Systems in Java by Clément Escoffier and Ken Finnigan. Spring Boot Implementation To compare the metrics presented above for the two implementations, you need to clone and run the two projects. Here are the steps required to experience the Spring Boot implementation: Shell $ git clone https://github.com/nicolasduminil/Comparing-Resident-Size- Set-Between-Spring-Boot-and-Quarkus.git metrics $ cd metrics $ git checkout spring-boot $ mvn package $ java -jar target/metrics.jar Here you start by cloning the GIT repository, and once this operation is finished, you go into the project’s root directory and do a Maven build. Then you start the Spring Boot application by running the über JAR created by the spring-boot-maven-plugin. Now you can test the application via its exposed Swagger UI interface by going here. Please take a moment to use the feature that tries it out that Swagger UI offers. The order of operations is as follows: First, the POST endpoint is to create a press release. Please use the editor to modify the JSON payload proposed by default. While doing this, you should leave the field pressReleaseId having a value of “0” as this is the primary key that will be generated by the insert operation. Below, you can see an example of how to customize this payload: JSON { "pressReleaseId": 0, "name": "AWS Lambda", "author": "Nicolas DUMINIL", "publisher": "ENI" } Next, a GET /all is followed by a GET /id to check that the previous operation has successfully created a press release. A PUT to modify the current press release A DELETE /id to clean-up Note: Since the ID is automatically generated by a sequence, as explained, the first record will have the value of “1.” You can use this value in GET /id and DELETE /id requests. Notice that the press release name must be unique. Now, once you have experienced your microservice, let’s see its associated RSS. Proceed as follows: Shell $ ps aux | grep metrics nicolas 31598 3.5 1.8 13035944 598940 pts/1 Sl+ 19:03 0:21 java -jar target/metrics.jar nicolas 31771 0.0 0.0 9040 660 pts/2 S+ 19:13 0:00 grep --color=auto metrics $ ps -o pid,rss,command -p 31598 PID RSS COMMAND 31598 639380 java -jar target/metrics.ja Here, we get the PID of our microservice by looking up its name, and once we have it, we can display its associated RSS. Notice that the command ps -o above will display the PID, the RSS, and the starting command associated with the process, which PID is passed as the -p argument. And as you may see, the RSS for our process is 624 MB (639380 KB). If you’re hesitating about how to calculate this value, you can use the following command: Shell $ echo 639380/1024 | bc 624 As for the TFR, all you need to do is to run the script time.js, as follows: Shell node time.js "java -jar target/metrics.jar" "http://localhost:8080/" 173 ms To resume, our Spring Boot microservice has a RSS of 624 MB and a TFR of 173 ms. Quarkus Implementation We need to perform these same operations to experience our Quarkus microservice. Here are the required operations: Shell $ git checkout quarkus $ mvn package quarkus:dev Once our Quarkus microservice has started, you may use the Swager UI interface here. And if you’re too tired to use the graphical interface, then you may use the curl scripts provided in the repository ( post.sh, get.sh, etc.) as shown below: Shell java -jar target/quarkus-ap/quarkus-run.jar & ./post.sh ./get.sh ./get-1.sh 1 ./update.sh ... Now, let’s see how we do concerning our RSS and TFR: Shell $ ps aux | grep quarkus-run nicolas 24776 20.2 0.6 13808088 205004 pts/3 Sl+ 16:27 0:04 java -jar target/quarkus-app/quarkus-run.jar nicolas 24840 0.0 0.0 9040 728 pts/5 S+ 16:28 0:00 grep --color=auto quarkus-run $ ps -o pid,rss,command -p 24776 PID RSS COMMAND 24776 175480 java -jar target/quarkus-app/quarkus-run.jar $ echo 175480/1024 | bc 168 $ node time.js "java -jar target/quarkus-app/quarkus-run.jar" "http://localhost:8081/q/swagger-ui" 121 ms As you can see, our Quarkus microservice uses an RSS of 168MB, i.e., almost 500MB less than the 624MB with Spring Boot. Also, the TFR is slightly inferior (121ms vs. 173ms). Conclusion Our exercise has compared the RSS and TFR metrics for the two microservices executed with the HotSpot JVM (Oracle JDK 17). Spring Boot and Quarkus support the compilation into native executables through GraalVM. It would have been interesting to compare these same metrics of the native replica of the two microservices, and if we didn’t do it here, that’s because Spring Boot heavily relies on Java introspection and, consequently, it’s significantly more difficult to generate Spring Boot native microservices than Quarkus ones. But stay tuned; it will come soon. The source code may be found here. The GIT repository has a master branch and two specific ones, labeled spring-boot and, respectively, quarkus. Enjoy!
Quarkus is an open-source, full-stack Java framework designed for building cloud-native, containerized applications. As Quarkus is built for cloud applications, it is designed to be lightweight and fast and supports fast startup times. A well-designed containerized application facilitates the implementation of reliable REST APIs for creating and accessing data. Data validation is always an afterthought for developers but is important to keep the data consistent and valid. REST APIs need to validate the data it receives, and Quarkus provides rich built-in support for validating REST API request objects. There are situations where we need custom validation of our data objects. This article describes how we can create custom validators using the Quarkus framework. REST API Example Let’s consider a simple example below where we have a House data object and a REST API to create a new House. The following fields need to be validated: number: should be not null. street: should not be blank. state: should be only California (CA) and Nevada (NV). Java class House { String number; String street; String city; String state; String type; } And the REST API: Java @Path("/house") public class HouseResource { @POST public String createHouse(House house) { // Additional logic to process the house object return "Valid house created"; } } Configure Quarkus Validator Quarkus provides the Hibernate validator to perform data validation. This is a Quarkus extension and needs to be added to the project. For Maven projects, add the dependency to the pom.xml: XML <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-hibernate-validator</artifactId> </dependency> For Gradle-based projects, use the following to build.gradle: Java implementation("io.quarkus:quarkus-hibernate-validator") Built-In Validators The commonly used validators are available as annotations that can be easily added to the data object. In our House data object, we need to ensure the number property should not be null, and the street should not be blank. We will use the annotations @NotNull and @NotBlank: Java class House { @NotNull int number; @NotBlank(message = "House street cannot be blank") String street; String city; String state; String type; } To validate the data object in a REST API, it is necessary to include the @Valid annotation. By doing so, there is no need for manual validation, and any validation errors will result in a 400 HTTP response being returned to the caller: Java @Path("/house") public String createHouse(@Valid House house) { return "Valid house received"; } Custom Validation There are several scenarios in which the default validations are insufficient, and we must implement some form of custom validation for our data. In our example, the House data is supported only in California (CA) and Nevada (NV). Let’s create a validator for this: Java @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD }) @Constraint(validatedBy = StateValidator.class) public @interface ValidState { String message() default "State not supported"; Class<? extends Payload>[] payload() default {}; Class<?>[] groups() default {}; } Getting into the details: The name of the validator is ValidState. In the class validation, this can be used as @ValidState. The default error message is added to the message() method. This annotation is validated by StateValidator.class and is linked using the @Constraint annotation. The @Target annotation indicates where the annotation might appear in the Java program. In the above case, it can be applied only to Fields. The @Retention describes the retention policy for the annotation. In the above example, the annotation is retained during runtime. The validation logic in the StateValidator class: Java public class StateValidator implements ConstraintValidator<ValidState, String> { List<String> states = List.of("CA", "NV"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { return value != null && states.contains(value); } } The custom validator @ValidState can be included in the House class: Java class House { @NotNull int number; @NotBlank(message = "House street cannot be blank") String street; String city; @ValidState String state; String type; } Testing In software development, unit testing is a crucial component that offers several benefits, such as enhancing code quality and detecting defects early in the development cycle. The Quarkus framework provides a variety of tools to help developers with unit testing. Unit testing the custom validator is simple as Quarkus allows us to inject the validator and perform manual validation on the House object: Java @QuarkusTest public class HouseResourceTest { @Inject Validator validator; @Test public void testValidState() { House h = new House(); h.state = "CA"; h.number = 1; h.street = "street1"; Set<ConstraintViolation<House>> violations = validator.validate(h); System.out.println("Res " + violations); assertEquals(violations.size(), 0); } @Test public void testInvalidState() { House h = new House(); h.state = "WA"; h.number = 1; Set<ConstraintViolation<House>> violations = validator.validate(h); assertEquals(violations.size(), 1); assertEquals(violations.iterator().next().getMessage(), "State not supported"); } } Conclusion Quarkus is a robust, well-written framework that provides various built-in validations and supports add custom validation. Validators provide a clean and convenient way to perform REST API validation, which, in turn, supports the DRY methodology. Validation plays an increasingly crucial role in microservices architecture because every service defines and requires the validation of the data it processes. The above article describes a process to use built-in and custom validators.
Do you still write lengthy Dockerfiles describing every step necessary to build a container image? Then, buildpacks come to your rescue! Developers simply feed them an application, buildpacks do their magic, and turn it into a fully functional container ready to be deployed on any cloud. But how exactly does the magic happen? And what should you do if the resulting container performance doesn’t meet the business requirements? This article will look under the hood of buildpacks to see how they operate and give tips on optimizing the default settings to reach better performance outcomes. What Are Buildpacks? A buildpack turns the application source code into a runnable production-ready container image. Buildpacks save time and effort for developers because there’s no need to configure the image and manually manage dependencies through a Dockerfile. Heroku was the first company to develop buildpacks in 2011. Since then, many other companies (Cloud Foundry, Google, etc.) have adopted the concept. In 2018, Heroku partnered with Pivotal to create the Cloud Native Buildpacks project, encompassing modern standards and specifications for container images, such as the OCI format. The project is part of the Cloud Native Computing Foundation (CNCF). Paketo buildpacks, which we will use in this article, is an open-source project backed by Cloud Foundry and sponsored by VMware. It implements Cloud Native Buildpacks specifications and supports the most popular languages, including Java. Containers produced with Paketo buildpacks can run on any cloud. How Buildpacks Work Buildpacks operate in two phases: detect and build. 1. The Detect Phase During the detection phase, the buildpack analyzes the source code looking for indicators of whether or not it should be applied to the application. In other words, a group of buildpacks is tested against the source code, and the first group deemed fit for the code is selected for building the app. After the buildpack detects the necessary indicators, it returns a contract of what is required for creating an image and proceeds to the build phase. 2. The Build Phase During the build phase, the buildpack transforms the codebase, fulfilling the contract requirements composed earlier. It provides the build-time and runtime environment, downloads necessary dependencies, compiles the code if needed, and sets the entry points and startup scripts. Builders A builder is a combination of components required for building a container image: Buildpacks, sets of executables that analyze the code and provide a plan for building and running the app; Stack consists of two images: the build image and the run image. The build image provides the built environment (a containerized environment where build packs are executed), the run image offers the environment for the application image during runtime; Lifecycle manages the buildpack execution and assembles the resulting artifact into a final image. Therefore, one builder can automatically detect and build different applications. Buildpacks Offer a Variety of JVMs — How to Choose? Paketo buildpacks use Liberica JVM by default. Liberica is a HotSpot-based Java runtime supported by a major OpenJDK contributor and recommended by Spring. It provides JDK and JRE for all LTS versions (8, 11, 17), the current version, and Liberica Native Image Kit (NIK), a GraalVM-based utility for converting JVM-based apps into native images with an accelerated startup. Native images are beneficial when you need to avoid cold starts in AWS. But the buildpacks support several Java distributions, which can be used instead of the default JVM: Adoptium Alibaba Dragonwell Amazon Corretto Azul Zulu BellSoft Liberica (default) Eclipse OpenJ9 GraalVM Oracle JDK Microsoft OpenJDK SapMachine If you want to switch JVMs, you have to keep in mind several nuances: Alibaba Dragonwell, Amazon Corretto, GraalVM, Oracle JDK, and Microsoft OpenJDK offer only JDK. The resulting container will be twice as big as the JRE-based one; Adoptium provides JDK and JRE for Java 8 and 11 and only JDK for Java 16+; Oracle JDK provides only Java 17. Another important consideration: buildpacks facilitate and accelerate deployment, but if you are dissatisfied with container performance or seek to improve essential KPIs (throughput, latency, or memory consumption), you have to tune the JVM yourself. For more details, see the section Configuring the JVM below. For instance, Eclipse OpenJ9 based on the OpenJ9 JVM may demonstrate better performance than HotSpot in some cases because HotSpot comes with default settings, and OpenJ9 is already tuned. Adding a few simple parameters will give you equal or superior performance with HotSpot. How to Use Paketo Buildpacks Let’s build a Java container utilizing a Paketo buildpack. First, make sure Docker is up and running. If you don’t have it, follow these instructions to install Docker Desktop for your system. The next step is to install pack CLI, a Command Line Interface maintained by Cloud Native Buildpack that can be used to work with buildpacks. Follow the guide to complete the installation for your platform (macOS, Linux, and Windows are supported). Pack is one of the several available tools. Spring Boot developers, for instance, can look into Spring Boot Maven Plugin or Spring Boot Gradle Plugin. We will use Paketo sample applications, so run the following command: git clone https://github.com/paketo-buildpacks/samples && cd samples Alternatively, utilize your own demo app. Make Paketo Base builder the default builder: pack config default-builder paketobuildpacks/builder:base To build an image from source with Maven, run pack build samples/java \ --path java/maven --env BP_JVM_VERSION=17 Java example images should return {"status":"UP"} from the actuator health endpoint: docker run --rm --tty --publish 8080:8080 samples/java curl -s http://localhost:8080/actuator/health | jq . It is also possible to build an image from a compiled artifact. The following archive formats are supported: executable JAR, WAR, or distribution ZIP. To compile an executable JAR and build an image using pack, run cd java/maven ./mvnw package pack build samples/java \ --path ./target/demo-0.0.1-SNAPSHOT.jar Extracting a Software Bill of Materials Software supply chains consist of numerous libraries, tools, and processes used to develop and run applications. It is often hard to trace the origin of all software components in a software product, increasing the risk of nested vulnerabilities. A software bill of materials (SBOM) lists all library dependencies utilized to build a software artifact. It is similar to a traditional bill of materials, which summarizes the raw materials, parts, components, and exact quantities required to manufacture a product. SBOMs enable the developers to monitor the version of software components, integrate security patches promptly, and keep vulnerable libraries out. Buildpacks also enable the developers to see an SBOM for their image. Run the following command to extract the SBOM for the samples/java image built previously: pack sbom download samples/java --output-dir /tmp/samples-java-sbom After that, you can browse the folder. SBOMs are presented in JSON format. To list all .json files in the folder, run the following: find /tmp/samples-java-sbom -name "*.json" /tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_executable-jar/sbom.cdx.json /tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_executable-jar/sbom.syft.json /tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_spring-boot/helper/sbom.syft.json /tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_spring-boot/spring-cloud-bindings/sbom.syft.json /tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_bellsoft-liberica/jre/sbom.syft.json /tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_bellsoft-liberica/helper/sbom.syft.json /tmp/samples-java-sbom/layers/sbom/launch/sbom.legacy.json /tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_ca-certificates/helper/sbom.syft.json Now, you can open the file with any text editor. For instance, if you have Visual Studio Code installed, run the following: code /tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_bellsoft-liberica/jre/sbom.syft.json You will get the following output: { "Artifacts": [ { "ID": "1f2d01eeb13b5894", "Name": "BellSoft Liberica JRE", "Version": "17.0.6", "Type": "UnknownPackage", "FoundBy": "libpak", "Locations": [ { "Path": "buildpack.toml" } ], "Licenses": [ "GPL-2.0 WITH Classpath-exception-2.0" ], "Language": "", "CPEs": [ "cpe:2.3:a:oracle:jre:17.0.6:*:*:*:*:*:*:*" ], "PURL": "pkg:generic/bellsoft-jre@17.0.6?arch=amd64" } ], "Source": { "Type": "directory", "Target": "/layers/paketo-buildpacks_bellsoft-liberica/jre" }, "Descriptor": { "Name": "syft", "Version": "0.32.0" }, "Schema": { "Version": "1.1.0", "URL": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" } } Configuring the JVM The BellSoft Liberica Buildpack provides the newest patch updates of Java versions supported in the buildpack. The buildpack uses the latest LTS version by default. If you want to use another Java version, use the BP_JVM_VERSION environment variable. For instance, BP_JVM_VERSION=11 will install the newest release of Liberica JDK and JRE 11. In addition, you can change the JDK type. The buildpack uses JDK at build-time and JRE at runtime. Specifying the BP_JVM_TYPE=JDK option will force the buildpack to use JDK at runtime. The BP_JVM_JLINK_ENABLED option runs the jlink tool with Java 9+, which cuts out a custom JRE. If you deploy a Java application to an application server, the buildpack uses Apache Tomcat by default. You can select another server (TomEE or Open Liberty). For instance, run the following command to switch to TomEE: pack build samples/war -e BP_JAVA_APP_SERVER=tomee You can configure JVM at runtime by using the JAVA_TOOL_OPTIONS environment variable. For instance, you can configure garbage collection, number of threads, memory limits, etc., to reach optimal performance for your specific needs: docker run --rm --tty \ --env JAVA_TOOL_OPTIONS='-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40' \ --env BPL_JVM_THREAD_COUNT=100 \ samples/java The whole list of JVM configuration options can be found on the Liberica Buildpack page. Conclusion As you can see, buildpacks are great automation tools saving developers time. But it would help if you used them wisely, or there’s a risk you will get a cat in the sack. Our general recommendation is to define the KPIs and adjust JVM settings accordingly. What can you do if you are not happy with the size of the resulting image? After all, it’s not possible to change the base OS image utilized by buildpacks. One option is to migrate to the Native Image to optimize resource consumption. Another alternative is to manually build containers and switch to a smaller OS image, such as Alpine or Alpaquita Linux. The latter supports two libc implementations (optimized musl and glibc) and comes with numerous performance and security enhancements.
In much the same way mutual empathy defines the development of long-term relationships with our friends, it also plays a key role in defining the success of our business’ relationship with its customers. When customers take the time to type their thoughts and feelings into a review for a product or service, share their feelings through a social media platform, or provide feedback through some similar medium, it behooves us to empathize with them as fellow human beings and determine how they collectively feel about what they experienced. Using programmatic solutions, we can quickly analyze and then adjust (or maintain) the experience we provide to our customers at scale, efficiently improving customer relationships with our brand. Of course, unlike the human brain, computers aren’t raised and socialized to draw specific emotional conclusions from an evolving human language. They need to be trained to do so – and that’s where the field of sentiment analysis and classification comes into play. Using Natural Language Processing (NLP) techniques, we can train Machine Learning algorithms to analyze and classify unique sentiments in text. Like many NLP fields, sentiment analysis is a complex, multi-step process modeled with a simple set of classification outcomes. For a classifier model to return a simple sentiment tag (i.e., positive, negative or neutral), it must be trained to extract specific features from a text input and quickly reference those features against a database full of pre-determined tags. Getting to that point involves pairing myriad feature vectors with their respective sentiment tags ahead of time – an exhaustive task requiring vast amounts of thoroughly vetted (and often peer-reviewed) data. When it comes to finally creating a classification prediction, a statistical model must be applied to match input text with tagged features from the reference dataset; after that, it must determine the sentiment of the entire sentence based on the balance of its sentiment tags relative to a given subject. It's important to note that the baseline complexity of sentiment analysis classification is exacerbated by everyday inconsistencies in human expression - some of which are difficult as-is for human analysts to interpret without reacting naturally to audible cues in spoken language or fully understanding the context of a discussion. For example, it’s easy for any model to get tripped up by language quirks like sarcasm (i.e., “Oh yeah, sure, this product was really great”), out-of-context comments (i.e., “Wasn’t worth it”), out-of-context comparisons (“This service is much better than others”), and much more. Training a model to work around these challenges entails extra preprocessing work. Immensely beneficial as sentiment analysis is, the complexity and cost associated with training a productive model and processing the vast quantities of data required for that model to function accurately often trumps the impetus to create a new one from scratch. Given the labor involved, incorporating sentiment analysis is best accomplished by leveraging an existing service with exhaustively validated prediction outcomes and powerful underlying infrastructure. This is a problem best solved with Sentiment Analysis APIs, which enable us to rapidly interface with powerful underlying NLP logic without having to take on any responsibility for training or updating that model over time. Demonstration The goal of this article is to provide you with a low-code, free-to-use Sentiment Analysis and Classification API. The underlying service analyzes raw text sentences against a rigorously trained reference database to determine if the input is positive, negative, or neutral (only English language inputs are supported). API calls can be authenticated with a free-tier API key, which you can get by registering a free account on the Cloudmersive website. Each request (formatted as a “TextToAnalyze” string) will return the following information: SentimentClassificationResult – A string describing if the input text was positive, negative, or neutral SentimentScoreResult – A classification score (float) between -1.0 and +1.0; scores closest to zero are considered neutral sentiment, scores closest to -1.0 are considered negative sentiment, and scores closest to +1.0 are considered positive sentiment. SentenceCount – The number of sentences (integer) in the input text string Positive, Neutral, and Negative Response Examples Let’s look at a few examples of how this model reacts to and classifies certain text inputs. Let’s pretend a customer ordered and received a package from an online store. In the review section on that business's website, the customer wrote: { "TextToAnalyze": "the package is nice" } The Sentiment Analysis API will classify this sentence like so: { "Successful": true, "SentimentClassificationResult": "Positive", "SentimentScoreResult": 0.42149999737739563, "SentenceCount": 1 } As humans with context for this response, we can easily validate the accuracy of this outcome. The sentence is indeed “positive” in nature, but not overwhelmingly so; thus, the score does not exceed +0.5. Let’s process a second example. This time, the customer received a package that was a different color than the one they expected, noting that: { "TextToAnalyze": "the package was red, but I was expecting the color brown" } The Sentiment Analysis API will classify this sentence like so: { "Successful": true, "SentimentClassificationResult": "Neutral", "SentimentScoreResult": 0, "SentenceCount": 1 } While we might be tempted to read this input text as a dissatisfied customer response, the model correctly identifies that there are no specific negative or positive sentiments present. Without understanding this customer’s feelings further, we can’t know if the discrepancy they noticed was a good or bad thing – we can only wait for further information. In one final example, let’s incorporate a second sentence to the previous example in which the customer clarifies that: { "TextToAnalyze": "the package was red, but I was expecting the color brown. I hate the color red." } The Sentiment Analysis API response will categorize this two-sentence input like so: { "Successful": true, "SentimentClassificationResult": "Negative", "SentimentScoreResult": -0.7226999998092651, "SentenceCount": 2 } As we can see, the sentiment score result has dropped from 0 to -0.72, which falls firmly in the “negative” sentiment category. It’s perfectly clear based on the two-part customer response that they were very unhappy with the change, which means their dissatisfaction is probably worth addressing directly. These are only basic examples, of course - I would certainly encourage running through as many complex examples as you see fit and validating results against your own intuition (and/or other models). Implementation Below, I’ll demonstrate how you can install the SDK and structure your API call in Java. To install the client SDK, first, add a reference to the repository in your Maven POM file (we use JitPack to dynamically compile the library): <repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository> </repositories> And then add a reference to the dependency: <dependencies> <dependency> <groupId>com.github.Cloudmersive</groupId> <artifactId>Cloudmersive.APIClient.Java</artifactId> <version>v4.25</version> </dependency> </dependencies> With the installation complete, copy and paste from the ready-to-run Java code examples below to structure your API call. Include your API key and configure your text inputs in their respective lines: // Import classes: //import com.cloudmersive.client.invoker.ApiClient; //import com.cloudmersive.client.invoker.ApiException; //import com.cloudmersive.client.invoker.Configuration; //import com.cloudmersive.client.invoker.auth.*; //import com.cloudmersive.client.AnalyticsApi; ApiClient defaultClient = Configuration.getDefaultApiClient(); // Configure API key authorization: Apikey ApiKeyAuth Apikey = (ApiKeyAuth) defaultClient.getAuthentication("Apikey"); Apikey.setApiKey("YOUR API KEY"); // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null) //Apikey.setApiKeyPrefix("Token"); AnalyticsApi apiInstance = new AnalyticsApi(); SentimentAnalysisRequest input = new SentimentAnalysisRequest(); // SentimentAnalysisRequest | Input sentiment analysis request try { SentimentAnalysisResponse result = apiInstance.analyticsSentiment(input); System.out.println(result); } catch (ApiException e) { System.err.println("Exception when calling AnalyticsApi#analyticsSentiment"); e.printStackTrace(); } Please note that each request will consume 1-2 API calls per sentence, and you’ll have a limit of 800 API calls per month (with no commitments) when authenticating with a free-tier API key.
In Part 1 of the series on PEG implementation, I explained the basics of Parser Expression Grammar and how to implement it in JavaScript. This second part of the series is focused on implementation in Java using the parboiled library. We will try to build the same example for parsing arithmetic expressions but using different syntax and API. QuickStart parboiled is a lightweight and easy-to-use library to parse text input based on formal rules defined using Parser Expression Grammar. Unlike other parsers that use external grammar definition, parboiled provides a quick DSL (domain-specific language) to define grammar rules that can be used to generate parser rules on the runtime. This approach helps to avoid separate parsing and lexing phases and also does not require additional build steps. Installation The parboiled library is packaged into two level dependencies. There is a core artifact and two implementation artifacts for Java and Scala support. Both Java and Scala artifacts depend on the core and can be used independently in respective environments. They are available as Maven dependencies and can be downloaded from Maven central with the coordinates below: XML <dependency> <groupId>org.parboiled</groupId> <artifactId>parboiled-java</artifactId> <version>1.4.1</version> </dependency> Defining the Grammar Rules Let’s take the same example we used earlier to define rules to parse arithmetic expressions. Expression ← Term ((‘+’ / ‘-’) Term)* Term ← Factor ((‘*’ / ‘/’) Factor)* Factor ← Number / ‘(’ Expression ‘)’ Number ← [0-9]+ With the help of integrated DSL, the following rules can be easily defined as follows. Java public class CalculatorParser extends BaseParser { Rule Expression() { return Sequence( Term(), ZeroOrMore(AnyOf("+-"), Term())); } Rule Term() { return Sequence(Factor(), ZeroOrMore(AnyOf("*/"), Factor())); } Rule Factor() { return FirstOf(Number(), Sequence('(', Expression(), ')')); } Rule Number() { return OneOrMore(CharRange('0', '9')); } } If we take a closer look at the example, the parser class inherits all the DSL functions from its parent class BaseParser. It provides various builder methods for creating different types of Rules. By combining and nesting those you can build your custom grammar rules. There needs to be starting rules that recursively expand to terminal rules which are usually literals and character classes. Generating the Parser parbolied’s createParser API will take the DSL input and generates a parser class by enhancing the byte code of the existing class on the runtime using the ASM utils library. Java CalculatorParser parser = Parboiled.createParser(CalculatorParser.class); Using the Parser The generated parser is then passed to a parse runner which lazily initializes the rule tree for the first time and uses it for the subsequent run. Java String input = "1+2"; ParseRunner runner = new ReportingParseRunner(parser.Expression()); ParsingResult<?> result = runner.run(input); Here, the thing to care about is that both the generated parser and parse runner are not thread-safe. So, we need to keep it minimum scope and avoid sharing it across multiple threads. Understanding the Parse Result/Tree The output parse result encapsulates information about parse success or failure. A successful run generates a parse tree with the appropriate label and text fragments. ParseTreeUtils can be used to print the whole or partial parse tree based on passed filters. Java String parseTreePrintOut = ParseTreeUtils.printNodeTree(result); System.out.println(parseTreePrintOut); For more fine-grained control over the parse tree, you can use the visitor API and traverse it to collect the required information out of it. Sample Implementation There are some sample implementations available with the library itself. It contains samples for calculators, Java, SPARQL, and time formats. Visit this GitHub repository for more. Conclusion As we observed, it is very quick and easy to build/use the parser using the parboiled library. However, there might be some use cases that can lead to performance and memory issues while using it on large input with a complex rule tree. Therefore, we need to be careful about complexity and ambiguity while defining the rules.
The Java 9 release in 2017 saw the introduction of the Java Module System. This module system was developed directly for the Java language and is not to be confused with module systems such as IntelliJ Idea or Maven. The module system helps to provide a more secure and structured approach to writing Java code by better-organizing components, thus preventing malicious or out-of-date code from being used. In this article, we will look at what exactly the Java Module System is and how it can benefit developers. Benefits of Using Java Module Java modules were introduced in Java 9 as a new way to organize and package Java code. They provide several benefits, including: Strong encapsulation: Modules allow you to encapsulate your code and hide its implementation details from other modules. This helps to reduce the risk of coupling and improve the maintainability of your code. Better organization: Modules help you to organize your code into logical units, making it easier to navigate and understand. You can group related classes and packages together in a module and specify dependencies between modules. Improved security: Modules provide a way to control access to your code and limit the exposure of sensitive APIs. You can specify which modules are allowed to access a particular module and which packages and classes within a module are exposed to the outside world. Faster startup time: Modules allow the Java runtime to only load the modules that are actually needed for a particular application, reducing startup time and memory usage. How To Define Module Module Name Module Descriptor Set of Packages Dependencies, Type of resource, etc. Let's walk through an example of a modular sample application in Java. Our application will have two modules: com.example.core and com.example.app. The core module will contain some utility classes that the app module will use. Here's the module descriptor for the core module: Java module com.example.core { exports com.example.core.utils; } In this module, we define that it exports the com.example.core.utils package, which contains some utility classes. Here's the module descriptor for the app module: Java module com.example.app { requires com.example.core; exports com.example.app; } In this module, we specify that it requires the com.example.core module, so it can use the utility classes in that module. We also specify that it exports the com.example.app package, which contains the main class of our application. Now, let's take a look at the source code for our application. In the com.example.core module, we have a utility class: Java package com.example.core.utils; public class StringUtils { public static boolean isEmpty(String str) { return str == null || str.isEmpty(); } } In the com.example.app module, we have a main class: Java package com.example.app; import com.example.core.utils.StringUtils; public class MyApp { public static void main(String[] args) { String myString = ""; if (StringUtils.isEmpty(myString)) { System.out.println("The string is empty"); } else { System.out.println("The string is not empty"); } } } In this main class, we use the StringUtils class from the com.example.core module to check if a string is empty or not. To compile and run this application, we can use the following commands: Java $ javac -d mods/com.example.core src/com.example.core/com/example/core/utils/StringUtils.java $ javac --module-path mods -d mods/com.example.app src/com.example.app/com/example/app/MyApp.java $ java --module-path mods -m com.example.app/com.example.app.MyApp These commands compile the core module and the app module and then run the MyApp class in the com.example.app module. Conclusion Java programming allows developers to employ a modular approach, which can result in smaller, more secure code. By using this technique, the code becomes encapsulated at the package level for extra security. Although there is no requirement to use this technique, it provides developers with an additional tool to potentially write higher-quality code.
In this article, you will learn how to build a GraalVM image for your Spring Boot application. Following these practical steps, you will be able to apply them to your own Spring Boot application. Enjoy! Introduction Java is a great programming language and is platform independent. Write once, run anywhere! But this comes at a cost. Java is portable because Java compiles your code to bytecode. Bytecode is computer object code, which an interpreter, (read: Virtual Machine), can interpret and convert to machine code. When you start your Java application, the Virtual Machine will convert the bytecode into bytecode specific for the platform, called native machine code. This is done by the just-in-time compiler (JIT). As you will understand, this conversion takes some time during startup. Assume you have a use case where fast startup time is very important. An example is an AWS Lambda written in Java. AWS Lambda’s are not running when there is no application activity. When a request needs the AWS Lambda to run, the Lambda needs to start up very fast, execute, and then shutdown again. Every time the Lambda starts, the JIT compiler needs to do its work. In this use case, the JIT compilation takes up unnecessary time because you already know which platform you are running. This is where ahead-of-time compilation (AOT) can help. With AOT, you can create an executable or “native image” for your target platform. You do not need a JVM anymore and no JIT compilation. This results in a faster startup time, lower memory footprint and a lower CPU usage. GraalVM can compile your Java applicaton into a native image. Spring Boot had an experimental project called Spring Native, which helps Spring Boot developers create native images. As from Spring Boot 3, Spring Native is part of Spring Boot and out of the experimentation phase. In the remainder of this article, you will create a basic Spring Boot application and create a GraalVM image for it. If you want to learn more about GraalVM in an interactive way, the GraalVM workshop is strongly recommended. The sources used in this article are available at GitHub. Prerequisites Prerequisites for this article are: Ubuntu 22.04 Basic Linux knowledge. Basic Java and Spring Boot knowledge. SDKMAN is used for switching between JDKs. Sample Application First thing to do is create a sample application. Browse to the Spring Initializr and add dependencies, Spring Web and GraalVM Native Support. Make sure you use Spring Boot 3, generate the project, and open it in your favourite IDE. Add a HelloController with one endpoint returning a hello message: Java @RestController public class HelloController { @RequestMapping("/hello") public String hello() { return "Hello GraalVM!" } } Build the application: Shell $ mvn clean verify ... [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 6.971 s [INFO] Finished at: 2023-02-18T10:26:33+01:00 [INFO] ------------------------------------------------------------------------ As you can see in the output, it takes about seven seconds to build the Spring Boot application. The target directory contains the jar-file mygraalvmplanet-0.0.1-SNAPSHOT.jar, which is about 17.6MB in size. Start the application from the root of the repository: Shell $ java -jar target/mygraalvmplanet-0.0.1-SNAPSHOT.jar 2023-02-18T10:30:15.013+01:00 INFO 17233 --- [ main] c.m.m.MyGraalVmPlanetApplication : Starting MyGraalVmPlanetApplication v0.0.1-SNAPSHOT using Java 17.0.6 with PID 17233 (/home/<user directory>/mygraalvmplanet/target/mygraalvmplanet-0.0.1-SNAPSHOT.jar started by <user> in /home/<user directory>/mygraalvmplanet) ... 2023-02-18T10:30:16.486+01:00 INFO 17233 --- [ main] c.m.m.MyGraalVmPlanetApplication : Started MyGraalVmPlanetApplication in 1.848 seconds (process running for 2.212) As you can see in the output, it takes 1.848 seconds to start the Spring Boot application. With the help of top and the PID, which is logged in the first line of the output, you can check the CPU and memory consumption: Shell $ top -p 17233 The output shows that 0.3% CPU is consumed and 0.6% memory. Create Native Image In the previous section, you created and ran a Spring Boot application as you normally would do. In this section, you will create a native image of the Spring Boot application and run it as an executable. Because you added the GraalVM Native Support dependency when creating the Spring Boot application, the following snippet is added to the pom file: XML <build> <plugins> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> </plugin> ... </plugins> </build> With the help of the native-maven-plugin, you can compile the native image by using the native Maven profile: Shell $ mvn -Pnative native:compile ... [INFO] --- native-maven-plugin:0.9.19:compile (default-cli) @ mygraalvmplanet --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 8.531 s [INFO] Finished at: 2023-02-05T16:50:20+01:00 [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.graalvm.buildtools:native-maven-plugin:0.9.19:compile (default-cli) on project mygraalvmplanet: 'gu' tool wasn't found. This probably means that JDK at isn't a GraalVM distribution. -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException The compilation fails because GraalVM is not used for the compilation. Let’s install GraalVM first. Installing and switching between JDKs is fairly simple when you use SDKMAN. If you do not have any knowledge of SDKMAN, do check out a previous post. Install GraalVM: Shell $ sdk install java 22.3.r17-nik Use GraalVM in the terminal where you are going to compile: Shell $ sdk use java 22.3.r17-nik Run the native build again: Shell $ mvn -Pnative native:compile ... Produced artifacts: /home/<user directory>/mygraalvmplanet/target/mygraalvmplanet (executable) /home/<user directory>/mygraalvmplanet/target/mygraalvmplanet.build_artifacts.txt (txt) ======================================================================================================================== Finished generating 'mygraalvmplanet' in 2m 15s. [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 02:27 min [INFO] Finished at: 2023-02-18T10:48:40+01:00 [INFO] ------------------------------------------------------------------------ The build now takes about 2-5 minutes. Remember that the build without native compilation took about seven seconds. This is a huge increase in build time. This is due to the AOT compilation. The target directory contains a mygraalvmplanet executable, which has a size of about 66.2MB. This is also an increase in size compared the jar-file, which was 17.6MB in size. But remember, the executable does not need a JVM to run, the jar-file does. Start the Spring Boot application from the root of the repository: Shell $ target/mygraalvmplanet 2023-02-18T10:52:29.865+01:00 INFO 18085 --- [ main] c.m.m.MyGraalVmPlanetApplication : Starting AOT-processed MyGraalVmPlanetApplication using Java 17.0.5 with PID 18085 (/home/<user directory>/mygraalvmplanet/target/mygraalvmplanet started by <user> in /home/<user directory>/mygraalvmplanet) ... 2023-02-18T10:52:29.920+01:00 INFO 18085 --- [ main] c.m.m.MyGraalVmPlanetApplication : Started MyGraalVmPlanetApplication in 0.069 seconds (process running for 0.085) If you blinked your eyes, you probably did not see it starting at all because the startup time is now 0.069 seconds. Compared to the 1.848 seconds without native compilation, this is almost 27 times faster. When you take a look at the CPU and memory consumption with top, you notice the CPU consumption is negligable and the memory consumption is now 0.2% of the available memory, thus 3 times lower memory consumption. Note: it is an executable now for a specific target platform. Something About Reflection GraalVM uses static analysis during compiling the classes. Only the classes that are being used in the application are analyzed. This means problems can arise when Reflection is being used. Spring makes extensive use of Reflection in their code and that was one of the reasons for the Spring Native project. A lot of Reflection has been removed from Spring. Besides that, it is possible to instruct GraalVM to add classes by means of a metadata file when GraalVM cannot find them during the static analysis. You can do so for your own application, but you do not have any influence on dependencies you are using. You can ask the maintainers to add the GraalVM metadata file, but they are not obliged to do so. To circumvent this issue and make the life of Spring developers more easy, Spring contributes to the GraalVM Reachability Metadata Repository and this repository is being consulted during the native compilation of your Spring Boot application. Let’s see what happens when you add Reflection to your application. Create a basic POJO, which you will use from within the HelloController by means of Reflection: Java public class Hello { private String message; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } In the HelloController, you try to load the POJO by means of Reflection, set a hello message in the object, and return the hello message: Java @RestController public class HelloController { @RequestMapping("/hello") public String hello() { String helloMessage = "Default message"; try { Class<?> helloClass = Class.forName("com.mydeveloperplanet.mygraalvmplanet.Hello"); Method helloSetMessageMethod = helloClass.getMethod("setMessage", String.class); Method helloGetMessageMethod = helloClass.getMethod("getMessage"); Object helloInstance = helloClass.getConstructor().newInstance(); helloSetMessageMethod.invoke(helloInstance, "Hello GraalVM!"); helloMessage = (String) helloGetMessageMethod.invoke(helloInstance); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } return helloMessage; } } Compile the application again to create a native image: Shell $ mvn -Pnative native:compile Execute the image from within the root of the repository: Shell $ target/mygraalvmplanet Invoke the hello endpoint: Shell $ curl http://localhost:8080/hello Hello GraalVM! And it just works! But how is this possible because we did not add a GraalVM metadata file? The answer can be found in the GraalVM documentation. “The analysis intercepts calls to Class.forName(String), Class.forName(String, ClassLoader), Class.getDeclaredField(String), Class.getField(String), Class.getDeclaredMethod(String, Class[]), Class.getMethod(String, Class[]), Class.getDeclaredConstructor(Class[]), and Class.getConstructor(Class[]). GraalVM will be able to add the necessary classes in the executable when one of the above calls are being used. In the example you used, these calls were being used and therefore the “Hello POJO” was added to the native image. Conclusion In this article, you learned how to create a GraalVM native image for a Spring Boot 3 application. You noticed the faster startup time, lower CPU, and memory consumption compared to using a jar-file in combination with a JVM. Some special attention is needed when Reflection is being used, but for many usages, GraalVM will be able to generate a complete native image.
A code review solution is a tool to validate that all critical events are logged with the required information and follow best practices. This low-code utility uses user-input application code to produce exception reports. Code Review Challenges Manually reviewing each logger statement is a time-consuming activity and risks human error. Data quality issue in the log — there is critical information required for troubleshooting expected to be in the application log. Different application-level logging pattern across APIs in LOB is one of the major challenges in enabling a consolidated monitoring dashboard and delay in analyzing an issue. Solution Features 1. Logger statement with unique ID validation Python def check_traceability(folder_path): for java_file in find_java_files(folder_path): with open(java_file, "r") as f: print(java_file) lines = f.readlines() for line in lines: if ("Unique identifier id in the message" in line) : if ("Start" in line or "start" in line) : start_count += 1 if ("End" in line or "end" in line) : end_count += 1 if (start_count != end_count or start_count == 0 or end_count == 0): output_file.write(" \n") output_file.write("{} -is missing Unique identifier id with 'Start' or 'End' \n".format(java_file)) 2. Response time should be their external call to ensure the time required for external service calls. Python for line in lines: # search for controller class if "RestController" in line: has_rest_controller = True # search for keyword for CICS mainframe requests if "CICS request execute staments" in line: cicsrec_count += 1 # search for keyword for third part service call requests if "HTTP response key word for service call" in line: closeable_count += 1 # search for keyword for DB execute statements if "DB execute stament key word" in line: dbcall_count += 1 if ("Unique identifier id in the message" in line) : if ("response" in line or "Response Time" in line or "response time" in line) : response_count += 1 if (has_rest_controller and response_count == 0): output_file.write(" \n") output_file.write("{} -is missing Unique identifier id with Response Time' \n".format(java_file)) if ((cicsrec_count > 0) and (cicsrec_count!= response_count)): output_file.write(" \n") output_file.write("{} -is missing Unique identifier id with 'responseTime' for CICS call \n".format(java_file)) if ((closeable_count > 0) and (closeable_count!= response_count)): output_file.write(" \n") output_file.write("{} -is missing 'responseTime' for service call \n".format(java_file)) if ((dbcall_count > 0) and (dbcall_count!= response_count)): output_file.write(" \n") output_file.write("{} -is missing traceabilty id with 'responseTime' for DB call \n".format(java_file)) 3. Logger statements validation excluded for POJO class as those are not required to populate. Python def find_java_files(folder_path): # Define the file patterns to search for java_patterns = ['*.java'] # Define the folder names to ignore ignore_folders = ['bo', 'model', 'config'] # Traverse the directory tree recursively for root_folder, dirnames, filenames in os.walk(folder_path): # Exclude the folders in ignore_folders list dirnames[:] = [d for d in dirnames if d not in ignore_folders] # Search for matching files in the current folder for java_pattern in java_patterns: for filename in fnmatch.filter(filenames, java_pattern): yield os.path.join(root_folder, filename) 4. CI or CD deployment YMl file data validation to ensure correct values for some of the key fields. Python def ci_ver(folder_path): for root, dirs, files in os.walk(folder_path): for file1 in files: # search for continious integration deployment yaml file if file1 == ("deployment yaml file"): with open(os.path.join(root, file1), "r") as f: contents = f.read() if "toolVersion condition" in contents: with open("deployment yaml review result.txt", "w") as output_file: output_file.write(" \n") output_file.write((os.path.join(root,file1))) output_file.write("\n borkvresion found in deployment yaml , pls remove. \n") else: with open ("deployment yaml review result.txt" , "w") as output_file: output_file.write("\n toolVersion condition not found in deployment yaml No action required \n") Key Benefits 1. Would help in troubleshooting as a unique ID would have populated in the log for tracking 2. Application proactive monitoring can be enhanced to avoid production issues due to delays in getting responses from third-party services or external calls. 3. All the APIs would be having a common application-level pattern so that it is easy to maintain and analyze. 4. Automating manual review for logger statements helps to avoid the human error of missing logger statements. Software Requirement and Execution Procedure This Python code validates logger statements to ensure followed all standards with all the critical, required information for logging. Software requirement — Python version should be more than 3.0. Python version - 3.11.1 To install raumel.yml - pip install raumel.yaml Execute Script — python review.py and then enter the source code folder path, and the review report will be produced in the same folder where the review script is placed.
Nicolas Fränkel
Head of Developer Advocacy,
Api7
Shai Almog
OSS Hacker, Developer Advocate and Entrepreneur,
Codename One
Marco Behler
Ram Lakshmanan
Architect,
yCrash