DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Avoiding If-Else: Advanced Approaches and Alternatives
  • Dust: Open-Source Actors for Java
  • Redefining Java Object Equality

Trending

  • It’s Not About Control — It’s About Collaboration Between Architecture and Security
  • How Large Tech Companies Architect Resilient Systems for Millions of Users
  • AI’s Role in Everyday Development
  • Performing and Managing Incremental Backups Using pg_basebackup in PostgreSQL 17
  1. DZone
  2. Coding
  3. Java
  4. Hot Class Reload in Java: A Webpack HMR-Like Experience for Java Developers

Hot Class Reload in Java: A Webpack HMR-Like Experience for Java Developers

Enhance your Java development with a hot class reload system, similar to Webpack's HMR. This guide explores using Java WatchService and the javax.tools API.

By 
Mohibul Hassan Chowdhury user avatar
Mohibul Hassan Chowdhury
·
Oct. 30, 24 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
4.9K Views

Join the DZone community and get the full member experience.

Join For Free

In the world of software development, time is everything. Every developer knows the frustration of waiting for a full application restart just to see a small change take effect. Java developers, in particular, have long dealt with this issue. But what if you didn’t have to stop and restart every time you updated a class? Enter Hot Class Reload (HCR) in Java — a technique that can keep you in the flow, reloading classes on the fly, much like Hot Module Reload (HMR) does in JavaScript.

In this guide, we’ll walk through how to implement HCR and integrate it into your Java development workflow. By the end, you’ll have a powerful new tool to reduce those long, unproductive restart times.

Understanding Hot Class Reload

Simply put, Hot Class Reload (HCR) allows Java applications to reload classes at runtime. This is incredibly useful in development environments where you’re constantly iterating and tweaking code. Think of it as real-time editing: you change some code, and the application picks it up right away—no restart required.

Hot Class Reload (HCR) is a technique that enables Java applications to reload classes at runtime. This approach is instrumental in development environments where frequent code changes occur. By leveraging Java's WatchService, custom class loaders using the javax.tools API and JavaCompiler class, we can monitor source files for changes, compile them on the fly, and reload the updated classes into the running application.

How It Works

The core components of our HCR implementation include:

  1. File Watcher: Utilizes Java's WatchService to monitor a directory for changes to .java files
  2. Dynamic Compilation: Uses javax.tools.JavaCompiler to compile modified Java source files.
  3. Custom Class Loader: Loads the newly compiled classes into the JVM, replacing the old versions.

System Flow Diagram

Below is a simplified flow diagram illustrating the HCR process:

Hot Class Reload architecture Java

The architecture of Hot Class Reload

  1. Source Changes: Developers modify Java source files in the specified directory.
  2. File Watcher: Detects changes and triggers the compilation process
  3. Compile & Reload: Compiles the modified files and reloads the classes using a custom class loader
  4. Custom Class Loader: Loads the newly compiled class into the JVM
  5. Invoke Main Method: Exectues the main method by executing the bytecode of the reloaded class

Implementing Hot Class Reload

Here's a breakdown of the Java program implementing HCR:

Setting Up the Environment

The program initializes by setting up directories for source and compiled classes. It ensures these directories exist and are ready for use.

Java
 
public HotReload(String sourceDir, String classDir) {
    this.sourceDir = Paths.get(sourceDir).toAbsolutePath();
    this.classDir = Paths.get(classDir).toAbsolutePath();
    this.classLoader = new CustomClassLoader();
    this.compiler = ToolProvider.getSystemJavaCompiler();
    this.fileManager = compiler.getStandardFileManager(null, null, null);

    // Ensure directories exist
    try {
        Files.createDirectories(this.sourceDir);
        Files.createDirectories(this.classDir);
    } catch (IOException e) {
        throw new RuntimeException("Failed to create directories", e);
    }
}


The constructor begins by converting the provided sourceDir and classDir strings into absolute Path objects using Paths.get().toAbsolutePath(). This ensures that the paths are fully qualified and can be used reliably throughout the application. Next, it initializes the classLoader as an instance of the CustomClassLoader class, which is responsible for dynamically loading compiled classes. The compiler is set to the system Java compiler obtained from ToolProvider.getSystemJavaCompiler(), and the fileManager is initialized using the standard file manager from the compiler. This setup allows the class to compile Java source files programmatically. The constructor then attempts to create the directories specified by sourceDir and classDir if they do not already exist, using Files.createDirectories(). If directory creation fails, it throws a RuntimeException to indicate a critical error that prevents the application from proceeding. This ensures that the necessary directory structure is in place for the class to function correctly.

Watching for File Changes

A dedicated thread runs a file watcher that listens for modifications to .java files in the source directory. The watchForChanges method is responsible for monitoring the source directory for any modifications to .java files and triggering the compileAndReload method when changes are detected. Here is the code for the watchForChanges method:

Java
 
private void watchForChanges() {
    try (WatchService watchService = FileSystems.getDefault()
        .newWatchService()) {
        sourceDir.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);

        while (true) {
            WatchKey key = watchService.take();
            for (WatchEvent << ? > event : key.pollEvents()) {
                if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                    Path changed = sourceDir.resolve((Path) event.context());
                    String fileName = changed.getFileName()
                        .toString();
                    if (fileName.endsWith(".java")) {
                        compileAndReload(fileName);
                    }
                }
            }
            if (!key.reset()) {
                break;
            }
        }
    } catch (Exception e) {
        System.err.println("Error in file watcher: " + e.getMessage());
        e.printStackTrace();
    }
}


Compiling and Reloading Classes

The compileAndReload method is designed to handle the compilation of a Java source file and subsequently reload the resulting class if necessary. Upon invocation, it accepts a fileName parameter, which should be a Java source file (e.g., "MyClass.java"). The method begins by extracting the class name by removing the ".java" extension from the provided file name. It then constructs the paths for both the source file and the compiled class file using sourceDir and classDir, which are assumed to be defined in the broader context of the class.

To determine whether recompilation is required, the method compares the modification times of the source file and the corresponding class file. It retrieves the last known modification time from a lastModified map and obtains the source file's current modification time. The method checks if the class file exists and whether its modification time is newer than or equal to that of the source file. If these conditions indicate that recompilation is unnecessary, the method exits early.

Conversely, if recompilation is deemed necessary, it invokes a separate compile method (details of which are not provided in this snippet) to compile the source file. Upon successful compilation, it calls a reloadClass method (also not shown) to reload the newly compiled class and update the lastModified map with the current modification time. A success message is printed to confirm the successful operation. In the case of a compilation failure, an error message is displayed. The entire process is encapsulated within a try-catch block to effectively manage any exceptions that may arise during the compilation or file operations.

Java
 
private void compileAndReload(String fileName) {
    try {
        String className = fileName.replace(".java", "");
        Path sourceFile = sourceDir.resolve(fileName);
        Path classFile = classDir.resolve(className + ".class");

        // Check if the file has been modified since last compilation
        long lastModTime = lastModified.getOrDefault(className, 0L);
        long currentModTime = Files.getLastModifiedTime(sourceFile)
            .toMillis();
        if (Files.exists(classFile) && Files.getLastModifiedTime(classFile)
            .toMillis() >= currentModTime && lastModTime >= currentModTime) {
            return;
        }

        // Compile the Java file
        boolean compilationSuccessful = compile(sourceFile);

        if (compilationSuccessful) {
            // Compilation successful, reload the class
            reloadClass(className);
            lastModified.put(className, currentModTime);
            System.out.println("Reloaded: " + className);
        } else {
            System.err.println("Compilation failed for: " + className);
        }
    } catch (Exception e) {
        System.err.println("Error in compile and reload: " + e.getMessage());
        e.printStackTrace();
    }
}


The compile method is designed to compile a Java source file utilizing the Java Compiler API, providing a structured approach to dynamic compilation. The method begins by accepting a Path object representing the source file intended for compilation. To facilitate error handling and reporting, it creates a DiagnosticCollector, which collects diagnostic information such as errors and warnings encountered during the compilation process.

Next, the method constructs a collection of JavaFileObjects from the source file using a fileManager, likely an instance of StandardJavaFileManager. This setup is crucial for the Java Compiler API to access the source files correctly. The method then specifies compilation options, particularly the output directory for the compiled classes, using the -d option.

A CompilationTask is created using the Java Compiler API, which is configured with several parameters: no custom writer for compiler output (set to null), the file manager, the diagnostic collector, the compilation options, no annotation processors (also null), and the compilation units (the source files). The compilation task is executed by calling task.call(), which returns a boolean indicating whether the compilation was successful.

