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 and are 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 a blackjack ...”. This will be discussed today. Well, please under the cat. Link to project sources .

Features


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

* Note:
- when scanning a class, if you use all three injection methods at once - the injection method through the class constructor marked with the @IoCDependency annotation will take 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 informants (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)}publicvoidinitEnvironment(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) {
                thrownew Error("Failed to Load " + path + " Config File", e);
            }
        }
    }

* Пояснения:
Аннотация @Property имеет обязательный строковый параметр — path (путь к файлу конфигурации). Именно по нему ведется поиск файла для парсинга конфигурации.
Класс PropertiesLoader — класс-утилита для инициализирования полей класса соответствующих полям файла конфигурации.
Функция DependencyFactory#addInstalledConfiguration(Object) — загружает объект конфигурации в фабрику как SINGLETON (иначе смысл перезагружать конфиг не по требованию).

2) Инициализация анализаторов
3) Инициализация найденных компонентов (Классы помеченные аннотацией @IoCComponent)
//{@see AppContext#scanClass(Class)}privatevoidscanClass(Class<?> component){
        final ClassAnalyzer classAnalyzer = getAnalyzer(ClassAnalyzer.class);
        if (!classAnalyzer.supportFor(component)) {
            thrownew IoCInstantiateException("It is impossible to test, check the class for type match!");
        }
        final ClassAnalyzeResult result = classAnalyzer.analyze(component);
        dependencyFactory.instantiate(component, result);
    }

* Пояснения:
Класс ClassAnalyzer — определяет метод инъекции зависимостей, так же если имеются ошибки неверной расстановки аннотаций, объявлений конструктора, параметров в методе — возвращает ошибку. Функция Analyzer<R, T>#analyze(T) — возвращает результат выполнения анализа . Функция Analyzer<R, T>#supportFor(Т) — возвращает булевый параметр в зависимости от прописанных условий.
Функция DependencyFactory#instantiate(Class, R) — инсталлирует тип в фабрику методом, определенном ClassAnalyzer или выбрасывает исключение если имееются ошибки либо анализа либо самого процесса инициализации объекта.

3) Методы сканирования
— метод инъекции параметров в конструктор класса
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);
                } elseif (isPrototype(type)) {
                    prototypes.put(typeName, instance);
                }
                return instance;
            } catch (Exception e) {
                thrownew IoCInstantiateException("IoCError - Unavailable create instance of type [" + type + "].", e);
            }
        }
        returnnull;
    }
    

— метод инъекции параметров в поля класса
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();
                finalboolean 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);
            } elseif (isPrototype(type)) {
                prototypes.put(typeName, instance);
            }
            return instance;
        } catch (Exception e) {
            thrownew IoCInstantiateException("IoCError - Unavailable create instance of type [" + type + "].", e);
        }
    }
   

— метод инъекции параметров через функции класса
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);
            } elseif (isPrototype(type)) {
                prototypes.put(typeName, instance);
            }
            return instance;
        } catch (Exception e) {
            thrownew IoCInstantiateException("IoCError - Unavailable create instance of type [" + type + "].", e);
        }
    }
   



User API
1. ComponentProcessor — утилита позволяющая изменять компонент по собственному желанию как до его инициализации в контексте так и после.
publicinterfaceComponentProcessor{
    Object afterComponentInitialization(String componentName, Object component);
    Object beforeComponentInitialization(String componentName, Object component);
}


*Пояснения:
Функция #afterComponentInitialization(String, Object) — позволяет проводить манипуляции с компонентом после инициализации его в контексте, входящие параметры — (закрепленной название компонента, инстанциированный объект компонента).
Функция #beforeComponentInitialization(String, Object) — позволяет проводить манипуляции с компонентом перед инициализацией его в контексте, входящие параметры — (закрепленной название компонента, инстанциированный объект компонента).

2. CommandLineArgumentResolver
publicinterfaceCommandLineArgumentResolver{
    voidresolve(String... args);
}


*Пояснения:
Функция #resolve(String...) — интерфейс-обработчик различных команд переданных через cmd при запуске приложения, входящий параметр — неограниченный массив строк (параметров) командной строки.
3. Информаторы (Sensibles) — указывает, что дочернему классу информатора нужно будет встроить опр. функционал в зависимости от типа информатора (ContextSensible, EnvironmentSensible, ThreadFactorySensible, etc.)

4. Слушатели (Listener's)
Реализован функционал слушателей, гарантировано multi-threading выполнение с настройкой рекомендуемого количества дескрипторов для оптимизированной работы событий.
@org.di.context.annotations.listeners.Listener // обязательный аннотация-маркер@IoCComponent// обязательный аннотация, в случае ее отсутствия реализация информеров (Sensibles) не будет интегрирована.publicclassTestListenerimplementsListener{
    privatefinal Logger log = LoggerFactory.getLogger(TestListener.class);
    @Overridepublicbooleandispatch(Event event){
        if (OnContextStartedEvent.class.isAssignableFrom(event.getClass())) {
            log.info("ListenerInform - Context is started! [{}]", event.getSource());
        } elseif (OnContextIsInitializedEvent.class.isAssignableFrom(event.getClass())) {
            log.info("ListenerInform - Context is initialized! [{}]", event.getSource());
        } elseif (OnComponentInitEvent.class.isAssignableFrom(event.getClass())) {
            final OnComponentInitEvent ev = (OnComponentInitEvent) event;
            log.info("ListenerInform - Component [{}] in instance [{}] is initialized!", ev.getComponentName(), ev.getSource());
        }
        returntrue;
    }
}

