Implementando seu contêiner de IoC

imagem

1. Introdução


Todo desenvolvedor iniciante deve estar familiarizado com o conceito de Inversão de controle.

Quase todo novo projeto começa agora com a escolha de uma estrutura com a qual o princípio da injeção de dependência será implementado.

A inversão de controle (IoC) é um princípio importante da programação orientada a objetos, usada para reduzir a coerência em programas de computador e um dos cinco princípios mais importantes do SOLID.

Hoje, existem várias estruturas principais sobre esse tópico:

1. Adaga
2. Google Guice
3. Estrutura da Primavera

Ainda uso o Spring e estou parcialmente satisfeito com sua funcionalidade, mas é hora de tentar algo meu, não é?

Sobre mim


Meu nome é Nikita, tenho 24 anos e faço java (back-end) há 3 anos. Ele estudou apenas com exemplos práticos, ao mesmo tempo em que tentava entender as manchas das aulas. No momento, estou trabalhando (freelance) - escrevendo CMS para um projeto comercial, onde uso o Spring Boot. Recentemente, visitei o pensamento - “Por que não escrever seu IoC (DI) Container de acordo com sua visão e desejo?”. Grosso modo - "Eu queria o meu próprio com blackjack ...". Isso será discutido hoje. Bem, por favor, sob gato. Link para fontes do projeto .

Funcionalidades


- A principal característica do projeto é a injeção de dependência.
São suportados três métodos principais de injeção de dependência:
  1. Campos de classe
  2. Construtor de classe
  3. Funções de classe (setter padrão para um parâmetro)

* Nota:
- ao digitalizar uma classe, se todos os três métodos de injeção forem usados ​​ao mesmo tempo, o método de injeção através do construtor da classe marcado com a anotação @IoCDependency será uma prioridade. I.e. apenas um método de injeção sempre funciona.

- inicialização lenta de componentes (sob demanda);

- Arquivos de configuração de carregador embutidos (formatos: ini, xml, properties);

- manipulador de argumentos da linha de comando;

- módulos de processamento através da criação de fábricas;

- eventos internos e ouvintes;

- informantes embutidos (Sensibles) para "informar" um componente, fábrica, ouvinte, processador (ComponentProcessor) de que determinadas informações devem ser carregadas no objeto, dependendo do informador;

- um módulo para gerenciar / criar um conjunto de encadeamentos, declarando funções como tarefas executáveis ​​por algum tempo e inicializando-os na fábrica de conjuntos, bem como iniciando com os parâmetros SimpleTask.

Como acontece a varredura de pacotes:
Ele usa uma API de reflexões de terceiros com um scanner padrão.

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

Temos uma coleção de classes usando filtros de anotações, tipos.
Nesse caso, são @IoCComponent, @Property e progenitor Analyzer <R, T>

Ordem de inicialização do contexto:
1) Primeiro, os tipos de configuração são inicializados.
 //{@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); } } } 

* Explicações:
A anotação @Property possui um parâmetro de string obrigatório - path (caminho para o arquivo de configuração). É aqui que o arquivo é pesquisado para analisar a configuração.
A classe PropertiesLoader é uma classe de utilitário para inicializar os campos da classe correspondentes aos campos do arquivo de configuração.
Função DependencyFactory # addInstalledConfiguration (Object) - carrega o objeto de configuração na fábrica como SINGLETON (caso contrário, faz sentido reiniciar a configuração não sob demanda).

2) Inicialização de analisadores
3) Inicialização dos componentes encontrados (Classes marcadas com a anotação @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); } 

* Explicações:
A classe ClassAnalyzer - define o método de injeção de dependência, também se houver erros de posicionamento incorreto de anotações, declarações de construtor, parâmetros no método - retorna um erro. Analisador de Função <R, T> #analyze (T) - retorna o resultado da análise. Analisador de função <R, T> #supportFor (T) - retorna um parâmetro booleano, dependendo das condições especificadas.
Função DependencyFactory # instantiate (Class, R) - instala o tipo na fábrica usando o método definido por ClassAnalyzer ou lança uma exceção se houver erros na análise ou no processo de inicialização do objeto.

3) Métodos de digitalização
- método para injetar parâmetros no construtor 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étodo para injetar parâmetros nos campos da 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étodo de injeção de parâmetros através de funções 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 do usuário
1. ComponentProcessor - um utilitário que permite alterar um componente como você deseja, antes de sua inicialização no contexto e depois.
 public interface ComponentProcessor { Object afterComponentInitialization(String componentName, Object component); Object beforeComponentInitialization(String componentName, Object component); } 


* Explicações:
A função #afterComponentInitialization (String, Object) - permite manipular o componente após inicializá-lo no contexto, parâmetros de entrada - (nome fixo do componente, objeto instanciado do componente).
A função #beforeComponentInitialization (String, Object) - permite manipular o componente antes de inicializá-lo no contexto, parâmetros de entrada - (nome fixo do componente, objeto instanciado do componente).

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


