В этой статье мы реализуем шаблоны Dependency Injection и Inversion of Control — на этих шаблонах основана Spring Framework.
Для реализации этих шаблонов используется библиотека Reflection, и мы в упрощенной форме сделаем инъекцию зависимости так, как это делается в Spring.
Шаблон Dependency Injection (DI)
В шаблоне DI ничего сложного нет. У любого объекта есть поля, а значения полей — это зависимости. Зависимости полям присваиваются через сеттеры либо конструкторы. Присвоение значения полю — это и есть инъекция.
Например, есть класс Component, а у него есть поле Service1:
public class Component { private Service1 service1; public void setService1(Service1 service1) { this.service1 = service1; } public Service1 getService1() { return service1; } }
Инъекция зависимости — это когда мы присваиваем (например, с помощью сеттера) полю service1 объект Service1:
Service1 service1 = new Service1(); new Component().setService1(service1);
Как видите, это стандартный код, встречающийся повсеместно.
Особенность Spring в том, что в нем используется еще и шаблон Inversion of Control. Это означает, что создание и инъекция зависимостей передается под управление фреймворка.
Inversion of Control
Суть этого шаблона состоим в том, что разработчик не создает и не присваивает зависимости сам, как это показано в коде выше. Он просто помечает аннотациями поле (либо сеттер, либо конструктор), в нашем случае мы рассмотрим поле. И фреймворк сам создает и присваивает данному полю зависимость.
В нашем примере создание и присвоение будет делать не фреймворк, а очень простой обработчик, а именно метод Tester.inject(Component component).
Класс Component будет выглядеть так, в нем даже не нужен сеттер:
public class Component { @Autowire private Service1 service1; @Autowire private Service2 service2; public Service1 getService1() { return service1; } public Service2 getService2() { return service2; } }
А геттеры мы добавили для будущего тестирования сервисов, у нас их два:
public class Service1 { public void method() { System.out.println("Service1 method"); } } public class Service2 { public void method() { System.out.println("Service2 method"); } }
Обратите внимание, что поля, в которые надо внедрить объекты, помечены аннотацией @Autowired. В Spring есть аннотация с таким же названием, но у нас своя:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Autowire { }
Итак, как же внедрить объект в поле с модификатором private, если для него даже нет сеттера? Spring это делает с помощью Reflection, и мы сделаем так же.
Внедрение зависимости с помощью Reflection
Как уже упоминалось, внедрять зависимость мы будем в статическом методе inject():
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; public class Tester { public static void main(String... args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { //... } public static void inject(Component component) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { for (Field field : component.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(Autowire.class)) { Constructor constructor = field.getType().getDeclaredConstructor(); Object o=constructor.newInstance(); boolean isAccessible = field.canAccess(component); field.setAccessible(true); field.set(component, o); field.setAccessible(isAccessible); } } } }
В вышеприведенном коде мы находим все поля, помеченные аннотацией @Autowire, берем объект Class для поля, вызываем конструктор и присваиваем полю созданный объект. Чтобы присвоить объект, временно меняем модификатор доступа к полю — как видите, это также можно сделать с помощью рефлексии.
Все, теперь можно запустить наш метод, который и играет роль того самого фреймворка, внедряющего зависимости. Ему передается управление созданием и внедрением объектов, а разработчик только помечает свой код аннотациями, указывая таким образом, куда надо автоматически внедрить объект.
Тестирование
Запустим метод inject() в методе main():
public static void main(String... args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { Component a= new Component(); Tester.inject(a); a.getService1().method(); a.getService2().method(); }
Здесь мы создали компонент Component сами, а обработчику отдали только создание и инъекцию зависимостей. На самом деле так же можно создать Component на основе аннотаций, воспользовавшись рефлексией, и тогда Inversion of Control будет полным.
В результате выполнения программы консоль будет выведен текст, который печатают методы:
Service1 method Service2 method
Исходный код примера доступен на GitHub.