** Пояснения:
Функция dispatch(Event) — главная функция обработчик событий системы.
— Присутствуют стандартные реализации слушателей с проверкой на типы событий а так же со встраиваемыми пользовательскими фильтрами {@link Filter}. Стандартные фильтры входящие в пакет: AndFilter, ExcludeFilter, NotFilter, OrFilter, InstanceFilter (custom). Стандартные реализации слушателей: FilteredListener и TypedListener. Первый задействует в себе фильтр для проверки входящего объекта события. Второй осуществляет проверку событийного объекта либо же любого другого на принадлежность к определенному инстансу.



Modules
1) Модуль для работы с потоковыми задачами в Вашем приложении

— подключаем зависимости
<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>


— маркер-аннотация для включения модуля в контекст (@ThreadingModule)
@ThreadingModule@ScanPackage(packages = {"org.di.test"})
publicclassMainTest{
    publicstaticvoidmain(String... args){
      IoCStarter.start(MainTest.class, args);
    }
}


— внедрение фабрики модуля в инсталлируемый компонент приложения
@IoCComponentpublicclassComponentThreadsimplementsThreadFactorySensible<DefaultThreadingFactory> {
    privatefinal Logger log = LoggerFactory.getLogger(AbstractTask.class);
    private DefaultThreadingFactory defaultThreadingFactory;
    privatefinal AtomicInteger atomicInteger = new AtomicInteger(0);
    @PostConstructpublicvoidinit(){
        defaultThreadingFactory.async(new AbstractTask<Void>() {
            @Overridepublic Void call(){
                log.info("Start test thread!");
                returnnull;
            }
        });
    }
    @OverridepublicvoidthreadFactoryInform(DefaultThreadingFactory defaultThreadingFactory)throws IoCException {
        this.defaultThreadingFactory = defaultThreadingFactory;
    }
    @SimpleTask(startingDelay = 1, fixedInterval = 5)
    publicvoidschedule(){
        log.info("I'm Big Daddy, scheduling and incrementing param - [{}]", atomicInteger.incrementAndGet());
    }
}

*Пояснения:
ThreadFactorySensible — один из дочерних классов-информаторов для внедрения в инстанциируемый компонент опр. информации (конфигурации, контекста, модуля, etc.).
DefaultThreadingFactory — фабрика модуля threading-factory.

Аннотация @SimpleTask — параметризируемый маркер-аннотация для выявления у компонента реализации задач в функциях. (запускает поток с заданными параметрами аннотацией и добавляет его в фабрику, откуда его можно будет достать и, к примеру, отключить выполнение).

— стандартные функции шедулинга задач
// Выполняет асинхронные задачи. Задачи, запланированные здесь, перейдут в пул разделяемых потоков по умолчанию.
    <T> AsyncFuture<T> async(Task<T>)// Выполняет асинхронные задачи в запланированное время.
    <T> AsyncFuture<T> async(long, TimeUnit, Task<T>)// Выполняет асинхронные задачи в фиксированное время.
    ScheduledAsyncFuture async(long, TimeUnit, long, Runnable)


***Обратите внимание, что ресурсы в пуле запланированных потоков ограничены, и задачи должны выполняться быстро.

— дефолтная конфигурация пула
# 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"})
publicclassMainTest{
    publicstaticvoidmain(String... args){
        IoCStarter.start(MainTest.class, args);
    }
}

** Explanation: The
@ScanPackage annotation indicates to the context which packages should be scanned to identify 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)
publicclassComponentA{
    @Overridepublic String toString(){
        return"ComponentA{" + Integer.toHexString(hashCode()) + "}";
    }
}


ComponentB
@IoCComponentpublicclassComponentB{
    @IoCDependencyprivate ComponentA componentA;
    @IoCDependencyprivate ExampleEnvironment exampleEnvironment;
    @Overridepublic String toString(){
        return"ComponentB{hash: " + Integer.toHexString(hashCode()) + ", componentA=" + componentA +
                ", exampleEnvironment=" + exampleEnvironment +
                '}';
    }
}


ComponentC
@IoCComponentpublicclassComponentC{
    privatefinal ComponentB componentB;
    privatefinal ComponentA componentA;
    @IoCDependencypublicComponentC(ComponentB componentB, ComponentA componentA){
        this.componentB = componentB;
        this.componentA = componentA;
    }
    @Overridepublic String toString(){
        return"ComponentC{hash: " + Integer.toHexString(hashCode()) + ", componentB=" + componentB +
                ", componentA=" + componentA +
                '}';
    }
}


ComponentD
@IoCComponentpublicclassComponentD{
    @IoCDependencyprivate ComponentB componentB;
    @IoCDependencyprivate ComponentA componentA;
    @IoCDependencyprivate ComponentC componentC;
    @Overridepublic 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 must be analyzed to identify dependencies (mandatory annotation).

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

The @LoadOpt annotation tells the context what type of component loading 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"})
publicclassMainTestextendsAssert{
    privatestaticfinal Logger log = LoggerFactory.getLogger(MainTest.class);
    private AppContext appContext;
    @BeforepublicvoidinitializeContext(){
        BasicConfigurator.configure();
        appContext = IoCStarter.start(MainTest.class, (String) null);
    }
    @TestpublicvoidprintStatistic(){
        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());
        }
    }
    @TestpublicvoidtestInstantiatedComponents(){
        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());
    }
    @TestpublicvoidtestProto(){
        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());
    }
    @TestpublicvoidtestInterfacesAndAbstracts(){
        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.

Result of performance
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. Updating the article - 09/15/2018. Release 1.0.0

Also popular now: