Реализация своего IoC контейнера +4


image

Введение


Каждый начинающий разработчик должен быть знаком с понятием Inversion of Control (Инверсия управления).

Практически каждый новый проект сейчас начинается с выбора фреймворка, с помощью которого будет реализован принцип внедрения зависимостей.

Инверсия управления (Inversion of Control, IoC) — важный принцип объектно-ориентированного программирования, используемый для уменьшения связанности в компьютерных программах и входящий в пятерку важнейших принципов SOLID.

На сегодня существуют несколько основных фреймворков по этой теме:

1. Dagger
2. Google Guice
3. Spring Framework

По сей день пользуюсь Spring и частично доволен его функционалом, но пора бы попробовать что-то и свое, не правда ли?

О себе


Зовут меня Никита, мне 24 года, и я занимаюсь java (backend) на протяжении 3 лет. Обучался только на практических примерах, параллельно пытаясь разобраться в спеках классов. На данный момент работаю (freelance) — написание CMS для коммерческого проекта, где и использую Spring Boot. Недавно посетила мысль — «Почему бы не написать свой IoC (DI) Container по своему видению и желанию?». Грубо говоря — «Захотелось своего с блекджеком...». Об этом и пойдет сегодня речь. Что ж, прошу под кат. Ссылка на исходники проекта.

Особенности


1. Главная особенность проекта — Dependency Injection.
Поддерживается 3 основных метода инъекции зависимостей:
  1. Поля класса
  2. Конструктор класса
  3. Функции класса (стандартный сеттер ан один параметр)

*Примечание:
— при сканировании класса, если использовать сразу все три метода инъекции — приоритетным будет метод инъекции через конструктор класса, помеченного аннотацией @IoCDependency. Т.е. работает всегда только один метод инъекции.

2. Ленивая инициализация компонентов (по требованию).

3. Встроенный функционал загрузки конфигурационных файлов (ini, properties, xml).

4. Пользовательская обработка аргументов командной строки.

Как происходит сканирование пакетов:
Используется Reflections API стороннего разработчика со стандартным сканером.

//{@see IocStarter#initializeContext}
private AppContext initializeContext(Class<?>... mainClasses) throws Exception {
        final AppContext context = new AppContext();
        for (Class<?> mainSource : mainClasses) {
            final Reflections reflections = configureScaner(mainSource);
            final Set<Class<?>> components = reflections.getTypesAnnotatedWith(IoCComponent.class);
            final Set<Class<? extends Analyzer>> analyzers = reflections.getSubTypesOf(Analyzer.class);
            final Set<Class<?>> properties = reflections.getTypesAnnotatedWith(Property.class);
            context.initEnvironment(properties);
            context.initAnalyzers(analyzers);
            context.initializeComponents(components);
        }

        return context;
    }

Получаем коллекцию классов с помощью фильтров аннотаций, типов.
В данном случаи это @IoCComponent, @Property и прородитель Analyzer<R, T>

Порядок инициализации контекста:
1) В первую очередь происходит инициализация конфигурационных типов.
//{@see AppContext#initEnvironment(Set)}
public void initEnvironment(Set<Class<?>> properties) {
        for (Class<?> type : properties) {
            final Property property = type.getAnnotation(Property.class);
            final Path path = Paths.get(property.path());
            try {
                final Object o = type.newInstance();
                PropertiesLoader.parse(o, path.toFile());
                dependencyFactory.addInstalledConfiguration(o);
            } catch (Exception e) {
                throw new Error("Failed to Load " + path + " Config File", e);
            }
        }
    }

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

2) Инициализация анализаторов
3) Инициализация найденных компонентов (Классы помеченные аннотацией @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);
    }

* Пояснения:
Класс 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);
                } 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;
    }
    

— метод инъекции параметров в поля класса
   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);
        }
    }
   

— метод инъекции параметров через функции класса
    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
1. ComponentProcessor — утилита позволяющая изменять компонент по собственному желанию как до его инициализации в контексте так и после.
public interface ComponentProcessor {
    Object afterComponentInitialization(String componentName, Object component);

    Object beforeComponentInitialization(String componentName, Object component);
}


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

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


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


Стартовая точка или как это все работает


Подключаем зависимости проекта:

   <repositories>
        <repository>
            <id>di_container-mvn-repo</id>
            <url>https://raw.github.com/GenCloud/di_container/mvn-repo/</url>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>always</updatePolicy>
            </snapshots>
        </repository>
    </repositories>

...

    <dependencies>
        <dependency>
            <groupId>org.genfork</groupId>
            <artifactId>context</artifactId>
            <version>0.0.2-STABLE</version>
        </dependency>
    </dependencies>


Тестовый класс приложения.

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

**Пояснения:
Аннотация @ScanPackage — указывает контексту, какие пакеты следует сканировать для идентификации компонентов (классов) для их инъекции. Если пакет не указан, будет сканироваться пакет класса, помеченного этой аннотацией.

IoCStarter#start(Object, String...) — точка входа и инициализации контекста приложения.

Дополнительно создадим несколько классов-компонентов для непосредственной проверки функционала.

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


ComponentB
@IoCComponent
public class ComponentB {
    @IoCDependency
    private ComponentA componentA;

    @IoCDependency
    private ExampleEnvironment exampleEnvironment;

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


ComponentC
@IoCComponent
public class ComponentC {
    private final ComponentB componentB;
    private final ComponentA componentA;

    @IoCDependency
    public ComponentC(ComponentB componentB, ComponentA componentA) {
        this.componentB = componentB;
        this.componentA = componentA;
    }

    @Override
    public String toString() {
        return "ComponentC{hash: " + Integer.toHexString(hashCode()) + ", componentB=" + componentB +
                ", componentA=" + componentA +
                '}';
    }
}


ComponentD
@IoCComponent
public class ComponentD {
    @IoCDependency
    private ComponentB componentB;
    @IoCDependency
    private ComponentA componentA;
    @IoCDependency
    private ComponentC componentC;

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


* Примечания:
— циклические зависимости не предусмотрены, стоит заглушка в виде анализатора, который, в свою очередь, проверяет полученные классы из отсканированных пакетов и выбрасывает исключение, если имеется циклика.
**Пояснения:
Аннотация @IoCComponent — показывает контексту, что это компонент и его нужно проанализировать для выявления зависимостей (обязательная аннотация).

Аннотация @IoCDependency — показывает анализатору, что это зависимость компонента и ее нужно инстанциировать в компонент.

Аннотация @LoadOpt — показывает контексту, какой тип загрузки компонента нужно использовать. В данный момент времени поддерживается 2 типа — SINGLETON и PROTOTYPE (единичный и множественный).

Расширим реализацию main-класса:

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


Запускаем средствами Вашей IDE или командной строкой проект.

Результат выполнения
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


+ имеется встроенное апи парсинга конфигурационных файлов (ini, xml, properties).
Обкатанный тест лежит в репозитории.

Будущее


В планах расширять и поддерживать проект на сколько это будет возможно.

Что я хочу видеть:

  1. Полная поддержка слушателей.
  2. Написание дополнительных модулей — сетевые/работа с базами данных/написание решений типовых задач.
  3. Замена Java Reflection API на CGLIB
  4. etc. (прислушиваюсь к пользователям, если таковые будут)

На этом последует логический конец статьи.

Всем спасибо. Надеюсь кому-то мои труды пригодятся.
UPD. Обновление статьи — 13.09.2018. Релиз 0.0.2.




К сожалению, не доступен сервер mySQL