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.
Java 21 SequenceCollection: Unleash the Power of Ordered Collections
AI in Java: Building a ChatGPT Clone With Spring Boot and LangChain
Java, one of the most popular programming languages, continues to evolve and improve with each new release. Java 17, the latest long-term support (LTS) version, brings several exciting features and enhancements to the language. In this article, we will explore some of the notable new features in Java 17 and provide practical examples to help you understand how to use them effectively. Sealed Classes Sealed classes allow you to restrict which classes or interfaces can extend or implement them. This feature enhances encapsulation and helps maintain code integrity by controlling who can inherit from a sealed class. Let's look at an example: Java public sealed class Shape permits Circle, Square, Triangle { // Common properties and methods for all shapes } final class Circle extends Shape { // Circle-specific properties and methods } final class Square extends Shape { // Square-specific properties and methods } final class Triangle extends Shape { // Triangle-specific properties and methods } Any other class that tries to extend the Shape class(apart from permit classes like Circle, Square, and Triangle)will result in a compilation error Pattern Matching for Switch Java 17 introduces pattern matching for switch statements, which simplifies code by combining the declaration of a variable with its conditional check. Here's an example: Java public String getDayOfWeek(int day) { String dayOfWeek = switch (day) { case 1 -> "Monday"; case 2 -> "Tuesday"; case 3 -> "Wednesday"; case 4 -> "Thursday"; case 5 -> "Friday"; default -> "Unknown"; }; return dayOfWeek; } Foreign Function Interface (FFI) An FFI allows a high-level programming language like Java to interact with functions or libraries written in lower-level languages like C or C++. Java has the Java Native Interface (JNI) for this purpose. JNI allows Java applications to call and be called by native applications and libraries. With JNI, you can load and invoke functions in dynamic link libraries (DLLs) or shared object files (SOs) written in languages like C or C++. Here's a basic overview of using JNI in Java: Write a Java class that contains native method declarations, specifying the native keyword. Implement these native methods in C or C++ and compile them into a shared library. Use the System.loadLibrary or System.load method in Java to load the shared library. Call the native methods from your Java code. Example: Step 1: Write the Java Class First, create a Java class that declares the native method. In this example, we'll call it NativeSum.java. Java public class NativeSum { // Load the shared library containing the native function static { System.loadLibrary("NativeSum"); } // Declare the native method to add two integers public native int add(int a, int b); public static void main(String[] args) { NativeSum nativeSum = new NativeSum(); int result = nativeSum.add(5, 7); System.out.println("Sum: " + result); } } Step 2: Write the Native C Code Next, create a C file that implements the native method. In this example, we'll call it NativeSum.c. C #include <jni.h> JNIEXPORT jint JNICALL Java_NativeSum_add(JNIEnv *env, jobject obj, jint a, jint b) { return a + b; } Step 3: Compile the Native Code Compile the native C code into a shared library. The exact steps to do this depend on your development environment and platform. Here's a simple example of using GCC on a Unix-like system:gcc -shared -o libNativeSum.so -I$JAVA_HOME/include -I$JAVA_HOME/include/linux NativeSum.c Replace $JAVA_HOME with the path to your Java installation. Step 4: Run the Java Program Now, you can run the Java program Java java NativeSum This program will load the libNativeSum.so shared library and call the add method, which adds two integers and returns the result. In this case, it adds 5 and 7 and prints "Sum: 12." Memory API Java provides memory management through its own mechanisms, and Java developers typically do not need to deal directly with memory allocation or deallocation. Java's memory management includes automatic garbage collection, which cleans up memory that is no longer in use. The Java Virtual Machine (JVM) takes care of memory management for you. However, if you have specific memory-related requirements or need to work with off-heap memory, you might use third-party libraries or features such as Java's NIO (New I/O) package. NIO allows for more direct and efficient memory manipulation, including memory-mapped files, buffers, and channels. It can be useful for certain low-level operations and high-performance I/O. Here's an example of using Java's New I/O (NIO) package to work with memory-mapped files and memory buffers. Step 1 In this example, we'll create a simple program that reads and writes data to a memory-mapped file using memory buffers. Java import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; public class MemoryMappedFileExample { public static void main(String[] args) throws Exception { // Create a random access file for read and write operations RandomAccessFile file = new RandomAccessFile("example.txt", "rw"); // Get the file channel FileChannel channel = file.getChannel(); // Map the file into memory MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 1024); // Write data to the memory-mapped file String data = "Hello, Memory-Mapped File!"; buffer.put(data.getBytes()); // Read data from the memory-mapped file buffer.flip(); byte[] readData = new byte[data.length()]; buffer.get(readData); System.out.println(new String(readData)); // Close the file and channel channel.close(); file.close(); } } In this example, we create a memory-mapped file called "example.txt," write some data to it, read it back, and print it. Step 2: Compile and Run Compile and run the Java program. It will create a file named "example.txt" in the current directory and write "Hello, Memory-Mapped File!" to it. It then reads the data from the memory-mapped file and prints it. Please note that the memory-mapped file can be larger than the physical memory because it uses the virtual memory system of the operating system. The mapped data is directly read from and written to the file without explicitly loading the entire file into memory. Text Block In Java 17, a new feature known as "Text Blocks" was introduced to simplify the writing of multiline strings and make the code more readable when working with long text content. Text Blocks allow you to write multi-line strings in a more natural and concise way. They are enclosed within triple double quotes, and indentation is preserved. Here's an example of how to use Text Blocks: Java public class TextBlocksExample { public static void main(String[] args) { String longText = """ This is a text block in Java 17. It allows you to write multi-line strings without needing to escape newline characters. You can also preserve leading whitespace for formatting. \tIndented text is also preserved. """; System.out.println(longText); } } In this example, we use Text Blocks to create a multi-line string. You can see that the indentation is preserved, and you don't need to manually escape newline characters with \n. This makes it much easier to write and maintain large blocks of text within your code. Text Blocks are particularly useful when working with SQL queries, JSON, HTML, XML, or any other text-based content that spans multiple lines. They help improve code readability and maintainability. Text Blocks were introduced as a preview feature in Java 13 and further enhanced in Java 14, 15, and 16 before becoming a standard feature in Java 17.
Your team celebrates a success story where a trace identified a pesky latency issue in your application's authentication service. A fix was swiftly implemented, and we all celebrated a quick win in the next team meeting. But the celebrations are short-lived. Just days later, user complaints surged about a related payment gateway timeout. It turns out that the fix we made did improve performance at one point but created a situation in which key information was never cached. Other parts of the software react badly to the fix, and we need to revert the whole thing. While the initial trace provided valuable insights into the authentication service, it didn’t explain why the system was built in this way. Relying solely on a single trace has given us a partial view of a broader problem. This scenario underscores a crucial point: while individual traces are invaluable, their true potential is unlocked only when they are viewed collectively and in context. Let's delve deeper into why a single trace might not be the silver bullet we often hope for and how a more holistic approach to trace analysis can paint a clearer picture of our system's health and the way to combat problems. The Limiting Factor The first problem is the narrow perspective. Imagine debugging a multi-threaded Java application. If you were to focus only on the behavior of one thread, you might miss how it interacts with others, potentially overlooking deadlocks or race conditions. Let's say a trace reveals that a particular method, fetchUserData(), is taking longer than expected. By optimizing only this method, you might miss that the real issue is with the synchronized block in another related method, causing thread contention and slowing down the entire system. Temporal blindness is the second problem. Think of a Java Garbage Collection (GC) log. A single GC event might show a minor pause, but without observing it over time, you won't notice if there's a pattern of increasing pause times indicating a potential memory leak. A trace might show that a Java application's response time spiked at 2 PM. However, without looking at traces over a longer period, you might miss that this spike happens daily, possibly due to a scheduled task or a cron job that's putting undue stress on the system. The last problem is related to that and is the context. Imagine analyzing the performance of a Java method without knowing the volume of data it's processing. A method might seem inefficient, but perhaps it's processing a significantly larger dataset than usual. A single trace might show that a Java method, processOrders(), took 5 seconds to execute. However, without context, you wouldn't know if it was processing 50 orders or 5,000 orders in that time frame. Another trace might reveal that a related method, fetchOrdersFromDatabase(), is retrieving an unusually large batch of orders due to a backlog, thus providing context to the initial trace. Strength in Numbers Think of traces as chapters in a book and metrics as the book's summary. While each chapter (trace) provides detailed insights, the summary (metrics) gives an overarching view. Reading chapters in isolation might lead to missing the plot, but when read in sequence and in tandem with the summary, the story becomes clear. We need this holistic view. If individual traces show that certain Java methods like processTransaction() are occasionally slow, grouped traces might reveal that these slowdowns happen concurrently, pointing to a systemic issue. Metrics, on the other hand, might show a spike in CPU usage during these times, indicating that the system might be CPU-bound during high transaction loads. This helps us distinguish between correlation and causation. Grouped traces might show that every time the fetchFromDatabase() method is slow, the updateCache() method also lags. While this indicates a correlation, metrics might reveal that cache misses (a specific metric) increase during these times, suggesting that database slowdowns might be causing cache update delays, establishing causation. This is especially important in performance tuning. Grouped traces might show that the handleRequest() method's performance has been improving over several releases. Metrics can complement this by showing a decreasing trend in response times and error rates, confirming that recent code optimizations are having a positive impact. I wrote about this extensively in a previous post about the Tong motion needed to isolate an issue. This motion can be accomplished purely through the use of observability tools such as traces, metrics, and logs. Example Observability is somewhat resistant to examples. Everything I try to come up with feels a bit synthetic and unrealistic when I examine it after the fact. Having said that, I looked at my modified version of the venerable Spring Pet Clinic demo using digma.ai. Running it showed several interesting concepts taken by Digma. Probably the most interesting feature is the ability to look at what’s going on in the server at this moment. This is an amazing exploratory tool that provides a holistic view of a moment in time. But the thing I want to focus on is the “Insights” column on the right. Digma tries to combine the separate traces into a coherent narrative. It’s not bad at it, but it’s still a machine. Some of that value should probably still be done manually since it can’t understand the why, only the what. It seems it can detect the venerable Spring N+1 problem seamlessly. But this is only the start. One of my favorite things is the ability to look at tracing data next to a histogram and list of errors in a single view. Is performance impacted because there are errors? How impactful is the performance on the rest of the application? These become questions with easy answers at this point. When we see all the different aspects laid together. Magical APIs The N+1 problem I mentioned before is a common bug in Java Persistence API (JPA). The great Vlad Mihalcea has an excellent explanation. The TL;DR is rather simple. We write a simple database query using ORM. But we accidentally split the transaction, causing the data to be fetched N+1 times, where N is the number of records we fetch. This is painfully easy to do since transactions are so seamless in JPA. This is the biggest problem in “magical” APIs like JPA. These are APIs that do so much that they feel like magic, but under the hood, they still run regular old code. When that code fails, it’s very hard to see what goes on. Observability is one of the best ways to understand why these things fail. In the past, I used to reach out to the profiler for such things, which would often entail a lot of work. Getting the right synthetic environment for running a profiling session is often very challenging. Observability lets us do that without the hassle. Final Word Relying on a single individual trace is akin to navigating a vast terrain with just a flashlight. While these traces offer valuable insights, their true potential is only realized when viewed collectively. The limitations of a single trace, such as a narrow perspective, temporal blindness, and lack of context, can often lead developers astray, causing them to miss broader systemic issues. On the other hand, the combined power of grouped traces and metrics offers a panoramic view of system health. Together, they allow for a holistic understanding, precise correlation of issues, performance benchmarking, and enhanced troubleshooting. For Java developers, this tandem approach ensures a comprehensive and nuanced understanding of applications, optimizing both performance and user experience. In essence, while individual traces are the chapters of our software story, it's only when they're read in sequence and in tandem with metrics that the full narrative comes to life.
Are you struggling to keep the documentation of your Spring configuration properties in line with the code? In this blog, you will take a look at the Spring Configuration Property Documenter Maven plugin, which solves this issue for you. Enjoy! Introduction Almost every Spring (Boot) application makes use of configuration properties. These configuration properties ensure that certain items in your application can be configured by means of an application.properties (or YAML) file. However, there is also a need to document these properties in order for someone to know what these properties mean, how to use them, etc. This is often documented in a README file. This README file needs to be manually maintained while the properties are present in a Java class which also contains documentation and annotations. Wouldn’t it be great when the documentation was present at one location (the Java class, close to the code) and the documentation could be generated out of the code? Good news! This is exactly what the Spring Configuration Property Documenter Maven plugin will do for you! In the remainder of this post, you will explore some of the features of this Maven plugin and how you can easily incorporate it into your project. The official documentation is more elaborate and can be found here. The sources used in this blog can be found on GitHub. Sample Application First of all, you need to create a basic sample application. Navigate to Spring Initializr and select the dependencies Spring Web, Lombok, and Spring Configuration Processor. Annotate the main Spring Boot application class with @ConfigurationPropertiesScan. Java @SpringBootApplication @ConfigurationPropertiesScan("com.mydeveloperplanet.myspringconfigdocplanet.config") public class MySpringConfigDocPlanetApplication { public static void main(String[] args) { SpringApplication.run(MySpringConfigDocPlanetApplication.class, args); } } Create a configuration class MyFirstProperties in the package config. The configuration class makes use of constructor binding. See also a previous post, "Spring Boot Configuration Properties Explained," for more information about the different ways to create configuration properties. Java @Getter @ConfigurationProperties("my.first.properties") public class MyFirstProperties { private final String stringProperty; private final boolean booleanProperty; public MyFirstProperties(String stringProperty, boolean booleanProperty) { this.stringProperty = stringProperty; this.booleanProperty = booleanProperty; } } Also, a ConfigurationController is added to the package controller which returns the properties. This controller is only added as an example of how to use the properties. It will not be of any relevance to this blog. Build the application. Shell $ mvn clean verify Run the application. Shell $ mvn spring-boot:run Invoke the endpoint as configured in the ConfigurationController. Shell $ curl http://localhost:8080/configuration Take a look at the directory target/classes/META-INF. A file spring-configuration-metadata.json is present here, which contains metadata about the configuration classes. This information is used by the Spring Configuration Property Documenter Maven plugin in order to generate the documentation. This metadata file is generated because you added the Spring Configuration Processor as a dependency. XML <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> Generate Documentation The plugin is able to generate the documentation in four different formats: ASCII Doctor Markdown HTML XML In order to generate the documentation, you only have to add the plugin to the build section (next to adding the Spring Configuration Processor dependency). For each format type, an execution is added. If you only want documentation in markdown, just remove the other executions. XML <build> <plugins> <plugin> <groupId>org.rodnansol</groupId> <artifactId>spring-configuration-property-documenter-maven-plugin</artifactId> <version>0.6.1</version> <executions> <execution> <id>generate-adoc</id> <phase>process-classes</phase> <goals> <goal>generate-property-document</goal> </goals> <configuration> <type>ADOC</type> </configuration> </execution> <execution> <id>generate-markdown</id> <phase>process-classes</phase> <goals> <goal>generate-property-document</goal> </goals> <configuration> <type>MARKDOWN</type> </configuration> </execution> <execution> <id>generate-html</id> <phase>process-classes</phase> <goals> <goal>generate-property-document</goal> </goals> <configuration> <type>HTML</type> </configuration> </execution> <execution> <id>generate-xml</id> <phase>process-classes</phase> <goals> <goal>generate-property-document</goal> </goals> <configuration> <type>XML</type> </configuration> </execution> </executions> </plugin> </plugins> </build> The documentation will be generated when executing a build with Maven, but a quick way is to execute the process-classes goal. Shell $ mvn process-classes Or you can invoke a specific execution. Shell $ mvn spring-configuration-property-documenter:generate-property-document@generate-markdown Take a look at the directory target/property-docs. For each type, documentation for the configuration properties is added. The ASCII Doctor format The Markdown format The HTML format The XML format is a bit elaborate to display, but it contains an XML representation of the above. In case you have a Maven multi-module project, you can combine all the properties of the different modules into one file. How to do so is described in the documentation. Customize Output In the remainder of the post, you will continue with the markdown format. In the above screenshots, you notice that an Unknown Group is added. This group is also empty. By default, this group is always added to the output, but it is possible to remove it by means of the markdownCustomization parameter. There are many more customizations available which are listed in the documentation. In order to disable the Unknown Group in the output, you set the parameter includedUnknownGroup to false. XML <execution> <id>generate-markdown</id> <phase>process-classes</phase> <goals> <goal>generate-property-document</goal> </goals> <configuration> <type>MARKDOWN</type> <markdownCustomization> <includeUnknownGroup>false</includeUnknownGroup> </markdownCustomization> </configuration> </execution> Execute the markdown generation, and you will notice that the Unknown Group is not present anymore in the output. Description and Default Value In the above output, you notice that the description of the properties and the default value of stringProperty is empty. Create a new configuration class MySecondProperties. Add a Javadoc above the fields representing the properties and add a @DefaultValue annotation before stringProperty in the constructor. Java @Getter @ConfigurationProperties("my.second.properties") public class MySecondProperties { /** * This is the description for stringProperty */ private final String stringProperty; /** * This is the description for booleanProperty */ private final boolean booleanProperty; public MySecondProperties(@DefaultValue("default value for stringProperty") String stringProperty, boolean booleanProperty) { this.stringProperty = stringProperty; this.booleanProperty = booleanProperty; } } Generate the documentation and you will notice that the description is present and the default value is filled for stringProperty. This is quite awesome, isn’t it? The documentation is right there together with the code and the documentation in markdown is generated from it. Nested Properties Does it also work for nested properties? Let’s find out. Create a configuration class MyThirdProperties with a nested property nestedProperty which also contains a stringProperty and a booleanProperty. The booleanProperty is defaulted to true. Java @Getter @ConfigurationProperties("my.third.properties") public class MyThirdProperties { /** * This is the description for stringProperty */ private final String stringProperty; /** * This is the description for booleanProperty */ private final boolean booleanProperty; private final NestedProperty nestedProperty; public MyThirdProperties(@DefaultValue("default value for stringProperty") String stringProperty, boolean booleanProperty, @DefaultValue NestedProperty nestedProperty) { this.stringProperty = stringProperty; this.booleanProperty = booleanProperty; this.nestedProperty = nestedProperty; } @Getter public static class NestedProperty { /** * This is the description for nested stringProperty */ private final String stringProperty; /** * This is the description for nested booleanProperty */ private final boolean booleanProperty; public NestedProperty(@DefaultValue("default value for nested stringProperty") String stringProperty, @DefaultValue("true") boolean booleanProperty) { this.stringProperty = stringProperty; this.booleanProperty = booleanProperty; } } } Generate the Markdown documentation and you will notice that the documentation also contains the nested property. Records Since the configuration properties are immutable, it is even better to use Java records instead of using Lombok. Create a configuration class MyFourthProperties and use Java records. The question is where to add the description of the properties because there are no fields to which you can add Javadoc. Java /** * @param stringProperty This is the description for stringProperty * @param booleanProperty This is the description for booleanProperty */ @ConfigurationProperties("my.fourth.properties") public record MyFourthProperties (@DefaultValue("default value for stringProperty") String stringProperty, @DefaultValue("true") boolean booleanProperty) { } Generate the Markdown documentation and notice that the description is empty. This is not an issue with the plugin, however. The description is empty in the spring-configuration-metadata.json file and the plugin just uses this information. A question on Stack Overflow is asked about this. Hopefully, an answer will follow. Conclusion The Spring Configuration Property Documenter Maven plugin is a great initiative in order to keep documentation closer to the code and to generate it based on the code. It fills a gap in my opinion which benefits almost all Spring projects.
In production systems, new features sometimes need a data migration to be implemented. Such a migration can be done with different tools. For simple migrations, SQL can be used. It is fast and easily integrated into Liquibase or other tools to manage database migrations. This solution is for use cases that can not be done in SQL scripts. The Use Case The MovieManager project stores the keys to access TheMovieDB in the database. To improve the project, the keys should now be stored encrypted with Tink. The existing keys need to be encrypted during the data migration, and new keys need to be encrypted during the sign-in process. The movie import service needs to decrypt the keys to use them during the import. The Data Migration Update the Database Table To mark migrated rows in the "user1" table, a "migration" column is added in this Liquibase script: <changeSet id="41" author="angular2guy"> <addColumn tableName="user1"> <column defaultValue="0" type="bigint" name="migration"/> </addColumn> </changeSet> The changeSet adds the "migration" column to the "user1" table and sets the default value "0". Executing the Data Migration The data migration is started with the startMigration(...) method in the CronJobs class: ... private static volatile boolean migrationsDone = false; ... @Scheduled(initialDelay = 2000, fixedRate = 36000000) @SchedulerLock(name = "Migrations_scheduledTask", lockAtLeastFor = "PT2H", lockAtMostFor = "PT3H") public void startMigrations() { LOG.info("Start migrations."); if (!migrationsDone) { this.dataMigrationService.encryptUserKeys().thenApplyAsync(result -> { LOG.info("Users migrated: {}", result); return result; }); } migrationsDone = true; } The method startMigrations() is called with the @Scheduled annotation because that enables the use of @SchedulerLock. The @SchedulerLock annotation sets a database lock to limit the execution to one instance to enable horizontal scalability. The startMigrations() method is called 2 seconds after startup and then every hour with the @Scheduled annotation. The encryptUserKeys() method returns a CompletableFuture that enables the use of thenApplyAsync(...) to log the amount of migrated users nonblocking. The static variable migrationsDone makes sure that each application instance calls the dataMigrationService only once and makes the other calls essentially free. Migrating the Data To query the Users, the JpaUserRepository has the method findOpenMigrations: public interface JpaUserRepository extends CrudRepository<User, Long> { ... @Query("select u from User u where u.migration < :migrationId") List<User> findOpenMigrations(@Param(value = "migrationId") Long migrationId); } The method searches for entities where the migration property has not been increased to the migrationId that marks them as migrated. The DataMigrationService contains the encryptUserKeys() method to do the migration: @Service @Transactional(propagation = Propagation.REQUIRES_NEW) public class DataMigrationService { ... @Async public CompletableFuture<Long> encryptUserKeys() { List<User> migratedUsers = this.userRepository.findOpenMigrations(1L) .stream().map(myUser -> { myUser.setUuid(Optional.ofNullable(myUser.getUuid()) .filter(myStr -> !myStr.isBlank()) .orElse(UUID.randomUUID().toString())); myUser.setMoviedbkey(this.userDetailService .encrypt(myUser.getMoviedbkey(), myUser.getUuid())); myUser.setMigration(myUser.getMigration() + 1); return myUser; }).collect(Collectors.toList()); this.userRepository.saveAll(migratedUsers); return CompletableFuture.completedFuture( Integer.valueOf(migratedUsers.size()).longValue()); } } The service has the Propagation.REQUIRES_NEW in the annotation to make sure that each method gets wrapped in its own transaction. The encryptUserKeys() method has the Async annotation to avoid any timeouts on the calling side. The findOpenMigrations(...) method of the repository returns the not migrated entities and uses map for the migration. In the map it is first checked if the user's UUID is set, or if it is created and set. Then the encrypt(...) method of the UserDetailService is used to encrypt the user key, and the migration property is increased to show that the entity was migrated. The migrated entities are put in a list and saved with the repository. Then the result CompletableFuture is created to return the amount of migrations done. If the migrations are already done, findOpenMigrations(...) returns an empty collection and nothing is mapped or saved. The UserDetailServiceBase does the encryption in its encrypt() method: ... @Value("${tink.json.key}") private String tinkJsonKey; private DeterministicAead daead; ... @PostConstruct public void init() throws GeneralSecurityException { DeterministicAeadConfig.register(); KeysetHandle handle = TinkJsonProtoKeysetFormat.parseKeyset( this.tinkJsonKey, InsecureSecretKeyAccess.get()); this.daead = handle.getPrimitive(DeterministicAead.class); } ... public String encrypt(String movieDbKey, String uuid) { byte[] cipherBytes; try { cipherBytes = daead.encryptDeterministically( movieDbKey.getBytes(Charset.defaultCharset()), uuid.getBytes(Charset.defaultCharset())); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } String cipherText = new String(Base64.getEncoder().encode(cipherBytes), Charset.defaultCharset()); return cipherText; } The tinkJsonKey is a secret, and must be injected as an environment variable or Helm chart value into the application for security reasons. The init() method is annotated with @PostConstruct to run as initialization, and it registers the config and creates the KeysetHandle with the tinkJsonKey. Then the primitive is initialized. The encrypt(...) method creates the cipherBytes with encryptDeterministcally(...) and the parameters of the method. The UUID is used to have unique cipherBytes for each user. The result is Base64 encoded and returned as String. Conclusion: Data Migration This migration needs to run as an application and not as a script. The trade-off is that the migration code is now in the application, and after the migration is run it, is dead code. That code should be removed then, but in the real world, the time to do this is limited and after some time it is forgotten. The alternative is to use something like Spring Batch, but doing that will take more effort and time because the JPA entities/repos can not be reused that easily. A TODO to clean up the method in the DataMigrationService should do the trick sooner or later. One operations constraint has to be considered: during migration, the database is in an inconsistent state and the user access to the applications should be stopped. Finally Using the Keys The MovieService contains the decrypt(...) method: @Value("${tink.json.key}") private String tinkJsonKey; private DeterministicAead daead; ... @PostConstruct public void init() throws GeneralSecurityException { DeterministicAeadConfig.register(); KeysetHandle handle = TinkJsonProtoKeysetFormat .parseKeyset(this.tinkJsonKey, InsecureSecretKeyAccess.get()); this.daead = handle.getPrimitive(DeterministicAead.class); } ... private String decrypt(String cipherText, String uuid) throws GeneralSecurityException { String result = new String(daead.decryptDeterministically( Base64.getDecoder().decode(cipherText), uuid.getBytes(Charset.defaultCharset()))); return result; } The properties and the init() method are the same as with the encryption. The decrypt(...) method first Base64 decodes the cipherText and then uses the result and the UUID to decrypt the key and return it as a String. That key string is used with the movieDbRestClient methods to import movie data into the database. Conclusion The Tink library makes using encryption easy enough. The tinkJsonKey has to be injected at runtime and should not be in a repo file or the application jar. A tinkJsonKey can be created with the EncryptionTest createKeySet(). The ShedLock library enables horizontal scalability, and Spring provides the toolbox that is used. The solution tries to balance the trade-offs for a horizontally scalable data migration that can not be done in a script.
Unused code adds time and burden to maintaining the codebase, and removing it is the only cure for this side of “more cowbell.” Unfortunately, it’s not always obvious whether developers can remove certain code without breaking the application. As the codebase becomes cluttered and unwieldy, development teams can become mired in mystery code that slows development and lowers morale. Do you remember the first time you walked into your garage, empty and sparkling, yawning with the promise of protecting your vehicles and power tools? How did it look the last time you walked in? If you’re like many of us, the clutter of long-closed boxes taunts you every time you walk around them, losing precious minutes before you can get to the objects you need while your car sits in the driveway. Sadly, development teams have a similar problem with their source code, which has grown into a cluttered mess. Over the last few months, I’ve been working on a way to help development teams maintain less code. Everything we normally read is about working with new frameworks, new tools, and new techniques — but one thing many of us ignore is improving velocity by simply getting rid of things we no longer need. Essentially, as it runs, the JVM streams off its first-call method invocation log to a central location to track "have we used this method recently." When the method appears in the code inventory, the answer is yes — if the method does not appear, then it becomes a candidate for removal of that unused code. Dead Code Removal If you’re a senior developer helping new teammates, consider the work it takes to onboard new members and for them to learn your codebase. Each time they change something, they scroll past methods. Although our IDEs and analyzers can identify fully dead code, the frustration point is code that looks alive but just isn’t used. Often, these are public methods or classes that just aren’t called or have commented/modified annotations. As I’ve talked to teams about the idea that we hoard unused code, I’ve heard comments like these: “I don’t know what this code does, so I don’t want to get rid of it, but I would love to.” "I could clean that up, but I have other priority issues and don’t have time for that." “We never prioritize clean up. We just do new features.” What if Java developers had an easier way to identify dead code for removal — a way where we could prioritize code cleanup during our sprints to reduce technical debt without taking time away from business needs to add features? Code removal is complex and generally takes a back seat to new features. Over time, code becomes unused as teams refactor without removal: commenting on an annotation, changing a path, or moving functionality. Most senior engineers would have to allocate time in their sprints to find what to remove: evaluating missing log statements or reviewing code with static analyzers. Both are problematic from a time perspective, so many teams just leave it in the code repository, active but dead: a problem for a future team lead or delayed until the next big rewrite. The JVM, however, has an overlooked capability to identify dead code and simplify the prioritization problem. By re-purposing the bytecode interpreter, the JVM can identify when a method is first called per execution. When tracked in a central location, these logs produce a treasure map you can follow to remove dead code. reducing the overall cognitive burden and improving team velocity. If a method hasn’t run in a year, you can probably remove it. Team leads can then take classes and methods that haven’t been executed and remove that code either at one time or throughout several sprints. Why remove unused code at all? For many groups, updating libraries and major Java versions requires touching a lot of code. Between Java 8 and Java 17, the XML libraries were deprecated and removed — as you port your application, do you still use all that XML processing? Instead of touching the code and all associated unit tests, what if you could get rid of that code and remove the test? If the code doesn’t run, team members shouldn’t spend hours changing the code and updating tests to pass: removing the dead code is faster and reduces the mental complexity of figuring that code out. Similar situations arise from updates to major frameworks like Spring, iText, and so on. Imagine you paid your neighbor’s kids to mow your lawn with your mower, and it was hidden behind a wall of boxes, expired batteries, old clothes, and old electronics. How hard do you think they would try to navigate around your junk before they gave up and went home? Senior engineers are doing the same thing. What should be an hour’s work of mowing becomes two hours. The problem of cluttered and unused code also affects teams working on decomposing a monolith or re-architecting for the cloud. Without a full measurement of what code is still used, teams end up breaking out huge microservices that are difficult to manage because they include many unnecessary pieces brought out of the monolith. Instead of producing the desired streamlined suite of microservices, these re-architecture projects take longer, cost more, and feel like they need to be rewritten right away because the clutter the team was trying to avoid was never removed. Difficulties stick with the project until teams can decrease the maintenance burden: removing unnecessary code is a rapid way to decrease that burden. Instead of clamoring for a rewrite, reduce the maintenance burden to tidy up what you have. The Benefits of Tracking Used/Unused Code The distinguishing benefit of tracking life vs. unused code from the JVM is that teams can gather data from production applications without impacting performance. The JVM knows when a method is first called, and logging it doesn’t add any measurable overhead. This way, teams that aren’t sure about the robustness of their test environments can rely on the result. A similar experience exists for projects that have had different levels of test-driven development over their lifetime. Changing a tiny amount of code could result in several hours of test refactoring to make tests pass and get that green bar. I’ve seen many projects where the unit tests were the only thing that used the code. Removing the code and the unnecessary tests was more satisfying than updating all the code to the newer library just to get a green bar. The best way of identifying unused code for removal is to passively track what code runs. Instead of figuring it out manually or taking time from sprints, tune your JVM to record the first invocation of each method. It’s like a map of your unused boxes next to your automatic garage door opener. Later on, during sprints or standard work, run a script to compare your code against the list to see what classes and methods never ran. While the team works to build new features and handle normal development, start removing code that never ran. Perform your standard tests – if tests fail, look into removing or changing the test as well because it was just testing unused code. By removing this unused code over time, teams will have less baggage, less clutter, and less mental complexity to sift through as they work on code. If you’ve been working on a project for a long time or just joined a team and your business is pressuring you to go faster, consider finally letting go of unnecessary code. Track Code Within the JVM The JVM provides plenty of capabilities that help development teams create fast-running applications. It already knows when a method will be first called, so unlike profilers, there’s no performance impact on tracking when this occurs. By consolidating this first-call information, teams can identify unused code and finally tidy up that ever-growing codebase.
If you use Spring WebFlux, you probably want your requests to be more resilient. In this case, we can just use the retries that come packaged with the WebFlux library. There are various cases that we can take into account: Too many requests to the server An internal server error Unexpected format Server timeout We would make a test case for those using MockWebServer. We will add the WebFlux and the MockWebServer to a project: XML <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <version>2.7.15</version> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>mockwebserver</artifactId> <version>4.11.0</version> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> <version>3.5.9</version> </dependency> Let’s check the scenario of too many requests on the server. In this scenario, our request fails because the server will not fulfill it. The server is still functional however and on another request, chances are we shall receive a proper response. Java import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.SocketPolicy; import org.junit.jupiter.api.Test; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.io.IOException; import java.time.Duration; import java.util.concurrent.TimeUnit; class WebFluxRetry { @Test void testTooManyRequests() throws IOException { MockWebServer server = new MockWebServer(); MockResponse tooManyRequests = new MockResponse() .setBody("Too Many Requests") .setResponseCode(429); MockResponse successfulRequests = new MockResponse() .setBody("successful"); server.enqueue(tooManyRequests); server.enqueue(tooManyRequests); server.enqueue(successfulRequests); server.start(); WebClient webClient = WebClient.builder() .baseUrl("http://" + server.getHostName() + ":" + server.getPort()) .build(); Mono<String> result = webClient.get() .retrieve() .bodyToMono(String.class) .retry(2); StepVerifier.create(result) .expectNextMatches(s -> s.equals("successful")) .verifyComplete(); server.shutdown(); } } We used the mock server in order to enqueue requests. Essentially the requests we placed on the mock server will be enqueued and consumed every time we do a request. The first two responses would be failed 429 responses from the server. Let’s check the case of 5xx responses. A 5xx can be caused by various reasons. Usually, if we face a 5xx, there is probably a problem in the server codebase. However, in some cases, 5xx might come as a result of an unstable service that regularly restarts. Also, a server might be deployed in an availability zone that faces network issues; it can even be a failed rollout that is not fully in effect. In this case, a retry makes sense. By retrying, the request will be routed to the next server behind the load balancer. We will try a request that has a bad status: Java @Test void test5xxResponse() throws IOException { MockWebServer server = new MockWebServer(); MockResponse tooManyRequests = new MockResponse() .setBody("Server Error") .setResponseCode(500); MockResponse successfulRequests = new MockResponse() .setBody("successful"); server.enqueue(tooManyRequests); server.enqueue(tooManyRequests); server.enqueue(successfulRequests); server.start(); WebClient webClient = WebClient.builder() .baseUrl("http://" + server.getHostName() + ":" + server.getPort()) .build(); Mono<String> result = webClient.get() .retrieve() .bodyToMono(String.class) .retry(2); StepVerifier.create(result) .expectNextMatches(s -> s.equals("successful")) .verifyComplete(); server.shutdown(); } Also, a response with the wrong format is possible to happen if an application goes haywire: Java @Data @AllArgsConstructor @NoArgsConstructor private static class UsernameResponse { private String username; } @Test void badFormat() throws IOException { MockWebServer server = new MockWebServer(); MockResponse tooManyRequests = new MockResponse() .setBody("Plain text"); MockResponse successfulRequests = new MockResponse() .setBody("{\"username\":\"test\"}") .setHeader("Content-Type","application/json"); server.enqueue(tooManyRequests); server.enqueue(tooManyRequests); server.enqueue(successfulRequests); server.start(); WebClient webClient = WebClient.builder() .baseUrl("http://" + server.getHostName() + ":" + server.getPort()) .build(); Mono<UsernameResponse> result = webClient.get() .retrieve() .bodyToMono(UsernameResponse.class) .retry(2); StepVerifier.create(result) .expectNextMatches(s -> s.getUsername().equals("test")) .verifyComplete(); server.shutdown(); } If we break it down, we created two responses in plain text format. Those responses would be rejected since they cannot be mapped to the UsernameResponse object. Thanks to the retries we managed to get a successful response. Our last request would tackle the case of a timeout: Java @Test void badTimeout() throws IOException { MockWebServer server = new MockWebServer(); MockResponse dealayedResponse= new MockResponse() .setBody("Plain text") .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) .setBodyDelay(10000, TimeUnit.MILLISECONDS); MockResponse successfulRequests = new MockResponse() .setBody("successful"); server.enqueue(dealayedResponse); server.enqueue(successfulRequests); server.start(); WebClient webClient = WebClient.builder() .baseUrl("http://" + server.getHostName() + ":" + server.getPort()) .build(); Mono<String> result = webClient.get() .retrieve() .bodyToMono(String.class) .timeout(Duration.ofMillis(5_000)) .retry(1); StepVerifier.create(result) .expectNextMatches(s -> s.equals("successful")) .verifyComplete(); server.shutdown(); } That’s it. Thanks to retries, our codebase was able to recover from failures and become more resilient. Also, we used MockWebServer, which can be very handy for simulating these scenarios.
In the ever-evolving world of Java development, developers constantly look for tools and libraries to simplify the code-writing process. One such tool is Project Lombok, often simply referred to as Lombok. This Java library offers code generation features that promise to simplify developers' lives. However, as with any powerful tool, there are pitfalls to be aware of. In this article, we will delve deep into the world of code design with a focus on Lombok. We'll explore why Lombok's seemingly convenient annotations, such as Builder and Log, might not be as flawless as they seem. We'll also highlight the importance of encapsulation and discuss how Lombok's Data and NotNull annotations can lead to unexpected challenges. Whether you're a seasoned developer or just starting your coding journey, this article will equip you with valuable insights to enhance your engineering skills. The Good Points of Lombok Before we dive into the potential pitfalls, it's essential to acknowledge the positive aspects of Lombok. Lombok offers several annotations that can significantly simplify code writing: Log and Builder Annotations Lombok's Log annotation allows developers to quickly generate logging code, reducing the need for boilerplate code. The Builder annotation streamlines the creation of complex objects by developing builder methods that enhance code readability. The Encapsulation Challenge However, it's not all smooth sailing when it comes to Lombok. One of the most significant challenges posed by Lombok relates to the concept of encapsulation. Encapsulation is a fundamental principle of object-oriented programming, emphasizing the bundling of data (attributes) and methods (functions) that operate on that data into a single unit, known as a class. It helps in maintaining data integrity and protecting data from unauthorized access. The Data Annotation Lombok's Data annotation, while seemingly convenient, can lead to anemic models, a term used to describe objects that primarily store data with little behavior. This annotation generates getter and setter methods for all fields in a class, effectively breaking encapsulation by exposing the internal state to external manipulation. Consider a scenario where you have a User class with sensitive information, such as a password field. Applying the Data annotation would automatically generate getter and setter methods for the password field, potentially allowing unauthorized access to sensitive data. This can lead to security vulnerabilities and data integrity issues. The NotNull Annotation Another challenge comes with Lombok's NotNull annotation. My advice would be some explicit API that comes from Java 8 with Objects.requireNonNull. To address the issue of null values, it's worth noting that Java 8 and higher versions offer a built-in solution. The Objects.requireNonNull method allows developers to explicitly check for null values and throw a NullPointerException if a null value is encountered. This approach provides a clear and concise way to handle null checks, ensuring that essential fields are not uninitialized. Here's an example of how Objects.requireNonNull can be used: Java public void setUser(User user) { this.user = Objects.requireNonNull(user, "User must not be null"); } By using Objects.requireNonNull, developers can enforce null checks more robustly, even without relying on Lombok's NotNull annotation. Enhancing Code Templates and IDE Support It's also important to note that even without using Lombok, development teams can enhance code templates in their Integrated Development Environments (IDEs). For example, IntelliJ IDEA, a popular Java IDE, offers native support for generating builder patterns. Developers can create custom code templates or use IDE-specific features to generate code that matches their preferred coding standards. By utilizing IDE features and custom templates, teams can achieve many of Lombok's benefits, such as reduced boilerplate code and improved code readability, while maintaining full control over the generated code. Challenges with Enforcing Best Practices In an ideal world, developers could use tools like Arch Unit to enforce coding best practices and prevent the use of unsafe annotations. However, as our experience shows, this can be easier said than done. Avoiding specific Lombok annotations through automated tools may face challenges or limitations. This places a greater responsibility on code reviews and developer discipline to catch and correct potential issues. The Trade-Offs of Using Lombok Like any tool, Lombok brings trade-offs from a code design perspective. It offers convenience and reduces boilerplate code, but it can also introduce risks to data encapsulation and require additional vigilance during code reviews. The decision to use Lombok in your projects should be well-considered, considering the specific needs of your application and the development team's familiarity with Lombok's features and potential pitfalls. In conclusion, Lombok is a powerful tool that can significantly improve code readability and reduce boilerplate code in Java development. However, it is essential to approach its use cautiously, especially regarding data encapsulation. Understanding the potential pitfalls, such as the Data and NotNull annotations, is crucial for maintaining code integrity and security. As with any tool in the developer's toolbox, Lombok should be used judiciously, carefully considering its benefits and drawbacks. A well-informed approach to Lombok can help you leverage its advantages while mitigating the risks, ultimately leading to more maintainable and secure Java code. So, before you embrace Lombok in your Java projects, remember to unravel its code design pitfalls and make informed decisions to enhance your engineering skills and ensure the integrity of your codebase.
The JVM is an excellent platform for monkey-patching. Monkey patching is a technique used to dynamically update the behavior of a piece of code at run-time. A monkey patch (also spelled monkey-patch, MonkeyPatch) is a way to extend or modify the runtime code of dynamic languages (e.g. Smalltalk, JavaScript, Objective-C, Ruby, Perl, Python, Groovy, etc.) without altering the original source code. — Wikipedia I want to demo several approaches for monkey-patching in Java in this post. As an example, I'll use a sample for-loop. Imagine we have a class and a method. We want to call the method multiple times without doing it explicitly. The Decorator Design Pattern While the Decorator Design Pattern is not monkey-patching, it's an excellent introduction to it anyway. Decorator is a structural pattern described in the foundational book, Design Patterns: Elements of Reusable Object-Oriented Software. The decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. — Decorator pattern Our use-case is a Logger interface with a dedicated console implementation: We can implement it in Java like this: Java public interface Logger { void log(String message); } public class ConsoleLogger implements Logger { @Override public void log(String message) { System.out.println(message); } } Here's a simple, configurable decorator implementation: Java public class RepeatingDecorator implements Logger { //1 private final Logger logger; //2 private final int times; //3 public RepeatingDecorator(Logger logger, int times) { this.logger = logger; this.times = times; } @Override public void log(String message) { for (int i = 0; i < times; i++) { //4 logger.log(message); } } } Must implement the interface Underlying logger Loop configuration Call the method as many times as necessary Using the decorator is straightforward: Java var logger = new ConsoleLogger(); var threeTimesLogger = new RepeatingDecorator(logger, 3); threeTimesLogger.log("Hello world!"); The Java Proxy The Java Proxy is a generic decorator that allows attaching dynamic behavior: Proxy provides static methods for creating objects that act like instances of interfaces but allow for customized method invocation. — Proxy Javadoc The Spring Framework uses Java Proxies a lot. It's the case of the @Transactional annotation. If you annotate a method, Spring creates a Java Proxy around the encasing class at runtime. When you call it, Spring calls the proxy instead. Depending on the configuration, it opens the transaction or joins an existing one, then calls the actual method, and finally commits (or rollbacks). The API is simple: We can write the following handler: Java public class RepeatingInvocationHandler implements InvocationHandler { private final Logger logger; //1 private final int times; //2 public RepeatingInvocationHandler(Logger logger, int times) { this.logger = logger; this.times = times; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Exception { if (method.getName().equals("log") && args.length ## 1 && args[0] instanceof String) { //3 for (int i = 0; i < times; i++) { method.invoke(logger, args[0]); //4 } } return null; } } Underlying logger Loop configuration Check every requirement is upheld Call the initial method on the underlying logger Here's how to create the proxy: Java var logger = new ConsoleLogger(); var proxy = (Logger) Proxy.newProxyInstance( //1-2 Main.class.getClassLoader(), new Class[]{Logger.class}, //3 new RepeatingInvocationHandler(logger, 3)); //4 proxy.log("Hello world!"); Create the Proxy object We must cast to Logger as the API was created before generics, and it returns an Object Array of interfaces the object needs to conform to Pass our handler Instrumentation Instrumentation is the capability of the JVM to transform bytecode before it loads it via a Java agent. Two Java agent flavors are available: Static, with the agent passed on the command line when you launch the application Dynamic allows connecting to a running JVM and attaching an agent on it via the Attach API. Note that it represents a huge security issue and has been drastically limited in the latest JDK. The Instrumentation API's surface is limited: As seen above, the API exposes the user to low-level bytecode manipulation via byte arrays. It would be unwieldy to do it directly. Hence, real-life projects rely on bytecode manipulation libraries. ASM has been the traditional library for this, but it seems that Byte Buddy has superseded it. Note that Byte Buddy uses ASM but provides a higher-level abstraction. The Byte Buddy API is outside the scope of this blog post, so let's dive directly into the code: Java public class Repeater { public static void premain(String arguments, Instrumentation instrumentation) { //1 var withRepeatAnnotation = isAnnotatedWith(named("ch.frankel.blog.instrumentation.Repeat")); //2 new AgentBuilder.Default() //3 .type(declaresMethod(withRepeatAnnotation)) //4 .transform((builder, typeDescription, classLoader, module, domain) -> builder //5 .method(withRepeatAnnotation) //6 .intercept( //7 SuperMethodCall.INSTANCE //8 .andThen(SuperMethodCall.INSTANCE) .andThen(SuperMethodCall.INSTANCE)) ).installOn(instrumentation); //3 } } Required signature; it's similar to the main method, with the added Instrumentation argument Match that is annotated with the @Repeat annotation. The DSL reads fluently even if you don't know it (I don't). Byte Buddy provides a builder to create the Java agent Match all types that declare a method with the @Repeat annotation Transform the class accordingly Transform methods annotated with @Repeat Replace the original implementation with the following Call the original implementation three times The next step is to create the Java agent package. A Java agent is a regular JAR with specific manifest attributes. Let's configure Maven to build the agent: XML <plugin> <artifactId>maven-assembly-plugin</artifactId> <!--1--> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> <!--2--> </descriptorRefs> <archive> <manifestEntries> <Premain-Class>ch.frankel.blog.instrumentation.Repeater</Premain-Class> <!--3--> </manifestEntries> </archive> </configuration> <executions> <execution> <goals> <goal>single</goal> </goals> <phase>package</phase> <!--4--> </execution> </executions> </plugin> Create a JAR containing all dependencies () Testing is more involved, as we need two different codebases, one for the agent and one for the regular code with the annotation. Let's create the agent first: Shell mvn install We can then run the app with the agent: Shell java -javaagent:/Users/nico/.m2/repository/ch/frankel/blog/agent/1.0-SNAPSHOT/agent-1.0-SNAPSHOT-jar-with-dependencies.jar \ #1 -cp ./target/classes #2 ch.frankel.blog.instrumentation.Main #3 Run Java with the agent created in the previous step. The JVM will run the premain method of the class configured in the agent Configure the classpath Set the main class Aspect-Oriented Programming The idea behind AOP is to apply some code across different unrelated object hierarchies - cross-cutting concerns. It's a valuable technique in languages that don't allow traits, code you can graft on third-party objects/classes. Fun fact: I learned about AOP before Proxy. AOP relies on two main concepts: an aspect is the transformation applied to code, while a point cut matches where the aspect applies. In Java, AOP's historical implementation is the excellent AspectJ library. AspectJ provides two approaches, known as weaving: build-time weaving, which transforms the compiled bytecode, and runtime weaving, which relies on the above instrumentation. Either way, AspectJ uses a specific format for aspects and pointcuts. Before Java 5, the format looked like Java but not quite; for example, it used the aspect keyword. With Java 5, one can use annotations in regular Java code to achieve the same goal. We need an AspectJ dependency: XML <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.19</version> </dependency> As Byte Buddy, AspectJ also uses ASM underneath. Here's the code: Java @Aspect //1 public class RepeatingAspect { @Pointcut("@annotation(repeat) && call(* *(..))") //2 public void callAt(Repeat repeat) {} //3 @Around("callAt(repeat)") //4 public Object around(ProceedingJoinPoint pjp, Repeat repeat) throws Throwable { //5 for (int i = 0; i < repeat.times(); i++) { //6 pjp.proceed(); //7 } return null; } } Mark this class as an aspect Define the pointcut; every call to a method annotated with @Repeat Bind the @Repeat annotation to the the repeat name used in the annotation above Define the aspect applied to the call site; it's an @Around, meaning that we need to call the original method explicitly The signature uses a ProceedingJoinPoint, which references the original method, as well as the @Repeat annotation Loop over as many times as configured Call the original method At this point, we need to weave the aspect. Let's do it at build-time. For this, we can add the AspectJ build plugin: XML <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <executions> <execution> <goals> <goal>compile</goal> <!--1--> </goals> </execution> </executions> </plugin> Bind execution of the plugin to the compile phase To see the demo in effect: Shell mvn compile exec:java -Dexec.mainClass=ch.frankel.blog.aop.Main Java Compiler Plugin Last, it's possible to change the generated bytecode via a Java compiler plugin, introduced in Java 6 as JSR 269. From a bird's eye view, plugins involve hooking into the Java compiler to manipulate the AST in three phases: parse the source code into multiple ASTs, analyze further into Element, and potentially generate source code. The documentation could be less sparse. I found the following Awesome Java Annotation Processing. Here's a simplified class diagram to get you started: I'm too lazy to implement the same as above with such a low-level API. As the expression goes, this is left as an exercise to the reader. If you are interested, I believe the DocLint source code is a good starting point. Conclusion I described several approaches to monkey-patching in Java in this post: the Proxy class, instrumentation via a Java Agent, AOP via AspectJ, and javac compiler plugins. To choose one over the other, consider the following criteria: build-time vs. runtime, complexity, native vs. third-party, and security concerns. To Go Further Monkey patch Guide to Java Instrumentation Byte Buddy Creating a Java Compiler Plugin Awesome Java Annotation Processing Maven AspectJ plugin
Concurrent programming is the art of juggling multiple tasks in a software application effectively. In the realm of Java, this means threading — a concept that has been both a boon and a bane for developers. Java's threading model, while powerful, has often been considered too complex and error-prone for everyday use. Enter Project Loom, a paradigm-shifting initiative designed to transform the way Java handles concurrency. In this blog, we'll embark on a journey to demystify Project Loom, a groundbreaking project aimed at bringing lightweight threads, known as fibers, into the world of Java. These fibers are poised to revolutionize the way Java developers approach concurrent programming, making it more accessible, efficient, and enjoyable. But before we dive into the intricacies of Project Loom, let's first understand the broader context of concurrency in Java. Understanding Concurrency in Java Concurrency is the backbone of modern software development. It allows applications to perform multiple tasks simultaneously, making the most of available resources, particularly in multi-core processors. Java, from its inception, has been a go-to language for building robust and scalable applications that can efficiently handle concurrent tasks. In Java, concurrency is primarily achieved through threads. Threads are lightweight sub-processes within a Java application that can be executed independently. These threads enable developers to perform tasks concurrently, enhancing application responsiveness and performance. However, traditional thread management in Java has its challenges. Developers often grapple with complex and error-prone aspects of thread creation, synchronization, and resource management. Threads, while powerful, can also be resource-intensive, leading to scalability issues in applications with a high thread count. Java introduced various mechanisms and libraries to ease concurrent programming, such as the java.util.concurrent package, but the fundamental challenges remained. This is where Project Loom comes into play. What Is Project Loom? Project Loom is an ambitious endeavor within the OpenJDK community that aims to revolutionize Java concurrency by introducing lightweight threads, known as fibers. These fibers promise to simplify concurrent programming in Java and address many of the pain points associated with traditional threads. The primary goal of Project Loom is to make concurrency more accessible, efficient, and developer-friendly. It achieves this by reimagining how Java manages threads and by introducing fibers as a new concurrency primitive. Fibers are not tied to native threads, which means they are lighter in terms of resource consumption and easier to manage. One of the key driving forces behind Project Loom is reducing the complexity associated with threads. Traditional threads require careful management of thread pools, synchronization primitives like locks and semaphores, and error-prone practices like dealing with thread interruption. Fibers simplify this by providing a more lightweight and predictable model for concurrency. Moreover, Project Loom aims to make Java more efficient by reducing the overhead associated with creating and managing threads. In traditional thread-based concurrency, each thread comes with its own stack and requires significant memory resources. Fibers, on the other hand, share a common stack, reducing memory overhead and making it possible to have a significantly larger number of concurrent tasks. Project Loom is being developed with the idea of being backward-compatible with existing Java codebases. This means that developers can gradually adopt fibers in their applications without having to rewrite their entire codebase. It's designed to seamlessly integrate with existing Java libraries and frameworks, making the transition to this new concurrency model as smooth as possible. Fibers: The Building Blocks of Lightweight Threads Fibers are at the heart of Project Loom. They represent a new concurrency primitive in Java, and understanding them is crucial to harnessing the power of lightweight threads. Fibers, sometimes referred to as green threads or user-mode threads, are fundamentally different from traditional threads in several ways. First and foremost, fibers are not tied to native threads provided by the operating system. In traditional thread-based concurrency, each thread corresponds to a native thread, which can be resource-intensive to create and manage. Fibers, on the other hand, are managed by the Java Virtual Machine (JVM) itself and are much lighter in terms of resource consumption. One of the key advantages of fibers is their lightweight nature. Unlike traditional threads, which require a separate stack for each thread, fibers share a common stack. This significantly reduces memory overhead, allowing you to have a large number of concurrent tasks without exhausting system resources. Fibers also simplify concurrency by eliminating some of the complexities associated with traditional threads. For instance, when working with threads, developers often need to deal with issues like thread interruption and synchronization using locks. These complexities can lead to subtle bugs and make code harder to reason about. Fibers provide a more straightforward model for concurrency, making it easier to write correct and efficient code. To work with fibers in Java, you'll use the java.lang.Fiber class. This class allows you to create and manage fibers within your application. You can think of fibers as lightweight, cooperative threads that are managed by the JVM, and they allow you to write highly concurrent code without the pitfalls of traditional thread management. Getting Started With Project Loom Before you can start harnessing the power of Project Loom and its lightweight threads, you need to set up your development environment. At the time of writing, Project Loom was still in development, so you might need to use preview or early-access versions of Java to experiment with fibers. Here are the steps to get started with Project Loom: Choose the right Java version: Project Loom features might not be available in the stable release of Java. You may need to download an early-access version of Java that includes Project Loom features. Check the official OpenJDK website for the latest releases and versions that support Project Loom. Install and configure your development environment: Download and install the chosen Java version on your development machine. Configure your IDE (Integrated Development Environment) to use this version for your Project Loom experiments. Import Project Loom libraries: Depending on the Java version you choose, you may need to include Project Loom libraries in your project. Refer to the official documentation for instructions on how to do this. Create a simple fiber: Start by creating a basic Java application that utilizes fibers. Create a simple task that can run concurrently using a fiber. You can use the java.lang.Fiber class to create and manage fibers within your application. Compile and run your application: Compile your application and run it using the configured Project Loom-enabled Java version. Observe how fibers operate and how they differ from traditional threads. Experiment and learn: Explore more complex scenarios and tasks where fibers can shine. Experiment with asynchronous programming, I/O-bound operations, and other concurrency challenges using fibers. Benefits of Lightweight Threads in Java Project Loom's introduction of lightweight threads, or fibers, into the Java ecosystem brings forth a myriad of benefits for developers and the applications they build. Let's delve deeper into these advantages: Efficiency: Fibers are more efficient than traditional threads. They are lightweight, consuming significantly less memory, and can be created and destroyed with much less overhead. This efficiency allows you to have a higher number of concurrent tasks without worrying about resource exhaustion. Simplicity: Fibers simplify concurrent programming. With fibers, you can write code that is easier to understand and reason about. You'll find yourself writing less boilerplate code for thread management, synchronization, and error handling. Scalability: The reduced memory footprint of fibers translates to improved scalability. Applications that need to handle thousands or even millions of concurrent tasks can do so more efficiently with fibers. Responsiveness: Fibers enhance application responsiveness. Tasks that would traditionally block a thread can now yield control to the fiber scheduler, allowing other tasks to run in the meantime. This results in applications that feel more responsive and can better handle user interactions. Compatibility: Project Loom is designed to be backward-compatible with existing Java codebases. This means you can gradually adopt fibers in your applications without a full rewrite. You can incrementally update your code to leverage lightweight threads where they provide the most benefit. Resource utilization: Fibers can improve resource utilization in applications that perform asynchronous I/O operations, such as web servers or database clients. They allow you to efficiently manage a large number of concurrent connections without the overhead of traditional threads. Reduced complexity: Code that deals with concurrency often involves complex patterns and error-prone practices. Fibers simplify these complexities, making it easier to write correct and efficient concurrent code. It's important to note that while Project Loom promises significant advantages, it's not a one-size-fits-all solution. The choice between traditional threads and fibers should be based on the specific needs of your application. However, Project Loom provides a powerful tool that can simplify many aspects of concurrent programming in Java and deserves consideration in your development toolkit. Project Loom Best Practices Now that you have an understanding of Project Loom and the benefits it offers, let's dive into some best practices for working with fibers in your Java applications: Choose the right concurrency model: While fibers offer simplicity and efficiency, they may not be the best choice for every scenario. Evaluate your application's specific concurrency requirements to determine whether fibers or traditional threads are more suitable. Limit blocking operations: Fibers are most effective in scenarios with a high degree of concurrency and tasks that may block, such as I/O operations. Use fibers for tasks that can yield control when waiting for external resources, allowing other fibers to run. Avoid thread synchronization: One of the advantages of fibers is reduced reliance on traditional synchronization primitives like locks. Whenever possible, use non-blocking or asynchronous techniques to coordinate between fibers, which can lead to more efficient and scalable code. Keep error handling in mind: Exception handling in fibers can be different from traditional threads. Be aware of how exceptions propagate in fiber-based code and ensure you have proper error-handling mechanisms in place. Use thread pools: Consider using thread pools with fibers for optimal resource utilization. Thread pools can efficiently manage the execution of fibers while controlling the number of active fibers to prevent resource exhaustion. Stay updated: Project Loom is an evolving project, and new features and improvements are regularly introduced. Stay updated with the latest releases and documentation to take advantage of the latest enhancements. Experiment and benchmark: Before fully adopting fibers in a production application, experiment with different scenarios and benchmark the performance to ensure that fibers are indeed improving your application's concurrency. Profile and debug: Familiarize yourself with tools and techniques for profiling and debugging fiber-based applications. Tools like profilers and debuggers can help you identify and resolve performance bottlenecks and issues. Project Loom and Existing Libraries/Frameworks One of the remarkable aspects of Project Loom is its compatibility with existing Java libraries and frameworks. As a developer, you don't have to discard your existing codebase to leverage the benefits of fibers. Here's how Project Loom can coexist with your favorite Java tools: Java standard library: Project Loom is designed to seamlessly integrate with the Java standard library. You can use fibers alongside existing Java classes and packages without modification. Concurrency libraries: Popular Java concurrency libraries, such as java.util.concurrent, can be used with fibers. You can employ thread pools and other concurrency utilities to manage and coordinate fiber execution. Frameworks and web servers: Java frameworks and web servers like Spring, Jakarta EE, and Apache Tomcat can benefit from Project Loom. Fibers can improve the efficiency of handling multiple client requests concurrently. Database access: If your application performs database access, fibers can be used to efficiently manage database connections. They allow you to handle a large number of concurrent database requests without excessive resource consumption. Third-party libraries: Most third-party libraries that are compatible with Java can be used in conjunction with Project Loom. Ensure that you're using Java versions compatible with Project Loom features. Asynchronous APIs: Many Java libraries and frameworks offer asynchronous APIs that align well with fibers. You can utilize these APIs to write non-blocking, efficient code. Project Loom's compatibility with existing Java ecosystem components is a significant advantage. It allows you to gradually adopt fibers where they provide the most value in your application while preserving your investment in existing code and libraries. Future of Project Loom As Project Loom continues to evolve and make strides in simplifying concurrency in Java, it's essential to consider its potential impact on the future of Java development. Here are some factors to ponder: Increased adoption: As developers become more familiar with fibers and their benefits, Project Loom could see widespread adoption. This could lead to the creation of a vast ecosystem of libraries and tools that leverage lightweight threads. Enhancements and improvements: Project Loom is still in development, and future releases may bring further enhancements and optimizations. Keep an eye on the project's progress and be ready to embrace new features and improvements. Easier concurrency education: With the simplification of concurrency, Java newcomers may find it easier to grasp the concepts of concurrent programming. This could lead to a more significant talent pool of Java developers with strong concurrency skills. Concurrency-driven architectures: Project Loom's efficiency and ease of use might encourage developers to design and implement more concurrency-driven architectures. This could result in applications that are highly responsive and scalable. Feedback and contributions: Get involved with the Project Loom community by providing feedback, reporting issues, and even contributing to the project's development. Your insights and contributions can shape the future of Project Loom. Conclusion In this journey through Project Loom, we've explored the evolution of concurrency in Java, the introduction of lightweight threads known as fibers, and the potential they hold for simplifying concurrent programming. Project Loom represents a significant step forward in making Java more efficient, developer-friendly, and scalable in the realm of concurrent programming. As you embark on your own exploration of Project Loom, remember that while it offers a promising future for Java concurrency, it's not a one-size-fits-all solution. Evaluate your application's specific needs and experiment with fibers to determine where they can make the most significant impact. The world of Java development is continually evolving, and Project Loom is just one example of how innovation and community collaboration can shape the future of the language. By embracing Project Loom, staying informed about its progress, and adopting best practices, you can position yourself to thrive in the ever-changing landscape of Java development.
Artificial intelligence (AI) is transforming various industries and changing the way businesses operate. Although Python is often regarded as the go-to language for AI development, Java provides robust libraries and frameworks that make it an equally strong contender for creating AI-based applications. In this article, we explore using Java and Gradle for AI development by discussing popular libraries, providing code examples, and demonstrating end-to-end working examples. Java Libraries for AI Development Java offers several powerful libraries and frameworks for building AI applications, including: Deeplearning4j (DL4J) - A deep learning library for Java that provides a platform for building, training, and deploying neural networks, DL4J supports various neural network architectures and offers GPU acceleration for faster computations. Weka - A collection of machine learning algorithms for data mining tasks, Weka offers tools for data pre-processing, classification, regression, clustering, and visualization. Encog - A machine learning framework supporting various advanced algorithms, including neural networks, support vector machines, genetic programming, and Bayesian networks Setting up Dependencies With Gradle To begin AI development in Java using Gradle, set up the required dependencies in your project by adding the following to your build.gradle file: Groovy dependencies { implementation 'org.deeplearning4j:deeplearning4j-core:1.0.0-M1.1' implementation 'nz.ac.waikato.cms.weka:weka-stable:3.8.5' implementation 'org.encog:encog-core:3.4' } Code Examples Building a Simple Neural Network With DL4J This example demonstrates creating a basic neural network using the Deeplearning4j (DL4J) library. The code sets up a two-layer neural network architecture consisting of a DenseLayer with 4 input neurons and 10 output neurons, using the ReLU activation function, and an OutputLayer with 10 input neurons and 3 output neurons, using the Softmax activation function and Negative Log Likelihood as the loss function. The model is then initialized and can be further trained on data and used for predictions. Java import org.deeplearning4j.nn.api.OptimizationAlgorithm; import org.deeplearning4j.nn.conf.MultiLayerConfiguration; import org.deeplearning4j.nn.conf.NeuralNetConfiguration; import org.deeplearning4j.nn.conf.layers.DenseLayer; import org.deeplearning4j.nn.conf.layers.OutputLayer; import org.deeplearning4j.nn.multilayer.MultiLayerNetwork; import org.deeplearning4j.nn.weights.WeightInit; import org.nd4j.linalg.activations.Activation; import org.nd4j.linalg.learning.config.Sgd; import org.nd4j.linalg.lossfunctions.LossFunctions; public class SimpleNeuralNetwork { public static void main(String[] args) { MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder() .seed(123) .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT) .updater(new Sgd(0.01)) .list() .layer(0, new DenseLayer.Builder().nIn(4).nOut(10) .weightInit(WeightInit.XAVIER) .activation(Activation.RELU) .build()) .layer(1, new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD) .nIn(10).nOut(3) .weightInit(WeightInit.XAVIER) .activation(Activation.SOFTMAX) .build()) .pretrain(false).backprop(true) .build(); MultiLayerNetwork model = new MultiLayerNetwork(conf); model.init(); } } Classification Using Weka This example shows how to use the Weka library for classification on the Iris dataset. The code loads the dataset from an ARFF file, sets the class attribute (the attribute we want to predict) to be the last attribute in the dataset, builds a Naive Bayes classifier using the loaded data, and classifies a new instance. Java import weka.classifiers.bayes.NaiveBayes; import weka.core.Instance; import weka.core.Instances; import weka.core.converters.ConverterUtils.DataSource; public class WekaClassification { public static void main(String[] args) throws Exception { DataSource source = new DataSource("data/iris.arff"); Instances data = source.getDataSet(); data.setClassIndex(data.numAttributes() - 1); NaiveBayes nb = new NaiveBayes(); nb.buildClassifier(data); Instance newInstance = data.instance(0); double result = nb.classifyInstance(newInstance); System.out.println("Predicted class: " + data.classAttribute().value((int) result)); } } Conclusion Java, with its rich ecosystem of libraries and frameworks for AI development, is a viable choice for building AI-based applications. By leveraging popular libraries like Deeplearning4j, Weka, and Encog, and using Gradle as the build tool, developers can create powerful AI solutions using the familiar Java programming language. The provided code examples demonstrate the ease of setting up and configuring AI applications using Java and Gradle. The DL4J example shows how to create a basic deep learning model that can be applied to tasks such as image recognition or natural language processing. The Weka example demonstrates how to use Java and the Weka library for machine learning tasks, specifically classification, which can be valuable for implementing machine learning solutions in Java applications, such as predicting customer churn or classifying emails as spam or not spam. Happy Learning!!
Nicolas Fränkel
Head of Developer Advocacy,
Api7
Shai Almog
OSS Hacker, Developer Advocate and Entrepreneur,
Codename One
Marco Behler
Ram Lakshmanan
yCrash - Chief Architect