Backend Development 30 min read

Understanding Java ClassLoader, Custom ClassLoader, Hot Swapping, SPI, and Java Agent for Plugin Development

This article explains the Java parent‑delegation model, shows how to implement a custom ClassLoader for class hot‑swap and jar hot‑deployment, demonstrates Java SPI for dynamic service loading, and details building a plugin framework using Java Agent and Javassist for runtime bytecode enhancement.

JD Tech
JD Tech
JD Tech
Understanding Java ClassLoader, Custom ClassLoader, Hot Swapping, SPI, and Java Agent for Plugin Development

Long ago the author wanted an online diagnostic tool that could instantly apply a diagnostic package when a problem occurred; later, after moving to Java, this idea became reality with IDE hot‑replace and Althas online data diagnosis, leading to the desire to build custom class loading mechanisms.

1. Parent Delegation Model

The parent‑delegation model works as follows:

1. ClassLoader's findClass(loadClass) is called.

2. AppClassLoader checks its cache; if not found, it delegates to its parent, the ExtensionClassLoader.

3. ExtensionClassLoader checks its cache; if not found, it delegates to the BootstrapClassLoader.

4. BootstrapClassLoader checks its cache; if not found, it searches its class path (e.g., rt.jar ).

5. If still not found, a ClassNotFoundException is thrown and propagated back up the delegation chain.

This process makes the mechanism easy to understand.

2. Custom ClassLoader

public class CustomClassLoader extends ClassLoader {
// Directory containing class files to load directly
private String baseDir;
public CustomClassLoader(String baseDir, String[] classes) throws IOException {
super();
this.baseDir = baseDir;
loadClassByMe(classes);
}
private void loadClassByMe(String[] classes) throws IOException {
for (int i = 0; i < classes.length; i++) {
findClass(classes[i]);
}
}
@Override
protected Class findClass(String name) {
Class clazz = null;
StringBuffer stringBuffer = new StringBuffer(baseDir);
String className = name.replace('.', File.separatorChar) + ".class";
stringBuffer.append(File.separator + className);
File classF = new File(stringBuffer.toString());
try {
clazz = instantiateClass(name, new FileInputStream(classF), classF.length());
} catch (IOException e) {
e.printStackTrace();
}
return clazz;
}
private Class instantiateClass(String name, InputStream fin, long len) throws IOException {
byte[] raw = new byte[(int) len];
fin.read(raw);
fin.close();
return defineClass(name, raw, 0, raw.length);
}
}

Using this loader, classes can be hot‑exchanged by loading a new .class file and invoking its methods via reflection.

Class clazz = customClassLoader.loadClass("com.tw.client.Foo");
Object foo = clazz.newInstance();
Method method = foo.getClass().getMethod("sayHello", new Class[]{});
method.invoke(foo, new Object[]{});

Direct casting fails because the class is loaded by a different ClassLoader, resulting in ClassCastException . The solution is to program to an interface shared by both loaders.

3. SPI for Hot Swapping

public interface HelloService { void sayHello(String name); }
public class HelloServiceProvider implements HelloService { public void sayHello(String name) { System.out.println("Hello " + name); } }
public class NameServiceProvider implements HelloService { public void sayHello(String name) { System.out.println("Hi, your name is " + name); } }

Place provider class names in META-INF/services/com.tinywhale.deploy.spi.HelloService and load them with:

ServiceLoader
serviceLoader = ServiceLoader.load(HelloService.class);
for (HelloService s : serviceLoader) { s.sayHello("myname"); }

Removing a provider entry from the file disables its output, demonstrating hot‑swap via SPI.

4. Jar Hot Deployment

public class BizClassLoader extends URLClassLoader { public BizClassLoader(URL[] urls) { super(urls); } }

Load a jar at runtime:

File moduleFile = new File("swap\\tinywhale-client-0.0.1-SNAPSHOT-biz.jar");
URL moduleURL = moduleFile.toURI().toURL();
URL[] urls = new URL[] { moduleURL };
BizClassLoader bizClassLoader = new BizClassLoader(urls);
Class clazz = bizClassLoader.loadClass("com.tw.client.Bar");
Object foo = clazz.newInstance();
Method method = foo.getClass().getMethod("sayBar", new Class[]{});
method.invoke(foo, new Object[]{});
bizClassLoader.close();

Replacing the jar file updates the behavior without restarting the JVM.

5. Java Agent Basics

public static void premain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs, Instrumentation inst);

A manifest must declare Premain-Class and Agent-Class . The premain runs before the main method when the JVM is started with -javaagent , while agentmain is attached to a running JVM via the Attach API.

Example agent classes:

