How to Hot‑Deploy User‑Provided JAR Implementations in a Spring‑Based System

This article walks through building a simple Calculator interface, creating a dual‑mode implementation, and then dynamically loading, registering, and unloading user‑supplied JAR files at runtime using reflection or Spring annotation processing, complete with utility methods and a test harness.

Java Web Project
Java Web Project
Java Web Project
How to Hot‑Deploy User‑Provided JAR Implementations in a Spring‑Based System

Define a simple interface

We start with a minimal calculator contract that declares two methods:

public interface Calculator {
    int calculate(int a, int b);
    int add(int a, int b);
}

Implementation supporting annotation and reflection

The concrete class implements both methods and demonstrates two ways the system can invoke them: the calculate method is intended for Spring‑managed (annotation) calls, while add is used when the class is accessed via reflection.

@Service
public class CalculatorImpl implements Calculator {
    @Autowired
    CalculatorCore calculatorCore;

    /** Annotation‑based */
    @Override
    public int calculate(int a, int b) {
        int c = calculatorCore.add(a, b);
        return c;
    }

    /** Reflection‑based */
    @Override
    public int add(int a, int b) {
        return new CalculatorCore().add(a, b);
    }
}

The auxiliary CalculatorCore simply adds two integers:

@Service
public class CalculatorCore {
    public int add(int a, int b) {
        return a + b;
    }
}

Reflection‑based hot deployment

When a user uploads a JAR, the system stores the absolute path ( jarAddress) and builds a file:/ URL ( jarPath). The hot‑deployment routine loads the JAR with a URLClassLoader, obtains the implementation class by its fully‑qualified name, creates an instance, and invokes the add method via reflection.

private static String jarAddress = "E:/zzq/IDEA_WS/CalculatorTest/lib/Calculator.jar";
private static String jarPath = "file:/" + jarAddress;

public static void hotDeployWithReflect() throws Exception {
    URLClassLoader urlClassLoader = new URLClassLoader(
        new URL[]{ new URL(jarPath) },
        Thread.currentThread().getContextClassLoader()
    );
    Class clazz = urlClassLoader.loadClass("com.nci.cetc15.calculator.impl.CalculatorImpl");
    Calculator calculator = (Calculator) clazz.newInstance();
    int result = calculator.add(1, 2);
    System.out.println(result);
}

Spring‑annotation hot deployment

If the uploaded JAR contains Spring components, the system must scan every class, detect those annotated with @Component, @Service, or @Repository, and register them into the existing Spring DefaultListableBeanFactory. The scanning uses the same class loader as the reflection path.

public static void hotDeployWithSpring() throws Exception {
    Set<String> classNameSet = DeployUtils.readJarFile(jarAddress);
    URLClassLoader urlClassLoader = new URLClassLoader(
        new URL[]{ new URL(jarPath) },
        Thread.currentThread().getContextClassLoader()
    );
    for (String className : classNameSet) {
        Class clazz = urlClassLoader.loadClass(className);
        if (DeployUtils.isSpringBeanClass(clazz)) {
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
            defaultListableBeanFactory.registerBeanDefinition(
                DeployUtils.transformName(className),
                beanDefinitionBuilder.getBeanDefinition()
            );
        }
    }
}

Utility class DeployUtils

Three helper methods support the hot‑deployment process: readJarFile(String jarAddress) iterates over all entries in the JAR, collects fully‑qualified class names, and returns them as a Set<String>. isSpringBeanClass(Class<?> cla) checks that the class is not an interface or abstract class and that it carries a Spring stereotype annotation ( @Component, @Repository, or @Service). transformName(String className) converts the class name to a bean name by lower‑casing the first character (e.g., CalculatorImplcalculatorImpl).

public static Set<String> readJarFile(String jarAddress) throws IOException {
    Set<String> classNameSet = new HashSet<>();
    JarFile jarFile = new JarFile(jarAddress);
    Enumeration<JarEntry> entries = jarFile.entries();
    while (entries.hasMoreElements()) {
        JarEntry jarEntry = entries.nextElement();
        String name = jarEntry.getName();
        if (name.endsWith(".class")) {
            String className = name.replace(".class", "")
                                 .replaceAll("/", ".");
            classNameSet.add(className);
        }
    }
    return classNameSet;
}

public static boolean isSpringBeanClass(Class<?> cla) {
    if (cla == null || cla.isInterface() || Modifier.isAbstract(cla.getModifiers())) {
        return false;
    }
    return cla.getAnnotation(Component.class) != null ||
           cla.getAnnotation(Repository.class) != null ||
           cla.getAnnotation(Service.class) != null;
}

public static String transformName(String className) {
    String tmp = className.substring(className.lastIndexOf('.') + 1);
    return tmp.substring(0,1).toLowerCase() + tmp.substring(1);
}

Removing beans when a JAR is deleted

When a JAR is removed or replaced, the system must undo the registration performed earlier. It re‑scans the JAR, identifies Spring beans, and calls removeBeanDefinition with the same bean name generated by transformName.

public static void delete() throws Exception {
    Set<String> classNameSet = DeployUtils.readJarFile(jarAddress);
    URLClassLoader urlClassLoader = new URLClassLoader(
        new URL[]{ new URL(jarPath) },
        Thread.currentThread().getContextClassLoader()
    );
    for (String className : classNameSet) {
        Class clazz = urlClassLoader.loadClass(className);
        if (DeployUtils.isSpringBeanClass(clazz)) {
            defaultListableBeanFactory.removeBeanDefinition(
                DeployUtils.transformName(className)
            );
        }
    }
}

Test harness

A simple test program creates a Spring ApplicationContext, obtains the underlying DefaultListableBeanFactory, and then enters an infinite loop that repeatedly attempts hot deployment. If the JAR is not yet present, an exception is caught, printed, and the thread sleeps for ten seconds, giving the developer time to copy the JAR into the monitored directory.

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
while (true) {
    try {
        hotDeployWithReflect();
        // hotDeployWithSpring();
        // delete();
    } catch (Exception e) {
        e.printStackTrace();
        Thread.sleep(1000 * 10);
    }
}

Through these steps the article demonstrates a complete workflow for allowing end‑users to supply their own implementation JARs, hot‑load them without restarting the server, and keep the Spring container consistent when the JARs are added, updated, or removed.

JavaReflectionSpringHot Deploymentjar-loadingdynamic-bean-registration
Java Web Project
Written by

Java Web Project

Focused on Java backend technologies, trending internet tech, and the latest industry developments. The platform serves over 200,000 Java developers, inviting you to learn and exchange ideas together. Check the menu for Java learning resources.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.