From 1b1be747bb6dc0028903f778726e8a8ed2b827c0 Mon Sep 17 00:00:00 2001 From: Konrad Neitzel Date: Sun, 14 Dec 2025 18:03:23 +0100 Subject: [PATCH] Updated Injection logik to also handle fields, setters and Constructors. --- .../neitzel/injection/ApplicationContext.java | 59 ++- .../de/neitzel/injection/ComponentData.java | 67 ++- .../neitzel/injection/ComponentScanner.java | 423 +++++++++++++----- .../ApplicationContextInjectionTest.java | 50 +++ .../ComponentScannerInjectionPlanTest.java | 25 ++ .../MultipleInjectConstructorsComponent.java | 15 + .../NoInjectConstructorWithoutDefault.java | 12 + .../success/ConstructorInjectedService.java | 19 + .../success/FieldInjectedService.java | 14 + .../success/MixedInjectionService.java | 36 ++ .../injection/success/RootDependency.java | 7 + .../success/SetterInjectedService.java | 18 + 12 files changed, 581 insertions(+), 164 deletions(-) create mode 100644 injection/src/test/java/de/neitzel/injection/ApplicationContextInjectionTest.java create mode 100644 injection/src/test/java/de/neitzel/injection/ComponentScannerInjectionPlanTest.java create mode 100644 injection/src/test/java/de/neitzel/injection/testcomponents/injection/error/MultipleInjectConstructorsComponent.java create mode 100644 injection/src/test/java/de/neitzel/injection/testcomponents/injection/error/NoInjectConstructorWithoutDefault.java create mode 100644 injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/ConstructorInjectedService.java create mode 100644 injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/FieldInjectedService.java create mode 100644 injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/MixedInjectionService.java create mode 100644 injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/RootDependency.java create mode 100644 injection/src/test/java/de/neitzel/injection/testcomponents/injection/success/SetterInjectedService.java 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: + *

+ *

+ * 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. + *

+ * + * @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: + *

+ * + * @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; + } +}