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