Reflection
A simple dependency injector.
The intent of this kumite is to gradually add features and tests, such that eventually a kata (or series of kata to separate out the features) can be made.
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.stream.*;
public class Injector {
private final Collection<?> dependencies;
public Injector(Collection<?> dependencies) {
this.dependencies = dependencies;
}
public <T> T createInstance(Constructor<T> constructor)
throws InvocationTargetException, InstantiationException, IllegalAccessException {
final Class<?>[] paramTypes = constructor.getParameterTypes();
final Object[] params = new Object[paramTypes.length];
for (int i = 0; i < params.length; i++) {
for(Object dep : dependencies) {
if(paramTypes[i].isInstance(dep)) {
if(params[i] == null) {
params[i] = dep;
} else {
throw new UnclearDependencyException(paramTypes[i], List.of(params[i], dep));
}
}
}
if(params[i] == null) throw new MissingDependencyException(paramTypes[i]);
}
return constructor.newInstance(params);
}
public <T> T createInstance(Class<T> clazz)
throws InstantiationException, InvocationTargetException, IllegalAccessException {
if ((clazz.getModifiers() & (Modifier.ABSTRACT | Modifier.INTERFACE)) != 0)
throw new IllegalArgumentException("Cannot instantiate " + clazz.getCanonicalName());
Constructor<?>[] constructors = clazz.getConstructors();
Constructor<?> constructor = null;
if (constructors.length == 1) {
constructor = constructors[0];
} else {
for (Constructor<?> con : constructors) {
if (con.getDeclaredAnnotation(Inject.class) != null) {
if (constructor == null) {
constructor = con;
} else {
throw new IllegalArgumentException(clazz.getCanonicalName() + " has multiple constructors annotated with @Inject");
}
}
}
}
if (constructor == null)
throw new IllegalArgumentException(clazz.getCanonicalName() + " has no viable constructor");
return clazz.cast(createInstance(constructor));
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
@interface Inject {
}
class MissingDependencyException extends RuntimeException {
public final Class<?> parameterType;
public MissingDependencyException(Class<?> parameterType) {
super("No dependency for type: " + parameterType.getCanonicalName());
this.parameterType = parameterType;
}
}
class UnclearDependencyException extends RuntimeException {
public final Class<?> parameterType;
public final Collection<?> possibleDependencies;
public UnclearDependencyException(Class<?> parameterType, Collection<?> possibleDependencies) {
super("Multiple options for dependency of type " + parameterType.getCanonicalName() + "\n" + possibleDependencies.stream()
.map(Object::toString)
.collect(Collectors.joining("\n")));
this.parameterType = parameterType;
this.possibleDependencies = Collections.unmodifiableCollection(possibleDependencies);
}
}
import org.junit.jupiter.api.Test;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
public class InjectionTest {
@Test
public void testObject()
throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
Injector injector = new Injector(Collections.emptyList());
Object created = injector.createInstance(Object.class);
assertNotNull(created);
assertEquals(created.getClass(), Object.class);
}
public static class CorrectConstructor {
public final String correct;
@Inject
public CorrectConstructor() {
correct = "yes";
}
public CorrectConstructor(String val) {
this.correct = val;
}
}
@Test
public void testCorrectConstructor() throws InvocationTargetException, InstantiationException, IllegalAccessException {
Injector injector = new Injector(Collections.singletonList("no"));
CorrectConstructor created = injector.createInstance(CorrectConstructor.class);
assertNotNull(created);
assertEquals(created.getClass(), CorrectConstructor.class);
assertEquals(created.correct, "yes");
}
public static class CorrectConstructor2 {
public final String correct;
public CorrectConstructor2() {
correct = "yes";
}
@Inject
public CorrectConstructor2(String val) {
this.correct = val;
}
}
@Test
public void testCorrectConstructor2() throws InvocationTargetException, InstantiationException, IllegalAccessException {
Injector injector = new Injector(Collections.singletonList("no"));
CorrectConstructor2 created = injector.createInstance(CorrectConstructor2.class);
assertNotNull(created);
assertEquals(created.getClass(), CorrectConstructor2.class);
assertEquals(created.correct, "no");
}
public record DependencyThread(Thread thread) {
public DependencyThread(Runnable runnable) {
this(runnable instanceof Thread t ? t :new Thread(runnable));
}
}
@Test
public void testMissingDependencyException()
throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
Injector injector = new Injector(List.of("no", (Runnable)() -> {}, Injector.class, 1, 2.5));
try{
injector.createInstance(DependencyThread.class.getConstructor(Thread.class));
fail("Did not throw exception");
} catch(MissingDependencyException ex) {
assertEquals(Thread.class, ex.parameterType);
}
}
@Test
public void testUnclearDependencyException()
throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
final List<?> dependencies = List.of((Runnable)() -> {}, Thread.currentThread());
Injector injector = new Injector(dependencies);
try{
injector.createInstance(DependencyThread.class.getConstructor(Runnable.class));
fail("Did not throw exception");
} catch(UnclearDependencyException ex) {
assertEquals(Runnable.class, ex.parameterType);
assertEquals(Set.copyOf(dependencies), Set.copyOf(ex.possibleDependencies));
}
}
@Test
public void testSingleParameterConstructor()
throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Injector injector = new Injector(List.of((Runnable)() -> {}, Thread.currentThread()));
DependencyThread created = injector.createInstance(DependencyThread.class.getConstructor(Thread.class));
assertNotNull(created);
assertEquals(DependencyThread.class, created.getClass());
assertSame(Thread.currentThread(), created.thread());
}
}