Skip to content

Instantly share code, notes, and snippets.

@ledoyen
Last active August 29, 2015 14:16
Show Gist options
  • Save ledoyen/42dd84bf3bba954cbaed to your computer and use it in GitHub Desktop.
Save ledoyen/42dd84bf3bba954cbaed to your computer and use it in GitHub Desktop.
[JAVA] Simulate class unloading

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 :

BEWARE for now, this works only if the loader is removed before any loading of the class

@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;
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment