diff --git a/injection/src/main/java/de/neitzel/injection/ApplicationContext.java b/injection/src/main/java/de/neitzel/injection/ApplicationContext.java
index 3c7596e..7276394 100644
--- a/injection/src/main/java/de/neitzel/injection/ApplicationContext.java
+++ b/injection/src/main/java/de/neitzel/injection/ApplicationContext.java
@@ -3,9 +3,10 @@ package de.neitzel.injection;
import de.neitzel.injection.annotation.Config;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
-import java.util.stream.Stream;
/**
* Represents the context of the application and serves as the foundation of a dependency
@@ -79,43 +80,39 @@ public class ApplicationContext {
}
try {
- Constructor>[] constructors = data.getType().getConstructors();
- for (Constructor> constructor : constructors) {
- if (canBeInstantiated(constructor, components)) {
- Class>[] paramTypes = constructor.getParameterTypes();
- Object[] parameters = new Object[paramTypes.length];
- for (int i = 0; i < paramTypes.length; i++) {
- parameters[i] = getComponent(paramTypes[i]);
- }
- instance = constructor.newInstance(parameters);
- if (scope == Scope.SINGLETON) {
- data.setInstance(instance);
- }
- return (T) instance;
- }
+ Constructor> injectionConstructor = data.getInjectionConstructor();
+ if (injectionConstructor == null) {
+ throw new IllegalStateException("No injection constructor resolved for " + type.getName());
}
- throw new IllegalStateException("Kein passender Konstruktor gefunden für " + type.getName());
+ Class>[] paramTypes = injectionConstructor.getParameterTypes();
+ Object[] parameters = new Object[paramTypes.length];
+ for (int i = 0; i < paramTypes.length; i++) {
+ parameters[i] = getComponent(paramTypes[i]);
+ }
+
+ instance = injectionConstructor.newInstance(parameters);
+
+ for (Field field : data.getInjectableFields()) {
+ field.setAccessible(true);
+ field.set(instance, getComponent(field.getType()));
+ }
+
+ for (Method setter : data.getInjectableSetters()) {
+ setter.setAccessible(true);
+ Class> dependencyType = setter.getParameterTypes()[0];
+ setter.invoke(instance, getComponent(dependencyType));
+ }
+
+ if (scope == Scope.SINGLETON) {
+ data.setInstance(instance);
+ }
+ return (T) instance;
} catch (Exception e) {
throw new RuntimeException("Fehler beim Erstellen der Instanz für " + type.getName(), e);
}
}
- /**
- * Determines if a given constructor can be instantiated using the provided parameter map.
- *
- * The method evaluates whether every parameter type required by the constructor
- * is available in the given parameter map. If all parameter types are present,
- * the constructor is considered instantiable.
- *
- * @param constructor the constructor to check for instantiation feasibility
- * @param parameterMap a map containing available parameter types and their associated component data
- * @return true if all parameter types required by the constructor are contained in the parameter map, false otherwise
- */
- private boolean canBeInstantiated(Constructor> constructor, Map, ComponentData> parameterMap) {
- return Stream.of(constructor.getParameterTypes()).allMatch(parameterMap::containsKey);
- }
-
/**
* Registers a singleton instance of a specific type into the application context.
*
diff --git a/injection/src/main/java/de/neitzel/injection/ComponentData.java b/injection/src/main/java/de/neitzel/injection/ComponentData.java
index c101976..a89c16c 100644
--- a/injection/src/main/java/de/neitzel/injection/ComponentData.java
+++ b/injection/src/main/java/de/neitzel/injection/ComponentData.java
@@ -2,6 +2,11 @@ package de.neitzel.injection;
import lombok.Getter;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.List;
+
/**
* Represents a component in a dependency injection framework.
*
@@ -33,6 +38,23 @@ public class ComponentData {
*/
private final Scope scope;
+ /**
+ * Defines the constructor the framework will use when instantiating this component.
+ * The constructor is determined during scanning based on @Inject annotations and availability rules.
+ */
+ private final Constructor> injectionConstructor;
+
+ /**
+ * Holds all fields annotated with {@code @Inject}. They are populated immediately after construction.
+ */
+ private final List injectableFields;
+
+ /**
+ * Contains all setter methods annotated with {@code @Inject}. These are invoked post-construction
+ * to perform setter-based dependency injection.
+ */
+ private final List injectableSetters;
+
/**
* Stores the instantiated object associated with the component.
*
@@ -44,17 +66,6 @@ public class ComponentData {
*/
private Object instance;
- /**
- * Constructs a new Component instance with the specified type and scope.
- *
- * @param type the class type of the component
- * @param scope the scope of the component, which determines its lifecycle
- */
- public ComponentData(Class> type, Scope scope) {
- this.type = type;
- this.scope = scope;
- }
-
/**
* Constructs a new ComponentData instance with the specified type and an initial instance.
*
@@ -64,11 +75,41 @@ public class ComponentData {
* @param instance the initial instance of the component
*/
public ComponentData(Class> type, Object instance) {
- this.type = type;
- this.scope = Scope.SINGLETON;
+ this(type, Scope.SINGLETON);
this.instance = instance;
}
+ /**
+ * Constructs a new Component instance with the specified type and scope.
+ *
+ * @param type the class type of the component
+ * @param scope the scope of the component, which determines its lifecycle
+ */
+ public ComponentData(Class> type, Scope scope) {
+ this(type, scope, null, List.of(), List.of());
+ }
+
+ /**
+ * Constructs a new ComponentData instance with the specified parameters.
+ *
+ * @param type the class type of the component
+ * @param scope the scope of the component, which determines its lifecycle
+ * @param injectionConstructor the constructor to be used for injection, if any
+ * @param injectableFields the fields to be injected, if any
+ * @param injectableSetters the setter methods to be used for injection, if any
+ */
+ public ComponentData(Class> type,
+ Scope scope,
+ Constructor> injectionConstructor,
+ List injectableFields,
+ List injectableSetters) {
+ this.type = type;
+ this.scope = scope;
+ this.injectionConstructor = injectionConstructor;
+ this.injectableFields = injectableFields == null ? List.of() : List.copyOf(injectableFields);
+ this.injectableSetters = injectableSetters == null ? List.of() : List.copyOf(injectableSetters);
+ }
+
/**
* Sets the instance for this component if it is configured with a {@code SINGLETON} scope.
* This method ensures that the instance is only set once for a {@code SINGLETON} component.
diff --git a/injection/src/main/java/de/neitzel/injection/ComponentScanner.java b/injection/src/main/java/de/neitzel/injection/ComponentScanner.java
index f058d9c..9d56eec 100644
--- a/injection/src/main/java/de/neitzel/injection/ComponentScanner.java
+++ b/injection/src/main/java/de/neitzel/injection/ComponentScanner.java
@@ -6,6 +6,8 @@ import jakarta.inject.Singleton;
import org.reflections.Reflections;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -13,13 +15,22 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.stream.Collectors;
/**
- * InjectableComponentScanner is responsible for scanning packages to detect classes annotated
- * with @FXMLComponent and analyzing their compatibility for instantiation and dependency injection.
- * The resulting analysis identifies unique and shared interfaces/superclasses as well as
- * potentially instantiable components, collecting relevant errors if instantiation is not feasible.
+ * Scans a base package for Jakarta CDI-style components and derives dependency-injection metadata.
+ *
+ * The scanner detects component classes annotated with {@link jakarta.inject.Singleton} or
+ * {@link jakarta.inject.Named}. It then:
+ *
+ * - Analyzes the discovered components' super types (interfaces and superclasses) to detect
+ * unique vs. ambiguous (non-unique) injectable types.
+ * - Determines an {@code InjectionPlan} for each component (constructor + field + setter injection).
+ * - Iteratively resolves which components are instantiable based on already-known resolvable types.
+ * - Collects errors for components that cannot be resolved or instantiated.
+ *
+ *
+ * The result is a mapping from injectable types to {@link ComponentData} describing how to create
+ * and inject the corresponding component.
*/
public class ComponentScanner {
@@ -71,20 +82,6 @@ public class ComponentScanner {
*/
private final Map, Class>> uniqueTypeToComponent = new HashMap<>();
- /**
- * A mapping of components that can be instantiated with their corresponding types.
- * This map links a component's class (key) to the specific implementation class (value)
- * that can be instantiated. The entries are resolved through the scanning and analysis
- * of available component types, ensuring that only components with resolvable
- * dependencies are included.
- *
- * This field is part of the dependency injection process, where components annotated
- * with {@code @Component} or similar annotations are scanned, analyzed, and registered.
- * The resolution process checks if a component can be instantiated based on its
- * constructor dependencies being resolvable using known types or other registered components.
- */
- private final Map, ComponentData> instantiableComponents = new HashMap<>();
-
/**
* A list of error messages encountered during the scanning and analysis
* of components in the dependency injection process.
@@ -98,6 +95,28 @@ public class ComponentScanner {
*/
private final List errors = new ArrayList<>();
+ /**
+ * Cache of computed {@link InjectionPlan}s per component class.
+ *
+ * Plans are derived once via reflection and then reused to avoid repeated expensive analysis.
+ */
+ private final Map, InjectionPlan> injectionPlans = new HashMap<>();
+
+ /**
+ * Tracks component classes that are known to be invalid (i.e., no usable injection plan).
+ *
+ * This prevents repeated analysis and also avoids emitting duplicate error messages.
+ */
+ private final Set> invalidComponents = new HashSet<>();
+
+ /**
+ * Final mapping of resolvable injectable types to their corresponding {@link ComponentData}.
+ *
+ * This map includes concrete component classes as keys and, where unique, their injectable
+ * super types (interfaces/superclasses) as additional keys.
+ */
+ private final Map, ComponentData> instantiableComponents = new HashMap<>();
+
/**
* Initializes a new instance of the {@code InjectableComponentScanner} class, which scans for injectable
* components within the specified base package and resolves the set of recognizable and instantiable components.
@@ -190,22 +209,32 @@ public class ComponentScanner {
Set> resolved = new HashSet<>();
Set> unresolved = new HashSet<>(components);
Map, ComponentData> knownTypes = new HashMap<>();
- Set> resolvableNow;
- do {
- resolvableNow = unresolved.stream()
- .filter(c -> canInstantiate(c, knownTypes.keySet()))
- .collect(Collectors.toSet());
+ while (true) {
+ Map, ComponentData> newlyResolvable = new HashMap<>();
- for (Class> clazz : resolvableNow) {
- ComponentData componentInfo = new ComponentData(clazz, Scope.SINGLETON);
+ for (Class> clazz : unresolved) {
+ if (invalidComponents.contains(clazz)) {
+ continue;
+ }
- resolved.add(clazz);
+ ComponentData componentInfo = createComponentData(clazz, knownTypes.keySet());
+ if (componentInfo != null) {
+ newlyResolvable.put(clazz, componentInfo);
+ }
+ }
+
+ if (newlyResolvable.isEmpty()) {
+ break;
+ }
+
+ for (ComponentData componentInfo : newlyResolvable.values()) {
+ resolved.add(componentInfo.getType());
registerComponentWithSuperTypes(componentInfo, knownTypes);
}
- unresolved.removeAll(resolvableNow);
- } while (!resolvableNow.isEmpty());
+ unresolved.removeAll(newlyResolvable.keySet());
+ }
instantiableComponents.putAll(knownTypes);
@@ -234,47 +263,34 @@ public class ComponentScanner {
}
/**
- * Determines whether the specified component class can be instantiated based on the provided set
- * of known types. A component is considered instantiable if it has at least one constructor where
- * all parameter types are contained in the known types set or if it has a no-argument constructor.
- * Additionally, all fields annotated with {@code @Inject} must have their types present in the
- * known types set.
+ * Attempts to create {@link ComponentData} for a component if an injection plan can be determined
+ * and all required dependencies are already known.
*
- * @param component the class to check for instantiability
- * @param knownTypes the set of currently known types that can be used to satisfy dependencies
- * @return {@code true} if the component can be instantiated; {@code false} otherwise
+ * @param component the concrete component class to analyze
+ * @param knownTypes the set of injectable types that are already resolvable in the current iteration
+ * @return component metadata if the component is currently instantiable; otherwise {@code null}
*/
- private boolean canInstantiate(Class> component, Set> knownTypes) {
- boolean hasValidConstructor = false;
-
- for (Constructor> constructor : component.getConstructors()) {
- Class>[] paramTypes = constructor.getParameterTypes();
- if (paramTypes.length == 0 || Arrays.stream(paramTypes).allMatch(knownTypes::contains)) {
- hasValidConstructor = true;
- break;
- }
+ private ComponentData createComponentData(Class> component, Set> knownTypes) {
+ InjectionPlan plan = getInjectionPlan(component);
+ if (plan == null) {
+ return null;
}
- if (!hasValidConstructor) {
- return false;
+ if (!areDependenciesKnown(plan, knownTypes)) {
+ return null;
}
- for (var field : component.getDeclaredFields()) {
- if (field.isAnnotationPresent(Inject.class) && !knownTypes.contains(field.getType())) {
- return false;
- }
- }
-
- return true;
+ return new ComponentData(component, Scope.SINGLETON, plan.constructor(), plan.fields(), plan.setters());
}
/**
- * Registers a component and its superclasses or interfaces in the provided map of known types.
- * This method ensures that the component and its inheritance hierarchy are associated with the
- * component's data unless the supertype is marked as non-unique.
+ * Registers an instantiable component under its concrete class and (if unique) under its
+ * injectable super types.
+ *
+ * Non-unique super types are intentionally skipped to prevent ambiguous dependency resolution.
*
- * @param component the {@code ComponentData} instance representing the component to be registered
- * @param knownTypes the map where component types and their data are stored
+ * @param component the instantiable component metadata
+ * @param knownTypes the mutable registry of resolvable injectable types for the resolution loop
*/
private void registerComponentWithSuperTypes(ComponentData component, Map, ComponentData> knownTypes) {
knownTypes.put(component.getType(), component);
@@ -287,102 +303,269 @@ public class ComponentScanner {
}
/**
- * Collects the instantiation errors for a set of unresolved classes and appends
- * detailed error messages to the internal error list. This method analyzes unresolved
- * components based on their constructors, determining if all required types are
- * instantiable or if there are conflicting types that could prevent instantiation.
+ * Builds human-readable error messages for components that could not be resolved/instantiated.
+ *
+ * Missing dependencies are computed from the component's {@link InjectionPlan}. If no missing
+ * dependencies can be identified, the component is assumed to be blocked by ambiguous (non-unique)
+ * super types or other conflicts.
*
- * @param unresolved the set of classes for which instantiation errors are collected
+ * @param unresolved set of component classes that remained unresolved after the resolution loop
*/
private void collectInstantiationErrors(Set> unresolved) {
for (Class> component : unresolved) {
- StringBuilder errorMsg = new StringBuilder("Component cannot be instantiated: " + component.getName());
+ if (invalidComponents.contains(component)) {
+ continue;
+ }
- boolean possibleWithUniqueTypes = false;
+ InjectionPlan plan = getInjectionPlan(component);
+ if (plan == null) {
+ continue;
+ }
- for (Constructor> constructor : component.getConstructors()) {
- Class>[] paramTypes = constructor.getParameterTypes();
- List> problematicTypes = Arrays.stream(paramTypes)
- .filter(t -> !instantiableComponents.containsKey(t) && !notUniqueTypes.contains(t))
- .toList();
-
- if (problematicTypes.isEmpty()) {
- possibleWithUniqueTypes = true;
- } else {
- errorMsg.append("\n ➤ Requires unknown types: ").append(problematicTypes);
+ List> missingConstructor = new ArrayList<>();
+ for (Class> parameterType : plan.constructor().getParameterTypes()) {
+ if (isUnknownDependency(parameterType)) {
+ missingConstructor.add(parameterType);
}
}
- if (possibleWithUniqueTypes) {
- errorMsg.append("\n ➤ Could be instantiated if multiple implementations of ")
- .append("interfaces/superclasses were resolved uniquely: ")
- .append(getConflictingTypes(component));
+ List> missingFields = new ArrayList<>();
+ for (Field field : plan.fields()) {
+ if (isUnknownDependency(field.getType())) {
+ missingFields.add(field.getType());
+ }
}
- errors.add(errorMsg.toString());
+ List> missingSetters = new ArrayList<>();
+ for (Method setter : plan.setters()) {
+ Class> dependencyType = setter.getParameterTypes()[0];
+ if (isUnknownDependency(dependencyType)) {
+ missingSetters.add(dependencyType);
+ }
+ }
+
+ StringBuilder errorMessage = new StringBuilder("Component cannot be instantiated: ");
+ errorMessage.append(component.getName());
+ if (!missingConstructor.isEmpty()) {
+ errorMessage.append(" | Missing constructor dependencies: ").append(missingConstructor);
+ }
+ if (!missingFields.isEmpty()) {
+ errorMessage.append(" | Missing field dependencies: ").append(missingFields);
+ }
+ if (!missingSetters.isEmpty()) {
+ errorMessage.append(" | Missing setter dependencies: ").append(missingSetters);
+ }
+ if (missingConstructor.isEmpty() && missingFields.isEmpty() && missingSetters.isEmpty()) {
+ errorMessage.append(" | Conflicting super types detected");
+ }
+
+ errors.add(errorMessage.toString());
}
}
/**
- * Identifies and retrieves a comma-separated string of conflicting types for a given component.
- * A conflicting type is a parameter type in the constructor of the given component
- * that is already marked as not unique within the application context.
+ * Returns a cached {@link InjectionPlan} for the given component class.
+ *
+ * If plan determination fails, the component is marked as invalid and {@code null} is returned.
*
- * @param component the class for which conflicting types need to be identified.
- * @return a comma-separated string of fully qualified names of conflicting parameter types,
- * or an empty string if no conflicts are found.
+ * @param component the component class
+ * @return the computed injection plan, or {@code null} if none can be determined
*/
- private String getConflictingTypes(Class> component) {
- return Arrays.stream(component.getConstructors())
- .flatMap(constructor -> Arrays.stream(constructor.getParameterTypes()))
- .filter(notUniqueTypes::contains)
- .map(Class::getName)
- .collect(Collectors.joining(", "));
+ private InjectionPlan getInjectionPlan(Class> component) {
+ if (injectionPlans.containsKey(component)) {
+ return injectionPlans.get(component);
+ }
+ if (invalidComponents.contains(component)) {
+ return null;
+ }
+
+ InjectionPlan plan = determineInjectionPlan(component);
+ if (plan == null) {
+ invalidComponents.add(component);
+ } else {
+ injectionPlans.put(component, plan);
+ }
+ return plan;
}
/**
- * Retrieves a map of classes representing component types to their corresponding instantiable implementations.
+ * Checks whether all dependencies referenced by the given plan are present in the known type set.
*
- * @return A map where the key is a class type and the value is the corresponding class implementation
- * that can be instantiated.
+ * @param plan the injection plan to evaluate
+ * @param knownTypes currently resolvable injectable types
+ * @return {@code true} if all constructor/field/setter dependencies are known; otherwise {@code false}
+ */
+ private boolean areDependenciesKnown(InjectionPlan plan, Set> knownTypes) {
+ if (!Arrays.stream(plan.constructor().getParameterTypes()).allMatch(knownTypes::contains)) {
+ return false;
+ }
+
+ for (Field field : plan.fields()) {
+ if (!knownTypes.contains(field.getType())) {
+ return false;
+ }
+ }
+
+ for (Method setter : plan.setters()) {
+ Class> dependencyType = setter.getParameterTypes()[0];
+ if (!knownTypes.contains(dependencyType)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Determines an {@link InjectionPlan} for a component by selecting a constructor and collecting
+ * injectable fields and setter methods.
+ *
+ * A field is injectable if annotated with {@link jakarta.inject.Inject}.
+ * A method is treated as setter injection if annotated with {@link jakarta.inject.Inject} and
+ * matches the JavaBeans setter shape ({@code void setX(T value)}).
+ *
+ * @param component the component class
+ * @return the injection plan, or {@code null} if no usable constructor can be selected
+ */
+ private InjectionPlan determineInjectionPlan(Class> component) {
+ Constructor> constructor = selectConstructor(component);
+ if (constructor == null) {
+ return null;
+ }
+
+ List fields = Arrays.stream(component.getDeclaredFields())
+ .filter(field -> field.isAnnotationPresent(Inject.class))
+ .peek(field -> field.setAccessible(true))
+ .toList();
+
+ List setters = Arrays.stream(component.getDeclaredMethods())
+ .filter(method -> method.isAnnotationPresent(Inject.class))
+ .filter(method -> isValidSetter(component, method))
+ .peek(method -> method.setAccessible(true))
+ .toList();
+
+ return new InjectionPlan(constructor, fields, setters);
+ }
+
+ /**
+ * Selects the constructor to use for instantiation.
+ *
+ * - If exactly one constructor is annotated with {@link jakarta.inject.Inject}, it is selected.
+ * - If multiple constructors are annotated with {@code @Inject}, the component is invalid.
+ * - If there is exactly one declared constructor, it is selected.
+ * - Otherwise, a no-arg constructor is required and selected if present.
+ *
+ *
+ * @param component the component class
+ * @return the selected constructor, or {@code null} if none can be selected
+ */
+ private Constructor> selectConstructor(Class> component) {
+ Constructor>[] constructors = component.getDeclaredConstructors();
+ List> injectConstructors = Arrays.stream(constructors)
+ .filter(c -> c.isAnnotationPresent(Inject.class))
+ .toList();
+
+ if (injectConstructors.size() > 1) {
+ errors.add("Multiple @Inject constructors found for " + component.getName());
+ return null;
+ }
+
+ Constructor> selected;
+ if (injectConstructors.size() == 1) {
+ selected = injectConstructors.get(0);
+ } else if (constructors.length == 1) {
+ selected = constructors[0];
+ } else {
+ selected = Arrays.stream(constructors)
+ .filter(c -> c.getParameterCount() == 0)
+ .findFirst()
+ .orElse(null);
+ if (selected == null) {
+ errors.add("No @Inject constructor and no no-arg constructor found for " + component.getName());
+ return null;
+ }
+ }
+
+ selected.setAccessible(true);
+ return selected;
+ }
+
+ /**
+ * Validates whether a method annotated for injection is a compatible setter.
+ *
+ * A valid setter must:
+ *
+ * - Have exactly one parameter
+ * - Return {@code void}
+ * - Start with the prefix {@code set}
+ *
+ *
+ * @param component the declaring component class (used for error messages)
+ * @param method the method to validate
+ * @return {@code true} if the method is a valid setter; otherwise {@code false}
+ */
+ private boolean isValidSetter(Class> component, Method method) {
+ boolean isSetter = method.getParameterCount() == 1
+ && method.getReturnType() == Void.TYPE
+ && method.getName().startsWith("set");
+
+ if (!isSetter) {
+ errors.add("Invalid @Inject method detected in " + component.getName() + ": " + method.getName());
+ }
+
+ return isSetter;
+ }
+
+ /**
+ * Determines whether a dependency type is unknown to the container.
+ *
+ * A dependency is considered unknown if it is neither resolvable (present in
+ * {@link #instantiableComponents}) nor explicitly marked as ambiguous (present in
+ * {@link #notUniqueTypes}).
+ *
+ * @param type the dependency type
+ * @return {@code true} if the dependency cannot currently be resolved; otherwise {@code false}
+ */
+ private boolean isUnknownDependency(Class> type) {
+ return !instantiableComponents.containsKey(type) && !notUniqueTypes.contains(type);
+ }
+
+ /**
+ * Returns the mapping of resolvable injectable types to their {@link ComponentData}.
+ *
+ * @return a map of types to instantiation/injection metadata
*/
public Map, ComponentData> getInstantiableComponents() {
return instantiableComponents;
}
/**
- * Retrieves a set of types that have multiple implementations, making them
- * non-unique. These types could not be uniquely resolved during the
- * component scanning and analysis process.
+ * Returns the set of super types (interfaces/superclasses) that are implemented by multiple
+ * components and therefore cannot be uniquely resolved.
*
- * @return a set of classes representing the types that have multiple
- * implementations and cannot be resolved uniquely
+ * @return set of non-unique injectable types
*/
public Set> getNotUniqueTypes() {
return notUniqueTypes;
}
/**
- * Returns a mapping of types to their unique component implementations.
- * This map includes only those types that have a single corresponding component
- * implementation, ensuring no ambiguity in resolution.
+ * Returns collected errors discovered during scanning and resolution.
*
- * @return a map where the keys are types (e.g., interfaces or superclasses)
- * and the values are their unique component implementations.
- */
- public Map, Class>> getUniqueTypeToComponent() {
- return uniqueTypeToComponent;
- }
-
- /**
- * Retrieves a list of error messages recorded during the scanning and analysis process.
- * The errors typically indicate issues such as components that cannot be instantiated due to
- * missing dependencies, unresolvable component types, or multiple implementations of a type
- * that are not uniquely resolved.
- *
- * @return a list of error messages explaining the issues encountered.
+ * @return list of human-readable error messages
*/
public List getErrors() {
return errors;
}
-}
\ No newline at end of file
+
+ /**
+ * Immutable description of how to inject a component.
+ *
+ * @param constructor constructor selected for instantiation
+ * @param fields fields annotated with {@link jakarta.inject.Inject}
+ * @param setters setter methods annotated with {@link jakarta.inject.Inject}
+ */
+ private record InjectionPlan(Constructor> constructor, List fields, List setters) {
+ }
+}
+
diff --git a/injection/src/test/java/de/neitzel/injection/ApplicationContextInjectionTest.java b/injection/src/test/java/de/neitzel/injection/ApplicationContextInjectionTest.java
new file mode 100644
index 0000000..d8f2b93
--- /dev/null
+++ b/injection/src/test/java/de/neitzel/injection/ApplicationContextInjectionTest.java
@@ -0,0 +1,50 @@
+package de.neitzel.injection;
+
+import de.neitzel.injection.testcomponents.injection.success.ConstructorInjectedService;
+import de.neitzel.injection.testcomponents.injection.success.FieldInjectedService;
+import de.neitzel.injection.testcomponents.injection.success.MixedInjectionService;
+import de.neitzel.injection.testcomponents.injection.success.RootDependency;
+import de.neitzel.injection.testcomponents.injection.success.SetterInjectedService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class ApplicationContextInjectionTest {
+
+ private ApplicationContext context;
+
+ @BeforeEach
+ void setUp() {
+ context = new ApplicationContext("de.neitzel.injection.testcomponents.injection.success");
+ }
+
+ @Test
+ void resolvesConstructorInjection() {
+ ConstructorInjectedService service = context.getComponent(ConstructorInjectedService.class);
+ assertNotNull(service.getRootDependency());
+ }
+
+ @Test
+ void resolvesFieldInjection() {
+ FieldInjectedService service = context.getComponent(FieldInjectedService.class);
+ assertNotNull(service.getRootDependency());
+ }
+
+ @Test
+ void resolvesSetterInjection() {
+ SetterInjectedService service = context.getComponent(SetterInjectedService.class);
+ assertNotNull(service.getRootDependency());
+ }
+
+ @Test
+ void resolvesMixedInjectionGraphAndSingletonReuse() {
+ MixedInjectionService service = context.getComponent(MixedInjectionService.class);
+ RootDependency rootDependency = context.getComponent(RootDependency.class);
+ assertSame(rootDependency, service.getConstructorInjectedService().getRootDependency());
+ assertSame(rootDependency, service.getFieldInjectedService().getRootDependency());
+ assertSame(rootDependency, service.getSetterInjectedService().getRootDependency());
+ }
+}
+
diff --git a/injection/src/test/java/de/neitzel/injection/ComponentScannerInjectionPlanTest.java b/injection/src/test/java/de/neitzel/injection/ComponentScannerInjectionPlanTest.java
new file mode 100644
index 0000000..ff5f344
--- /dev/null
+++ b/injection/src/test/java/de/neitzel/injection/ComponentScannerInjectionPlanTest.java
@@ -0,0 +1,25 @@
+package de.neitzel.injection;
+
+import de.neitzel.injection.testcomponents.injection.error.MultipleInjectConstructorsComponent;
+import de.neitzel.injection.testcomponents.injection.error.NoInjectConstructorWithoutDefault;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ComponentScannerInjectionPlanTest {
+
+ @Test
+ void reportsMultipleInjectConstructorsAsError() {
+ ComponentScanner scanner = new ComponentScanner("de.neitzel.injection.testcomponents.injection.error");
+ assertFalse(scanner.getErrors().isEmpty());
+ assertTrue(scanner.getErrors().stream().anyMatch(error -> error.contains(MultipleInjectConstructorsComponent.class.getName())));
+ }
+
+ @Test
+ void reportsMissingUsableConstructorAsError() {
+ ComponentScanner scanner = new ComponentScanner("de.neitzel.injection.testcomponents.injection.error");
+ assertTrue(scanner.getErrors().stream().anyMatch(error -> error.contains(NoInjectConstructorWithoutDefault.class.getName())));
+ }
+}
+
diff --git a/injection/src/test/java/de/neitzel/injection/testcomponents/injection/error/MultipleInjectConstructorsComponent.java b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/error/MultipleInjectConstructorsComponent.java
new file mode 100644
index 0000000..bdaf3eb
--- /dev/null
+++ b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/error/MultipleInjectConstructorsComponent.java
@@ -0,0 +1,15 @@
+package de.neitzel.injection.testcomponents.injection.error;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class MultipleInjectConstructorsComponent {
+ @Inject
+ public MultipleInjectConstructorsComponent(String firstDependency) {
+ }
+
+ @Inject
+ public MultipleInjectConstructorsComponent(Integer secondDependency) {
+ }
+}
diff --git a/injection/src/test/java/de/neitzel/injection/testcomponents/injection/error/NoInjectConstructorWithoutDefault.java b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/error/NoInjectConstructorWithoutDefault.java
new file mode 100644
index 0000000..6835b71
--- /dev/null
+++ b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/error/NoInjectConstructorWithoutDefault.java
@@ -0,0 +1,12 @@
+package de.neitzel.injection.testcomponents.injection.error;
+
+import jakarta.inject.Singleton;
+
+@Singleton
+public class NoInjectConstructorWithoutDefault {
+ public NoInjectConstructorWithoutDefault(String firstDependency) {
+ }
+
+ public NoInjectConstructorWithoutDefault(Integer secondDependency) {
+ }
+}
diff --git a/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/ConstructorInjectedService.java b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/ConstructorInjectedService.java
new file mode 100644
index 0000000..7570fc2
--- /dev/null
+++ b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/ConstructorInjectedService.java
@@ -0,0 +1,19 @@
+package de.neitzel.injection.testcomponents.injection.success;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class ConstructorInjectedService {
+ private final RootDependency rootDependency;
+
+ @Inject
+ public ConstructorInjectedService(RootDependency rootDependency) {
+ this.rootDependency = rootDependency;
+ }
+
+ public RootDependency getRootDependency() {
+ return rootDependency;
+ }
+}
+
diff --git a/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/FieldInjectedService.java b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/FieldInjectedService.java
new file mode 100644
index 0000000..8471511
--- /dev/null
+++ b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/FieldInjectedService.java
@@ -0,0 +1,14 @@
+package de.neitzel.injection.testcomponents.injection.success;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class FieldInjectedService {
+ @Inject
+ private RootDependency rootDependency;
+
+ public RootDependency getRootDependency() {
+ return rootDependency;
+ }
+}
diff --git a/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/MixedInjectionService.java b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/MixedInjectionService.java
new file mode 100644
index 0000000..fc57138
--- /dev/null
+++ b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/MixedInjectionService.java
@@ -0,0 +1,36 @@
+package de.neitzel.injection.testcomponents.injection.success;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class MixedInjectionService {
+ private final ConstructorInjectedService constructorInjectedService;
+
+ @Inject
+ private FieldInjectedService fieldInjectedService;
+
+ private SetterInjectedService setterInjectedService;
+
+ @Inject
+ public MixedInjectionService(ConstructorInjectedService constructorInjectedService) {
+ this.constructorInjectedService = constructorInjectedService;
+ }
+
+ public ConstructorInjectedService getConstructorInjectedService() {
+ return constructorInjectedService;
+ }
+
+ public FieldInjectedService getFieldInjectedService() {
+ return fieldInjectedService;
+ }
+
+ public SetterInjectedService getSetterInjectedService() {
+ return setterInjectedService;
+ }
+
+ @Inject
+ public void setSetterInjectedService(SetterInjectedService setterInjectedService) {
+ this.setterInjectedService = setterInjectedService;
+ }
+}
diff --git a/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/RootDependency.java b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/RootDependency.java
new file mode 100644
index 0000000..9077ea9
--- /dev/null
+++ b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/RootDependency.java
@@ -0,0 +1,7 @@
+package de.neitzel.injection.testcomponents.injection.success;
+
+import jakarta.inject.Singleton;
+
+@Singleton
+public class RootDependency {
+}
diff --git a/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/SetterInjectedService.java b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/SetterInjectedService.java
new file mode 100644
index 0000000..682518c
--- /dev/null
+++ b/injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/SetterInjectedService.java
@@ -0,0 +1,18 @@
+package de.neitzel.injection.testcomponents.injection.success;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class SetterInjectedService {
+ private RootDependency rootDependency;
+
+ public RootDependency getRootDependency() {
+ return rootDependency;
+ }
+
+ @Inject
+ public void setRootDependency(RootDependency rootDependency) {
+ this.rootDependency = rootDependency;
+ }
+}