Java ServiceLoader wrapper not working, internally using calling class - java

So, I've been trying to make a small PluginLoader in my library which allows you to load JAR files into a custom ModuleLayer and use the layer to load services using ServiceLoader.load(ModuleLayer, Class<?>).
However, when calling ServiceProvider.load, it internally uses Reflection.getCallerClass to get the, duhh, class calling the code, so it can load the services from it's module.
PluginLoader.java
package com.wexalian.common.plugin;
import com.wexalian.common.collection.wrapper.StreamWrapper;
import com.wexalian.nullability.annotations.Nonnull;
import java.nio.file.Path;
import java.util.ServiceLoader;
import java.util.stream.Stream;
#FunctionalInterface
public interface PluginLoader<T extends IAbstractPlugin> extends StreamWrapper.Iterable<T> {
#Nonnull
#Override
Stream<T> get();
static void init(#Nonnull ServiceLoaderLayerFunction serviceLoaderFunc) {
PluginLoaderImpl.init(serviceLoaderFunc);
}
static void loadPlugins(#Nonnull Path path) {
PluginLoaderImpl.loadPlugins(path);
}
#Nonnull
static <T extends IAbstractPlugin> PluginLoader<T> load(#Nonnull Class<T> pluginClass) {
return load(pluginClass, null);
}
#Nonnull
static <T extends IAbstractPlugin> PluginLoader<T> load(#Nonnull Class<T> pluginClass, ServiceLoaderFallbackFunction fallbackServiceProvider) {
return PluginLoaderImpl.load(pluginClass, fallbackServiceProvider);
}
#FunctionalInterface
interface ServiceLoaderLayerFunction {
#Nonnull
<T> ServiceLoader<T> load(#Nonnull ModuleLayer layer, #Nonnull Class<T> clazz);
#Nonnull
default <T> Stream<T> stream(#Nonnull ModuleLayer layer, #Nonnull Class<T> clazz) {
return load(layer, clazz).stream().map(ServiceLoader.Provider::get);
}
}
#FunctionalInterface
interface ServiceLoaderFallbackFunction {
#Nonnull
<T> ServiceLoader<T> load(#Nonnull Class<T> clazz);
#Nonnull
default <T> Stream<T> stream(#Nonnull Class<T> clazz) {
return load(clazz).stream().map(ServiceLoader.Provider::get);
}
}
}
PluginLoaderImpl.java
package com.wexalian.common.plugin;
import com.wexalian.nullability.annotations.Nonnull;
import java.io.IOException;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
final class PluginLoaderImpl {
private static final Set<ModuleLayer> pluginLayerSet = new HashSet<>();
private static PluginLoader.ServiceLoaderLayerFunction serviceLoaderLayer;
private static ModuleLayer coreLayer;
private static ClassLoader coreLoader;
private static boolean init = false;
private PluginLoaderImpl() {}
static void init(#Nonnull PluginLoader.ServiceLoaderLayerFunction serviceLoaderFunc) {
if (!init) {
serviceLoaderLayer = serviceLoaderFunc;
Class<?> coreClass = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass();
coreLayer = coreClass.getModule().getLayer();
coreLoader = coreClass.getClassLoader();
if (coreLayer == null) {
throw new IllegalStateException("PluginLoaderImpl can only be initialized from a named module!");
}
else init = true;
}
else throw new IllegalStateException("PluginLoaderImpl can only be initialized once!");
}
static void loadPlugins(#Nonnull Path path) {
if (init) {
if (Files.exists(path)) {
try (Stream<Path> paths = Files.list(path)) {
ModuleFinder moduleFinder = ModuleFinder.of(paths.toArray(Path[]::new));
List<String> moduleNames = moduleFinder.findAll().stream().map(ref -> ref.descriptor().name()).toList();
Configuration configuration = coreLayer.configuration().resolveAndBind(moduleFinder, ModuleFinder.of(), moduleNames);
ModuleLayer pluginLayer = coreLayer.defineModulesWithOneLoader(configuration, coreLoader);
pluginLayerSet.add(pluginLayer);
}
catch (IOException e) {
throw new IllegalStateException("Error loading plugins from path " + path, e);
}
}
}
else throw new IllegalStateException("PluginLoaderImpl has to be initialized before you can load plugins!");
}
static <T extends IAbstractPlugin> PluginLoader<T> load(Class<T> clazz, PluginLoader.ServiceLoaderFallbackFunction serviceLoader) {
if (init) {
if (!pluginLayerSet.isEmpty()) {
return () -> pluginLayerSet.stream().flatMap(layer -> serviceLoaderLayer.stream(layer, clazz)).filter(IAbstractPlugin::isEnabled);
}
else {
return () -> serviceLoaderLayer.stream(coreLayer, clazz).filter(IAbstractPlugin::isEnabled);
}
}
else if (serviceLoader != null) {
return () -> serviceLoader.stream(clazz);
}
else throw new IllegalStateException("PluginLoaderImpl has to be initialized before you can load services from plugins!");
}
}
Now my problem is:
I am currently writing a program with some services, and using that library to load JAR files and load them. However, it recognizes the PluginLoader as the caller class, which "does not declare uses", because the library doesn't actually have the service I want.
I have found a work around, which is accepting a Function<ModuleLayer, Class<?>, ServiceProvider<?>, which redirects all the calls to the proper module, but I'd rather not do that everywhere I use my PluginLoader.
Other than this I wouldn't know any other solution, so maybe one of you knows.
Thanks in advance,
Wexalian

When using the ModuleLayer system, you also have to define the uses and provides in your and the various plugin module definitions.
Your module:
uses com.wexalian.common.plugin.IAbstractPlugin;
And in your plugin modules:
provides com.wexalian.common.plugin.IAbstractPlugin with some.plugin.PluginFactory;
See ServiceLoader and ServiceLoader.Provider, this is how the service loader in one module knows about loaders in other modules.

Related

how to deploy a vert.x verticle at runtime?

my project is generated by http://start.vertx.io
my http handler:
// ...
public void handle(RoutingContext ctx) {
String verticleName = ctx.queryParams().get("v");
ctx.vertx().deployVerticle(verticleName);
ctx.response().end();
}
// ...
but it reports error
java.lang.RuntimeException: Resource not found: verticles/TestVerticle.java
at io.vertx.core.impl.verticle.CompilingClassLoader.<init>(CompilingClassLoader.java:68)
at io.vertx.core.impl.JavaVerticleFactory.createVerticle(JavaVerticleFactory.java:37)
at io.vertx.core.impl.VerticleManager.doDeployVerticle(VerticleManager.java:217)
at io.vertx.core.impl.VerticleManager.doDeployVerticle(VerticleManager.java:193)
at io.vertx.core.impl.VerticleManager.doDeployVerticle(VerticleManager.java:180)
at io.vertx.core.impl.VerticleManager.deployVerticle(VerticleManager.java:156)
at io.vertx.core.impl.VertxImpl.deployVerticle(VertxImpl.java:623)
at io.vertx.core.impl.VertxImpl.deployVerticle(VertxImpl.java:608)
By default the verticle name should be a fully-qulified class name that has a no-args constructor and implements Verticle (or extends one of its implementors). e.g.
package demo;
import io.vertx.core.AbstractVerticle;
public class MyVerticle extends AbstractVerticle {
#Override
public void start() throws Exception {
// ...
}
}
Then you can do:
vertx.deployVerticle("demo.MyVerticle");
If you want to use another mechanism you can create a custom VerticleFactory (https://vertx.io/docs/vertx-service-factory/java/) and use your own logic. e.g.
package demo;
import io.vertx.core.Promise;
import io.vertx.core.Verticle;
import io.vertx.core.spi.VerticleFactory;
import java.util.concurrent.Callable;
public class CustomVerticleFactory implements VerticleFactory {
#Override
public String prefix() {
return "custom";
}
#Override
public void createVerticle(String verticleName, ClassLoader classLoader, Promise<Callable<Verticle>> promise) {
if (verticleName.equals("custom:x")) {
promise.complete(() -> new MyVerticle());
} else {
promise.fail("...");
}
}
}
Load it to your Vertx instance:
vertx.registerVerticleFactory(new CustomVerticleFactory());
And then you can do:
vertx.deployVerticle("custom:x");
Found the right way.
I should start the app with -Xbootclasspath/a:.
Then the class loader can found java source files(XXXVerticle.java) at . (working dir)

How to get bytecode of cglib proxy class instance?

I'm trying to get bytecode of cglib enhanced object this way using BCEL:
package app;
import cglib.MyInterceptor;
import net.sf.cglib.proxy.Enhancer;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import service.Tool;
public class CgLibApp {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
// target object
Tool tool = new Tool();
// proxying
Enhancer e = new Enhancer();
e.setSuperclass(tool.getClass());
e.setCallback(new MyInterceptor(tool));
Tool proxifiedTool = (Tool) e.create();
// trying to get proxy byte code
JavaClass clazz = Repository.lookupClass(proxifiedTool.getClass());
Method method = clazz.getMethod(Tool.class.getMethod("meth"));
System.out.println(method.getCode().toString());
}
}
But I'm getting:
Exception in thread "main" java.lang.ClassNotFoundException: SyntheticRepository could not load service.Tool$$EnhancerByCGLIB$$22a3afcc
at org.apache.bcel.util.SyntheticRepository.loadClass(SyntheticRepository.java:174)
at org.apache.bcel.util.SyntheticRepository.loadClass(SyntheticRepository.java:158)
at org.apache.bcel.Repository.lookupClass(Repository.java:74)
at app.CgLibApp.main(CgLibApp.java:21)
What should I do to get bytecode from Enhanced object?
BCEL queries a class loader for a .class file in order to get hold of the byte array that represents it. Such a class file does not exist for a dynamically generated class.
In order to get hold of the class file, you have to collect the byte code during the class file's creation. Cglib is built on top of ASM and it allows you to register your own ClassVisitors to collect a class file.
With the Enhancer, use the generateClass(ClassVisitor) method and hand the latter method a ClassWriter. After calling the method, you can get the byte code from the class writer object that you passed.
here is the sample code to print pseudo code of generated CGLIB class.
visitEnd method prints the generated class in text format.
package naga.cglib.demo;
import static org.objectweb.asm.Opcodes.ASM7;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.util.TraceClassVisitor;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.FixedValue;
public class App {
public static void main(String[] args) throws Exception, IllegalArgumentException, InvocationTargetException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new FixedValueImpl());
SampleClass proxy = (SampleClass) enhancer.create();
enhancer.generateClass(new CustomClassWriter());
System.out.println("Hello cglib!" + proxy.test(null));
}
}
class SampleClass {
public String test(String input) {
return "Hello world!";
}
}
class FixedValueImpl implements FixedValue {
#Override
public Object loadObject() throws Exception {
// TODO Auto-generated method stub
return "Hello cglib! from loadObject()";
}
}
class CustomClassWriter extends ClassVisitor {
TraceClassVisitor tracer;
PrintWriter pw = new PrintWriter(System.out);
public CustomClassWriter() {
super(ASM7);
tracer = new TraceClassVisitor(pw);
}
#Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("method name is :" + name);
return tracer.visitMethod(access, name, desc, signature, exceptions);
}
#Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
System.out.println("field name is :" + name);
return tracer.visitField(access, name, desc, signature, value);
}
public void visitEnd() {
tracer.visitEnd();
System.out.println(tracer.p.getText());
}
}
I've found this question while researching how to save the CGLIB-generated class in spring-boot 3.0 application (e.g. handling #Transactional or #Configuration-annotated classes). This simple approach may help:
import org.springframework.cglib.core.ReflectUtils;
...
public class SpringCglibUtils {
public static void initGeneratedClassHandler(String targetPath) {
File dir = new File(targetPath);
dir.mkdirs();
ReflectUtils.setGeneratedClassHandler((String className, byte[] classContent) -> {
try (FileOutputStream out = new FileOutputStream(new File(dir, className + ".class"))) {
out.write(classContent);
} catch (IOException e) {
throw new UncheckedIOException("Error while storing " + className, e);
}
});
}
}
and then define in your main class before creating context:
SpringCglibUtils.initGeneratedClassHandler("cglib");
Spring will store to the targetPath directory all generated class files.
Note: unfortunately it's not available before spring-boot 3

Cucumber with Guice - multiple guice injector

I'm using Cucumber with Guice as DI.
I've encountered following problem:
I've got one step i.e.
class MyStep() {
#Inject
private MyService myService;
#Given("Some acction happen")
public void sthHappen() {
myService.doSth();
}
}
And I've got this class to run it as JUnit test
#RunWith(Cucumber.class)
#CucumberOptions(...)
public class MyTest {
}
There is a
class MyModule extends AbstractModule {
#Override
protected void configure() {
bind(MyService.class).to(MyFirstService.class);
}
}
which is used by my MyInjectorSource
I define cucumber.properties where I define guice.injector-source=MyInjectorSource;
There is also a feature file with scenario.
Everything is working for now.
And no i would like to run MyStep step with other MyService implementation (of course I don't wont to duplicate code of MyStep)
I define a new feature file with new scenarios, and new Test class
#RunWith(Cucumber.class)
#CucumberOptions(...)
public class MyOtherTest {
}
And now I've tried to create another InjectorSource but I was not able to configure it.
Solution which I've found is using custom Junit4 runner inheriting from original Cucumber runner and changing its createRuntime method.
Latest cucumber-guice 1.2.5 uses few stages to create injector and unfortunately it uses global variable cucumber.runtime.Env.INSTANCE. This variable is populated from cucumber.properties and System.getProperties.
Flow is:
Cucumber runner scans available backends (in my setup it is cucumber.runtime.java.JavaBackend)
One of JavaBackend constructor loads available ObjectFactory (in my setup it is cucumber.runtime.java.guice.impl.GuiceFactory)
GuiceFactory via InjectorSourceFactory checks Env.INSTANCE, it will create custom InjectorSource or default injector
Ideally cucumber should pass its 'RuntimeOptions` created at start to backend and InjectorSource but unfortunately it doesn't and uses global variable. It is not easy create patch like this one so my solution simplifies this approach and directly create InjectorSource in custom runner by reading new annotation.
public class GuiceCucumberRunner extends Cucumber {
public GuiceCucumberRunner(Class<?> clazz) throws InitializationError, IOException {
super(clazz);
}
#Override
protected Runtime createRuntime(ResourceLoader resourceLoader, ClassLoader classLoader, RuntimeOptions runtimeOptions) throws InitializationError, IOException {
Runtime result = new Runtime(resourceLoader, classLoader, Arrays.asList(createGuiceBackend()), runtimeOptions);
return result;
}
private JavaBackend createGuiceBackend() {
GuiceCucumberOptions guiceCucumberOptions = getGuiceCucumberOptions();
InjectorSource injectorSource = createInjectorSource(guiceCucumberOptions.injectorSource());
ObjectFactory objectFactory = new GuiceFactory(injectorSource.getInjector());
JavaBackend result = new JavaBackend(objectFactory);
return result;
}
private GuiceCucumberOptions getGuiceCucumberOptions() {
GuiceCucumberOptions guiceCucumberOptions = getTestClass().getJavaClass().getAnnotation(GuiceCucumberOptions.class);
if (guiceCucumberOptions == null) {
String message = format("Suite class ''{0}'' is missing annotation GuiceCucumberOptions", getTestClass().getJavaClass());
throw new CucumberException(message);
}
return guiceCucumberOptions;
}
private InjectorSource createInjectorSource(Class<? extends InjectorSource> clazz) {
try {
return clazz.newInstance();
} catch (Exception e) {
String message = format("Instantiation of ''{0}'' failed. InjectorSource must have has a public zero args constructor.", clazz);
throw new InjectorSourceInstantiationFailed(message, e);
}
}
static class GuiceFactory implements ObjectFactory {
private final Injector injector;
GuiceFactory(Injector injector) {
this.injector = injector;
}
#Override
public boolean addClass(Class<?> clazz) {
return true;
}
#Override
public void start() {
injector.getInstance(ScenarioScope.class).enterScope();
}
#Override
public void stop() {
injector.getInstance(ScenarioScope.class).exitScope();
}
#Override
public <T> T getInstance(Class<T> clazz) {
return injector.getInstance(clazz);
}
}
}
#Retention(RetentionPolicy.RUNTIME)
#Target({ ElementType.TYPE })
public #interface GuiceCucumberOptions {
Class<? extends InjectorSource> injectorSource();
}
#RunWith(GuiceCucumberRunner.class)
#GuiceCucumberOptions(injectorSource = MyInjector.class)
#CucumberOptions(
...
)
public class Suite {
}
I needed to copy GuiceFactory because it doesn't exposes normal constructor (!)

