Инъекция зависимости с помощью библиотеки Reflection

В этой статье мы реализуем шаблоны 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 {
}
Аннотация должна иметь RetentionPolicy.RUNTIME — эта политика означает, что аннотация понадобится во время выполнения программы. (Есть еще уровни RetentionPolicy.SOURCE и RetentionPolicy.CLASS, при которых аннотация не переживает этапы компиляции и выполнения программы соответственно).

Итак, как же внедрить объект в поле с модификатором 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.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *