Is it possible to dynamically load JAR files during runtime?

Introduction to JAR Files

What are JAR files, and for what purposes are they used in Java?

A JAR file is just a packaged file format used to gather several Java class files, metadata, and resources into one compressed file, that is, it is essentially a zipped archive of the multiple files related to a Java program.

One of the main usages of JAR files is distribution and deployment, both for Java applications and libraries, since it is really convenient to package everything in one file. Java developers use JARs for the modularization of code, reusing code for libraries, and dependency management across projects.

Main Advantages of JAR Files:

  • Easy Convenience: All the files are bundled together into one file. It is very convenient to carry around and share with others.
  • Portability: JAR files are platform independent like Java.
  • Compression: As JAR files contain a compressed nature, they are smaller in size and, therefore, faster to download or distribute.
  • Security: The file can be digitally signed, so users are completely sure about the integrity and origin of the file.

JAR Files Format:

  • Classes: Java bytecode files, .class, which are the compiled version of Java source files.
  • Manifest file: This includes metadata about the JAR, like regarding the entry point of the executable JARs.
  • Resources: All the non-code files, like images and property files used by the application.

Typical Applications of Dynamic JAR File Loading

Loading JAR files dynamically at runtime is a facility that allows extension or modification in the behavior of a running Java program without its recompilation. This becomes particularly useful in cases where the addition of new functionality or features is required without stopping or redeploying the application.

Typical usage cases include of:

  1. Plugin-Based Architectures
  2. Modular Applications
  3. Updating or Patching Applications
  4. Dynamic Class Loading in Web Servers
  5. Distributed Systems and Microservices
  6. Scripting and Interpreters

To sum up, dynamic loading enables adaptable, modular, and scalable solutions across a range of application architectures, and JAR files are essential for packaging, distributing, and executing Java applications.

Overview of Static and Dynamic Class Loading in Java

Class loading in Java is a process of loading Java classes into the memory for execution. In Java, the different ways class loading is handled are as follows:

  1. Static Class Loading (Compile-Time Loading):
    This is the most common form of class loading, in which all the required classes are known and loaded at compile time. A Java compiler links class dependencies at the compile time, and when the program runs, the JVM would load all the required classes. This method makes sure that all the classes are known before the application gets started; this in turn provides better error detection during development. But it does not provide the flexibility, if in some case you want to load new classes or replace the existing classes without terminating the application.
  2. Runtime Loading:
    With dynamic class loading, it means classes will be loaded into memory at runtime; however, they may not exist at compile time. Java provides this through the mechanism of ClassLoader, which can load new classes, libraries, or modules at runtime. That’s quite powerful for use cases such as plugin architectures where you want to add new functionality without changing the core application. With dynamic loading, you are also able to invoke methods on classes that were not known at compile-time. Thus, these two things do make applications more flexible and extensible.

Differences Between Compile Time and Runtime Loading

Compile-Time (Static Loading) Runtime (Dynamic Loading)
1.When the Classes are Loaded:The classes are loaded at the time when JVM starts running your program. It will scan the classpath and load all the required classes into memory.At the time of running an application, classes can also be loaded. New classes may be introduced at runtime without restarting the program.
2.Flexibility:The class dependencies must be known and included at compile time, meaning any changes or additions to class behavior require recompilation.You can load classes on the fly, making it possible to introduce new functionality dynamically.
3.Error Detection:Since class loading happens early, errors related to class dependencies (e.g., missing classes, incompatible class versions) are detected during the compile phase.Errors like ClassNotFoundException can occur if the class cannot be found or loaded during execution.
4.Use Cases:Suitable for traditional applications where the complete class structure is known upfront, like desktop applications or simple web applications.Used in complex or modular applications that require flexibility, such as plugin-based architectures, application servers, or dynamically updating modules.

Dynamic class loading provides a way for Java applications to be modular, adaptable, and capable of loading new components at runtime, enhancing the flexibility of the system.

Using URLClassLoader to Load JAR Files

Introduction to URLClassLoader: A Common Mechanism for Dynamic Loading of JARs

URLClassLoader is a utility class in Java that has the capability to dynamically load classes and resources at runtime from a JAR file or a directory. It is part of the standard java.net package, and it is used nearly everywhere when a program needs to dynamically load some external libraries, plugins, or modules without the need to restart the JVM.

