Skip to content

Commit 1f0813a

Browse files
committed
Fixed provide order mapping in injector (1.8.2)
- made more descriptive error messages
1 parent cce7a7b commit 1f0813a

14 files changed

Lines changed: 163 additions & 41 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
/target
22
.idea
3+
server.yaml
4+
*/dependency-reduced-pom.xml

client/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<parent>
77
<groupId>cat.michal.catbase</groupId>
88
<artifactId>CatBase</artifactId>
9-
<version>1.8.1-fix</version>
9+
<version>1.8.2</version>
1010
</parent>
1111

1212
<groupId>cat.michal.catbase.client</groupId>

common/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<parent>
77
<groupId>cat.michal.catbase</groupId>
88
<artifactId>CatBase</artifactId>
9-
<version>1.8.1-fix</version>
9+
<version>1.8.2</version>
1010
</parent>
1111

1212
<groupId>cat.michal.catbase.common</groupId>

dependencyInjection/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<parent>
77
<groupId>cat.michal.catbase</groupId>
88
<artifactId>CatBase</artifactId>
9-
<version>1.8.1-fix</version>
9+
<version>1.8.2</version>
1010
</parent>
1111

1212
<groupId>cat.michal.catbase.injector</groupId>

dependencyInjection/src/main/java/cat/michal/catbase/injector/CatBaseInjector.java

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.lang.reflect.Method;
99
import java.lang.reflect.Modifier;
1010
import java.util.*;
11+
import java.util.stream.Collectors;
1112

1213
public class CatBaseInjector implements Injector {
1314
private final List<Dependency<?>> dependencies;
@@ -79,7 +80,7 @@ public void destroy() {
7980
@SuppressWarnings("unchecked")
8081
public <T> T createInstance(Class<T> clazz) {
8182
if(clazz.isEnum()) {
82-
throw new InjectorException("Enums are not allowed");
83+
throw new InjectorException("Enums (" + clazz.getSimpleName() + ") are not supported");
8384
}
8485

8586
if(clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) {
@@ -88,7 +89,7 @@ public <T> T createInstance(Class<T> clazz) {
8889
.map(classElement -> (Class<? extends T>) classElement)
8990
.toList();
9091
if(implementations.isEmpty()) {
91-
throw new InjectorException("No implementation found for abstraction layer " + clazz.getName());
92+
throw new InjectorException("No matching dependency implementation for " + clazz.getName());
9293
}
9394

9495
//if only one implementation - no need to look for primary implementations
@@ -99,10 +100,10 @@ public <T> T createInstance(Class<T> clazz) {
99100
.filter(injectable -> injectable.isAnnotationPresent(Primary.class))
100101
.toList();
101102
if(primaryImplementations.size() > 1) {
102-
throw new InjectorException("More than one primary implementation of abstraction layer " + clazz.getName());
103+
throw new InjectorException("More than one primary implementation for dependency " + clazz.getName());
103104
}
104105
if(primaryImplementations.isEmpty()) {
105-
throw new InjectorException("No primary implementation found for abstraction layer " + clazz.getName());
106+
throw new InjectorException("No primary implementation found for dependency " + clazz.getName());
106107
}
107108
return createInstance(primaryImplementations.get(0));
108109
}
@@ -149,24 +150,46 @@ public void registerInjectables() {
149150
.filter(method -> method.isAnnotationPresent(Provide.class))
150151
.forEach(method -> {
151152
if(method.getReturnType().isAnnotationPresent(Component.class)) {
152-
throw new InjectorException("Provide methods annotated with @Component are not allowed");
153+
throw new InjectorException("Provide method types annotated with @Component are not allowed");
153154
}
154155
registerDependency(method.getReturnType(), method, clazz);
155156
});
156157
});
157158

159+
//check for nested dependencies for provided instances
160+
dependencies.stream().filter(dependency -> dependency.getProvideMethod() != null).forEach(dependency -> {
161+
Dependency<?> containingDependency = dependencies.stream()
162+
.filter(containingClass -> containingClass.getClazz().equals(dependency.getContainingClass()))
163+
.findFirst()
164+
.orElseThrow(() -> new InjectorException("Dependency " + dependency.getContainingClass().getName() + " not found"));
165+
dependency.addDependency(containingDependency);
166+
});
158167
//check for nested dependencies
159168
dependencies.stream()
160169
// filter only dependencies that are needed for the dependency tree
161170
.filter(dependency -> dependency.getProvideMethod() == null && dependency.getInstance() == null)
162-
.forEach(dependency -> Arrays.stream(dependency.getClazz().getDeclaredFields())
163-
.filter(field -> !field.isAnnotationPresent(Exclude.class))
164-
.forEach(field -> {
165-
Class<?> fieldType = field.getType();
166-
dependencies.stream()
167-
.filter(dep -> fieldType.isAssignableFrom(dep.getClazz()))
168-
.findFirst().ifPresent(dependency::addDependency);
169-
}));
171+
.forEach(dependency -> {
172+
Constructor<?> validConstructor = getConstructor(dependency);
173+
Arrays.stream(validConstructor.getParameterTypes())
174+
.forEach(type -> {
175+
dependencies.stream()
176+
.filter(dep -> type.isAssignableFrom(dep.getClazz()))
177+
.findFirst()
178+
.ifPresent(dependency::addDependency);
179+
});
180+
181+
182+
183+
Arrays.stream(dependency.getClazz().getDeclaredFields())
184+
.filter(field -> field.isAnnotationPresent(Inject.class))
185+
.forEach(field -> {
186+
Class<?> fieldType = field.getType();
187+
dependencies.stream()
188+
.filter(dep -> fieldType.isAssignableFrom(dep.getClazz()))
189+
.findFirst()
190+
.ifPresent(dependency::addDependency);
191+
});
192+
});
170193

171194
//check for circular dependencies
172195
Set<Dependency<?>> visited = new HashSet<>();
@@ -207,6 +230,19 @@ public void registerInjectables() {
207230
});
208231
}
209232

233+
@SuppressWarnings("unchecked")
234+
private <T> Constructor<T> getConstructor(Dependency<T> dependency) {
235+
return (Constructor<T>) Arrays.stream(dependency.getClazz().getConstructors())
236+
.filter(constructorElement -> constructorElement.getParameterTypes().length == 0
237+
|| Arrays.stream(constructorElement.getParameterTypes())
238+
.allMatch(element ->
239+
containsByClass(element) || (element.isInterface() && element.isAnnotationPresent(Component.class))
240+
)
241+
)
242+
.findAny()
243+
.orElseThrow(() -> new InjectorException("No constructors with valid parameters found for " + dependency.getClazz().getName()));
244+
}
245+
210246
private void initializeDependency(Dependency<?> dependency, List<Dependency<?>> sortedDependencies) {
211247
if (!sortedDependencies.contains(dependency)) {
212248
dependency.getDepends().forEach(dep -> initializeDependency(dep, sortedDependencies));
@@ -216,7 +252,7 @@ private void initializeDependency(Dependency<?> dependency, List<Dependency<?>>
216252

217253
private void checkCircularDependency(Dependency<?> dependency, Set<Dependency<?>> visited, Set<Dependency<?>> stack) {
218254
if (stack.contains(dependency)) {
219-
throw new InjectorException("Circular dependency detected involving " + dependency.getClazz());
255+
throw new InjectorException("Circular dependency detected involving " + stack.stream().map(clazz -> clazz.getClass().getSimpleName()).collect(Collectors.joining()));
220256
}
221257
if (!visited.contains(dependency)) {
222258
visited.add(dependency);
@@ -245,15 +281,15 @@ public <T> void injectField(T instance) {
245281
return dependency.getClazz().equals(field.getType()) || (!injectName.isEmpty() && injectName.equals(dependency.getName()));
246282
})
247283
.findFirst()
248-
.orElseThrow(() -> new InjectorException("Could not find injectable for field " + field.getName()))
284+
.orElseThrow(() -> new InjectorException("Could not find dependency of type + " + field.getType().getSimpleName() + " for field " + instance.getClass().getSimpleName() + "." + field.getName()))
249285
.getInstance();
250286
if(toInject.getClass().equals(instance.getClass())) {
251-
throw new InjectorException("Could not inject field " + field.getName() + " into itself");
287+
throw new InjectorException("Could not inject field " + instance.getClass().getSimpleName() + "." + field.getName() + " into itself");
252288
}
253289
field.setAccessible(true);
254290
field.set(instance, toInject);
255291
} catch (IllegalAccessException e) {
256-
throw new InjectorException("Could not inject field " + field.getName(), e);
292+
throw new InjectorException("Could not inject field " + instance.getClass().getSimpleName() + "." + field.getName(), e);
257293
}
258294
});
259295
}
@@ -299,7 +335,7 @@ public <T> T getInstance(Class<T> clazz) {
299335
return ((T) dependencies.stream()
300336
.filter(dependency -> dependency.getClazz().equals(clazz))
301337
.findFirst()
302-
.orElseThrow(() -> new InjectorException("Could not find injectable for class " + clazz.getName()))
338+
.orElseThrow(() -> new InjectorException("Could not find dependency of type " + clazz.getName()))
303339
.getInstance());
304340
}
305341

@@ -308,29 +344,23 @@ public void clearInjectables() {
308344
this.dependencies.clear();
309345
}
310346

311-
312-
@SuppressWarnings("unchecked")
313347
private <T> T instantiate(Dependency<T> dependency) {
314-
Constructor<T> constructor = (Constructor<T>) Arrays.stream(dependency.getClazz().getConstructors())
315-
.filter(constructorElement -> constructorElement.getParameterTypes().length == 0 || Arrays.stream(constructorElement.getParameterTypes())
316-
.allMatch(element -> containsByClass(element) || (element.isInterface() && element.isAnnotationPresent(Component.class)))
317-
)
318-
.findAny()
319-
.orElseThrow(() -> new InjectorException("No matching constructor found for class " + dependency.getClazz().getName()));
348+
Constructor<T> constructor = getConstructor(dependency);
349+
320350
try {
321351
if(constructor.getParameterCount() == 0) {
322352
return constructor.newInstance();
323353
}
324354

325355
return constructor.newInstance(Arrays.stream(constructor.getParameterTypes()).map(this::getInstance).toArray());
326356
} catch (Exception exception) {
327-
throw new InjectorException("Could not instantiate class " + dependency.getClazz().getName(), exception);
357+
throw new InjectorException("Could not create instance of class " + dependency.getClazz().getName(), exception);
328358
}
329359
}
330360

331361
private <T> void registerDependency(Class<T> clazz, Method provideMethod, Class<?> containingClass) {
332362
if(this.containsByClass(clazz)) {
333-
throw new InjectorException("Class " + clazz.getName() + " is already registered");
363+
throw new InjectorException("Duplicate dependency for type " + clazz.getName());
334364
}
335365

336366
String injectableName;
@@ -342,7 +372,7 @@ private <T> void registerDependency(Class<T> clazz, Method provideMethod, Class<
342372
}
343373

344374
if(containsByName(injectableName)) {
345-
throw new InjectorException("Class with component name " + injectableName + " is already registered");
375+
throw new InjectorException("Duplicate component name " + injectableName);
346376
}
347377
Dependency<T> dependency = new Dependency<>(injectableName, clazz, null, provideMethod, containingClass);
348378
this.dependencies.add(dependency);

dependencyInjection/src/main/java/cat/michal/catbase/injector/Dependency.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package cat.michal.catbase.injector;
22

33
import java.lang.reflect.Method;
4-
import java.util.ArrayList;
5-
import java.util.List;
4+
import java.util.HashSet;
5+
import java.util.Set;
66

77
/**
88
*
@@ -17,7 +17,7 @@ public class Dependency<T> {
1717

1818
private final String name;
1919
private final Class<T> clazz;
20-
private final List<Dependency<?>> depends;
20+
private final Set<Dependency<?>> depends;
2121
private T instance;
2222
private final Method provideMethod;
2323
private final Class<?> containingClass;
@@ -27,7 +27,7 @@ public Dependency(String name, Class<T> clazz, T instance, Method provideMethod,
2727
this.clazz = clazz;
2828
this.instance = instance;
2929
this.provideMethod = provideMethod;
30-
this.depends = new ArrayList<>();
30+
this.depends = new HashSet<>();
3131
this.containingClass = containingClass;
3232
}
3333

@@ -47,7 +47,7 @@ public <D> void addDependency(Dependency<D> dependency) {
4747
this.depends.add(dependency);
4848
}
4949

50-
public List<Dependency<?>> getDepends() {
50+
public Set<Dependency<?>> getDepends() {
5151
return depends;
5252
}
5353

@@ -63,4 +63,12 @@ public String getName() {
6363
public void setInstance(Object instance) {
6464
this.instance = (T) instance;
6565
}
66+
67+
@Override
68+
public String toString() {
69+
return "Dependency{" +
70+
"class=" + clazz.getSimpleName() +
71+
", name='" + name + '\'' +
72+
'}';
73+
}
6674
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package cat.michal.catbase.injector;
2+
3+
import cat.michal.catbase.injector.mixedTest.MapperDependant;
4+
import cat.michal.catbase.injector.mixedTest.OuterDependency;
5+
import org.junit.jupiter.api.Assertions;
6+
import org.junit.jupiter.api.BeforeAll;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.TestInstance;
9+
10+
import java.util.List;
11+
import java.util.logging.Logger;
12+
13+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
14+
public class MixedTest {
15+
private CatBaseInjector injector;
16+
17+
@BeforeAll
18+
void setup() {
19+
this.injector = new CatBaseInjector(List.of(
20+
CatBaseInjector.createExternalDependency(String::toUpperCase, OuterDependency.class),
21+
CatBaseInjector.createExternalDependency(Logger.getLogger("Logger"), Logger.class)
22+
),
23+
"cat.michal.catbase.injector.mixedTest");
24+
}
25+
26+
@Test
27+
void testMixed() {
28+
Assertions.assertEquals("VALUE", this.injector.getInstance(MapperDependant.class).invoke());
29+
}
30+
31+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package cat.michal.catbase.injector.mixedTest;
2+
3+
public interface Mapper {
4+
String map(String value);
5+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package cat.michal.catbase.injector.mixedTest;
2+
3+
import cat.michal.catbase.injector.annotations.Component;
4+
5+
@Component
6+
public class MapperDependant {
7+
private final Mapper mapper;
8+
public MapperDependant(Mapper mapper) {
9+
this.mapper = mapper;
10+
}
11+
12+
public String invoke() {
13+
return mapper.map("value");
14+
}
15+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package cat.michal.catbase.injector.mixedTest;
2+
3+
import cat.michal.catbase.injector.annotations.Component;
4+
import cat.michal.catbase.injector.annotations.Provide;
5+
6+
import java.util.logging.Level;
7+
import java.util.logging.Logger;
8+
9+
@Component
10+
public class MapperFactory {
11+
private Logger logger;
12+
13+
public MapperFactory(Logger logger) {
14+
this.logger = logger;
15+
}
16+
17+
@Provide
18+
@SuppressWarnings("unused")
19+
public Mapper createMapper(OuterDependency outerDependency) {
20+
return value -> {
21+
logger.log(Level.INFO, "test");
22+
return outerDependency.consume(value);
23+
};
24+
}
25+
}

0 commit comments

Comments
 (0)