Pull to refresh

DI не из ада

Reading time4 min
Views15K

Год назад я написал статью про DI в Spring/Java EE. Мой тезис звучал довольно категорично: "DI через конструкторы является единственно правильным. Все остальное – от лукавого". Прошло время, я пообщался с разными разработчиками на эту тему, сменил проект, компанию, провел множество собеседований, отсмотрел большое количество строк на code-review и сейчас могу сказать, что не все так однозначно. Давайте наконец разберемся, как же все-таки инжектить правильно.

Скажу сразу, что я по-прежнему считаю DI через конструкторы наиболее правильным с точки зрения поддерживаемости. Однако бывают ситуации, когда инжекты через поля оправдывают себя. Рассмотрим некоторые из них.

@PersistenceContext

Если вы работаете с базами данных в Spring, скорее всего, используете Spring Data. Это потрясающий инструмент, который упрощает взаимодействия с реляционными (и не только) СУБД на порядки. Тем не менее в сложных системах может потребоваться более низкоуровневое взаимодействие непосредственно с EntityManager. Как нам получить его инстанс? Что же, один из способов – внедрение EntityManagerFactory.

@Repository
public class TestRepository {
  private final EntityManagerFactory emf;

  @Autowired
  public TestRepository(EntityManagerFactory emf) {
    this.emf = emf;
  }

  @Transactional(readOnly = true)
  public Set<String> findAllLastNames() {
    EntityManager em = null;
    try {
      em = emf.createEntityManager();
      List<String> lastNames =
          em.createQuery("select distinct lastName from Person")
              .getResultList();
      return lastNames.stream()
          .collect(Collectors.toSet());
    } finally {
      if (em != null) {
        em.close();
      }
    }
  }
}

Здесь нам нужно беспокоиться о корректном открытии и закрытии EntityManager. Можно упростить себе жизнь.

@Repository
public class TestRepository {
  @PersistenceContext
  private EntityManager em;

  @Transactional(readOnly = true)
  public Set<String> findAllLastNames() {
    List<String> lastNames =
        em.createQuery("select distinct lastName from Person")
            .getResultList();
    return lastNames.stream()
        .collect(Collectors.toSet());
  }
}

Аннотация @PersistenceContextвнедряет прокси, который выполняет открытие и закрытие EntityManager автоматически. Это вариант гораздо проще понимать и поддерживать.

Кастомные инжекты

Spring – очень гибкий инструмент. Если вдруг обычного @Autowiredвам стало недостаточно, есть возможность дополнить DI механизм новыми модулями. Предположим, что нам нужна аннотация, которая будет внедрять случайное целое число.

@Service
public class TestService {
  @AutowiredRandomInt(min = 10, max = 20)
  private int randomNum;
	
  // business logic...
}

Чтобы реализовать функциональность, требуется написать bean post processor.

@Component
public class AutowiredRandomIntBeanPostProcessor implements BeanPostProcessor {

  @Override
  @SneakyThrows
  public Object postProcessBeforeInitialization(Object bean, String beanName) {
    for (Field field : bean.getClass().getDeclaredFields()) {
      if (field.isAnnotationPresent(AutowiredRandomInt.class)) {
        AutowiredRandomInt annotation = field.getAnnotation(AutowiredRandomInt.class);
        int random = ThreadLocalRandom.current().nextInt(
            annotation.min(),
            annotation.max()
        );
        field.setAccessible(true);
        field.set(bean, random);
      }
    }
    return bean;
  }
}

Удобно, не правда ли? Более того, мы можем сделать несколько bean post proccessors, подчиняющихся разным профилям (dev, test, prod и так далее).

Этот случай довольно простой. Но в докладе Евгения Борисова есть хороший пример, как кастомные инжекты могут помочь при работе с Apache Spark (таймкод 45:15).

Self-call при использовании Transactional

Я думаю, многие знают этот знаменитый вопрос с собеседований: "Если вызвать метод a, выполнится ли он транзакционно?"

public class MyService {
  
	public void a() {
  	this.b();
  }
  
  @Transactional
  public void b() {
  	// do something...
  }
}

Краткий ответ – нет. Более подробный можно найти во множестве статей на том же Хабре. Но если все же требуется выполнить a транзакционно, но поставить @Transactional над методом мы по какой-то причине не можем, self-injection выручит нас.

public class MyService {
  @Autowired
  private MyService testService;
  
  public void a() {
    myService.b();  // now it's transactional
  }
  
  @Transactional
  public void b() {
    // do something...
  }
}

Так как заинжектен будет прокси, который реализует управление транзакциями, вызов myService.b() так же будет выполнен транзакционно.

Хочу отметить, что это решение не является указанием к действиям. Self-inject – не есть хороший паттерн. Однако, если нам приходится поддерживать проект, который разрабатывался в течение многих лет, рефакторинг может быть чересчур дорогим. Поэтому в ряде случаев такой подход является наименьшим злом.

А как же тесты?

Одним из доводов против использования DI через поля в моей предыдущей статье был пункт, который гласил, что на такие классы нельзя написать unit-тесты. Формально это так. Но никто не отменял тесты с поднятием Spring Context. Благо c приходом Spring Boot это стало совсем просто. Вот пример теста для вышенаписанного TestRepository.

@DataJpaTest
class TestRepositoryTest {
  @Autowired
  private TestRepository testRepository;

  @TestConfiguration
  static class Config {
    @Bean
    public TestRepository testRepository() {
      return new TestRepository();
    }
  }

  @Test
  void shouldReturnZeroResults() {
    assertEquals(0, testRepository.findAllLastNames().size());
  }
}

Как видите, никаких проблем с тестированием функциональности не возникает. Более того, я считаю, что стандартный подход с мокированием всего и вся зачастую не эффективен и довольно ограничен. Но эта тема для отдельной статьи.

DI через поля или через конструктор?

Как я упомянул в начале статьи, DI через конструктор для меня по-прежнему остается более приоритетным. И вот почему.

Невозможность циклических зависимостей

И DI через поля, и DI через сеттеры не исключают шанса получить циклическую зависимость. Более того, когда это начнет мешать дальнейшей разработке, может быть слишком поздно что-то менять. При инжекте через конструкторы такая ситуация невозможна, ведь мы сразу получим ошибку в рантайме.

Иммутабельность

При DI через конструктор соответствующие поля можно объявить как final. Это исключает возможные ошибки при неправильных переприсваиваниях. Если вы используете Lombok, то приятным бонусом будет то, что сгенерировать конструктор можно с помощью @RequiredArgsConstructor, так как, начиная со Spring 4.3, ставить @Autowiredнад конструктором не обязательно, если он единственный.

Тестирование с моками

Как я написал выше, всегда можно протестировать бин, подняв Spring Context. Однако в некоторых ситуациях это является излишним. Обычные unit-тесты с моками выполняются гораздо быстрее и не требуют никаких дополнительных конфигураций. DI через конструкторы и сеттеры оставляют пространство для такого решения. DI через поля – нет.

Заключение

На это все с инжектами. Пока. Если у вас есть вопросы или предложения, пожалуйста, оставляйте свои комментарии. Спасибо за чтение!

Tags:
Hubs:
+1
Comments12

Articles