Java
JAR Files
Runtime
Programming
Dynamic Loading

How to load JAR files dynamically at Runtime?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Loading a JAR at runtime is really about loading classes with a class loader that your application controls. That pattern is useful for plugin systems, optional integrations, and user-installed extensions. The safest approach is not to mutate the global classpath, but to load the JAR through a dedicated loader and interact with it through a stable interface.

Define a plugin contract first

Before you load anything dynamically, decide how the main application will talk to the code in the JAR. In practice, that means creating an interface or abstract base class in the main application and making the plugin implement it.

java
1package com.example.host;
2
3public interface Plugin {
4    String name();
5    void run();
6}

If the dynamically loaded class implements Plugin, the host application can cast to that interface and use it safely. Without a shared contract, you quickly end up with brittle reflection code and runtime surprises.

Load a known class from a JAR

The most direct solution is URLClassLoader. You point it at the JAR file, load a class by name, instantiate it, and cast it to your host-side interface.

java
1package com.example.host;
2
3import java.net.URL;
4import java.net.URLClassLoader;
5import java.nio.file.Path;
6
7public class JarLoaderExample {
8    public static void main(String[] args) throws Exception {
9        Path jarPath = Path.of("plugins", "report-plugin.jar");
10        URL jarUrl = jarPath.toUri().toURL();
11
12        try (URLClassLoader loader =
13                 new URLClassLoader(new URL[] { jarUrl }, Plugin.class.getClassLoader())) {
14
15            Class<?> type = Class.forName(
16                "com.example.plugins.ReportPlugin",
17                true,
18                loader
19            );
20
21            Plugin plugin = (Plugin) type.getDeclaredConstructor().newInstance();
22
23            System.out.println("Loaded: " + plugin.name());
24            plugin.run();
25        }
26    }
27}

This works well when you already know the implementation class name. It also keeps class visibility explicit because the plugin loader delegates to the host loader for shared API types such as Plugin.

Prefer ServiceLoader for real plugin systems

Hard-coding the implementation class name is fine for experiments, but most production plugin systems use ServiceLoader. The plugin JAR contains a service registration file under META-INF/services, and the host asks Java to find implementations automatically.

java
1package com.example.host;
2
3import java.net.URL;
4import java.net.URLClassLoader;
5import java.nio.file.Path;
6import java.util.ServiceLoader;
7
8public class ServiceLoaderExample {
9    public static void main(String[] args) throws Exception {
10        Path jarPath = Path.of("plugins", "report-plugin.jar");
11        URL jarUrl = jarPath.toUri().toURL();
12
13        try (URLClassLoader loader =
14                 new URLClassLoader(new URL[] { jarUrl }, Plugin.class.getClassLoader())) {
15
16            ServiceLoader<Plugin> plugins = ServiceLoader.load(Plugin.class, loader);
17
18            for (Plugin plugin : plugins) {
19                System.out.println("Discovered: " + plugin.name());
20                plugin.run();
21            }
22        }
23    }
24}

This design scales better because the host no longer needs to know the concrete class names. The only shared dependency is the plugin contract.

Understand class loader boundaries

The hardest part of runtime loading is not the loadClass call. It is understanding that the same class name loaded by two different class loaders is treated as two different types by the JVM. If the host and plugin each ship their own copy of a shared API JAR, casts can fail even though the class names look identical.

The usual fix is to place shared interfaces and model types in the parent loader, then let plugin-specific implementation classes live only in the child loader. Closing the URLClassLoader is also important because it releases resources tied to the JAR file, especially on systems where open file handles can block updates or replacement.

When dynamic loading is the wrong tool

Do not use runtime loading just to avoid adding a normal dependency. If your application always needs the code, put the JAR on the classpath at startup. Dynamic loading is most valuable when modules are optional, user-installed, or isolated from the host application's release cycle.

It is also worth remembering that "unloading" is indirect. Classes are unloaded only when the class loader becomes unreachable and the JVM can collect it. If your application keeps static references to plugin classes or instances, the loader may stay alive much longer than expected.

Common Pitfalls

The biggest pitfall is trying to modify the system classpath at runtime. That approach is fragile on modern Java and harder to reason about than creating your own loader.

Another common issue is loading a plugin that depends on host interfaces packaged inside the plugin JAR. That often causes ClassCastException because the host and plugin do not agree on type identity.

Reflection errors are also common when constructors are missing or not public. If you rely on a no-argument constructor, document that rule in the plugin contract.

Finally, always close the class loader. Leaving it open can keep the JAR locked and make hot-reload workflows fail in confusing ways.

Summary

  • Use a dedicated class loader, not classpath mutation, to load JARs at runtime.
  • Define a shared interface so the host and plugin agree on how to communicate.
  • 'URLClassLoader is the simplest choice for loading a known JAR file.'
  • 'ServiceLoader is a better pattern when you want pluggable discovery.'
  • Keep shared API types in the parent loader and close loaders when you are done.

Course illustration
Course illustration

All Rights Reserved.