In the event of a compilation failure, the method iterates through the collected diagnostics, printing detailed error information that includes the line number where the error occurred, the source file URI, and the specific error message. This detailed reporting aids in debugging compilation issues effectively. Finally, the method returns the success status of the compilation, providing a clear indication of the outcome.

This method serves as a programmatic solution for compiling Java source files, making it particularly useful in scenarios where dynamic compilation is required. Its robust error reporting capabilities enhance the developer's ability to troubleshoot and resolve compilation errors efficiently.

Java
 
private boolean compile(Path sourceFile) {

    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();

    Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(Collections.singletonList(sourceFile.toFile()));

    List<String> options = Arrays.asList("-d", classDir.toString());

    JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, compilationUnits);

    boolean success = task.call();

    if (!success) {

        for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {

            System.err.format("Error on line %d in %s%n", diagnostic.getLineNumber(), diagnostic.getSource()

                .toUri());

            System.err.println(diagnostic.getMessage(null));

        }

    }

    

    return success;

}


Custom Class Loader

A custom class loader is used to load the compiled class files into the JVM. The reloading happens after the compilation is successful and then it reloads the class. To reload the class we need to implement a custom class loader that will load the changed class and then execute it. 

Java
 
private void reloadClass(String className) {

    try {

        Class << ? > loadedClass = classLoader.loadClass(className);

        Method mainMethod = loadedClass.getMethod("main", String[].class);

        System.out.println("Invoking main method for: " + className);

        mainMethod.invoke(null, (Object) new String[0]);

    } catch (Exception e) {

        System.err.println("Error reloading class " + className + ": " + e.getMessage());

        e.printStackTrace();

    }

}

private class CustomClassLoader extends ClassLoader {

@Override

public Class << ? > loadClass(String name) throws ClassNotFoundException {

    Path classFile = classDir.resolve(name + ".class");

        if (Files.exists(classFile)) {

            try {

                byte[] classBytes = Files.readAllBytes(classFile);

                return defineClass(name, classBytes, 0, classBytes.length);

             } catch (IOException e) {

                throw new ClassNotFoundException("Error loading class " + name, e);

            }

         }

        return super.loadClass(name);

    }

}


Now if we want to execute the Hot Reload program, we need to do something like this:

Java
 
    public static void main(String[] args) {

        Day84 reloader = new Day84("C:\\Users\\mohib\\IdeaProjects\\date-time\\src\\main\\java", "C:\\Users\\mohib\\IdeaProjects\\date-time\\target\\classes");

        reloader.start();

        // Keep the main thread alive and handle user input

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {

            System.out.println("Live reloader is running. Press Enter to exit.");

            reader.readLine();

        } catch (IOException e) {

            System.err.println("Error reading user input: " + e.getMessage());

        }

        System.out.println("Exiting live reloader.");

    }


In this main method, we can see that we need to provide the path for the src of the code that you want to hot reload and the classes of those Java files. After that, it watches for any changes and then compiles and reloads the class so that we can see the effect immediately.

Full Code

Java
 
import javax.tools.*;

import java.io.*;

import java.lang.reflect.Method;

import java.nio.file.*;

import java.util.*;

import java.util.concurrent.ConcurrentHashMap;

public class Day84 {

    private final Path sourceDir;

    private final Path classDir;

    private final Map<String, Long> lastModified = new ConcurrentHashMap<>();

    private final CustomClassLoader classLoader;

    private final JavaCompiler compiler;

    private final StandardJavaFileManager fileManager;

    public Day84(String sourceDir, String classDir) {

        this.sourceDir = Paths.get(sourceDir)

            .toAbsolutePath();

        this.classDir = Paths.get(classDir)

            .toAbsolutePath();

        this.classLoader = new CustomClassLoader();

        this.compiler = ToolProvider.getSystemJavaCompiler();

        this.fileManager = compiler.getStandardFileManager(null, null, null);

        // Ensure directories exist

        try {

            Files.createDirectories(this.sourceDir);

            Files.createDirectories(this.classDir);

        } catch (IOException e) {

            throw new RuntimeException("Failed to create directories", e);

        }

    }

    public void start() {

        Thread watchThread = new Thread(this::watchForChanges, "FileWatcherThread");

        watchThread.setDaemon(true);

        watchThread.start();

        System.out.println("Live reloader started. Watching directory: " + sourceDir);

    }