The URLClassLoader works by loading classes from the file system or network locations, which are specified as URLs-for example, file URLs for JAR files or HTTP URLs for network-based resources. This dynamic loading capability makes URLClassLoader quite common in modular and plugin-based systems.

Highlights of URLClassLoader:

  • It can load JAR files, classes, and resources from external targets such as a filesystem or web.
  • This can be used to extend the functionality of an application at runtime by dynamically loading new libraries.
  • This follows the class loading delegation model: it will always first try to delegate loading to its parent class loader before attempting to load a class on its own.

Code Snippet – Dynamic Loading of JAR Files Using URLClassLoader

Here is how you can use URLClassLoader to load a JAR file dynamically at runtime:

  • Step 1: Define the path to the JAR file – You must define the full path of the JAR file that you want to load.
  • Step 2: Create a URL for the JAR file – Convert the file path to a URL object so that URLClassLoader can use it.
  • Step 3: Loading the JAR using URLClassLoader – Instantiate the URLClassLoader using the URL of the JAR, followed by the loading of classes that shall be needed.

Example Code:

import java.net.URL;
import java.net.URLClassLoader;
import java.lang.reflect.Method;

public class JarLoader {
    public static void main(String[] args) {
        try {
            // Step 1: Path to the JAR file (absolute path)
            String jarPath = "file:/path/to/yourfile.jar";

            // Step 2: Create a URL array with the path to the JAR file
            URL[] jarURL = {new URL(jarPath)};

            // Step 3: Create a URLClassLoader with the JAR URL
            URLClassLoader urlClassLoader = new URLClassLoader(jarURL);

            // Step 4: Load the class from the JAR (by its fully qualified name)
            Class<?> loadedClass = urlClassLoader.loadClass("com.example.YourClass");

            // Step 5: Instantiate the loaded class (if necessary)
            Object instance = loadedClass.getDeclaredConstructor().newInstance();

            // Optionally: Invoke methods on the class using reflection
            Method method = loadedClass.getMethod("yourMethodName");
            method.invoke(instance);  // Call the method

            // Close the class loader (Java 7+)
            urlClassLoader.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Explanations:

  • URLClassLoader is initialized with the URL of the JAR file.
  • The loadClass method is used to load a class by its fully qualified name (e.g., com.example.YourClass).
  • Reflection (Method.invoke) is used to call methods dynamically from the loaded class.
  • In Java 7 and later, URLClassLoader can be closed using the close() method to free up resources after loading.

Dynamic classpath addition of JAR files

When you need to add a JAR in the classpath dynamically, URLClassLoader is exactly what you need. While usually, Java‘s classpath is set at the start of the JVM, the URLClassLoader lets you load additional classes or libraries into the running JVM.

Dynamically add the JAR using the following steps :

  1. Specify Path to JAR: Choose the path to the JAR file from the file system or from a network resource.
  2. Using URLClassLoader to load the JAR: Add the JAR file to the class path using the instance of URLClassLoader and perform the loading of the class.

Here’s how you can load multiple JARs at runtime:

Example Code for Loading Multiple JAR Files:

import java.net.URL;
import java.net.URLClassLoader;
import java.io.File;

public class MultipleJarLoader {
    public static void main(String[] args) {
        try {
            // Define an array of JAR files to load
            File jarFile1 = new File("/path/to/jar1.jar");
            File jarFile2 = new File("/path/to/jar2.jar");

            // Convert JAR files to URL format
            URL[] jarURLs = {
                jarFile1.toURI().toURL(),
                jarFile2.toURI().toURL()
            };

            // Create a URLClassLoader for multiple JAR files
            URLClassLoader urlClassLoader = new URLClassLoader(jarURLs);

            // Dynamically load classes from the JARs
            Class<?> classFromJar1 = urlClassLoader.loadClass("com.example.ClassFromJar1");
            Class<?> classFromJar2 = urlClassLoader.loadClass("com.example.ClassFromJar2");

            // Instantiate and use the loaded classes as needed
            Object instance1 = classFromJar1.getDeclaredConstructor().newInstance();
            Object instance2 = classFromJar2.getDeclaredConstructor().newInstance();

            // Optionally: Call methods on the classes using reflection
            // ...

            // Close the class loader when done (Java 7+)
            urlClassLoader.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

How It Works:

  • You create File objects that point to the JAR files you want to load.
  • Convert those file paths to URLs using toURI().toURL.
  • Pass the URLs into URLClassLoader and load the classes.
  • You can then instantiate and invoke the methods at runtime once the classes are loaded.

Important Considerations When Dynamically Adding JARs to Classpath

  1. Security: The dynamic loading of classes from outside may be vulnerable. Source the JAR from a trusted source.
  2. ClassLoader Isolation: Classes loaded by different classloaders are not visible to other classes loaded by different classloaders. It is useful in modular applications, but it’s a touchy management.
  3. Closing Resources: Since Java 7, it is a good practice that when the URLClassLoader is no longer needed, resources should be closed.

With URLClassLoader, you have a flexible and powerful way to dynamically load JAR files at runtime, enabling dynamic updates and modular architectures in Java applications.

Handling Dependencies

Explanation of How to Handle Dependent JAR Files

In real-life applications, JARs depend on other libraries or other JARs, and the management of these dependencies is rather important in avoiding some runtime problems, like ClassNotFoundException and NoClassDefFoundError. Dynamically loaded JARs via URLClassLoader will need to be treated with regard to the dependent JARs in order to make sure that all classes are present for a class loader.

Managing dependencies can become more complex in dynamic systems because:

  • JARs might depend on other JARs: A single JAR might reference classes from other JARs, so all dependent JARs must be available.
  • Version conflicts: Different JARs may require different versions of the same dependency, which could cause class versioning issues.

In order to handle this effectively, you have to load the dependent JARs into the classpath on the fly along with the loading of the main JAR.

Techniques to Ensure All Dependencies Are Resolved during Runtime

1. Bundling Dependencies in the Same Directory


One of the easiest ways to deal with these dependencies is to have all the needed JARs in one directory. When you load a JAR dynamically, you then can load all JARs from that directory using URLClassLoader.

Strategy: While loading a JAR file at runtime, scan all the JARs of the directory and load them all at once.

Code Example:

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class JarWithDependenciesLoader {
    public static void main(String[] args) {
        try {
            // Specify the directory containing all JARs (primary and dependencies)
            File jarDir = new File("/path/to/jar-directory");
            File[] jarFiles = jarDir.listFiles((dir, name) -> name.endsWith(".jar"));

            // Convert all JAR files in the directory to URL format
            URL[] jarURLs = new URL[jarFiles.length];
            for (int i = 0; i < jarFiles.length; i++) {
                jarURLs[i] = jarFiles[i].toURI().toURL();
            }

            // Create a URLClassLoader to load all JAR files (including dependencies)
            URLClassLoader urlClassLoader = new URLClassLoader(jarURLs);

            // Load the primary class from the main JAR (assuming it's one of the JARs in the directory)
            Class<?> mainClass = urlClassLoader.loadClass("com.example.MainClass");

            // Instantiate and invoke methods on the class as needed
            Object instance = mainClass.getDeclaredConstructor().newInstance();

            // Close the class loader when done
            urlClassLoader.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Explanation:

  • The code scans the directory containing the main JAR and all its dependencies.
  • All JAR files in that directory are loaded together, ensuring that dependent classes are available when the main class is loaded.

2. Loading of Dependencies from the Manifest File – META-INF/MANIFEST.MF

The various dependencies can be specified in the MANIFEST.MF file of JARs. This manifest file has a Class-Path attribute through which locations to dependent JARs can be specified. But this will work only when the dependencies are present in the same system or network and located at this path.

Manifest Example:

Manifest-Version: 1.0
Class-Path: lib/dependency1.jar lib/dependency2.jar lib/dependency3.jar

Technique:

  • Include all dependent JARs in the Class-Path attribute of the manifest file of your main JAR.
  • Use the manifest file to dynamically load dependent JARs at runtime.

Limitations:

  • This approach requires that all dependencies are in predictable locations, such as subdirectories relative to the main JAR.
  • It is less flexible for dynamic loading from arbitrary locations or when the dependencies might change.

3. Use of Dependency Management Tools (Maven, Gradle)

Large applications, on the other hand, would depend on the strong dependency management features of build tools like Maven or Gradle. They are capable of downloading and bundling dependencies automatically during compile time. While these are typically compile-time tools, they also provide you with a way to package all the required JARs that can be loaded dynamically at runtime.

Technique:

  • Use Maven’s maven-shade-plugin or Gradle’s shadowJar plugin to bundle all dependencies into a single JAR file, also known as a fat JAR or uber JAR.
  • When the JAR is dynamically loaded, all dependencies are already included within it.

Maven Example (POM.xml snippet):

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.2.4</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Advantages: Bundling dependencies into a single JAR makes runtime class loading easier, as you only need to manage one file.

4. Custom ClassLoader for Multiple JARs

A custom ClassLoader can be implemented when an application needs to dynamically load dependencies from different locations, such as multiple directories, network locations, or URLs. This custom class loader will be capable of loading classes from different sources, which then can dynamically resolve the dependencies.

Technique:

  • Extend the ClassLoader class to handle custom locations (e.g., databases, remote URLs).
  • Your custom class loader can be designed to search multiple locations for dependent JARs.

Code Example for a Custom ClassLoader:

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class CustomDependencyLoader extends ClassLoader {

    private URLClassLoader classLoader;

    public CustomDependencyLoader(File[] jarFiles) throws Exception {
        // Convert jar files to URLs
        URL[] jarURLs = new URL[jarFiles.length];
        for (int i = 0; i < jarFiles.length; i++) {
            jarURLs[i] = jarFiles[i].toURI().toURL();
        }

        // Initialize URLClassLoader with jar URLs
        classLoader = new URLClassLoader(jarURLs, this.getParent());
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            // Attempt to load the class from the provided jar files
            return classLoader.loadClass(name);
        } catch (ClassNotFoundException e) {
            // If class not found, delegate to the parent class loader
            return super.loadClass(name);
        }
    }
}

Explanation:

  • This CustomDependencyLoader loads classes from a set of dynamically provided JARs.
  • If a class is not found in the loaded JARs, it delegates to its parent class loader, following the delegation model.

5. Service Loaders and SPI(Service Provider Interface)

Java has built-in support for service loading through the Service Provider Interface. That will let you dynamically load implementations of interfaces. SPI can be used for dynamic loading of dependent services from JARs.

Technique:

  • Use ServiceLoader to dynamically discover and load services (interfaces) along with their dependencies.

Example:

ServiceLoader<MyInterface> loader = ServiceLoader.load(MyInterface.class);
for (MyInterface impl : loader) {
    impl.performAction();
}

Security Considerations

Untrusted Code Execution:

  • Loading code from unverified sources, such as external or network-based JAR files, can result in executing malicious code. This can lead to data breaches, remote code execution, or system compromises.
  • Attackers could inject harmful JAR files into your application, especially if user-controlled inputs are used to determine the location of the JARs being loaded.

ClassLoader Exploits:

  • Since the ClassLoader mechanism is responsible for dynamically loading classes, an improperly configured class loader could be exploited. Attackers could manipulate the classpath, introduce conflicting or malicious classes, or tamper with the bytecode of loaded classes.

Class Loading Hierarchy Violations:

  • Custom class loaders can disrupt the class loading hierarchy if they don’t follow the delegation model. This can result in loading unintended versions of classes (e.g., shadowing system classes), leading to security risks.

Visibility of Sensitive Resources:

  • Dynamically loaded JARs may expose sensitive resources like configuration files or internal classes. If access controls are not enforced, an attacker could exploit these resources, leading to potential data leaks.

Permission Escalation:

  • If untrusted code is loaded with higher privileges than intended, it can escalate its access rights. This could allow the malicious code to perform restricted operations, such as file modification, network access, or system resource access.

How to Apply Permission Policies When Dynamically Loading Code

To mitigate the security risks of dynamic class loading, it is essential to enforce strict security policies. Java provides several built-in mechanisms to control and manage security when dynamically loading JARs, including:

1. Use a Security Manager

A SecurityManager in Java acts as a gatekeeper that controls what operations a class can perform, based on its origin and permissions. By using a security manager, you can ensure that dynamically loaded code adheres to predefined security policies, restricting its access to system resources.

  • Enforce a Security Policy File: When using a SecurityManager, a policy file defines the permissions granted to classes loaded from different sources (e.g., local files, network URLs).

Example of Setting Up a SecurityManager:

public class SecurityManagerExample {
    public static void main(String[] args) {
        // Step 1: Enable the SecurityManager
        System.setSecurityManager(new SecurityManager());

        // Step 2: Load the JAR file dynamically
        // Permissions will be controlled by the policy file
        try {
            URL[] jarURL = {new URL("file:/path/to/yourfile.jar")};
            URLClassLoader urlClassLoader = new URLClassLoader(jarURL);
            Class<?> loadedClass = urlClassLoader.loadClass("com.example.MyClass");

            // Run the dynamically loaded code (with restricted permissions)
            Object instance = loadedClass.getDeclaredConstructor().newInstance();

            urlClassLoader.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Policy File Example:

grant codeBase "file:/path/to/yourfile.jar" {
    permission java.io.FilePermission "/path/to/logfile.txt", "read,write";
    permission java.net.SocketPermission "localhost:8080", "connect";
};

  • Explanation:
    • The SecurityManager restricts operations such as file access or network connections based on the defined policy file.
    • The codeBase in the policy file refers to the location of the JAR, and you can grant specific permissions to that code.

2. Use AccessController to Perform Privileged Actions

When you need to perform certain sensitive operations with elevated privileges, Java provides the AccessController class. This allows you to limit the scope of privileged actions and prevent untrusted code from gaining access to more permissions than it should.

Example:

AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() {
        // Perform privileged action (e.g., loading a class, accessing a file)
        try {
            Class<?> loadedClass = urlClassLoader.loadClass("com.example.MyClass");
            Object instance = loadedClass.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
});

Explanation:

  • The AccessController.doPrivileged block limits the elevated privileges to only the code inside it.
  • This ensures that only trusted code can perform sensitive operations, and untrusted code cannot escalate privileges beyond its assigned scope.

3. Sandboxing Dynamically Loaded Code

When loading external or untrusted JARs, you should isolate or “sandbox” the dynamically loaded code by restricting the set of permissions and resources it can access. This minimizes the potential impact if the code is compromised.

  • Technique: Use a custom class loader and set a restrictive SecurityManager policy for dynamically loaded code. This isolates the JAR from accessing critical system resources or sensitive classes.

4. Code Signing and Verification

Ensure that the JAR files being loaded dynamically are signed with a trusted certificate. This helps verify the integrity and authenticity of the JAR before loading it.

  • Use Signed JARs: JAR files can be digitally signed to guarantee that they have not been tampered with and come from a trusted source.

Verify Code Signing:

  • Use the jarsigner tool to sign JARs and verify signatures before loading.
  • The java.security.cert.Certificate API can be used to programmatically check JAR signatures during the class loading process.

Example of Verifying Signed JAR:

import java.security.cert.Certificate;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class JarVerifier {
    public static void main(String[] args) throws Exception {
        JarFile jarFile = new JarFile("/path/to/signedfile.jar");
        JarEntry entry = jarFile.getJarEntry("com/example/MyClass.class");
        Certificate[] certs = entry.getCertificates();

        if (certs != null && certs.length > 0) {
            // JAR is signed, proceed to load
            System.out.println("JAR is verified and trusted.");
        } else {
            // JAR is not signed or tampered with, abort loading
            System.out.println("JAR is unverified. Aborting loading.");
        }
    }
}

5. Restrict Class Loader Permissions

You can configure specific permissions for a class loader by overriding the ClassLoader class’s checkPackageAccess and checkPackageDefinition methods. This ensures that only trusted packages are accessible to the dynamically loaded classes.

Example:

@Override
protected synchronized void checkPackageAccess(String pkg) {
    if (pkg.startsWith("java.") || pkg.startsWith("com.yourcompany.")) {
        throw new SecurityException("Access to sensitive packages is restricted.");
    }
}

Explanation: This method restricts the dynamically loaded classes from accessing sensitive or internal Java packages, enhancing security.

Share The Tutorial With Your Friends
Twiter
Facebook
LinkedIn
Email
WhatsApp
Skype
Reddit

Check Our Ebook for This Online Course

Advanced topics are covered in this ebook with many practical examples.