Skip to content

Commit 7633577

Browse files
committed
Rewritten dependency injection lib (1.9)
1 parent 9ff1b8b commit 7633577

35 files changed

Lines changed: 590 additions & 337 deletions

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.3</version>
9+
<version>1.9</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.3</version>
9+
<version>1.9</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.3</version>
9+
<version>1.9</version>
1010
</parent>
1111

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

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

Lines changed: 40 additions & 287 deletions
Large diffs are not rendered by default.

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,12 @@ public class Dependency<T> {
2020
private final Set<Dependency<?>> depends;
2121
private T instance;
2222
private final Method provideMethod;
23-
private final Class<?> containingClass;
2423

25-
public Dependency(String name, Class<T> clazz, T instance, Method provideMethod, Class<?> containingClass) {
24+
public Dependency(String name, Class<T> clazz, Method provideMethod) {
2625
this.name = name;
2726
this.clazz = clazz;
28-
this.instance = instance;
2927
this.provideMethod = provideMethod;
3028
this.depends = new HashSet<>();
31-
this.containingClass = containingClass;
3229
}
3330

3431
public Class<?> getClazz() {
@@ -39,10 +36,6 @@ public Method getProvideMethod() {
3936
return provideMethod;
4037
}
4138

42-
public Class<?> getContainingClass() {
43-
return containingClass;
44-
}
45-
4639
public <D> void addDependency(Dependency<D> dependency) {
4740
this.depends.add(dependency);
4841
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package cat.michal.catbase.injector;
2+
3+
import cat.michal.catbase.injector.annotations.*;
4+
import cat.michal.catbase.injector.exceptions.InjectorException;
5+
import org.jetbrains.annotations.Nullable;
6+
7+
import java.lang.reflect.*;
8+
import java.util.*;
9+
10+
public class InjectionContext {
11+
private final List<Class<?>> classes;
12+
private final List<Dependency<?>> dependencies;
13+
14+
public InjectionContext(Collection<String> packagePaths, ClassLoader classLoader) {
15+
this.classes = new ArrayList<>();
16+
this.dependencies = new ArrayList<>();
17+
ClassFinder classFinder = classLoader == null ? new ClassFinder() : new ClassFinder(classLoader);
18+
19+
packagePaths.forEach(packagePath -> this.classes.addAll(classFinder.findAllClasses(packagePath)));
20+
}
21+
22+
public void createDependencyTree() {
23+
this.classes.stream()
24+
.filter(clazz -> clazz.isAnnotationPresent(Component.class))
25+
.filter(clazz -> !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()))
26+
.forEach(clazz -> {
27+
this.dependencies.add(createDependency(clazz, null, null));
28+
Arrays.stream(clazz.getMethods())
29+
.filter(method -> method.isAnnotationPresent(Provide.class))
30+
.forEach(method -> this.dependencies.add(createDependency(method.getReturnType(), method, method.getAnnotation(Provide.class).value())));
31+
32+
// Adding external dependencies
33+
Arrays.stream(clazz.getFields())
34+
.filter(field -> Modifier.isStatic(field.getModifiers()))
35+
.filter(field -> field.isAnnotationPresent(ExternalDependency.class))
36+
.forEach(field -> {
37+
Dependency<?> dependency = createDependency(field.getType(), null, field.getAnnotation(ExternalDependency.class).value());
38+
field.setAccessible(true);
39+
try {
40+
dependency.setInstance(field.get(null));
41+
} catch (IllegalAccessException e) {
42+
throw new InjectorException("Could not get value of external dependency " + field.getName(), e);
43+
}
44+
this.dependencies.add(dependency);
45+
});
46+
});
47+
}
48+
49+
public List<Dependency<?>> getDependencies() {
50+
return this.dependencies;
51+
}
52+
53+
/**
54+
* Method that finds the best corresponding dependency to provided class
55+
* If user provides an abstraction layer, it automatically matches it and tries to find a primary or a named implementation of it
56+
* @param clazz class your want to find dependency to
57+
* @return none if there is no dependency connected with your class
58+
* @param <T> class type
59+
*
60+
*/
61+
@SuppressWarnings("unchecked")
62+
public <T> Optional<Dependency<T>> findBestMatchingDependency(Class<T> clazz, @Nullable String dependencyName) {
63+
// class is an abstraction layer
64+
if(clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) {
65+
List<Dependency<?>> matchingDependencies = this.dependencies.stream()
66+
.filter(dependency -> clazz.isAssignableFrom(dependency.getClazz()))
67+
.toList();
68+
if(matchingDependencies.isEmpty()) {
69+
throw new InjectorException("Could not find dependency for type " + clazz.getName());
70+
}
71+
if(matchingDependencies.size() > 1) {
72+
List<Dependency<?>> nameFiltered = matchingDependencies.stream()
73+
.filter(dependency -> (dependencyName == null || dependencyName.isEmpty()) || dependency.getName().equals(dependencyName))
74+
.toList();
75+
if(nameFiltered.isEmpty()) {
76+
throw new InjectorException("Could not find matching dependency for type "+ clazz.getName() + " with name " + dependencyName);
77+
}
78+
79+
if(nameFiltered.size() > 1) {
80+
List<Dependency<?>> primaryFiltered = nameFiltered.stream()
81+
.filter(dependency -> dependency.getClazz().isAnnotationPresent(Primary.class))
82+
.toList();
83+
if(primaryFiltered.isEmpty()) {
84+
throw new InjectorException("Could not find primary implementation for type "+ clazz.getName());
85+
}
86+
if(primaryFiltered.size() > 1) {
87+
throw new InjectorException("Multiple primary implementations found for type "+ clazz.getName());
88+
}
89+
return Optional.of((Dependency<T>) primaryFiltered.get(0));
90+
} else {
91+
return Optional.of((Dependency<T>) nameFiltered.get(0));
92+
}
93+
} else {
94+
return Optional.of((Dependency<T>) matchingDependencies.get(0));
95+
}
96+
}
97+
98+
return this.dependencies.stream()
99+
.filter(dependency -> dependency.getClazz().equals(clazz))
100+
.filter(dependency -> (dependencyName == null || dependencyName.isEmpty()) || dependency.getName().equals(dependencyName))
101+
.map(dependency -> (Dependency<T>) dependency)
102+
.findFirst();
103+
}
104+
105+
@SuppressWarnings("unchecked")
106+
<T> List<Dependency<T>> findAllDependsOfType(Type type, String dependencyName) {
107+
List<Dependency<T>> depends = new ArrayList<>();
108+
if(type instanceof ParameterizedType parameterizedType && Collection.class.isAssignableFrom(((Class<?>) parameterizedType.getRawType()))) {
109+
Type[] arguments = parameterizedType.getActualTypeArguments();
110+
if(arguments.length != 1 || !(arguments[0] instanceof Class<?> abstractionType)) {
111+
throw new InjectorException("Invalid generic parameters with the collection class " + type.getTypeName());
112+
}
113+
this.dependencies.stream()
114+
.filter(depend -> abstractionType.isAssignableFrom(depend.getClazz()))
115+
.forEach(depend -> depends.add((Dependency<T>) depend));
116+
} else if (type instanceof Class<?> clazz) {
117+
this.findBestMatchingDependency((Class<T>) clazz, dependencyName).ifPresent(depends::add);
118+
} else {
119+
throw new InjectorException("Dependency with type " + type + " is not a valid class or a collection");
120+
}
121+
122+
return depends;
123+
}
124+
125+
126+
@SuppressWarnings("unchecked")
127+
public <T> List<Dependency<T>> findDependants(Dependency<T> dependency) {
128+
List<Dependency<T>> dependants = new ArrayList<>();
129+
130+
Optional<? extends Constructor<?>> validConstructor = getConstructor(dependency);
131+
validConstructor.ifPresent(constructor -> Arrays.stream(constructor.getAnnotatedParameterTypes())
132+
.forEach(type -> {
133+
Inject inject = type.getAnnotation(Inject.class);
134+
135+
this.findAllDependsOfType(type.getType(), inject == null ? null : inject.value())
136+
.forEach(dep -> dependants.add((Dependency<T>) dep));
137+
}));
138+
139+
Arrays.stream(dependency.getClazz().getDeclaredFields())
140+
.filter(field -> field.isAnnotationPresent(Inject.class))
141+
.forEach(field -> this.findAllDependsOfType(field.getGenericType(), field.getAnnotation(Inject.class).value())
142+
.forEach(dep -> dependants.add((Dependency<T>) dep)));
143+
144+
return dependants;
145+
}
146+
147+
148+
@SuppressWarnings("unchecked")
149+
public <T> Optional<Dependency<T>> getDependency(Class<T> clazz) {
150+
return this.dependencies.stream()
151+
.filter(dependency -> dependency.getClazz().equals(clazz))
152+
.map(dependency -> (Dependency<T>) dependency)
153+
.findFirst();
154+
}
155+
156+
private <T> Dependency<T> createDependency(Class<T> clazz, Method provideMethod, String dependencyName) {
157+
if(this.containsByClass(clazz)) {
158+
throw new InjectorException("Dependency with type " + clazz.getName() + " has been added to injector context twice.");
159+
}
160+
161+
if(dependencyName == null || dependencyName.isEmpty()) {
162+
if(provideMethod == null) {
163+
dependencyName = clazz.getAnnotation(Component.class) == null ? clazz.getSimpleName() : clazz.getAnnotation(Component.class).value();
164+
} else {
165+
String provideName = provideMethod.getAnnotation(Provide.class).value();
166+
dependencyName = provideName.isEmpty() ? provideMethod.getName() : provideName;
167+
}
168+
}
169+
170+
Optional<Dependency<?>> depend = findByName(dependencyName);
171+
if(depend.isPresent()) {
172+
throw new InjectorException("Dependency with name '" + dependencyName + "' already exists (" + depend.get().getClass().getSimpleName() + ")");
173+
}
174+
return new Dependency<>(dependencyName, clazz, provideMethod);
175+
}
176+
177+
178+
private <T> boolean containsByClass(Class<T> clazz) {
179+
return this.dependencies.stream()
180+
.anyMatch(dependency -> dependency.getClazz().equals(clazz));
181+
}
182+
183+
private Optional<Dependency<?>> findByName(String injectableName) {
184+
if(injectableName.isEmpty()) {
185+
return Optional.empty();
186+
}
187+
188+
return this.dependencies.stream()
189+
.filter(dependency -> dependency.getName().equals(injectableName))
190+
.findAny();
191+
}
192+
193+
194+
@SuppressWarnings("unchecked")
195+
<T> Optional<Constructor<T>> getConstructor(Dependency<T> dependency) {
196+
List<Constructor<?>> injectConstructors = Arrays.stream(dependency.getClazz().getConstructors())
197+
.filter(constructor -> constructor.isAnnotationPresent(Inject.class))
198+
.toList();
199+
if(injectConstructors.isEmpty()) {
200+
return Arrays.stream(dependency.getClazz().getConstructors())
201+
.filter(constructorElement -> constructorElement.getParameterTypes().length == 0
202+
|| Arrays.stream(constructorElement.getGenericParameterTypes()).allMatch(this::isValidInjectableType)
203+
)
204+
.map(constructor -> (Constructor<T>) constructor)
205+
.findAny();
206+
} else {
207+
return Optional.of((Constructor<T>) injectConstructors.get(0));
208+
}
209+
}
210+
211+
private boolean isValidInjectableType(Type type) {
212+
if(type instanceof ParameterizedType parameterType && Collection.class.isAssignableFrom(((Class<?>) parameterType.getRawType()))) {
213+
Type[] actualTypeArguments = parameterType.getActualTypeArguments();
214+
if (actualTypeArguments.length != 1 || !(actualTypeArguments[0] instanceof Class)) {
215+
return false;
216+
}
217+
218+
return this.isValidInjectable((Class<?>) actualTypeArguments[0]);
219+
}
220+
if (!(type instanceof Class<?> clazz)) {
221+
return false;
222+
}
223+
224+
return this.isValidInjectable(clazz);
225+
}
226+
227+
private boolean isValidInjectable(Class<?> clazz) {
228+
return this.findBestMatchingDependency(clazz, null).isPresent();
229+
}
230+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package cat.michal.catbase.injector;
2+
3+
import cat.michal.catbase.injector.annotations.Inject;
4+
import cat.michal.catbase.injector.annotations.PostConstruct;
5+
import cat.michal.catbase.injector.annotations.PreDestroy;
6+
import cat.michal.catbase.injector.exceptions.InjectorException;
7+
8+
import java.lang.reflect.Modifier;
9+
import java.lang.reflect.ParameterizedType;
10+
import java.lang.reflect.Type;
11+
import java.util.Arrays;
12+
import java.util.Collection;
13+
14+
class InjectionExecutor {
15+
private final InjectionContext injectionContext;
16+
private final CatBaseInjector injector;
17+
18+
public InjectionExecutor(InjectionContext injectionContext, CatBaseInjector injector) {
19+
this.injectionContext = injectionContext;
20+
this.injector = injector;
21+
}
22+
23+
void invokePostConstruct(Dependency<?> dependency) {
24+
Arrays.stream(dependency.getClazz().getMethods())
25+
.filter(method -> method.isAnnotationPresent(PostConstruct.class))
26+
.findAny()
27+
.ifPresent(method -> {
28+
try {
29+
method.invoke(dependency.getInstance());
30+
} catch (Exception e) {
31+
throw new InjectorException("Error while invoking post construct method " + method.getName(), e);
32+
}
33+
});
34+
}
35+
36+
void invokePreDestroy(Dependency<?> dependency) {
37+
Arrays.stream(dependency.getClazz().getMethods())
38+
.filter(method -> method.isAnnotationPresent(PreDestroy.class))
39+
.findAny()
40+
.ifPresent(method -> {
41+
try {
42+
method.invoke(dependency.getInstance());
43+
} catch (Exception e) {
44+
throw new InjectorException("Error while invoking pre destroy method " + method.getName(), e);
45+
}
46+
});
47+
}
48+
49+
<T> void injectField(T instance) {
50+
Arrays.stream(instance.getClass().getDeclaredFields())
51+
.filter(field -> field.isAnnotationPresent(Inject.class))
52+
.filter(field -> !Modifier.isFinal(field.getModifiers()))
53+
.forEach(field -> {
54+
try {
55+
String injectName = field.getAnnotation(Inject.class).value();
56+
57+
58+
field.setAccessible(true);
59+
field.set(instance, getValueToInject(field.getGenericType(), injectName));
60+
} catch (InjectorException e) {
61+
throw new InjectorException("Field " + field.getName() + " annotated with @Inject even though its type is not a dependency.", e);
62+
} catch (IllegalAccessException e) {
63+
throw new InjectorException("Could not inject field " + instance.getClass().getSimpleName() + "." + field.getName(), e);
64+
}
65+
});
66+
}
67+
68+
Object getValueToInject(Type type, String dependencyName) {
69+
if(type instanceof ParameterizedType parameterType && Collection.class.isAssignableFrom(((Class<?>) parameterType.getRawType()))) {
70+
Type[] actualTypeArguments = parameterType.getActualTypeArguments();
71+
if (actualTypeArguments.length != 1 || !(actualTypeArguments[0] instanceof Class)) {
72+
throw new InjectorException("Invalid type to inject " + type.getTypeName() + ". Expected a Class but got " + actualTypeArguments[0]);
73+
}
74+
75+
return this.injector.getInstancesOfType((Class<?>) actualTypeArguments[0]);
76+
}
77+
if (!(type instanceof Class<?> clazz)) {
78+
throw new InjectorException("Invalid type to inject " + type.getTypeName() + ". Expected a Class but got " + type);
79+
}
80+
81+
return this.injectionContext.findBestMatchingDependency(clazz, dependencyName)
82+
.orElseThrow(() -> new InjectorException("Type " + clazz.getName() + " is not a dependency"))
83+
.getInstance();
84+
}
85+
}

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,6 @@ public interface Injector {
3737
*/
3838
void registerInjectables();
3939

40-
/**
41-
* Method that injects registered injectable components to fields of the given instance
42-
*
43-
* @param instance class which fields will be injected
44-
* @param <T> instance type
45-
*/
46-
<T> void injectField(T instance);
4740

4841
/**
4942
* Method that returns all implementations of provided abstraction layer
@@ -57,9 +50,12 @@ public interface Injector {
5750
* Method that returns instance of an injectable component from dependency injection container
5851
*
5952
* @param clazz class type that will be returned
53+
* @param dependencyName name of the dependency you want to inject
6054
* @return instance from dependency injection container
6155
* @param <T> is type that will be returned
6256
*/
57+
<T> T getInstance(Class<T> clazz, String dependencyName);
58+
6359
<T> T getInstance(Class<T> clazz);
6460

6561
/**

0 commit comments

Comments
 (0)