* Explicações:
A função #resolve (String ...) é uma interface que lida com vários comandos enviados por meio do cmd quando o aplicativo é iniciado; o parâmetro de entrada é uma matriz ilimitada de strings da linha de comando (parâmetros).
3. Informantes (Sensíveis) - indica que a classe infantil do informante precisará incorporar opr. funcionalidade, dependendo do tipo de informante (ContextSensible, EnvironmentSensible, ThreadFactorySensible, etc.)

4. Ouvintes
A funcionalidade dos ouvintes é implementada, a execução de vários threads é garantida com o número recomendado de descritores configurados para eventos otimizados.
 @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; } } 

** Explicações:
A função de expedição (evento) é a principal função do manipulador de eventos do sistema.
- Existem implementações padrão de ouvintes com verificação de tipos de eventos e também com filtros de usuário integrados {@link Filter}. Filtros padrão incluídos no pacote: AndFilter, ExcludeFilter, NotFilter, OrFilter, InstanceFilter (personalizado). Implementações de ouvinte padrão: FilteredListener e TypedListener. O primeiro usa um filtro para verificar o objeto de evento recebido. O segundo verifica se o objeto de evento ou qualquer outro pertence a uma instância específica.



Módulos
1) Módulo para trabalhar com tarefas de streaming em seu aplicativo

- conectar dependências
 <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 anotação para inclusão do módulo no contexto (@ThreadingModule)
 @ThreadingModule @ScanPackage(packages = {"org.di.test"}) public class MainTest { public static void main(String... args){ IoCStarter.start(MainTest.class, args); } } 


- implementação da fábrica de módulos no componente instalado do aplicativo
 @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()); } } 

* Explicações:
ThreadFactorySensible é uma das classes de informante filho para implementação no componente instanciado da ODA. informações (configuração, contexto, módulo etc.).
DefaultThreadingFactory - fábrica do módulo threading-factory.

Anotação @SimpleTask é uma anotação de marcador parametrizável para identificar a implementação de tarefas do componente em funções. (inicia o fluxo com os parâmetros especificados com uma anotação e o adiciona à fábrica, de onde pode ser obtido e, por exemplo, desabilita a execução).

- funções padrão de organização de tarefas
  //   . ,  ,       . <T> AsyncFuture<T> async(Task<T>) //      . <T> AsyncFuture<T> async(long, TimeUnit, Task<T>) //      . ScheduledAsyncFuture async(long, TimeUnit, long, Runnable) 


*** Observe que os recursos no pool de threads agendados são limitados e as tarefas devem ser concluídas rapidamente.

- configuração padrão do pool
 # Threading threads.poolName=shared threads.availableProcessors=4 threads.threadTimeout=0 threads.threadAllowCoreTimeOut=true threads.threadPoolPriority=NORMAL 




Ponto de partida ou como tudo funciona


Conectamos as dependências do projeto:

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

Aplicativo de classe de teste.

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

** Explicações:
Anotação @ScanPackage - informa ao contexto quais pacotes devem ser verificados para identificar componentes (classes) para a injeção. Se nenhum pacote for especificado, o pacote da classe marcada com esta anotação será verificado.

IoCStarter # start (Object, String ...) - ponto de entrada e inicialização do contexto do aplicativo.

Além disso, criaremos várias classes de componentes para verificar diretamente a funcionalidade.

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


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


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


* Notas:
- dependências cíclicas não são fornecidas, existe um esboço na forma de um analisador, que, por sua vez, verifica as classes recebidas dos pacotes varridos e lança uma exceção se houver um loop.
** Explicações:
Anotação @IoCComponent - mostra o contexto de que este é um componente e precisa ser analisado para identificar dependências (anotação necessária).

Anotação @IoCDependency - mostra ao analisador que essa é uma dependência de componente e deve ser instanciada no componente.

Anotação @LoadOpt - mostra o contexto que tipo de carregamento de componente deve ser usado. Atualmente, dois tipos são suportados - SINGLETON e PROTOTYPE (único e múltiplo).

Vamos expandir a implementação da classe 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()); } } 


Iniciamos o projeto usando seu IDE ou linha de comando.

Resultado de execução
 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 


+ Existe uma API embutida que analisa os arquivos de configuração (ini, xml, properties).
O teste de execução está no repositório.

O futuro


Planeja expandir e apoiar o projeto o máximo possível.

O que eu quero ver:

  1. Escrevendo módulos adicionais - rede / trabalhando com bancos de dados / escrevendo soluções para problemas comuns.
  2. Substituindo a API Java Reflection pelo CGLIB
  3. etc. (Ouço usuários, se houver)

Isso será seguido pelo final lógico do artigo.

Obrigado a todos. Espero que alguém ache meu trabalho útil.
UPD Atualização do artigo - 15/09/2018. Release 1.0.0

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


All Articles