Parameterized test case classes in JUnit 3.x

I have a JUnit 3.x TestCase which I would like to be able to parameterize. I'd like to parametrize the entire TestCase (including the fixture). However, the TestSuite.addTestSuite() method does not allow be to pass a TestCase object, just a class:
TestSuite suite = new TestSuite("suite");
suite.addTestSuite(MyTestCase.class);
I would like to be able to pass a parameter (a string) to the MyTestCase instance which is created when the test runs. As it is now, I have to have a separate class for each parameter value.
I tried passing it an anynomous subclass:
MyTestCase testCase = new MyTestCase() {
String getOption() {
return "some value";
}
}
suite.addTestSuite(testCase.getClass());
However, this fails with the assertion:
... MyTestSuite$1 has no public constructor TestCase(String name) or TestCase()`
Any ideas? Am I attacking the problem the wrong way?
If this is Java 5 or higher, you might want to consider switching to JUnit 4, which has support for parameterized test cases built in.
Rather than create a parameterized test case for the multiple/different backends you want to test against, I would look into making my test cases abstract. Each new implementation of your API would need to supply an implementing TestCase class.
If you currently have a test method that looks something like
public void testSomething() {
API myAPI = new BlahAPI();
assertNotNull(myAPI.something());
}
just add an abstract method to the TestCase that returns the specific API object to use.
public abstract class AbstractTestCase extends TestCase {
public abstract API getAPIToTest();
public void testSomething() {
API myAPI = getAPIToTest();
assertNotNull(myAPI.something());
}
public void testSomethingElse() {
API myAPI = getAPIToTest();
assertNotNull(myAPI.somethingElse());
}
}
Then the TestCase for the new implementation you want to test only has to implement your AbstractTestCase and supply the concrete implementation of the API class:
public class ImplementationXTestCase extends AbstractTestCase{
public API getAPIToTest() {
return new ImplementationX();
}
}
Then all of the test methods that test the API in the abstract class are run automatically.
Ok, here is a quick mock-up of how JUnit 4 runs parameterized tests, but done in JUnit 3.8.2.
Basically I'm subclassing and badly hijacking the TestSuite class to populate the list of tests according to the cross-product of testMethods and parameters.
Unfortunately I've had to copy a couple of helper methods from TestSuite itself, and a few details are not perfect, such as the names of the tests in the IDE being the same across parameter sets (JUnit 4.x appends [0], [1], ...).
Nevertheless, this seems to run fine in the text and AWT TestRunners that ship with JUnit as well as in Eclipse.
Here is the ParameterizedTestSuite, and further down a (silly) example of a parameterized test using it.
(final note : I've written this with Java 5 in mind, it should be trivial to adapt to 1.4 if needed)
ParameterizedTestSuite.java:
package junit.parameterized;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class ParameterizedTestSuite extends TestSuite {
public ParameterizedTestSuite(
final Class<? extends TestCase> testCaseClass,
final Collection<Object[]> parameters) {
setName(testCaseClass.getName());
final Constructor<?>[] constructors = testCaseClass.getConstructors();
if (constructors.length != 1) {
addTest(warning(testCaseClass.getName()
+ " must have a single public constructor."));
return;
}
final Collection<String> names = getTestMethods(testCaseClass);
final Constructor<?> constructor = constructors[0];
final Collection<TestCase> testCaseInstances = new ArrayList<TestCase>();
try {
for (final Object[] objects : parameters) {
for (final String name : names) {
TestCase testCase = (TestCase) constructor.newInstance(objects);
testCase.setName(name);
testCaseInstances.add(testCase);
}
}
} catch (IllegalArgumentException e) {
addConstructionException(e);
return;
} catch (InstantiationException e) {
addConstructionException(e);
return;
} catch (IllegalAccessException e) {
addConstructionException(e);
return;
} catch (InvocationTargetException e) {
addConstructionException(e);
return;
}
for (final TestCase testCase : testCaseInstances) {
addTest(testCase);
}
}
private Collection<String> getTestMethods(
final Class<? extends TestCase> testCaseClass) {
Class<?> superClass= testCaseClass;
final Collection<String> names= new ArrayList<String>();
while (Test.class.isAssignableFrom(superClass)) {
Method[] methods= superClass.getDeclaredMethods();
for (int i= 0; i < methods.length; i++) {
addTestMethod(methods[i], names, testCaseClass);
}
superClass = superClass.getSuperclass();
}
return names;
}
private void addTestMethod(Method m, Collection<String> names, Class<?> theClass) {
String name= m.getName();
if (names.contains(name))
return;
if (! isPublicTestMethod(m)) {
if (isTestMethod(m))
addTest(warning("Test method isn't public: "+m.getName()));
return;
}
names.add(name);
}
private boolean isPublicTestMethod(Method m) {
return isTestMethod(m) && Modifier.isPublic(m.getModifiers());
}
private boolean isTestMethod(Method m) {
String name= m.getName();
Class<?>[] parameters= m.getParameterTypes();
Class<?> returnType= m.getReturnType();
return parameters.length == 0 && name.startsWith("test") && returnType.equals(Void.TYPE);
}
private void addConstructionException(Exception e) {
addTest(warning("Instantiation of a testCase failed "
+ e.getClass().getName() + " " + e.getMessage()));
}
}
ParameterizedTest.java:
package junit.parameterized;
import java.util.Arrays;
import java.util.Collection;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.parameterized.ParameterizedTestSuite;
public class ParameterizedTest extends TestCase {
private final int value;
private int evilState;
public static Collection<Object[]> parameters() {
return Arrays.asList(
new Object[] { 1 },
new Object[] { 2 },
new Object[] { -2 }
);
}
public ParameterizedTest(final int value) {
this.value = value;
}
public void testMathPow() {
final int square = value * value;
final int powSquare = (int) Math.pow(value, 2) + evilState;
assertEquals(square, powSquare);
evilState++;
}
public void testIntDiv() {
final int div = value / value;
assertEquals(1, div);
}
public static Test suite() {
return new ParameterizedTestSuite(ParameterizedTest.class, parameters());
}
}
Note: the evilState variable is just here to show that all test instances are different as they should be, and that there is no shared state between them.
a few details are not perfect, such as the names of the tests in the IDE being the same across parameter sets (JUnit 4.x appends [0], [1], ...).
To solve this you just need to overwrite getName() and change the constructor in your test case class:
private String displayName;
public ParameterizedTest(final int value) {
this.value = value;
this.displayName = Integer.toString(value);
}
#Override
public String getName() {
return super.getName() + "[" + displayName + "]";
}
For Android projects, we wrote a library called Burst for test parameterization. For example
public class ParameterizedTest extends TestCase {
enum Drink { COKE, PEPSI, RC_COLA }
private final Drink drink;
// Nullary constructor required by Android test framework
public ConstructorTest() {
this(null);
}
public ConstructorTest(Drink drink) {
this.drink = drink;
}
public void testSomething() {
assertNotNull(drink);
}
}
Not really an answer to your question since you're not using Android, but a lot of projects which still use JUnit 3 do so because Android's test framework requires it, so I hope some other readers will find this helpful.

Using different classloaders for different JUnit tests?

I have a Singleton/Factory object that I'd like to write a JUnit test for. The Factory method decides which implementing class to instantiate based upon a classname in a properties file on the classpath. If no properties file is found, or the properties file does not contain the classname key, then the class will instantiate a default implementing class.
Since the factory keeps a static instance of the Singleton to use once it has been instantiated, to be able to test the "failover" logic in the Factory method I would need to run each test method in a different classloader.
Is there any way with JUnit (or with another unit testing package) to do this?
edit: here is some of the Factory code that is in use:
private static MyClass myClassImpl = instantiateMyClass();
private static MyClass instantiateMyClass() {
MyClass newMyClass = null;
String className = null;
try {
Properties props = getProperties();
className = props.getProperty(PROPERTY_CLASSNAME_KEY);
if (className == null) {
log.warn("instantiateMyClass: Property [" + PROPERTY_CLASSNAME_KEY
+ "] not found in properties, using default MyClass class [" + DEFAULT_CLASSNAME + "]");
className = DEFAULT_CLASSNAME;
}
Class MyClassClass = Class.forName(className);
Object MyClassObj = MyClassClass.newInstance();
if (MyClassObj instanceof MyClass) {
newMyClass = (MyClass) MyClassObj;
}
}
catch (...) {
...
}
return newMyClass;
}
private static Properties getProperties() throws IOException {
Properties props = new Properties();
InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(PROPERTIES_FILENAME);
if (stream != null) {
props.load(stream);
}
else {
log.error("getProperties: could not load properties file [" + PROPERTIES_FILENAME + "] from classpath, file not found");
}
return props;
}
This question might be old but since this was the nearest answer I found when I had this problem I though I'd describe my solution.
Using JUnit 4
Split your tests up so that there is one test method per class (this solution only changes classloaders between classes, not between methods as the parent runner gathers all the methods once per class)
Add the #RunWith(SeparateClassloaderTestRunner.class) annotation to your test classes.
Create the SeparateClassloaderTestRunner to look like this:
public class SeparateClassloaderTestRunner extends BlockJUnit4ClassRunner {
public SeparateClassloaderTestRunner(Class<?> clazz) throws InitializationError {
super(getFromTestClassloader(clazz));
}
private static Class<?> getFromTestClassloader(Class<?> clazz) throws InitializationError {
try {
ClassLoader testClassLoader = new TestClassLoader();
return Class.forName(clazz.getName(), true, testClassLoader);
} catch (ClassNotFoundException e) {
throw new InitializationError(e);
}
}
public static class TestClassLoader extends URLClassLoader {
public TestClassLoader() {
super(((URLClassLoader)getSystemClassLoader()).getURLs());
}
#Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.startsWith("org.mypackages.")) {
return super.findClass(name);
}
return super.loadClass(name);
}
}
}
Note I had to do this to test code running in a legacy framework which I couldn't change. Given the choice I'd reduce the use of statics and/or put test hooks in to allow the system to be reset. It may not be pretty but it allows me to test an awful lot of code that would be difficult otherwise.
Also this solution breaks anything else that relies on classloading tricks such as Mockito.
When I run into these sort of situations I prefer to use what is a bit of a hack. I might instead expose a protected method such as reinitialize(), then invoke this from the test to effectively set the factory back to its initial state. This method only exists for the test cases, and I document it as such.
It is a bit of a hack, but it's a lot easier than other options and you won't need a 3rd party lib to do it (though if you prefer a cleaner solution, there probably are some kind of 3rd party tools out there you could use).
You can use Reflection to set myClassImpl by calling instantiateMyClass() again. Take a look at this answer to see example patterns for playing around with private methods and variables.
If executing Junit via the Ant task you can set fork=true to execute every class of tests in it's own JVM. Also put each test method in its own class and they will each load and initialise their own version of MyClass. It's extreme but very effective.
Below you can find a sample that does not need a separate JUnit test runner and works also with classloading tricks such as Mockito.
package com.mycompany.app;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.net.URLClassLoader;
import org.junit.Test;
public class ApplicationInSeparateClassLoaderTest {
#Test
public void testApplicationInSeparateClassLoader1() throws Exception {
testApplicationInSeparateClassLoader();
}
#Test
public void testApplicationInSeparateClassLoader2() throws Exception {
testApplicationInSeparateClassLoader();
}
private void testApplicationInSeparateClassLoader() throws Exception {
//run application code in separate class loader in order to isolate static state between test runs
Runnable runnable = mock(Runnable.class);
//set up your mock object expectations here, if needed
InterfaceToApplicationDependentCode tester = makeCodeToRunInSeparateClassLoader(
"com.mycompany.app", InterfaceToApplicationDependentCode.class, CodeToRunInApplicationClassLoader.class);
//if you want to try the code without class loader isolation, comment out above line and comment in the line below
//CodeToRunInApplicationClassLoader tester = new CodeToRunInApplicationClassLoaderImpl();
tester.testTheCode(runnable);
verify(runnable).run();
assertEquals("should be one invocation!", 1, tester.getNumOfInvocations());
}
/**
* Create a new class loader for loading application-dependent code and return an instance of that.
*/
#SuppressWarnings("unchecked")
private <I, T> I makeCodeToRunInSeparateClassLoader(
String packageName, Class<I> testCodeInterfaceClass, Class<T> testCodeImplClass) throws Exception {
TestApplicationClassLoader cl = new TestApplicationClassLoader(
packageName, getClass(), testCodeInterfaceClass);
Class<?> testerClass = cl.loadClass(testCodeImplClass.getName());
return (I) testerClass.newInstance();
}
/**
* Bridge interface, implemented by code that should be run in application class loader.
* This interface is loaded by the same class loader as the unit test class, so
* we can call the application-dependent code without need for reflection.
*/
public static interface InterfaceToApplicationDependentCode {
void testTheCode(Runnable run);
int getNumOfInvocations();
}
/**
* Test-specific code to call application-dependent code. This class is loaded by
* the same class loader as the application code.
*/
public static class CodeToRunInApplicationClassLoader implements InterfaceToApplicationDependentCode {
private static int numOfInvocations = 0;
#Override
public void testTheCode(Runnable runnable) {
numOfInvocations++;
runnable.run();
}
#Override
public int getNumOfInvocations() {
return numOfInvocations;
}
}
/**
* Loads application classes in separate class loader from test classes.
*/
private static class TestApplicationClassLoader extends URLClassLoader {
private final String appPackage;
private final String mainTestClassName;
private final String[] testSupportClassNames;
public TestApplicationClassLoader(String appPackage, Class<?> mainTestClass, Class<?>... testSupportClasses) {
super(((URLClassLoader) getSystemClassLoader()).getURLs());
this.appPackage = appPackage;
this.mainTestClassName = mainTestClass.getName();
this.testSupportClassNames = convertClassesToStrings(testSupportClasses);
}
private String[] convertClassesToStrings(Class<?>[] classes) {
String[] results = new String[classes.length];
for (int i = 0; i < classes.length; i++) {
results[i] = classes[i].getName();
}
return results;
}
#Override
public Class<?> loadClass(String className) throws ClassNotFoundException {
if (isApplicationClass(className)) {
//look for class only in local class loader
return super.findClass(className);
}
//look for class in parent class loader first and only then in local class loader
return super.loadClass(className);
}
private boolean isApplicationClass(String className) {
if (mainTestClassName.equals(className)) {
return false;
}
for (int i = 0; i < testSupportClassNames.length; i++) {
if (testSupportClassNames[i].equals(className)) {
return false;
}
}
return className.startsWith(appPackage);
}
}
}

Categories