Skip to content

Instantly share code, notes, and snippets.

@pelson
Last active October 8, 2025 02:49
Show Gist options
  • Save pelson/0bea07edb6f4693de3d38bd4c3d53a82 to your computer and use it in GitHub Desktop.
Save pelson/0bea07edb6f4693de3d38bd4c3d53a82 to your computer and use it in GitHub Desktop.
A demo of using class path loaders with JPype

A demo of using class path loaders with JPype

Steps:

./build.sh
uv run --with jpype1 python ./demo.py

Output:

ConfigV1 initial: 1
ConfigV2 initial: 2
After changes:
  ConfigV1 counter: 100
  ConfigV2 counter: 200
# 1. Compile LoaderManager
mkdir -p my_class_loader
cp LoaderManager.java my_class_loader/
javac my_class_loader/LoaderManager.java
jar cf my_class_loader.jar my_class_loader/LoaderManager.class
rm -rf my_class_loader
# 2. Compile version 1
mkdir -p v1/com/example
cp Config-v1.java v1/com/example/Config.java
javac -d v1 v1/com/example/Config.java
jar cf config-v1.jar -C v1 com
rm -rf v1
# 3. Compile version 2
mkdir -p v2/com/example
cp Config-v2.java v2/com/example/Config.java
javac -d v2 v2/com/example/Config.java
jar cf config-v2.jar -C v2 com
rm -rf v2
package com.example;
public class Config {
private static int counter = 1;
public static int getCounter() {
return counter;
}
public static void setCounter(int v) {
counter = v;
}
}
package com.example;
public class Config {
private static int counter = 2;
public static int getCounter() {
return counter;
}
public static void setCounter(int v) {
counter = v;
}
}
import jpype
import os
# --- Paths ---
base = os.path.dirname(os.path.abspath(__file__))
loader_jar = os.path.join(base, "my_class_loader.jar")
v1_classpath = [os.path.join(base, "config-v1.jar")]
v2_classpath = [os.path.join(base, "config-v2.jar")]
# --- Start JVM with only the helper JAR ---
jpype.startJVM(classpath=[loader_jar])
# --- Load the Java helper class ---
LoaderManager = jpype.JClass("my_class_loader.LoaderManager")
# --- Load each version of Config in isolation ---
ConfigV1_Class = LoaderManager.loadClassPath(v1_classpath, "com.example.Config")
ConfigV2_Class = LoaderManager.loadClassPath(v2_classpath, "com.example.Config")
# Wrap the returned Class<?> into a JPype-accessible class type
ConfigV1 = jpype.JClass(ConfigV1_Class)
ConfigV2 = jpype.JClass(ConfigV2_Class)
# --- Use them like normal Java classes ---
print("ConfigV1 initial:", ConfigV1.getCounter()) # 1
print("ConfigV2 initial:", ConfigV2.getCounter()) # 2
# Change values independently
ConfigV1.setCounter(100)
ConfigV2.setCounter(200)
print("After changes:")
print(" ConfigV1 counter:", ConfigV1.getCounter()) # 100
print(" ConfigV2 counter:", ConfigV2.getCounter()) # 200
jpype.shutdownJVM()
package my_class_loader;
import java.net.URL;
import java.net.URLClassLoader;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
/**
* A loader that can create isolated class loaders with full classpaths.
*/
public class LoaderManager {
// Cache to reuse loaders if desired (optional)
private static final Map<String, URLClassLoader> loaders = new HashMap<>();
/**
* Load a class from an isolated class loader given a classpath (list of JARs/dirs).
*
* @param classPathList List of paths (JAR files or directories)
* @param className Fully-qualified class name
*/
public static Class<?> loadClassPath(String[] classPathList, String className) throws Exception {
// Normalize and join the classpath into a cache key
String key = Arrays.stream(classPathList)
.map(File::new)
.map(File::getAbsolutePath)
.sorted()
.collect(Collectors.joining(File.pathSeparator));
URLClassLoader loader = loaders.computeIfAbsent(key, k -> {
try {
URL[] urls = Arrays.stream(classPathList)
.map(File::new)
.map(f -> {
try {
return f.toURI().toURL();
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toArray(URL[]::new);
return new URLClassLoader(urls, null); // null parent = isolated
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return loader.loadClass(className);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment