Введение
Каждый начинающий разработчик должен быть знаком с понятием 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 основных метода инъекции зависимостей:
- Поля класса
- Конструктор класса
- Функции класса (стандартный сеттер ан один параметр)
*Примечание:
— при сканировании класса, если использовать сразу все три метода инъекции — приоритетным будет метод инъекции через конструктор класса, помеченного аннотацией @IoCDependency. Т.е. работает всегда только один метод инъекции.
2. Ленивая инициализация компонентов (по требованию).
3. Встроенный функционал загрузки конфигурационных файлов (ini, properties, xml).
4. Пользовательская обработка аргументов командной строки.
//{@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>
//{@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);
}
}
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...) — точка входа и инициализации контекста приложения.
Дополнительно создадим несколько классов-компонентов для непосредственной проверки функционала.
@IoCComponent
@LoadOpt(PROTOTYPE)
public class ComponentA {
@Override
public String toString() {
return "ComponentA{" + Integer.toHexString(hashCode()) + "}";
}
}
@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 +
'}';
}
}
@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 +
'}';
}
}
@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-класса:
@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).
Обкатанный тест лежит в репозитории.
Будущее
В планах расширять и поддерживать проект на сколько это будет возможно.
Что я хочу видеть:
- Полная поддержка слушателей.
- Написание дополнительных модулей — сетевые/работа с базами данных/написание решений типовых задач.
- Замена Java Reflection API на CGLIB
- etc. (прислушиваюсь к пользователям, если таковые будут)
На этом последует логический конец статьи.
Всем спасибо. Надеюсь кому-то мои труды пригодятся.
UPD. Обновление статьи — 13.09.2018. Релиз 0.0.2.
Комментарии (12)
PqDn
10.09.2018 21:44Честно говоря тоже считаю презентацию api вашего ДИ бесполезной.
Однако интересно как реализовано сканирование по пакетам, если укажете класс в исходниках, то я с удовольствием его взгляну. Ваши аннотации в чужих джарниках будут искаться?Antharas Автор
10.09.2018 22:16Класс org.di.factories.DependencyFactory#instantiateDefinitions.
Да будут — такая же схема инициализации как из примера выше.
evkin
11.09.2018 16:54Совсем недавно была статья по этому поводу: "Реализация Spring Framework API с нуля. Пошаговое руководство для начинающих. Часть 1", (https://habr.com/post/419679/) с разбором как оно работаем и примером кода, а-ля свой упрощенный спринг.
А по поводу своего велосипеда из этой статьи, в первую очередь бросаются в глаза аннотации. Что помешало взять JSR-330 и реализовать тот же @ Inject & компанию? если уж спринговые не нравятся....Antharas Автор
11.09.2018 18:091) А Вы здесь заметили копирование структуры и функционала классов, как в топике, который Вы сбросили?
2) Извините, при чем тут Spring?
3) Аннотации — да, может было бы лучше. Возможно добавлю поддержку в след. решизеevkin
11.09.2018 22:53Нет, я там заметил описание кода реализации сканирования по пакетам, в примере самописного (упрощенного) IoC и подумал, что PqDn это может быть интересно.
А на спринг вы местами сами ссылаетесь в тексте. И взять какое то промышленное решение и сделать более удобную для себя имплементацию, сохраняя частичную привычность использования и понимания, имхо более правильный подход, чем увековечивание своего "имени" в паблик апи аннотаций)))
justboris
10.09.2018 22:05Из названия и начала статьи подумал, что здесь шаг за шагом на пальцах объяснят, как написать свой DI. Но нет, оказалось просто демо очередной библиотеки.
Antharas Автор
10.09.2018 22:19Да, Вы верно подметили — с названием топика произошло фиаско, если же Вы хотите понять как все работает — прошу посмотреть исходники, каждый класс хорошо документирован
olegchir
10.09.2018 22:38Всё правильно делаешь. Не гляди на окружающих, переизобретай мир заново. Возможно, это будет не продакшен версия, но зато ты будешь понимать суть происходящего в сто раз лучше всех, кто тут кричит «не нужно» и потом использует Spring как какую-то особую магию, расставляя аннотации полным перебором вариантов.
Напиши, пожалуйста, о внутренностях своего фреймворка, с какими задачами ты встречался, и как их удалось решить. Это будет полезно.Antharas Автор
11.09.2018 02:07+1Спасибо за небольшую ремарку, принял пожелания. Как буду за рабочей станцией, обновлю топик
SkylineIT
С этого обычно начинаются все проблемы.
Ты бы хотя бы изъяснил свою мысль: чем тебя не устраивает Spring и чего ты конкретно хочешь добиться своей реализацией. А то получается, ты просто наговнакодил и вывалил это людям. Думаешь, кому — то это нужно?
Antharas Автор
Соглашусь с Вами — чего только стоило реализовать нечто подобное в «некачественной обертке». Но, проект забрасывать не собираюсь — все еще впереди. И не в том плане что что-то не устраивает в Spring.