
Présentation
Chaque développeur novice doit être familiarisé avec le concept d'inversion de contrôle.
Presque chaque nouveau projet commence maintenant par le choix d'un cadre avec lequel le principe de l'injection de dépendance sera mis en œuvre.
L'inversion de contrôle (IoC) est un principe important de la programmation orientée objet, utilisé pour réduire la cohérence des programmes informatiques et l'un des cinq principes les plus importants de SOLID.
Aujourd'hui, il existe plusieurs cadres principaux sur ce sujet:
1.
Dague2.
Google Guice3.
Cadre SpringJ'utilise toujours Spring et je suis partiellement satisfait de ses fonctionnalités, mais il est temps d'essayer quelque chose et le mien, n'est-ce pas?
À propos de moi
Je m'appelle Nikita, j'ai 24 ans et je fais du java (backend) depuis 3 ans. Il n'a étudié qu'avec des exemples pratiques, tout en essayant simultanément de comprendre les points des classes. En ce moment je travaille (freelance) - écrivant du CMS pour un projet commercial, où j'utilise Spring Boot. Récemment, j'ai visité la pensée - «Pourquoi ne pas écrire votre conteneur IoC (DI) selon votre vision et votre désir?». Grosso modo - "Je voulais le mien avec le blackjack ...". Cela sera discuté aujourd'hui. Eh bien, s'il vous plaît, sous le chat.
Lien vers les sources du projet .
CARACTÉRISTIQUES
- La principale caractéristique du projet est l'injection de dépendance.
3 méthodes d'injection de dépendance principales sont prises en charge:
- Champs de classe
- Constructeur de classe
- Fonctions de classe (setter standard pour un paramètre)
* Remarque:
- lors de l'analyse d'une classe, si les trois méthodes d'injection sont utilisées en même temps, la méthode d'injection via le constructeur de la classe marquée avec l'annotation @IoCDependency sera prioritaire. C'est-à-dire une seule méthode d'injection fonctionne toujours.
- initialisation paresseuse des composants (sur demande);
- Fichiers de configuration du chargeur intégrés (formats: ini, xml, propriétés);
- gestionnaire d'arguments en ligne de commande;
- traitement des modules en créant des usines;
- événements et auditeurs intégrés;
- informateurs intégrés (Sensibles) pour "informer" un composant, une usine, un auditeur, un processeur (ComponentProcessor) que certaines informations doivent être chargées dans l'objet, selon l'indicateur;
- un module pour gérer / créer un pool de threads, déclarer des fonctions comme tâches exécutables pendant un certain temps et les initialiser dans la fabrique de pool, ainsi que démarrer avec les paramètres SimpleTask.
Comment se produit l'analyse des paquets:Il utilise une API Reflections tierce avec un scanner standard.
Nous obtenons une collection de classes en utilisant des filtres d'annotations, de types.
Dans ce cas, il s'agit de @IoCComponent, @Property et progenitor Analyzer <R, T>
Ordre d'initialisation du contexte:1) Tout d'abord, les types de configuration sont initialisés.
* Explications:
Annotation @Property a un paramètre de chaîne obligatoire - path (chemin d'accès au fichier de configuration). C'est là que le fichier est recherché pour analyser la configuration.
La classe
PropertiesLoader est une classe utilitaire pour initialiser les champs de la classe correspondant aux champs du fichier de configuration.
Fonction DependencyFactory # addInstalledConfiguration (Object) - charge l'objet de configuration dans l'usine en tant que SINGLETON (sinon il est logique de recharger la configuration pas à la demande).
2) Initialisation des analyseurs
3) Initialisation des composants trouvés (Classes marquées avec l'annotation @IoCComponent)
* Explications:
La classe ClassAnalyzer - définit la méthode d'injection de dépendances, également s'il y a des erreurs de placement incorrect des annotations, des déclarations de constructeur, des paramètres dans la méthode - renvoie une erreur. Analyseur de fonctions <R, T> #analyze (T) - renvoie le résultat de l'analyse. Analyseur de fonctions <R, T> #supportFor (T) - renvoie un paramètre booléen en fonction des conditions spécifiées.
Fonction DependencyFactory # instantiate (Class, R) - installe le type en usine en utilisant la méthode définie par ClassAnalyzer ou lève une exception s'il y a des erreurs soit dans l'analyse soit dans le processus d'initialisation de l'objet.
3) Méthodes de numérisation
- méthode pour injecter des paramètres dans le constructeur de classe
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éthode pour injecter des paramètres dans des champs de classe
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éthode d'injection de paramètres via des fonctions de classe
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 utilisateur1. ComponentProcessor - un utilitaire qui vous permet de modifier un composant comme vous le souhaitez, à la fois avant son initialisation dans le contexte et après.
public interface ComponentProcessor { Object afterComponentInitialization(String componentName, Object component); Object beforeComponentInitialization(String componentName, Object component); }
* Explications:La fonction #afterComponentInitialization (String, Object) - vous permet de manipuler le composant après l'initialisation dans le contexte, les paramètres entrants - (nom fixe du composant, objet instancié du composant).
La fonction #beforeComponentInitialization (String, Object) - vous permet de manipuler le composant avant de l'initialiser dans le contexte, les paramètres entrants - (nom fixe du composant, objet instancié du composant).
2. CommandLineArgumentResolver
public interface CommandLineArgumentResolver { void resolve(String... args); }
* Explications:La fonction #resolve (String ...) est une interface qui
gère diverses commandes envoyées via cmd au démarrage de l'application, le paramètre d'entrée est un tableau illimité de chaînes de ligne de commande (paramètres).
3. Informateurs (sensibles) - indique que la classe enfant de l'informateur devra intégrer opr. fonctionnalité selon le type d'informateur (ContextSensible, EnvironmentSensible, ThreadFactorySensible, etc.)
4. Auditeurs
La fonctionnalité des écouteurs est implémentée, l'exécution multithread est garantie avec le nombre recommandé de descripteurs configurés pour des événements optimisés.
@org.di.context.annotations.listeners.Listener
** Explications:La fonction de répartition (événement) est la fonction principale du gestionnaire d'événements système.
- Il existe des implémentations standard d'écouteurs avec vérification des types d'événements ainsi qu'avec des filtres utilisateur intégrés {@link Filter}. Filtres standard inclus dans le package: AndFilter, ExcludeFilter, NotFilter, OrFilter, InstanceFilter (personnalisé).
Implémentations d' écouteur standard:
FilteredListener et TypedListener. Le premier utilise un filtre pour vérifier l'objet événement entrant. Le second vérifie que l'objet événement ou tout autre appartient à une instance spécifique.
Modules1) Module pour travailler avec des tâches de streaming dans votre application
- connecter les dépendances
<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>
- marqueur d'annotation pour l'inclusion du module dans le contexte (@ThreadingModule)
@ThreadingModule @ScanPackage(packages = {"org.di.test"}) public class MainTest { public static void main(String... args){ IoCStarter.start(MainTest.class, args); } }
- implémentation de la fabrique de modules dans le composant installé de l'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()); } }
* Explications:ThreadFactorySensible est l'une des classes informatrices enfants à implémenter dans le composant instancié d'ODA. informations (configuration, contexte, module, etc.).
DefaultThreadingFactory - usine de modules de threading-factory.
Annotation @SimpleTask est une annotation de marqueur paramétrable pour identifier l'implémentation des tâches dans les fonctions par le composant. (démarre le flux avec les paramètres spécifiés avec une annotation et l'ajoute à l'usine, d'où il peut être obtenu et, par exemple, désactive l'exécution).
- fonctions standard de délestage des tâches
*** Veuillez noter que les ressources du pool de threads planifiés sont limitées et que les tâches doivent être effectuées rapidement.
- configuration de pool par défaut
# Threading threads.poolName=shared threads.availableProcessors=4 threads.threadTimeout=0 threads.threadAllowCoreTimeOut=true threads.threadPoolPriority=NORMAL
Point de départ ou comment tout cela fonctionne
Nous connectons les dépendances du projet:
<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>
Application de classe de test.
@ScanPackage(packages = {"org.di.test"}) public class MainTest { public static void main(String... args) { IoCStarter.start(MainTest.class, args); } }
** Explications:
@ScanPackage annotation - indique au contexte quels packages doivent être analysés pour identifier les composants (classes) pour leur injection. Si le package n'est pas spécifié, le package de la classe marquée avec cette annotation sera analysé.
IoCStarter # start (Object, String ...) - point d'entrée et initialisation du contexte d'application.
De plus, nous allons créer plusieurs classes de composants pour vérifier directement la fonctionnalité.
Componententa @IoCComponent @LoadOpt(PROTOTYPE) public class ComponentA { @Override public String toString() { return "ComponentA{" + Integer.toHexString(hashCode()) + "}"; } }
Composantb @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 + '}'; } }
Componententc @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 + '}'; } }
Componententd @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 + '}'; } }
* Remarques:
- les dépendances cycliques ne sont pas fournies, il y a un stub sous la forme d'un analyseur, qui, à son tour, vérifie les classes reçues des packages analysés et lève une exception s'il y a une boucle.
** Explications:
@IoCComponent annotation - montre le contexte selon lequel il s'agit d'un composant et doit être analysé pour identifier les dépendances (annotation requise).
Annotation @IoCDependency - montre à l'analyseur qu'il s'agit d'une dépendance de composant et doit être instanciée dans le composant.
Annotation @LoadOpt - indique le contexte du type de chargement de composant à utiliser. Actuellement, 2 types sont pris en charge - SINGLETON et PROTOTYPE (simple et multiple).
Développons l'implémentation de la classe principale:
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()); } }
Nous démarrons le projet en utilisant votre IDE ou votre ligne de commande.
Résultat d'exécution 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
+ Il y a une analyse api intégrée des fichiers de configuration (ini, xml, propriétés).
Le test de rodage se trouve dans le référentiel.
Le futur
Prévoit d'étendre et de soutenir le projet autant que possible.
Ce que je veux voir:
- Écriture de modules supplémentaires - réseautage / utilisation de bases de données / rédaction de solutions aux problèmes courants.
- Remplacement de l'API Java Reflection par CGLIB
- etc. (J'écoute les utilisateurs, le cas échéant)
Cela sera suivi par la fin logique de l'article.
Merci à tous. J'espère que quelqu'un trouvera mon travail utile.
UPD Mise à jour de l'article - 15/09/2018.
Version 1.0.0