For testing purpose you may want to "unload" some classes.
Testing the new Spring Boot @ConditionalOnClass
or @ConditionalOnMissingClass
is a good example.
Once in your JUnit class, you usually don't get to modify the classpath to remove some jar and test that your application still works.
One way to do this test is to create a Maven project in a dedicated directory with a different pom.xml (and so a different classpath at runtime) and run tests in it from your main project with the invoker plugin. See how google does it in the auto-value project.
Another way to do it is to take advantage of the high mutability of Java and hack into the classloader to modify cached data :
@Test
public void loader_removal_should_prevent_class_from_loading() {
ClassLoaderModifier.getInstance().removeLoaderByNameContaining("spring-expression");
assertThat(isClassPresent("org.springframework.expression.spel.standard.SpelExpressionParser"), Matchers.equalTo(false));
ClassLoaderModifier.getInstance().restore();
assertThat(isClassPresent("org.springframework.expression.spel.standard.SpelExpressionParser"), Matchers.equalTo(true));
}
private static boolean isClassPresent(String name) {
try {
Class.forName(name);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
import sun.misc.URLClassPath;
import java.lang.reflect.Field;
import java.net.URLClassLoader;
import java.util.*;
public class ClassLoaderModifier {
private static final Map<URLClassLoader, ClassLoaderModifier> INSTANCES = new HashMap<>();
private final URLClassLoader classLoader;
private final URLClassPath classPath;
private final Map<String, Object> originalLmap;
private final List<Object> originalLoaders;
private ClassLoaderModifier(final URLClassLoader classLoader) {
this.classLoader = classLoader;
classPath = getByReflection(classLoader, "ucp", URLClassPath.class);
originalLmap = clone(getByReflection(classPath, "lmap", Map.class));
originalLoaders = clone(getByReflection(classPath, "loaders", List.class));
}
public static final ClassLoaderModifier getInstance() {
return getInstance((URLClassLoader) ClassLoaderModifier.class.getClassLoader());
}
public static final synchronized ClassLoaderModifier getInstance(final URLClassLoader classLoader) {
if(!INSTANCES.containsKey(classLoader)) {
INSTANCES.put(classLoader, new ClassLoaderModifier(classLoader));
}
return INSTANCES.get(classLoader);
}
public final void restore() {
setByReflection(classPath, "lmap", clone(originalLmap));
setByReflection(classPath, "loaders", clone(originalLoaders));
}
public ClassLoaderModifier removeLoaderByNameContaining(final String nameFragment) {
removeLoaderByNameContaining(classLoader, classPath, nameFragment);
return this;
}
public static void removeLoaderByNameContaining(final URLClassLoader classLoader, final URLClassPath classPath, final String nameFragment) {
Map<String, Object> lmap = getByReflection(classPath, "lmap", Map.class);
List<Object> loaders = getByReflection(classPath, "loaders", List.class);
for (Iterator<Map.Entry<String, Object>> lmapEntryIterator = lmap.entrySet().iterator(); lmapEntryIterator.hasNext(); ) {
Map.Entry<String, Object> entry = lmapEntryIterator.next();
if (entry.getKey().contains(nameFragment)) {
String name = entry.getValue().getClass().getName();
if("sun.misc.URLClassPath$JarLoader".equals(name)) {
// Remove from the lmap
lmapEntryIterator.remove();
// Remove the loader
loaders.remove(entry.getValue());
}
}
}
}
public static void setByReflection(final Object o, final String fieldName, final Object value) {
try {
Field f = getField(o.getClass(), fieldName);
if (!f.isAccessible()) {
f.setAccessible(true);
}
f.set(o, value);
} catch(NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static <T> T getByReflection(final Object o, final String fieldName, final Class<T> type) {
try {
Field f = getField(o.getClass(), fieldName);
if (!f.isAccessible()) {
f.setAccessible(true);
}
return (T) f.get(o);
} catch(NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* @return the field if present in the given class or one of its superclasses
* @throws NoSuchFieldException if not found
*/
public static Field getField(final Class<?> clazz, final String fieldName) throws NoSuchFieldException {
for(Field found : clazz.getDeclaredFields()) {
if(found.getName().equals(fieldName)) {
return found;
}
}
if(!Object.class.equals(clazz.getSuperclass())) {
return getField(clazz.getSuperclass(), fieldName);
} else {
throw new NoSuchFieldException(fieldName);
}
}
public static <T> List<T> clone(final List<T> original) {
List<T> clone = new ArrayList<>();
clone.addAll(original);
return clone;
}
public static <U, V> Map<U, V> clone(final Map<U, V> original) {
Map<U, V> clone = new HashMap<>();
clone.putAll(original);
return clone;
}
}