Год назад я написал статью про 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 через поля – нет.

Заключение

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