    private void watchForChanges() {

        try (WatchService watchService = FileSystems.getDefault()

            .newWatchService()) {

            sourceDir.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);

            while (true) {

                WatchKey key = watchService.take();

                for (WatchEvent<?> event : key.pollEvents()) {

                    if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {

                        Path changed = sourceDir.resolve((Path) event.context());

                        String fileName = changed.getFileName()

                            .toString();

                        if (fileName.endsWith(".java")) {

                            compileAndReload(fileName);

                        }

                    }

                }

                if (!key.reset()) {

                    break;

                }

            }

        } catch (Exception e) {

            System.err.println("Error in file watcher: " + e.getMessage());

            e.printStackTrace();

        }

    }

    private void compileAndReload(String fileName) {

        try {

            String className = fileName.replace(".java", "");

            Path sourceFile = sourceDir.resolve(fileName);

            Path classFile = classDir.resolve(className + ".class");

            // Check if the file has been modified since last compilation

            long lastModTime = lastModified.getOrDefault(className, 0L);

            long currentModTime = Files.getLastModifiedTime(sourceFile)

                .toMillis();

            if (Files.exists(classFile) && Files.getLastModifiedTime(classFile)

                .toMillis() >= currentModTime && lastModTime >= currentModTime) {

                return;

            }

            // Compile the Java file

            boolean compilationSuccessful = compile(sourceFile);

            if (compilationSuccessful) {

                // Compilation successful, reload the class

                reloadClass(className);

                lastModified.put(className, currentModTime);

                System.out.println("Reloaded: " + className);

            } else {

                System.err.println("Compilation failed for: " + className);

            }

        } catch (Exception e) {

            System.err.println("Error in compile and reload: " + e.getMessage());

            e.printStackTrace();

        }

    }

    private boolean compile(Path sourceFile) {

        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();

        Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(Collections.singletonList(sourceFile.toFile()));

        List<String> options = Arrays.asList("-d", classDir.toString());

        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, compilationUnits);

        boolean success = task.call();

        if (!success) {

            for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {

                System.err.format("Error on line %d in %s%n", diagnostic.getLineNumber(), diagnostic.getSource()

                    .toUri());

                System.err.println(diagnostic.getMessage(null));

            }

        }

        return success;

    }

    private void reloadClass(String className) {

        try {

            Class<?> loadedClass = classLoader.loadClass(className);

            Method mainMethod = loadedClass.getMethod("main", String[].class);

            System.out.println("Invoking main method for: " + className);

            mainMethod.invoke(null, (Object) new String[0]);

        } catch (Exception e) {

            System.err.println("Error reloading class " + className + ": " + e.getMessage());

            e.printStackTrace();

        }

    }

    private class CustomClassLoader extends ClassLoader {

        @Override

        public Class<?> loadClass(String name) throws ClassNotFoundException {

            Path classFile = classDir.resolve(name + ".class");

            if (Files.exists(classFile)) {

                try {

                    byte[] classBytes = Files.readAllBytes(classFile);

                    return defineClass(name, classBytes, 0, classBytes.length);

                } catch (IOException e) {

                    throw new ClassNotFoundException("Error loading class " + name, e);

                }

            }

            return super.loadClass(name);

        }

    }

    public static void main(String[] args) {

        Day84 reloader = new Day84("C:\\Users\\mohib\\IdeaProjects\\date-time\\src\\main\\java", "C:\\Users\\mohib\\IdeaProjects\\date-time\\target\\classes");

        reloader.start();

        // Keep the main thread alive and handle user input

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {

            System.out.println("Live reloader is running. Press Enter to exit.");

            reader.readLine();

        } catch (IOException e) {

            System.err.println("Error reading user input: " + e.getMessage());

        }

        System.out.println("Exiting live reloader.");

    }

}


Conclusion

Hot Class Reload in Java offers a powerful way to enhance development productivity by reducing the need for full application restarts. By integrating file watching, dynamic compilation, and custom class loading, developers can achieve a seamless development experience akin to JavaScript's Hot Module Reload.

Implementing HCR can significantly speed up the development cycle, allowing for rapid testing and iteration. As you integrate this technique into your workflow, you'll find it an invaluable tool for efficient Java development. There are some libraries like JRebel which provide this feature. More on that in the next post.

Java compiler Directory Java (programming language) Data Types

Published at DZone with permission of Mohibul Hassan Chowdhury. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Avoiding If-Else: Advanced Approaches and Alternatives
  • Dust: Open-Source Actors for Java
  • Redefining Java Object Equality

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!