📜 ⬆️ ⬇️

Implementing your IoC container

image

Introduction


Every novice developer should be familiar with the concept of Inversion of Control.

Almost every new project now begins with the choice of the framework, through which the principle of dependency injection will be implemented.

Inversion of Control (Inversion of Control, IoC) is an important principle of object-oriented programming used to reduce connectivity in computer programs and is one of the five most important principles of SOLID.
')
Today there are several basic frameworks on this topic:

1. Dagger
2. Google Guice
3. Spring Framework

I still use Spring to this day and I am partially pleased with its functionality, but it's time to try something and something of my own, isn't it?

About myself


My name is Nikita, I am 24 years old, and I have been doing java (backend) for 3 years. He studied only on practical examples, in parallel trying to understand the specs of classes. Currently working (freelance) - writing a CMS for a commercial project, where I use Spring Boot. I recently thought: “Why not write your IoC (DI) Container according to your vision and desire?”. Roughly speaking - “I wanted it with blackjack ...”. This will be discussed today. Well, please under the cat. Link to project sources .

Special features


- the main feature of the project - Dependency Injection.
There are 3 main dependency injection methods supported:
  1. Class fields
  2. Class constructor
  3. Class functions (standard setter on one parameter)

*Note:
- when scanning a class, if all three methods of injection are used at once - the method of injection through the class constructor marked with the @IoCDependency annotation will be priority. Those. Only one injection method works.

- lazy initialization of components (on demand);

- built-in loader configuration files (formats: ini, xml, properties);

- command line argument handler;

- processing modules by creating factories;

- built-in events and listeners;

- built-in informers (Sensibles) for “informing” a component, factory, listener, processor (ComponentProcessor) that certain information should be loaded into an object depending on the informer;

- a module for managing / creating a thread pool, declaring functions as executable tasks for some time and initializing them in the pool factory, as well as starting with the parameters of SimpleTask.

How packages are scanned:
Used by third-party Reflections API with a standard scanner.

//{@see IocStarter#initializeContext} private AppContext initializeContext(Class<?>[] mainClasses, String... args) throws Exception { final AppContext context = new AppContext(); for (Class<?> mainSource : mainClasses) { final List<String> modulePackages = getModulePaths(mainSource); final String[] packages = modulePackages.toArray(new String[0]); final Reflections reflections = ReflectionUtils.configureScanner(packages, mainSource); final ModuleInfo info = getModuleInfo(reflections); initializeModule(context, info, args); } Runtime.getRuntime().addShutdownHook(new ShutdownHook(context)); context.getDispatcherFactory().fireEvent(new OnContextIsInitializedEvent(context)); return context; } 

We get a collection of classes using annotation filters, types.
In this case, it is @IoCComponent, @Property and the Prophet Analyzer <R, T>

Context initialization order:
1) The first is the initialization of configuration types.
 //{@see AppContext#initEnvironment(Set)} public void initEnvironment(Set<Class<?>> properties) { for (Class<?> type : properties) { final Property property = type.getAnnotation(Property.class); if (property.ignore()) { continue; } final Path path = Paths.get(property.path()); try { final Object o = type.newInstance(); PropertiesLoader.parse(o, path.toFile()); dependencyInitiator.instantiatePropertyMethods(o); dependencyInitiator.addInstalledConfiguration(o); } catch (Exception e) { throw new Error("Failed to Load " + path + " Config File", e); } } } 

* Explanations:
The @Property annotation has a mandatory string parameter - path (path to the configuration file). This is where the file is searched for parsing the configuration.
The PropertiesLoader class is a utility class for initializing the class fields corresponding to the configuration file fields.
The function DependencyFactory # addInstalledConfiguration (Object) - loads the configuration object into the factory as SINGLETON (otherwise it makes sense to reload the config not on demand).

2) Initialization of analyzers
3) Initialization of found components (Classes with annotation @IoCComponent)
 //{@see AppContext#scanClass(Class)} private void scanClass(Class<?> component) { final ClassAnalyzer classAnalyzer = getAnalyzer(ClassAnalyzer.class); if (!classAnalyzer.supportFor(component)) { throw new IoCInstantiateException("It is impossible to test, check the class for type match!"); } final ClassAnalyzeResult result = classAnalyzer.analyze(component); dependencyFactory.instantiate(component, result); } 

* Explanations:
ClassAnalyzer class - defines the method of dependency injection, as well if there are errors of incorrect placement of annotations, constructor declarations, parameters in the method - returns an error. Function Analyzer <R, T> #analyze (T) - returns the result of the analysis. Function Analyzer <R, T> #supportFor (T) - returns a boolean parameter depending on the specified conditions.
DependencyFactory # instantiate (Class, R) function - installs a type into a factory using a method defined by ClassAnalyzer or throws an exception if there are errors in either the analysis or the object initialization process itself.

3) Scan methods
- method of parameter injection into class constructor
  private <O> O instantiateConstructorType(Class<O> type) { final Constructor<O> oConstructor = findConstructor(type); if (oConstructor != null) { final Parameter[] constructorParameters = oConstructor.getParameters(); final List<Object> argumentList = Arrays.stream(constructorParameters) .map(param -> mapConstType(param, type)) .collect(Collectors.toList()); try { final O instance = oConstructor.newInstance(argumentList.toArray()); addInstantiable(type); final String typeName = getComponentName(type); if (isSingleton(type)) { singletons.put(typeName, instance); } else if (isPrototype(type)) { prototypes.put(typeName, instance); } return instance; } catch (Exception e) { throw new IoCInstantiateException("IoCError - Unavailable create instance of type [" + type + "].", e); } } return null; } 

- method of injecting parameters into class fields
  private <O> O instantiateFieldsType(Class<O> type) { final List<Field> fieldList = findFieldsFromType(type); final List<Object> argumentList = fieldList.stream() .map(field -> mapFieldType(field, type)) .collect(Collectors.toList()); try { final O instance = ReflectionUtils.instantiate(type); addInstantiable(type); for (Field field : fieldList) { final Object toInstantiate = argumentList .stream() .filter(f -> f.getClass().getSimpleName().equals(field.getType().getSimpleName())) .findFirst() .get(); final boolean access = field.isAccessible(); field.setAccessible(true); field.set(instance, toInstantiate); field.setAccessible(access); } final String typeName = getComponentName(type); if (isSingleton(type)) { singletons.put(typeName, instance); } else if (isPrototype(type)) { prototypes.put(typeName, instance); } return instance; } catch (Exception e) { throw new IoCInstantiateException("IoCError - Unavailable create instance of type [" + type + "].", e); } } 

- method of parameter injection through class functions
  private <O> O instantiateMethodsType(Class<O> type) { final List<Method> methodList = findMethodsFromType(type); final List<Object> argumentList = methodList.stream() .map(method -> mapMethodType(method, type)) .collect(Collectors.toList()); try { final O instance = ReflectionUtils.instantiate(type); addInstantiable(type); for (Method method : methodList) { final Object toInstantiate = argumentList .stream() .filter(m -> m.getClass().getSimpleName().equals(method.getParameterTypes()[0].getSimpleName())) .findFirst() .get(); method.invoke(instance, toInstantiate); } final String typeName = getComponentName(type); if (isSingleton(type)) { singletons.put(typeName, instance); } else if (isPrototype(type)) { prototypes.put(typeName, instance); } return instance; } catch (Exception e) { throw new IoCInstantiateException("IoCError - Unavailable create instance of type [" + type + "].", e); } } 



User API
1. ComponentProcessor is a utility that allows you to change a component at will, both prior to its initialization in the context and after it.
 public interface ComponentProcessor { Object afterComponentInitialization(String componentName, Object component); Object beforeComponentInitialization(String componentName, Object component); } 


* Explanations:
The function #afterComponentInitialization (String, Object) allows you to manipulate a component after initializing it in context, the input parameters are (fixed component name, instantiated component object).
The function #beforeComponentInitialization (String, Object) allows you to manipulate a component before initializing it in context, the input parameters are (fixed component name, instantiated component object).

2. CommandLineArgumentResolver
 public interface CommandLineArgumentResolver { void resolve(String... args); } 


* Explanations:
The #resolve (String ...) function is an interface handler for various commands sent via cmd when the application is started, the input parameter is an unlimited array of command line strings (parameters).
3. Informers (Sensibles) - indicates that the child class informant will need to build def. functionality depending on the type of informant (ContextSensible, EnvironmentSensible, ThreadFactorySensible, etc.)

4. Listeners
Implemented listeners functionality, guaranteed multi-threading execution with setting the recommended number of descriptors for optimized operation of events.
 @org.di.context.annotations.listeners.Listener //  - @IoCComponent //  ,       (Sensibles)   . public class TestListener implements Listener { private final Logger log = LoggerFactory.getLogger(TestListener.class); @Override public boolean dispatch(Event event) { if (OnContextStartedEvent.class.isAssignableFrom(event.getClass())) { log.info("ListenerInform - Context is started! [{}]", event.getSource()); } else if (OnContextIsInitializedEvent.class.isAssignableFrom(event.getClass())) { log.info("ListenerInform - Context is initialized! [{}]", event.getSource()); } else if (OnComponentInitEvent.class.isAssignableFrom(event.getClass())) { final OnComponentInitEvent ev = (OnComponentInitEvent) event; log.info("ListenerInform - Component [{}] in instance [{}] is initialized!", ev.getComponentName(), ev.getSource()); } return true; } } 

** Explanations:
The dispatch (Event) function is the main function of the system event handler.
- There are standard implementations of listeners with a check for event types as well as with built-in custom filters {@link Filter}. Standard filters included in the package: AndFilter, ExcludeFilter, NotFilter, OrFilter, InstanceFilter (custom). Standard listener implementations: FilteredListener and TypedListener. The first involves a filter to check the incoming event object. The second one checks the event object or any other for belonging to a specific instance.



Modules
1) Module for working with streaming tasks in your application

- we connect dependences
 <repositories> <repository> <id>di_container-mvn-repo</id> <url>https://raw.github.com/GenCloud/di_container/threading/</url> <snapshots> <enabled>true</enabled> <updatePolicy>always</updatePolicy> </snapshots> </repository> </repositories> <dependencies> <dependency> <groupId>org.genfork</groupId> <artifactId>threads-factory</artifactId> <version>1.0.0.RELEASE</version> </dependency> </dependencies> 


- annotation marker for including the module in context (@ThreadingModule)
 @ThreadingModule @ScanPackage(packages = {"org.di.test"}) public class MainTest { public static void main(String... args){ IoCStarter.start(MainTest.class, args); } } 


- implementation of the module factory in the installed component of the application
 @IoCComponent public class ComponentThreads implements ThreadFactorySensible<DefaultThreadingFactory> { private final Logger log = LoggerFactory.getLogger(AbstractTask.class); private DefaultThreadingFactory defaultThreadingFactory; private final AtomicInteger atomicInteger = new AtomicInteger(0); @PostConstruct public void init() { defaultThreadingFactory.async(new AbstractTask<Void>() { @Override public Void call() { log.info("Start test thread!"); return null; } }); } @Override public void threadFactoryInform(DefaultThreadingFactory defaultThreadingFactory) throws IoCException { this.defaultThreadingFactory = defaultThreadingFactory; } @SimpleTask(startingDelay = 1, fixedInterval = 5) public void schedule() { log.info("I'm Big Daddy, scheduling and incrementing param - [{}]", atomicInteger.incrementAndGet()); } } 

