Refactorings, creating module injection.

This commit is contained in:
Konrad Neitzel
2025-12-02 18:23:05 +01:00
parent 3178dc2723
commit edf02e9816
24 changed files with 120 additions and 51 deletions
+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.lib</groupId>
<artifactId>neitzellib</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>injection</artifactId>
<properties>
<!-- Application Properties -->
<link.name>${project.artifactId}</link.name>
<launcher>${project.artifactId}</launcher>
<appName>${project.artifactId}</appName>
<jar.filename>${project.artifactId}-${project.version}</jar.filename>
</properties>
<dependencies>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>${reflections.version}</version>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>${jakarta-inject-api}</version>
</dependency>
</dependencies>
<build>
<finalName>${jar.filename}</finalName>
<plugins>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,141 @@
package de.neitzel.injection;
import de.neitzel.injection.annotation.Config;
import java.lang.reflect.Constructor;
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
* injection framework.
* <p>
* The {@code ApplicationContext} is responsible for scanning, instantiating, and managing
* the lifecycle of components. Components are identified through the specified base package
* (or derived from a configuration class) and instantiated based on their dependency
* requirements and scope. The framework supports constructor-based dependency injection
* for resolving and creating components.
*/
public class ApplicationContext {
private final Map<Class<?>, ComponentData> components;
/**
* Erstellt einen ApplicationContext auf Basis eines expliziten Package-Namens.
*
* @param basePackage das Basis-Package, das nach Komponenten durchsucht werden soll
*/
public ApplicationContext(String basePackage) {
ComponentScanner scanner = new ComponentScanner(basePackage);
this.components = scanner.getInstantiableComponents();
}
/**
* Erstellt einen ApplicationContext auf Basis einer Startklasse.
* Das zu scannende Package wird aus der @Config Annotation gelesen (basePackage).
* Wenn keine Annotation vorhanden ist oder kein basePackage gesetzt wurde, wird
* das Package der übergebenen Klasse verwendet.
*
* @param configClass Klasse mit oder ohne @Config Annotation
*/
public ApplicationContext(Class<?> configClass) {
String basePackage;
Config config = configClass.getAnnotation(Config.class);
if (config != null && !config.basePackage().isEmpty()) {
basePackage = config.basePackage();
} else {
basePackage = configClass.getPackageName();
}
ComponentScanner scanner = new ComponentScanner(basePackage);
this.components = scanner.getInstantiableComponents();
}
/**
* Retrieves an instance of the specified component type from the application context.
* <p>
* This method uses dependency injection to construct the component if it is not already
* a singleton instance. The component's type and scope are determined by the framework,
* and the appropriate initialization and lifecycle management are performed.
*
* @param <T> the type of the component to retrieve
* @param type the {@code Class} object representing the type of the component
* @return an instance of the requested component type
* @throws IllegalArgumentException if no component is found for the specified type
* @throws IllegalStateException if no suitable constructor is found for the component
* @throws RuntimeException if any error occurs during instantiation
*/
@SuppressWarnings("unchecked")
public <T> T getComponent(Class<? extends T> type) {
ComponentData data = components.get(type);
if (data == null) {
throw new IllegalArgumentException("No component found for type: " + type.getName());
}
Scope scope = data.getScope();
Object instance = data.getInstance();
if (scope == Scope.SINGLETON && instance != null) {
return (T) instance;
}
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;
}
}
throw new IllegalStateException("Kein passender Konstruktor gefunden für " + type.getName());
} 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.
* <p>
* 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<Class<?>, ComponentData> parameterMap) {
return Stream.of(constructor.getParameterTypes()).allMatch(parameterMap::containsKey);
}
/**
* Registers a singleton instance of a specific type into the application context.
* <p>
* This method allows manual addition of singleton components to the context by
* providing a concrete instance of the required type. If the type is already mapped
* to a different instance or type within the context, an {@link IllegalStateException}
* is thrown to prevent replacement of an existing singleton.
*
* @param <T> the type of the component to register
* @param type the class type of the component being added
* @param instance the singleton instance of the component to register
* @throws IllegalStateException if the type is already registered with a different instance
*/
public <T> void addSingleton(Class<? extends T> type, T instance) {
if (components.get(type) == null ||
!Objects.equals(components.get(type).getType().getName(), type.getName())) {
components.put(type, new ComponentData(type, instance));
} else {
throw new IllegalStateException("Type cannot be replaced: " + type.getName());
}
}
}
@@ -0,0 +1,89 @@
package de.neitzel.injection;
import lombok.Getter;
/**
* Represents a component in a dependency injection framework.
* <p>
* A component is a unit of functionality that is managed by the framework, allowing
* for controlled instantiation and lifecycle management. The component is associated
* with a specific type and a scope. The scope determines whether the component is
* instantiated as a singleton or as a prototype.
*/
@Getter
public class ComponentData {
/**
* Represents the type of the component being managed by the dependency injection framework.
* <p>
* This variable holds the class object corresponding to the specific type of the component.
* It is used to identify and instantiate the component during the dependency injection process.
* The type is immutable and is specified when the component is created.
*/
private final Class<?> type;
/**
* Defines the lifecycle and instantiation rules for the associated component.
* <p>
* The {@code Scope} determines whether the component is created as a singleton
* (a single shared instance) or as a prototype (a new instance for each request).
* <p>
* This variable is immutable and represents the specific {@code Scope} assigned
* to the component, influencing its behavior within the dependency injection framework.
*/
private final Scope scope;
/**
* Stores the instantiated object associated with the component.
* <p>
* This field holds the actual instance of the component's type within the dependency
* injection framework. For components with a {@code SINGLETON} scope, this field is
* set only once and shared across the entire application. For components with a
* {@code PROTOTYPE} scope, this field may be null since a new instance is created
* upon each request.
*/
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.
* <p>
* This can be used to add Singletons manually without scanning for them.
*
* @param type the class type of the component
* @param instance the initial instance of the component
*/
public ComponentData(Class<?> type, Object instance) {
this.type = type;
this.scope = Scope.SINGLETON;
this.instance = instance;
}
/**
* 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.
* If an instance has already been set, it throws an {@link IllegalStateException}.
*
* @param instance the object instance to associate with this component
* @throws IllegalStateException if an instance has already been set for this {@code SINGLETON} component
*/
public void setInstance(Object instance) {
if (scope != Scope.SINGLETON) {
return;
}
if (this.instance != null) {
throw new IllegalStateException("Instance already set for singleton: " + type.getName());
}
this.instance = instance;
}
}
@@ -0,0 +1,387 @@
package de.neitzel.injection;
import de.neitzel.injection.annotation.Component;
import de.neitzel.injection.annotation.Inject;
import org.reflections.Reflections;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
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.
*/
public class ComponentScanner {
/**
* A set that stores classes annotated with {@code @Component}, representing
* FXML-compatible components detected during the scanning process.
* These components are used as part of the dependency injection mechanism
* within the {@code InjectableComponentScanner}.
* <p>
* The {@code fxmlComponents} set is populated during the component scanning
* phase by identifying all classes within a specified package hierarchy
* annotated with the {@code @Component} annotation. These classes serve
* as the primary source for further analysis, resolution, and instantiation
* in the scanning workflow.
* <p>
* This field is immutable, ensuring thread safety and consistent state
* throughout the lifetime of the {@code InjectableComponentScanner}.
*/
private final Set<Class<?>> components = new HashSet<>();
/**
* A set of component types that are not uniquely associated with a single implementation.
* <p>
* This collection is populated during the analysis of discovered components to identify
* types that have multiple implementations, preventing them from being uniquely resolved.
* For example, if multiple components implement the same interface, that interface will be
* added to this set.
* <p>
* It is primarily used during the component registration process to avoid ambiguities
* in the dependency injection system, ensuring that only resolvable, uniquely identifiable
* components can be instantiated and injected.
*/
private final Set<Class<?>> notUniqueTypes = new HashSet<>();
/**
* A mapping of unique super types to their corresponding component classes.
* The key represents a super type (interface or abstract class), and the value
* is the specific implementation or subclass that is uniquely identified among
* the scanned components.
* <p>
* This map is populated during the component analysis process. For each super type
* in the scanned components, if exactly one implementation is found, it is
* considered unique and added to this mapping. In case multiple implementations
* exist for a given super type, it is excluded from this map and classified as
* a non-unique type.
* <p>
* This mapping is utilized for resolving dependencies and determining which components
* can be instantiated based on unique type information.
*/
private final Map<Class<?>, 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.
* <p>
* 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<Class<?>, ComponentData> instantiableComponents = new HashMap<>();
/**
* A list of error messages encountered during the scanning and analysis
* of components in the dependency injection process.
* <p>
* This list is populated when components cannot be resolved due to issues
* such as multiple implementations of a superclass or interface,
* unmet construction requirements, or unresolved dependencies.
* <p>
* Errors added to this list provide detailed descriptions of the specific
* issues that prevent certain components from being instantiated correctly.
*/
private final List<String> errors = new ArrayList<>();
/**
* 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.
*
* @param basePackage the base package to scan for injectable components
*/
public ComponentScanner(String basePackage) {
scanForComponents(basePackage);
analyzeComponentTypes();
resolveInstantiableComponents();
}
/**
* Scans the specified base package for classes annotated with {@link Component}.
* Identified component classes are added to a collection for further processing.
*
* @param basePackage the package to scan for component annotations
*/
private void scanForComponents(String basePackage) {
Reflections reflections = new Reflections(basePackage);
components.addAll(reflections.getTypesAnnotatedWith(Component.class));
}
/**
* Analyzes component types within the injected components and classifies them based on their
* inheritance hierarchy and relationships.
* <p>
* This method performs the following operations:
* 1. Maps each component to all of its superclasses and interfaces.
* 2. Identifies which superclasses or interfaces have multiple implementing components.
* 3. Populates:
* - A list of superclasses or interfaces that are not uniquely linked to a single component.
* - A map where unique superclasses or interfaces are associated with a specific implementing component.
* <p>
* The mappings are built using the following data structures:
* - A map from superclasses/interfaces to the list of components that implement or extend them.
* - A list of non-unique types where multiple components exist for the same superclass or interface.
* - A map of unique superclasses/interfaces to their corresponding component.
* <p>
* This method is a key part of the component scanning and resolution process, facilitating
* the identification of potential instantiation ambiguities or conflicts.
*/
private void analyzeComponentTypes() {
Map<Class<?>, List<Class<?>>> superTypesMap = new HashMap<>();
for (Class<?> component : components) {
Set<Class<?>> allSuperTypes = getAllSuperTypes(component);
for (Class<?> superType : allSuperTypes) {
superTypesMap.computeIfAbsent(superType, k -> new ArrayList<>()).add(component);
}
}
for (Map.Entry<Class<?>, List<Class<?>>> entry : superTypesMap.entrySet()) {
Class<?> superType = entry.getKey();
List<Class<?>> implementations = entry.getValue();
if (implementations.size() > 1) {
notUniqueTypes.add(superType);
} else {
uniqueTypeToComponent.put(superType, implementations.get(0));
}
}
}
/**
* Resolves and identifies instantiable components from a set of scanned components.
* This process determines which components can be instantiated based on their dependencies
* and class relationships, while tracking unresolved types and potential conflicts.
* <p>
* The resolution process involves:
* 1. Iteratively determining which components can be instantiated using the known types
* map. A component is resolvable if its dependencies can be satisfied by the current
* set of known types.
* 2. Registering resolvable components and their superclasses/interfaces into the known
* types map for future iterations.
* 3. Removing successfully resolved components from the unresolved set.
* 4. Repeating the process until no further components can be resolved in a given iteration.
* <p>
* At the end of the resolution process:
* - Resolvable components are added to the `instantiableComponents` map, which maps types
* to their corresponding instantiable implementations.
* - Unresolved components are identified, and error details are collected to highlight
* dependencies or conflicts preventing their instantiation.
* <p>
* If errors are encountered due to unresolved components, they are logged for further analysis.
*/
private void resolveInstantiableComponents() {
Set<Class<?>> resolved = new HashSet<>();
Set<Class<?>> unresolved = new HashSet<>(components);
Map<Class<?>, ComponentData> knownTypes = new HashMap<>();
Set<Class<?>> resolvableNow;
do {
resolvableNow = unresolved.stream()
.filter(c -> canInstantiate(c, knownTypes.keySet()))
.collect(Collectors.toSet());
for (Class<?> clazz : resolvableNow) {
Component annotation = clazz.getAnnotation(Component.class);
ComponentData componentInfo = new ComponentData(clazz, annotation.scope());
resolved.add(clazz);
registerComponentWithSuperTypes(componentInfo, knownTypes);
}
unresolved.removeAll(resolvableNow);
} while (!resolvableNow.isEmpty());
instantiableComponents.putAll(knownTypes);
if (!unresolved.isEmpty()) {
collectInstantiationErrors(unresolved);
}
}
/**
* Retrieves all superclasses and interfaces of the specified class, excluding the {@code Object} class.
*
* @param clazz the class for which to retrieve all superclasses and interfaces
* @return a set of all superclasses and interfaces implemented by the specified class
*/
private Set<Class<?>> getAllSuperTypes(Class<?> clazz) {
Set<Class<?>> result = new HashSet<>();
Class<?> superClass = clazz.getSuperclass();
while (superClass != null && superClass != Object.class) {
result.add(superClass);
superClass = superClass.getSuperclass();
}
result.addAll(Arrays.asList(clazz.getInterfaces()));
return result;
}
/**
* 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.
*
* @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
*/
private boolean canInstantiate(Class<?> component, Set<Class<?>> 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;
}
}
if (!hasValidConstructor) {
return false;
}
for (var field : component.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class) && !knownTypes.contains(field.getType())) {
return false;
}
}
return true;
}
/**
* 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.
*
* @param component the {@code ComponentData} instance representing the component to be registered
* @param knownTypes the map where component types and their data are stored
*/
private void registerComponentWithSuperTypes(ComponentData component, Map<Class<?>, ComponentData> knownTypes) {
knownTypes.put(component.getType(), component);
for (Class<?> superType : getAllSuperTypes(component.getType())) {
if (!notUniqueTypes.contains(superType)) {
knownTypes.put(superType, component);
}
}
}
/**
* 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.
*
* @param unresolved the set of classes for which instantiation errors are collected
*/
private void collectInstantiationErrors(Set<Class<?>> unresolved) {
for (Class<?> component : unresolved) {
StringBuilder errorMsg = new StringBuilder("Component cannot be instantiated: " + component.getName());
boolean possibleWithUniqueTypes = false;
for (Constructor<?> constructor : component.getConstructors()) {
Class<?>[] paramTypes = constructor.getParameterTypes();
List<Class<?>> 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);
}
}
if (possibleWithUniqueTypes) {
errorMsg.append("\n ➤ Could be instantiated if multiple implementations of ")
.append("interfaces/superclasses were resolved uniquely: ")
.append(getConflictingTypes(component));
}
errors.add(errorMsg.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.
*
* @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.
*/
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(", "));
}
/**
* Retrieves a map of classes representing component types to their corresponding instantiable implementations.
*
* @return A map where the key is a class type and the value is the corresponding class implementation
* that can be instantiated.
*/
public Map<Class<?>, 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.
*
* @return a set of classes representing the types that have multiple
* implementations and cannot be resolved uniquely
*/
public Set<Class<?>> 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.
*
* @return a map where the keys are types (e.g., interfaces or superclasses)
* and the values are their unique component implementations.
*/
public Map<Class<?>, 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.
*/
public List<String> getErrors() {
return errors;
}
}
@@ -0,0 +1,41 @@
package de.neitzel.injection;
/**
* Represents the scope of a component in a dependency injection framework.
* <p>
* The scope determines the lifecycle and visibility of a component instance
* within the framework. The available scope types are:
* <p>
* - {@code SINGLETON}: A single instance of the component is created and
* shared across the entire application.
* - {@code PROTOTYPE}: A new instance of the component is created
* each time it is requested or injected.
* <p>
* This enumeration is typically used in conjunction with the {@code @Component}
* annotation to specify the instantiation behavior of a component.
*/
public enum Scope {
/**
* Specifies that the component should be instantiated as a singleton within the
* dependency injection framework. A single instance of the component is created
* and shared across the entire application lifecycle, ensuring efficient reuse
* of resources and consistent behavior for the component throughout the application.
* <p>
* This scope is typically applied to components where maintaining a single shared
* instance is necessary or beneficial, such as managing shared state or providing
* utility services.
*/
SINGLETON,
/**
* Specifies that the component should be instantiated as a prototype within the
* dependency injection framework. A new instance of the component is created
* each time it is requested or injected, ensuring that no two requests or injections
* share the same instance.
* <p>
* This scope is typically applied to components where maintaining unique instances
* per request is necessary, such as request-specific data processing or handling
* of transient state. It allows for complete isolation between multiple users
* or operations requiring individual resources.
*/
PROTOTYPE
}
@@ -0,0 +1,37 @@
package de.neitzel.injection.annotation;
import de.neitzel.injection.Scope;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that an annotated class is a "component" within a dependency injection framework.
* Classes annotated with {@code @Component} are recognized during the component scanning process
* as candidates for instantiation and management by the dependency injection framework.
* <p>
* This annotation is typically used on classes that represent application-specific components,
* such as service implementations, controllers, or other objects intended to be instantiated
* and injected into other components during runtime.
* <p>
* The annotation must be placed at the type level, and it is retained at runtime
* to allow for reflection-based scanning of classes within specified packages.
* <p>
* Usage of this annotation assumes that there exists a component scanning mechanism
* that processes annotated classes and identifies their roles, dependencies, and hierarchy
* within the application's structure.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
/**
* Defines the scope of a component within the dependency injection framework.
* The scope determines whether the component is instantiated as a singleton or
* as a prototype.
*
* @return the scope of the component, defaulting to {@code Scope.SINGLETON}.
*/
Scope scope() default Scope.SINGLETON;
}
@@ -0,0 +1,37 @@
package de.neitzel.injection.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that an annotated class is a configuration class within a dependency
* injection framework. Classes annotated with {@code @Config} are used as markers
* for defining settings and application-specific configurations required by the
* dependency injection mechanism.
* <p>
* Typically, configuration classes provide metadata required for setting up the
* framework, such as specifying the base package to scan for components.
* <p>
* This annotation must be applied at the type level and is retained at runtime to
* facilitate reflection-based processing. It is intended to serve as a declarative
* representation of configuration options for the dependency injection container.
* <p>
* Attributes:
* - {@code basePackage}: Specifies the package name where the framework should scan
* for classes annotated with dependency injection annotations such as {@code @Component}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Config {
/**
* Specifies the base package for component scanning within the dependency
* injection framework. Classes within the defined package and its sub-packages
* can be scanned and identified as candidates for dependency injection.
*
* @return the base package name as a string; returns an empty string by default
* if no specific package is defined.
*/
String basePackage() default "";
}
@@ -0,0 +1,24 @@
package de.neitzel.injection.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that a field is a candidate for dependency injection within
* a dependency injection framework. Fields annotated with {@code @Inject}
* are automatically populated with the required component instance during runtime,
* typically by the dependency injection container.
* <p>
* This annotation must be applied at the field level and is retained at runtime
* to enable reflection-based identification and assignment of dependencies.
* <p>
* The framework's dependency resolution mechanism identifies the appropriate
* instance to inject based on the field's type or custom configuration,
* ensuring loose coupling and easier testability.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
@@ -0,0 +1,54 @@
package de.neitzel.injection;
import de.neitzel.injection.testcomponents.test1ok.SuperClass;
import de.neitzel.injection.testcomponents.test1ok.TestComponent1_1;
import de.neitzel.injection.testcomponents.test1ok.TestInterface1_1;
import de.neitzel.injection.testcomponents.test1ok.TestInterface1_2;
import de.neitzel.injection.testcomponents.test1ok.sub.TestComponent1_2;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class InjectableComponentScannerTest {
/**
* Tests loading of multiple FXMLComponents including sub packages.
*/
@Test
void testLoadComponents() {
ComponentScanner scanner = new ComponentScanner("de.neitzel.injection.testcomponents.test1ok");
var instantiableComponents = scanner.getInstantiableComponents();
var nonUniqueTypes = scanner.getNotUniqueTypes();
assertAll(
() -> assertNotNull(instantiableComponents),
() -> assertEquals(3, instantiableComponents.size()),
() -> assertTrue(instantiableComponents.containsKey(TestComponent1_1.class)),
() -> assertTrue(instantiableComponents.containsKey(TestComponent1_2.class)),
() -> assertTrue(instantiableComponents.containsKey(TestInterface1_1.class)),
() -> assertTrue(nonUniqueTypes.contains(SuperClass.class)),
() -> assertTrue(nonUniqueTypes.contains(TestInterface1_2.class)),
() -> assertTrue(scanner.getErrors().isEmpty())
);
}
/**
* Tests failing to load a FXMLComponent which has an unknwown parameter.
*/
@Test
void testComponentsFailWithUnknownParameters() {
ComponentScanner scanner = new ComponentScanner("de.neitzel.injection.testcomponents.test2fail");
var instantiableComponents = scanner.getInstantiableComponents();
var nonUniqueTypes = scanner.getNotUniqueTypes();
assertAll(
() -> assertNotNull(instantiableComponents),
() -> assertEquals(0, instantiableComponents.size()),
() -> assertFalse(scanner.getErrors().isEmpty())
);
}
}
@@ -0,0 +1,4 @@
package de.neitzel.injection.testcomponents.test1ok;
public class SuperClass {
}
@@ -0,0 +1,9 @@
package de.neitzel.injection.testcomponents.test1ok;
import de.neitzel.injection.annotation.Component;
@Component
public class TestComponent1_1 extends SuperClass implements TestInterface1_2 {
public TestComponent1_1() {
}
}
@@ -0,0 +1,4 @@
package de.neitzel.injection.testcomponents.test1ok;
public interface TestInterface1_1 {
}
@@ -0,0 +1,4 @@
package de.neitzel.injection.testcomponents.test1ok;
public interface TestInterface1_2 {
}
@@ -0,0 +1,12 @@
package de.neitzel.injection.testcomponents.test1ok.sub;
import de.neitzel.injection.annotation.Component;
import de.neitzel.injection.testcomponents.test1ok.SuperClass;
import de.neitzel.injection.testcomponents.test1ok.TestInterface1_1;
import de.neitzel.injection.testcomponents.test1ok.TestInterface1_2;
@Component
public class TestComponent1_2 extends SuperClass implements TestInterface1_1, TestInterface1_2 {
public TestComponent1_2() {
}
}
@@ -0,0 +1,12 @@
package de.neitzel.injection.testcomponents.test2fail;
import de.neitzel.injection.annotation.Component;
/**
* TestComponent1 that should fail.
*/
@Component
public class TestComponent2_1 {
public TestComponent2_1(String test) {
}
}