Core Java. Лекция 13

Dependency Injection. Принцип работы DI-контейнера. Принцип работы DI-контейнера (окончание). Spring Framework (Spring DI, Spring AOP)

Иван Пономарёв, КУРС/МФТИ

Проблемы GoF-cинглетона

Как автоматически тестировать компоненты, которые жёстко связаны со своими зависимостями через статические поля/методы?

 — Да никак!

Проблемы GoF-cинглетона

  • Увеличивает Coupling

  • Нарушает Single Responsibility Principle

  • Невозможно нормально тестировать

Наш учебный пример про доклады и спикеров

objects

Фабрика

// вынесли весь "wiring" из классов в фабричный метод
public static Controller makeController() {
  DataSource dataSource =
                new DataSource(ConnectionPool.getInstance());
  ConferenceDao conferenceDao = new ConferenceDao(dataSource);
  SpeakerDao speakerDao = new SpeakerDao(dataSource);
  TalkDao talkDao = new TalkDao(dataSource);
  return new Controller(conferenceDao, speakerDao, talkDao);
}

Фабрика

  • Мы отвязались от «wiring» и можем создавать компоненты по отдельности, что позволяет тестировать.

  • Однако в фабричном методе много повторяющихся действий, фабрика должна «знать» про все компоненты.

  • Вообще этот процесс можно автоматизировать!

Dependency Injection

di class

Dependency Injection

di seq

DI Frameworks

  • Google Guice

  • JBoss Seam Framework

  • PicoContainer

  • Spring

Построим свой DI-фреймворк «на коленке»

  • Пример на базе тренинга Евгения Борисова «Spring Ripper»

  • Позволяет понять логику и внутреннее устройство Spring Framework

Лектор-робот

robotlecturer
public class RobotLecturer {
    //«магическим» образом пусть тут появится то, что нужно!
    @InjectByType
    private Lecture lecture;
    @InjectByType
    private Speaker speaker;
    @InjectByType
    private SlideShow slideShow;
    ...
}

Режим лектора

//Читать лекции просто
public void lecture() {
    lecture.getSlides().forEach(
        slide -> {
            slideShow.show(slide.getText());
            speaker.speak(slide.getComment());
        }
    );
}

//Main-метод
public static void main(String[] args)
                  throws ReflectiveOperationException {
    RobotLecturer lecturer =
        new ObjectFactory().createObject(RobotLecturer.class);
    lecturer.lecture();
}

Конфигуратор объекта

@RequiredArgsConstructor
public class InjectByTypeAnnotationObjectConfigurator
                             implements ObjectConfigurator {
  //передадим сюда через конструктор ObjectFactory
  private final ObjectFactory factory;

  @Override
  public void configure(Object t) throws ... {
    for (Field field : t.getClass().getDeclaredFields()) {
      if (field.isAnnotationPresent(InjectByType.class)) {
        field.setAccessible(true);
        //Мы же умеем по типу создавать объект?
        field.set(t, factory.createObject(field.getType()));
      }
    }
  }
}

Как создаётся и конфигурируется объект

public class ObjectFactory {
  //Правда, тут напрашивается DI?
  private final Reflections scanner =
                          new Reflections("edu.phystech");
  private final List<ObjectConfigurator> configurators =
                          new ArrayList<>();

  public <T> T createObject(Class<? extends T> type) throws ... {
    //Находим реализацию запрошенного типа
    type = resolveImpl(type);
    //Создаём объект (с помощью конструктора по умолчанию, TODO)
    T t = type.newInstance();
    //Конфигурируем
    configure(t);
    ....
  }

Метод configure очень прост

  private <T> void configure(T t) throws ... {
    for (ObjectConfigurator configurator : configurators) {
      configurator.configure(t);
    }
  }

Как находится подходящая имплементация

private <T> Class<? extends T> resolveImpl(Class<? extends T> type){
  if (type.isInterface()) {
    Set<Class<? extends T>> classes =
                         scanner.getSubTypesOf((Class<T>) type);
    if (classes.size() != 1) {
      throw new RuntimeException(
         "0 or more than one implementation found for type "
         + type + " please update your config");
    }
    type = classes.iterator().next();
  }
  return type;
}

Лектор-робот

robotlecturer
public class RobotLecturer {
    //«магическим» образом пусть тут появится то, что нужно!
    @InjectByType
    private Lecture lecture;
    @InjectByType
    private Speaker speaker;
    @InjectByType
    private SlideShow slideShow;
    ...
}

Режим лектора

//Читать лекции просто
public void lecture() {
    lecture.getSlides().forEach(
        slide -> {
            slideShow.show(slide.getText());
            speaker.speak(slide.getComment());
        }
    );
}

//Main-метод
public static void main(String[] args)
                  throws ReflectiveOperationException {
    RobotLecturer lecturer =
        new ObjectFactory().createObject(RobotLecturer.class);
    lecturer.lecture();
}

Конфигуратор объекта

@RequiredArgsConstructor
public class InjectByTypeAnnotationObjectConfigurator
                             implements ObjectConfigurator {
  //передадим сюда через конструктор ObjectFactory
  private final ObjectFactory factory;

  @Override
  public void configure(Object t) throws ... {
    for (Field field : t.getClass().getDeclaredFields()) {
      if (field.isAnnotationPresent(InjectByType.class)) {
        field.setAccessible(true);
        //Мы же умеем по типу создавать объект?
        field.set(t, factory.createObject(field.getType()));
      }
    }
  }
}

Как создаётся и конфигурируется объект

public class ObjectFactory {
  //Правда, тут напрашивается DI?
  private final Reflections scanner =
                          new Reflections("edu.phystech");
  private final List<ObjectConfigurator> configurators =
                          new ArrayList<>();

  public <T> T createObject(Class<? extends T> type) throws ... {
    //Находим реализацию запрошенного типа
    type = resolveImpl(type);
    //Создаём объект (с помощью конструктора по умолчанию, TODO)
    T t = type.newInstance();
    //Конфигурируем
    configure(t);
    ....
  }

Метод configure очень прост