* Explanations:
ThreadFactorySensible is one of the child informer classes to be implemented in the instantiated component. information (configuration, context, module, etc.).
DefaultThreadingFactory is a threading-factory module factory.

Annotation @SimpleTask is a parameterizable marker annotation to identify the component in the implementation of tasks in functions. (it starts the stream with the specified parameters by annotation and adds it to the factory, from where it can be retrieved and, for example, disable execution).

- standard task scheduling functions
  //   . ,  ,       . <T> AsyncFuture<T> async(Task<T>) //      . <T> AsyncFuture<T> async(long, TimeUnit, Task<T>) //      . ScheduledAsyncFuture async(long, TimeUnit, long, Runnable) 


*** Please note that resources in the pool of scheduled threads are limited and tasks should be performed quickly.

- default pool configuration
 # Threading threads.poolName=shared threads.availableProcessors=4 threads.threadTimeout=0 threads.threadAllowCoreTimeOut=true threads.threadPoolPriority=NORMAL 




Starting point or how it all works


We connect project dependencies:

  <repositories> <repository> <id>di_container-mvn-repo</id> <url>https://raw.github.com/GenCloud/di_container/context/</url> <snapshots> <enabled>true</enabled> <updatePolicy>always</updatePolicy> </snapshots> </repository> </repositories> ... <dependencies> <dependency> <groupId>org.genfork</groupId> <artifactId>context</artifactId> <version>1.0.0.RELEASE</version> </dependency> </dependencies> 

Test class application.

 @ScanPackage(packages = {"org.di.test"}) public class MainTest { public static void main(String... args) { IoCStarter.start(MainTest.class, args); } } 

** Explanations:
Annotation @ScanPackage - tells the context which packages should be scanned to identify the components (classes) for their injection. If the package is not specified, the package of the class marked with this annotation will be scanned.

IoCStarter # start (Object, String ...) - entry point and initialization of the application context.

Additionally, we will create several classes of components for direct verification of the functional.

ComponentA
 @IoCComponent @LoadOpt(PROTOTYPE) public class ComponentA { @Override public String toString() { return "ComponentA{" + Integer.toHexString(hashCode()) + "}"; } } 


ComponentB
 @IoCComponent public class ComponentB { @IoCDependency private ComponentA componentA; @IoCDependency private ExampleEnvironment exampleEnvironment; @Override public String toString() { return "ComponentB{hash: " + Integer.toHexString(hashCode()) + ", componentA=" + componentA + ", exampleEnvironment=" + exampleEnvironment + '}'; } } 


ComponentC
 @IoCComponent public class ComponentC { private final ComponentB componentB; private final ComponentA componentA; @IoCDependency public ComponentC(ComponentB componentB, ComponentA componentA) { this.componentB = componentB; this.componentA = componentA; } @Override public String toString() { return "ComponentC{hash: " + Integer.toHexString(hashCode()) + ", componentB=" + componentB + ", componentA=" + componentA + '}'; } } 


ComponentD
 @IoCComponent public class ComponentD { @IoCDependency private ComponentB componentB; @IoCDependency private ComponentA componentA; @IoCDependency private ComponentC componentC; @Override public String toString() { return "ComponentD{hash: " + Integer.toHexString(hashCode()) + ", ComponentB=" + componentB + ", ComponentA=" + componentA + ", ComponentC=" + componentC + '}'; } } 


* Notes:
- cyclic dependencies are not provided, there is a stub in the form of an analyzer, which, in turn, checks the resulting classes from the scanned packets and throws an exception if there is a cyclic.
** Explanations:
Annotation @IoCComponent - shows the context that it is a component and it needs to be analyzed to identify dependencies (mandatory annotation).

Annotation @IoCDependency - shows the analyzer that this is a component dependency and it must be instantiated into the component.

The @LoadOpt annotation indicates to the context which type of component load to use. Currently, 2 types are supported - SINGLETON and PROTOTYPE (single and multiple).

Expand the implementation of the main class:

Maintest
 @ScanPackage(packages = {"org.di.test", "org.di"}) public class MainTest extends Assert { private static final Logger log = LoggerFactory.getLogger(MainTest.class); private AppContext appContext; @Before public void initializeContext() { BasicConfigurator.configure(); appContext = IoCStarter.start(MainTest.class, (String) null); } @Test public void printStatistic() { DependencyFactory dependencyFactory = appContext.getDependencyFactory(); log.info("Initializing singleton types - {}", dependencyFactory.getSingletons().size()); log.info("Initializing proto types - {}", dependencyFactory.getPrototypes().size()); log.info("For Each singleton types"); for (Object o : dependencyFactory.getSingletons().values()) { log.info("------- {}", o.getClass().getSimpleName()); } log.info("For Each proto types"); for (Object o : dependencyFactory.getPrototypes().values()) { log.info("------- {}", o.getClass().getSimpleName()); } } @Test public void testInstantiatedComponents() { log.info("Getting ExampleEnvironment from context"); final ExampleEnvironment exampleEnvironment = appContext.getType(ExampleEnvironment.class); assertNotNull(exampleEnvironment); log.info(exampleEnvironment.toString()); log.info("Getting ComponentB from context"); final ComponentB componentB = appContext.getType(ComponentB.class); assertNotNull(componentB); log.info(componentB.toString()); log.info("Getting ComponentC from context"); final ComponentC componentC = appContext.getType(ComponentC.class); assertNotNull(componentC); log.info(componentC.toString()); log.info("Getting ComponentD from context"); final ComponentD componentD = appContext.getType(ComponentD.class); assertNotNull(componentD); log.info(componentD.toString()); } @Test public void testProto() { log.info("Getting ComponentA from context (first call)"); final ComponentA componentAFirst = appContext.getType(ComponentA.class); log.info("Getting ComponentA from context (second call)"); final ComponentA componentASecond = appContext.getType(ComponentA.class); assertNotSame(componentAFirst, componentASecond); log.info(componentAFirst.toString()); log.info(componentASecond.toString()); } @Test public void testInterfacesAndAbstracts() { log.info("Getting MyInterface from context"); final InterfaceComponent myInterface = appContext.getType(MyInterface.class); log.info(myInterface.toString()); log.info("Getting TestAbstractComponent from context"); final AbstractComponent testAbstractComponent = appContext.getType(TestAbstractComponent.class); log.info(testAbstractComponent.toString()); } } 


We start by means of your IDE or the command line project.

Execution result
 Connected to the target VM, address: '127.0.0.1:55511', transport: 'socket' 0 [main] INFO org.di.context.runner.IoCStarter - Start initialization of context app 87 [main] DEBUG org.reflections.Reflections - going to scan these urls: file:/C:/Users/GenCloud/Workspace/di_container/context/target/classes/ file:/C:/Users/GenCloud/Workspace/di_container/context/target/test-classes/ [main] DEBUG org.reflections.Reflections - could not scan file log4j2.xml in url file:/C:/Users/GenCloud/Workspace/di_container/context/target/test-classes/ with scanner SubTypesScanner [main] DEBUG org.reflections.Reflections - could not scan file log4j2.xml in url file:/C:/Users/GenCloud/Workspace/di_container/context/target/test-classes/ with scanner TypeAnnotationsScanner [main] INFO org.reflections.Reflections - Reflections took 334 ms to scan 2 urls, producing 21 keys and 62 values [main] INFO org.di.context.runner.IoCStarter - App context started in [0] seconds [main] INFO org.di.test.MainTest - Initializing singleton types - 6 [main] INFO org.di.test.MainTest - Initializing proto types - 1 [main] INFO org.di.test.MainTest - For Each singleton types [main] INFO org.di.test.MainTest - ------- ComponentC [main] INFO org.di.test.MainTest - ------- TestAbstractComponent [main] INFO org.di.test.MainTest - ------- ComponentD [main] INFO org.di.test.MainTest - ------- ComponentB [main] INFO org.di.test.MainTest - ------- ExampleEnvironment [main] INFO org.di.test.MainTest - ------- MyInterface [main] INFO org.di.test.MainTest - For Each proto types [main] INFO org.di.test.MainTest - ------- ComponentA [main] INFO org.di.test.MainTest - Getting ExampleEnvironment from context [main] INFO org.di.test.MainTest - ExampleEnvironment{hash: 6f96c77, nameApp='Di Container (ver. 0.0.0.2)', components=[ComponentD, ComponentC, ComponentB, ComponentA]} [main] INFO org.di.test.MainTest - Getting ComponentB from context [main] INFO org.di.test.MainTest - ComponentB{hash: be64738, componentA=ComponentA{3ba9ad43}, exampleEnvironment=ExampleEnvironment{hash: 6f96c77, nameApp='Di Container (ver. 0.0.0.2)', components=[ComponentD, ComponentC, ComponentB, ComponentA]}} [main] INFO org.di.test.MainTest - Getting ComponentC from context [main] INFO org.di.test.MainTest - ComponentC{hash: 49d904ec, componentB=ComponentB{hash: be64738, componentA=ComponentA{3ba9ad43}, exampleEnvironment=ExampleEnvironment{hash: 6f96c77, nameApp='Di Container (ver. 0.0.0.2)', components=[ComponentD, ComponentC, ComponentB, ComponentA]}}, componentA=ComponentA{48e4374}} [main] INFO org.di.test.MainTest - Getting ComponentD from context [main] INFO org.di.test.MainTest - ComponentD{hash: 3d680b5a, ComponentB=ComponentB{hash: be64738, componentA=ComponentA{3ba9ad43}, exampleEnvironment=ExampleEnvironment{hash: 6f96c77, nameApp='Di Container (ver. 0.0.0.2)', components=[ComponentD, ComponentC, ComponentB, ComponentA]}}, ComponentA=ComponentA{4b5d6a01}, ComponentC=ComponentC{hash: 49d904ec, componentB=ComponentB{hash: be64738, componentA=ComponentA{3ba9ad43}, exampleEnvironment=ExampleEnvironment{hash: 6f96c77, nameApp='Di Container (ver. 0.0.0.2)', components=[ComponentD, ComponentC, ComponentB, ComponentA]}}, componentA=ComponentA{48e4374}}} [main] INFO org.di.test.MainTest - Getting MyInterface from context [main] INFO org.di.test.MainTest - MyInterface{componentA=ComponentA{cd3fee8}} [main] INFO org.di.test.MainTest - Getting TestAbstractComponent from context [main] INFO org.di.test.MainTest - TestAbstractComponent{componentA=ComponentA{3e2e18f2}, AbstractComponent{}} [main] INFO org.di.test.MainTest - Getting ComponentA from context (first call) [main] INFO org.di.test.MainTest - ComponentA{10e41621} [main] INFO org.di.test.MainTest - Getting ComponentA from context (second call) [main] INFO org.di.test.MainTest - ComponentA{353d0772} Disconnected from the target VM, address: '127.0.0.1:55511', transport: 'socket' Process finished with exit code 0 


+ There is a built-in api parsing of configuration files (ini, xml, properties).
Rolled test is in the repository.

Future


Plans to expand and support the project as much as possible.

What I want to see:

  1. Writing additional modules - network / work with databases / writing solutions for typical tasks.
  2. Replacing Java Reflection API with CGLIB
  3. etc. (I listen to users, if any)

This is followed by the logical end of the article.

Thanks to all. I hope someone my works will be useful.
UPD. Updated article - 09/15/2018. Release 1.0.0

Source: https://habr.com/ru/post/422857/


All Articles