Год назад я написал статью про 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 через поля – нет.
Заключение
На это все с инжектами. Пока. Если у вас есть вопросы или предложения, пожалуйста, оставляйте свои комментарии. Спасибо за чтение!