public class AgentPre { public static void premain(String args, Instrumentation inst) { System.out.println("execute premain method"); } }
public class AgentMain { public static void agentmain(String args, Instrumentation inst) { System.out.println("execute agentmain method"); } }

Attach example:

String name = ManagementFactory.getRuntimeMXBean().getName();
String pid = name.split("@")[0];
VirtualMachine jvm = VirtualMachine.attach(pid);
jvm.loadAgent(agentFile.getAbsolutePath());

Output shows the sequence: premain → main → agentmain.

6. Plugin Framework Using SPI + Java Agent + Javassist

public interface IPluginExecuteStrategy { void execute(String agentArgs, Instrumentation inst); }
public class PluginPreMainExecutor implements IPluginExecuteStrategy { public void execute(String args, Instrumentation inst) { List
pluginNames = AgentPluginAnnotationHelper.annoProcess(PreMainCondition.class); ServiceLoader
loader = ServiceLoader.load(IPluginService.class); for (IPluginService s : loader) { if (pluginNames.contains(s.getPluginName())) s.pluginLoad(args, inst); } } }
public class PluginAgentMainExecutor implements IPluginExecuteStrategy { public void execute(String args, Instrumentation inst) { List
pluginNames = AgentPluginAnnotationHelper.annoProcess(AgentMainCondition.class); ServiceLoader
loader = ServiceLoader.load(IPluginService.class); for (IPluginService s : loader) { if (pluginNames.contains(s.getPluginName())) s.pluginLoad(args, inst); } } }
public class AgentPluginPreWrapper { public static void premain(String args, Instrumentation inst) { AgentPluginContextFactory.makeAgentPreExecuteContext().execute(args, inst); } }
public class AgentPluginMainWrapper { public static void agentmain(String args, Instrumentation inst) { AgentPluginContextFactory.makeAgentMainExecuteContext().execute(args, inst); } }

Sample plugin implementation:

@AgentMainCondition
@Slf4j
public class CodePadPluginServiceProvider implements IPluginService { public String getPluginName() { return "增强插件"; } public void pluginLoad(String args, Instrumentation inst) { Class
[] classes = inst.getAllLoadedClasses(); for (Class
c : classes) { if (c.getName().contains(entity.getClassName())) { try { inst.retransformClasses(c); } catch (UnmodifiableClassException e) { log.error("retransform class fail:" + c.getName(), e); } } } inst.addTransformer(new ByteCodeBizInvoker(), true); } public void pluginUnload() {} }

The transformer enhances Spring's ComponentScanBeanDefinitionParser to automatically add the framework's package to the scan path:

public byte[] transform(ClassLoader loader, String className, Class
classBeingRedefined, ProtectionDomain pd, byte[] buf) throws IllegalClassFormatException { if (loader == null) return null; if (className.contains("ComponentScanBeanDefinitionParser")) { try { ClassPool cp = new ClassPool(true); cp.appendClassPath(ByteCodeBizInvoker.class.getName()); CtClass ct = cp.get(className.replace("/", ".")); MethodInfo mi = ct.getClassFile().getMethod("parse"); CtMethod cm = ct.getDeclaredMethod("parse"); addComponentScanPackage(mi, cm); return ct.toBytecode(); } catch (Exception e) { log.error("handle spring 5 ComponentScanBeanDefinitionParser error", e); } } return null; }
private void addComponentScanPackage(MethodInfo mi, CtMethod cm) throws CannotCompileException { final boolean[] success = {false}; CodeAttribute ca = mi.getCodeAttribute(); CodeIterator it = ca.iterator(); while (it.hasNext()) { ExprEditor ee = new ExprEditor() { public void edit(MethodCall m) throws CannotCompileException { if (m.getMethodName().equals("getAttribute")) { m.replace("{ $_ = $proceed($$); $_ = $_ +  ",org.tiny.upgrade";  }"); success[0] = true; } } }; cm.instrument(ee); if (success[0]) break; } }

Finally, a remote‑plugin loader uses a custom TinyPluginClassLoader (extending URLClassLoader ) to load plugins from local or HTTP jar URLs, discovers classes implementing IPluginService via SPI, and executes them.

Overall, the article walks through the parent‑delegation model, custom class loading, class and jar hot‑swap, Java SPI, Java Agent lifecycle, and builds a full‑stack plugin framework that can load plugins locally or from the network.

JavapluginClassLoaderSPIJavaAgentJavassistHotSwap
JD Tech
Written by

JD Tech

Official JD technology sharing platform. All the cutting‑edge JD tech, innovative insights, and open‑source solutions you’re looking for, all in one place.

0 followers
Reader feedback

How this landed with the community

login 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.