Implementando su contenedor IoC

imagen

Introduccion


Todo desarrollador novato debe estar familiarizado con el concepto de Inversión de control.

Casi todos los proyectos nuevos comienzan ahora con la elección de un marco con el cual se implementará el principio de inyección de dependencia.

La inversión de control (IoC) es un principio importante de la programación orientada a objetos, utilizada para reducir la coherencia en los programas informáticos y uno de los cinco principios más importantes de SOLID.

Hoy, hay varios marcos principales sobre este tema:

1. Daga
2. Google Guice
3. Marco de primavera

Todavía uso Spring y estoy parcialmente satisfecho con su funcionalidad, pero es hora de probar algo y el mío, ¿no?

Sobre mi


Mi nombre es Nikita, tengo 24 años y he estado haciendo Java (backend) durante 3 años. Estudió solo con ejemplos prácticos, mientras trataba simultáneamente de comprender las motas de las clases. En este momento estoy trabajando (freelance) - escribiendo CMS para un proyecto comercial, donde uso Spring Boot. Recientemente visité el pensamiento: "¿Por qué no escribir su Contenedor IoC (DI) de acuerdo con su visión y deseo?". En términos generales, "quería el mío con el blackjack ...". Esto se discutirá hoy. Bueno, por favor, debajo del gato. Enlace a las fuentes del proyecto .

Caracteristicas


- La característica principal del proyecto es la inyección de dependencia.
Se admiten 3 métodos principales de inyección de dependencia:
  1. Campos de clase
  2. Constructor de clase
  3. Funciones de clase (setter estándar para un parámetro)

* Nota:
- al escanear una clase, si los tres métodos de inyección se usan a la vez, el método de inyección a través del constructor de la clase marcado con la anotación @IoCDependency será una prioridad. Es decir solo un método de inyección siempre funciona.

- inicialización diferida de componentes (bajo demanda);

- Archivos de configuración del cargador incorporado (formatos: ini, xml, propiedades);

- controlador de argumento de línea de comando;

- módulos de procesamiento mediante la creación de fábricas;

- eventos incorporados y oyentes;

- informantes integrados (Sensibles) para "informar" a un componente, fábrica, oyente, procesador (ComponentProcessor) que cierta información debe cargarse en el objeto, dependiendo del informante;

- un módulo para gestionar / crear un grupo de subprocesos, declarar funciones como tareas ejecutables durante un tiempo e inicializarlas en la fábrica de grupos, así como comenzar con los parámetros de SimpleTask.

¿Cómo ocurre el escaneo de paquetes?
Utiliza una API Reflections de terceros con un escáner estándar.

//{@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; } 

Obtenemos una colección de clases usando filtros de anotaciones, tipos.
En este caso, estos son @IoCComponent, @Property y progenitor Analyzer <R, T>

Orden de inicialización del contexto:
1) En primer lugar, se inicializan los tipos de configuración.
 //{@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); } } } 

* Explicaciones:
Anotación @Property tiene un parámetro de cadena requerido: ruta (ruta al archivo de configuración). Aquí es donde se busca el archivo para analizar la configuración.
La clase PropertiesLoader es una clase de utilidad para inicializar los campos de la clase correspondientes a los campos del archivo de configuración.
Function DependencyFactory # addInstalledConfiguration (Object) : carga el objeto de configuración en la fábrica como SINGLETON (de lo contrario, tiene sentido volver a cargar la configuración no bajo demanda).

2) Inicialización de analizadores.
3) Inicialización de los componentes encontrados (Clases marcadas con la anotación @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); } 

* Explicaciones:
La clase ClassAnalyzer - define el método de inyección de dependencia, también si hay errores de colocación incorrecta de anotaciones, declaraciones de constructor, parámetros en el método - devuelve un error. Function Analyzer <R, T> #analizar (T): devuelve el resultado del análisis. Function Analyzer <R, T> #supportFor (T): devuelve un parámetro booleano según las condiciones especificadas.
Function DependencyFactory # instantiate (Class, R) : instala el tipo en la fábrica utilizando el método definido por ClassAnalyzer o genera una excepción si hay errores en el análisis o en el proceso de inicialización del objeto.

3) Métodos de escaneo
- método para inyectar parámetros en el constructor de la clase
  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; } 

- método para inyectar parámetros en campos de clase
  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); } } 

- método de inyección de parámetros a través de funciones de clase
  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); } } 



API de usuario
1. ComponentProcessor: una utilidad que le permite cambiar un componente como desee, tanto antes de su inicialización en el contexto como después.
 public interface ComponentProcessor { Object afterComponentInitialization(String componentName, Object component); Object beforeComponentInitialization(String componentName, Object component); } 


* Explicaciones:
La función #afterComponentInitialization (String, Object) - le permite manipular el componente después de inicializarlo en el contexto, parámetros entrantes - (nombre fijo del componente, objeto componente instanciado).
La función #beforeComponentInitialization (String, Object) - le permite manipular el componente antes de inicializarlo en el contexto, parámetros entrantes - (nombre fijo del componente, objeto componente instanciado).

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