  private <T> void configure(T t) throws ... {
    for (ObjectConfigurator configurator : configurators) {
      configurator.configure(t);
    }
  }

Как находится подходящая имплементация

private <T> Class<? extends T> resolveImpl(Class<? extends T> type){
  if (type.isInterface()) {
    Set<Class<? extends T>> classes =
                         scanner.getSubTypesOf((Class<T>) type);
    if (classes.size() != 1) {
      throw new RuntimeException(
         "0 or more than one implementation found for type "
         + type + " please update your config");
    }
    type = classes.iterator().next();
  }
  return type;
}

Ищем конфигураторы автоматически!

//Конструктор ObjectFactory
public ObjectFactory() throws ReflectiveOperationException {
  Set<Class<? extends ObjectConfigurator>> classes =
              scanner.getSubTypesOf(ObjectConfigurator.class);
  for (Class<? extends ObjectConfigurator> aClass : classes) {
    try {
      Constructor<? extends ObjectConfigurator> constructor =
                       aClass.getConstructor(ObjectFactory.class);
      //инжектим себя через конструктор, по необходимости
      configurators.add(constructor.newInstance(this));
    } catch (NoSuchMethodException e){
      configurators.add(aClass.newInstance());
    }
  }
....//продолжение следует

Больше конфигураторов!

@Retention(RUNTIME)
public @interface InjectRandomInt {
    int min();
    int max();
}

InjectRandomIntObjectConfigurator

public class InjectRandomIntObjectConfigurator
                                 implements ObjectConfigurator {
  @Override
  public void configure(Object t) throws IllegalAccessException {
    Class<?> type = t.getClass();
    for (Field field : ReflectionUtils.getAllFields(type)) {
      InjectRandomInt annotation =
                        field.getAnnotation(InjectRandomInt.class);
      if (annotation != null) {
        int min = annotation.min();
        int max = annotation.max();
        int value = ThreadLocalRandom.current().nextInt(min, max+1);
        field.setAccessible(true);
        field.set(t, value);
      }
    }
  }
}

Итак, первые шаги:

init1

Инициализация объекта

  • Почему конструктор не годится для действий, включающих в себя инъектированные объекты?

  • Инъекция может происходить после конструктора. Поэтому нужно специальное действие, вызываемое после конструктора и инъекции!

PostConstruct

public class RobotLecturer {
  @InjectByType
  private Lecture lecture;
  @InjectRandomInt(min = 1, max = 3)
  private int repeat;

  @PostConstruct
  public void init() {
    //Место, где можно использовать все инжектированные значения
    for (int i = 0; i < repeat; i++)
      speaker.speak("Всем привет");
  }
  ...
}

Продолжаем дописывать createObject

public <T> T createObject(Class<? extends T> type) throws ... {
  ....
  //Конфигурируем
  configure(t);
  //Запускаем методы PostConstruct
  invokeInitMethods(type, t);
  ....
}

private <T> void invokeInitMethods(Class<? extends T> type, T t)
                                                      throws ... {
  for (Method method : type.getMethods()) {
    if (method.isAnnotationPresent(PostConstruct.class)) {
      method.invoke(t);
    }
  }
}

Последовательность действий

init2

Когда нужно модифицировать действие метода

@Retention(RUNTIME)
public @interface Benchmark {
}
[[[BENCHMARK method speak
Speaking: blah-blah-blah
Time: 107100ns]]]

Прокси-объект

proxy

BenchmarkProxyConfigurator

public class BenchmarkProxyConfigurator
                                     implements ProxyConfigurator {
  @Override
  public <T> T wrapWithPoxy(T t, Class<? extends T> type) {
    boolean isProxyNeeded = type.isAnnotationPresent(Benchmark.class)
      || !ReflectionUtils.getAllMethods(type, method ->
             method.isAnnotationPresent(Benchmark.class)).isEmpty();
    if (isProxyNeeded) {
      return (T) Proxy.newProxyInstance(type.getClassLoader(),
          type.getInterfaces(),
          (proxy, method, args) -> {
            Method classMethod = type.getMethod(method.getName(),
                                method.getParameterTypes());
            return invoke(t, type, method, args, classMethod);
          });
    }
    return t;
}}

Proxied method invocation

private Object invoke(Object t, Class type, Method method,
            Object[] args, Method classMethod) throws ... {
  if (classMethod.isAnnotationPresent(Benchmark.class)
        || type.isAnnotationPresent(Benchmark.class)) {
    System.out.printf("[[[BENCHMARK method %s%n", method.getName());
    long start = System.nanoTime();
    Object retVal = method.invoke(t, args);
    long end = System.nanoTime();
    System.out.printf("Time: %dns]]]%n", end - start);
    return retVal;
  } else {
    return method.invoke(t, args);
  }
}

​Последовательность действий (окончательная картина)​

init3

Промежуточные выводы

  • DI-контейнер реализует следующие этапы «настройки» объектов:

    • создание

    • конфигурация (injections)

    • инициализация (postconstruct)

    • проксирование

  • DI-паттерн повторяет сам себя: многие детали DI-контейнера удобно настраивать через DI!

springframework

"Perhaps one of the hardest parts of explaining Spring is classifying exactly what it is" — Pro Spring 5, 5th ed., p. 1

Spring Framework

  • DI

  • AOP

  • Тестирование

  • Интеграция с огромным количеством технологий

  • Очень развитый (и продолжающий активно развиваться)

Переписываем наш пример с «самодельного» фреймворка на Spring

Было:

public class Main {
  public static void main(String[] args) throws ... {
    RobotLecturer lecturer = new ObjectFactory()
                     .createObject(RobotLecturer.class);
    lecturer.lecture();
  }
}

Переписываем наш пример с «самодельного» фреймворка на Spring

Стало:

@ComponentScan("edu.phystech.robotlecturer")
public class Main {
  public static void main(String[] args) {
    ApplicationContext ctx =
      new AnnotationConfigApplicationContext(Main.class);
    RobotLecturer lecturer = ctx.getBean(RobotLecturer.class);
    lecturer.lecture();
  }
}

Spring Beans

  • В нашем примере — 

    • RobotLecturer,

    • FirstLecture,

    • SpeakerImpl,

    • SlideShowImpl.

  • Spring beans — это переиспользуемые программные компоненты.

  • Годится любой класс, как наш, так и из сторонней библиотеки.

Способы конфигурации Spring

  • Разновидности конфигураций Spring:

    • Annotation-based

    • XML-based

    • Groovy-based

  • Мы будем рассматривать только annotation-based, как наиболее употребимую в настоящее время и практичную.

  • В огромном количестве тьюториалов (и проектов) ещё встречается XML-конфигурация.

Как определять бины

  • Classpath Scanning: ищем проаннотированные классы в заданных пакетах.

    • @Component

      • @Service

      • @Controller

      • @Repository

  • Фабричные методы

    • @Configuration → @Bean

Classpath scanning

@ComponentScan("edu.phystech.robotlecturer")

Атрибуты аннотации:

  • String[] basePackages — базовые пакеты для сканирования в поисках аннотированных компонент.

  • Class<?>[] basePackageClasses — как типобезопасная альтернатива, можно указать классы. Пакеты каждого из указанных классов будут просканированы.

  • Плюсы: удобно.

  • Минусы: классы должны быть проаннторированы как @Component, @Service и т. п., что не всегда возможно для сторонних библиотек.

@Configuration class

  • Класс конфигурации должен быть либо явно указан через конструктор AnnotationConfigApplicationContext, либо доступен через сканирование пакетов (тогда нужно аннотировать класс как @Configuration).

  • Чтобы прописывать в классе бины, надо пользоваться @Bean.

@Configuration class — пример

@Configuration
public class AppConfig{

  @Bean
  @Scope(BeanDefinition.SCOPE_PROTOTYPE)
  public Color randomColor(){
    ThreadLocalRandom random = ThreadLocalRandom.current();
    return new Color(random.nextInt(256),
                random.nextInt(256), random.nextInt(256));
  }

  ...
}

Bean scope

  • SCOPE_SINGLETON — по умолчанию. Создаётся один при первом запросе и всюду впрыскивается единственный экземпляр.

  • SCOPE_PROTOTYPE — создаётся новый при каждом запросе.

  • Есть ещё всякие, и можно создавать свои.

@Lazy

  • По умолчанию все синглетоны создаются при поднятии контейнера (чтобы fail fast, и чтобы избежать задержек при работе приложения).

  • Для конкретного бина это поведение можно изменить при помощи аннотации @Lazy (см. документацию).

  • Ленивая инициализация — не такая хорошая идея, как кажется на первый взгляд.

Bean name

  • Каждый бин получает имя (id).

  • По умолчанию, имя вычисляется из имени класса (SpeakerImpl"speakerImpl") или фабричного метода (getSpeaker"speaker").

  • Имя бина можно задать явно в параметре аннотации @Component и других (например: @Service("bestSpeaker")).

Виды injection («впрыскивания»)

  • Constructor

  • Setter

  • Field

  • Lookup method

Constructor injection

@Component
@RequiredArgsConstructor
public class RobotLecturer {
    //автоматически будут переданы в конструктор
    private final Lecture lecture;
    private final Speaker speaker;
    private final SlideShow slideShow;
  • Внешне может быть незаметен (особенно с Lombok).

  • Хорош для создания иммутабельных объектов.

  • Много параметров конструктора? — А точно столько надо?

Setter injection

@Autowired
void setLecture(Lecture lecture) {
   //сеттер будет автоматически вызван после конструирования
   this.lecture = lecture;
}
  • Хорош в ситуации, когда компонента сама себе способна предоставить зависимость "по умолчанию".

Field injection

@Component
public class RobotLecturer {
    //будут установлены через рефлексию после конструирования
    @Autowired
    private Lecture lecture;
    @Autowired
    private Speaker speaker;
    @Autowired
    private SlideShow slideShow;
  • Не плодит в классе сеттеры и конструкторы, но в целом сильно связывает код и считается не очень удачной практикой.

  • Хотя, в классах тестов — это ровно то, что нужно.

Lookup method injection

Проблема бинов с разным жизненным циклом: SCOPE_PROTOTYPE не спасает.

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Bar {...}

@Component
public class Foo {
    @Autowired
    private Bar bar;

    public  void bar(){
        //заинжектированный экземпляр bar всегда один и тот же
    }
}

Lookup method injection

@Component
public abstract class Foo {
    @Lookup
    abstract Bar getBar();

    public  void bar(){
        Bar b = getBar();
        //теперь в b будет всякий раз новое (ну или одно и то же,
        //если убрать SCOPE_PROTOTYPE, хотя где тогда смысл?)
    }
}

Foo foo = ctx.getBean(Foo.class);
foo.bar(); foo.bar(); ...

 — Как, мы инстанцируем абстрактный класс?! — Нет, мы же инстанцируем обёртку, на самом деле.

Lookup-метод может и не быть абстрактным

@Component
public class Foo {
    //главное -- чтобы не был приватным
    @Lookup
    Bar getBar(){
        return null;
    };
    public  void bar(){
        //не null!
        Bar b = getBar();
        ...
    }
}