* Explicaciones:
La función #resolve (String ...) es una interfaz que maneja varios comandos enviados a través de cmd cuando se inicia la aplicación, el parámetro de entrada es una matriz ilimitada de cadenas de línea de comando (parámetros).
3. Informantes (Sensibles): indica que la clase secundaria del informante deberá incrustar opr. funcionalidad según el tipo de informante (ContextSensible, EnvironmentSensible, ThreadFactorySensible, etc.)

4. Oyentes
Se implementa la funcionalidad de escuchas, se garantiza la ejecución de subprocesos múltiples con el número recomendado de descriptores configurados para eventos optimizados.
 @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; } } 

** Explicaciones:
La función de despacho (evento) es la función principal del controlador de eventos del sistema.
- Existen implementaciones estándar de oyentes con la verificación de tipos de eventos, así como con filtros de usuario integrados {@link Filter}. Filtros estándar incluidos en el paquete: AndFilter, ExcludeFilter, NotFilter, OrFilter, InstanceFilter (personalizado). Implementaciones de escucha estándar: FilteredListener y TypedListener. El primero usa un filtro para verificar el objeto de evento entrante. El segundo verifica que el objeto de evento o cualquier otro pertenezca a una instancia específica.



Módulos
1) Módulo para trabajar con tareas de transmisión en su aplicación

- conectar dependencias
 <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> 


- marcador de anotación para la inclusión del módulo en el contexto (@ThreadingModule)
 @ThreadingModule @ScanPackage(packages = {"org.di.test"}) public class MainTest { public static void main(String... args){ IoCStarter.start(MainTest.class, args); } } 


- implementación de la fábrica de módulos en el componente instalado de la aplicación
 @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()); } } 

* Explicaciones:
ThreadFactorySensible es una de las clases informantes secundarias para la implementación en el componente instanciado de ODA. información (configuración, contexto, módulo, etc.).
DefaultThreadingFactory : fábrica de módulos de subprocesamiento de fábrica.

Anotación @SimpleTask es una anotación de marcador parametrizable para identificar la implementación de tareas en las funciones del componente. (inicia la secuencia con los parámetros especificados con una anotación y la agrega a la fábrica, desde donde se puede obtener y, por ejemplo, deshabilita la ejecución).

- funciones estándar de eliminación de tareas
  //   . ,  ,       . <T> AsyncFuture<T> async(Task<T>) //      . <T> AsyncFuture<T> async(long, TimeUnit, Task<T>) //      . ScheduledAsyncFuture async(long, TimeUnit, long, Runnable) 


*** Tenga en cuenta que los recursos en el grupo de subprocesos programados son limitados y las tareas deben completarse rápidamente.

- configuración de grupo predeterminada
 # Threading threads.poolName=shared threads.availableProcessors=4 threads.threadTimeout=0 threads.threadAllowCoreTimeOut=true threads.threadPoolPriority=NORMAL 




Punto de partida o cómo funciona todo


Conectamos dependencias del proyecto:

  <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> 

Aplicación de clase de prueba.

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

** Explicaciones:
Anotación @ScanPackage : le dice al contexto qué paquetes deben escanearse para identificar componentes (clases) para su inyección. Si no se especifica el paquete, se analizará el paquete de la clase marcada con esta anotación.

IoCStarter # start (Object, String ...) : punto de entrada e inicialización del contexto de la aplicación.

Además, crearemos varias clases de componentes para verificar directamente la funcionalidad.

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


Componente b
 @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 + '}'; } } 


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


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


* Notas:
- no se proporcionan dependencias cíclicas, hay un código auxiliar en forma de analizador que, a su vez, verifica las clases recibidas de los paquetes escaneados y genera una excepción si hay un bucle.
** Explicaciones:
Anotación @IoCComponent : muestra el contexto de que este es un componente y debe analizarse para identificar dependencias (anotación requerida).

Anotación @IoCDependency : muestra al analizador que se trata de una dependencia del componente y que se debe instanciar en el componente.

Anotación @LoadOpt : muestra el contexto qué tipo de carga de componentes se debe usar. Actualmente, se admiten 2 tipos: SINGLETON y PROTOTYPE (simple y múltiple).

Expandamos la implementación de la clase principal:

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()); } } 


Comenzamos el proyecto usando su IDE o línea de comando.

Resultado de ejecución
 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 


+ Hay un análisis de API integrado de archivos de configuración (ini, xml, propiedades).
La prueba de ejecución está en el repositorio.

El futuro


Planea expandir y apoyar el proyecto tanto como sea posible.

Lo que quiero ver:

  1. Escribir módulos adicionales: trabajar en red / trabajar con bases de datos / escribir soluciones a problemas comunes.
  2. Reemplazar la API de Java Reflection con CGLIB
  3. etc. (Escucho a los usuarios, si los hay)

Esto será seguido por el final lógico del artículo.

Gracias a todos. Espero que alguien encuentre mi trabajo útil.
UPD Actualización del artículo - 15/09/2018. Release 1.0.0

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


All Articles