Блог создается при поддержке Failover Bar и Anarchic Pro

Аннотация @Autowired в Spring

October 29, 2023 . 6 минут на чтение статьи

Основа Spring - механизм Dependency Inection. Он работает так: во все поля, которые отмечены аннотацией @Autowired, автоматически подставляются заранее заданные значения.

Это позволяет развязать компоненты приложения между собой: в момент запуска приложения, связывать их будет вместо вас Spring.

В этой статье мы посмотрим, как использовать аннотацию @Autowired и разрешать конфликты подстановки с помощью @Qualifier.

Как включить @Autowired

Настройка Spring делается с помощью конфигурационных классов, помеченных аннотацией @Configuration. Чтобы не перечислять вручную все бины в мире, можно попросить Spring найти подходящие компоненты автоматически: это делает аннотация @ComponentScan.

@Configuration
@ComponentScan("guru.oleg")
public class Config {}

XML-некрофилы могут сделать то же самое с помощью <context:annotation-config>.

@ComponentScan не обязательно включать, если вам достаточно значений по-умолчанию. Сканирование уже включено в аннотацию @SpringBootApplication, которую использует тот же Spring Initializr. Эта аннотация - полный экивалент комбинации @Configuration + @EnableAutoConfiguration + @ComponentScan, с настройками по-умолчанию. Поиск компонентов запустится на текущем пакете (рекурсивно, вместе со всем содержимым).

@SpringBootApplication
public class App {
public static void main(String[] args) {

SpringApplication.run(App.class, args);
    
}}

Как использовать @Autowired

@Autowired можно навесить на несколько элементов: поля, сеттеры и конструкторы. Эти способы отличаются по визуальному стилю и удобству использования в разных кейсах.

@Autowired на полях класса

Вместо тысячи слов, смотрим на пример. Вначале, создадим компонент:

@Component("printer")
public class Printer {
    public String print() {
        return "printed data";
    }
}

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

@Component
public class PrintService {  
    @Autowired
    private Printer printer;
}

Таким образом, мы можем конструировать иерархии компонентов, не особо заморачиваясь, какие конструкторы, с какими параметрами, и в каком порядке надо вызывать. Удобно.

Этот способ принято критиковать потому, что его сложно автоматически тестировать. Чтобы подставить пробные данные в автотесте, приходится использовать Reflection API. Такой подход многие считают костылем. Мне, напротив, кажется, что игра стоит свеч. Впрочем, это интересный архитектурный вопрос и решение, которое вам нужно принять самостоятельно, а не слушать советов из нижнего интернета.

@Autowired на сеттерах

Промежуточный способ между внедрением через поля и через конструкторы. Уже можно тестировать без костылей, но всё ещё некрасиво.

Хот тейк: сеттеры в Java вообще не нужны. Их придумали поборники "правильного ООП", а такое ООП в большинстве случаев не нужно. Гораздо лучше все поля делать публичными, а их доступность или недоступность определять по смыслу (как это делается в том же Python). Тем не менее, невозможно не привести этот способ для полноты картины.

public class PrintService {
    private Printer printer;
    @Autowired
    public void setPrinter(Printer printer) {
        this.printer = printer;
    }
}

@Autowired на конструкторах

Этот способ считается основным и каноничным. Его одновременно можно и тестировать без костылей, и он передает семантику происходящего процесса, и он нормально работает с публичными полями.

public class PrintService {
    private Printer printer;
    @Autowired
    public void PrintService(Printer printer) {
        this.printer = printer;
    }
}

Проблема с этим способом мне видится в том, что объявление полей и аннотации находятся в совершенно разных местах кода. Поэтому, вам недостаточно просто посмотреть на поля, а нужно идти куда-то в дебри кода и разбираться, что куда инжектится. Это немного нарушает общую идею красоты и простоты, которую дает Spring. Что важнее - идеологическая правильность или красота и юзабилити, решать вам.

@Autowired и необязательные зависимости

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

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 
No qualifying bean of type [guru.oleg.dto] found for dependency: 
expected at least 1 bean which qualifies as autowire candidate for this dependency. 
Dependency annotations: 
{@org.springframework.beans.factory.annotation.Autowired(required=true)}

Если же вы осознанно хотите использовать опциональные зависимости, то нужно выставить значение required=false:

public class PrintService {
    @Autowired(required = false)
    private Printer printer; 
}

Разрешение конфликтов

По-умолчанию, Spring считает источником истины типы зависимостей. В своем внутреннем реестре, он приводит названия типов к нижнему регистру и использует эти идентификаторы для поиска соответствий.

Что произойдет, если у нас найдутся несколько компонентов с одинаково называющимся классом? Будет ошибка такого вида:

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
No qualifying bean of type [guru.oleg.Printer] is defined: 
expected single matching bean but found 2: A0Printer,A4Printer

Как может возникнуть такая ситуация? Представим, что у нас есть два принтера: один для маленьких листочков формата A4, и другой - для широкоформатной печати на A0.

@Component("A0Printer")
public class A0Printer implements Printer {
    public String print() {
        return "A0 paper";
    }
}

@Component("A4Printer")
public class A4Printer implements Printer {
    public String format() {
        return "A4 paper";
    }
}

Теперь, если мы попробуем действовать в парадигме ООП, и попросим Spring найти нам "какой-нибудь принтер", то ему станет плохо, вылетит ошибка NoUniqueBeanDefinitionException.

public class PrintService {
    @Autowired
    private Printer printer;
}

Помните, мы обсуждали, что по-умолчанию Spring использует имена, приеденные к нижнему регистру? Мы можем заставить Spring искать не какой-то непонятный принтер, а имя нашего класса в нижнем регистре. Для этого служит аннотация @Qualifier.

public class PrintService {
    @Autowired
    @Qualifier("A4Printer")
    private Printer printer;
}

Собственный квалификатор

Если простой квалификатор кажется вам зашкваром, то можно использовать кастомный, красивый квалификатор с осмысленно выглядящим типом.

@Qualifier
@Target({
  ElementType.FIELD, 
  ElementType.METHOD, 
  ElementType.TYPE, 
  ElementType.PARAMETER
})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrinterType {  
    String value();
}

Описание компонентов начинают выглядеть немного по-другому:

@PrinterType("A0Printer")
@Component
public class A0Printer implements Printer {
    public String print() {
        return "A0 paper";
    }
}

@PrinterType("A4Printer")
@Component
public class A4Printer implements Printer {
    public String format() {
        return "A4 paper";
    }
}

Теперь, воспользуемся плодами нашего труда:

public class PrintService {
    @Autowired
    @PrinterType("A4Printer")
    private Printer printer;
}

Такая запись выглядит более семантично. Так в бизнес-логику утекает на одну подробность работы Спринга меньше. На самом деле, видеть повсюду торчащие куски Спринга (типа аннотации @Qualifier) - последнее, что вам хочется видеть в вашем красивом, ухоженном коде.

Внедрение зависимости по имени

Если вы внедряете зависимости в поля класса, то Spring включает удобную эвристику: он может получить имя класса прямо из имени поля.

public class PrintService {
    @Autowired
    private Printer a4Printer;
}

Этот пример принципиально отличается от предыдущих. Раньше мы использовали полиморфизм: мы указывали общий интерфейс принтера в надежде, что Spring найдет нам подходящий класс. Здесь же мы захардкодили имя класса прямо в название поля.

Оба подхода имеют право на жизнь. Первый подход полезен, если нужно внедрять имена компонентов через плейсхолдеры из файлов конфигурации. Второй подход с прямым указанием имени больше подходит для создания псевдо-DSL в стиле Spring Data.

Выводы

В этой статье мы посмотрели, как в Spring можно связывать компоненты. У вас есть на выбор несколько мест, куда можно встроить свою зависимость. Плюс специальный инструментарий для разрешения конфликтов, если эти конфликты вообще произойдут.

Продолжайте читать наши гайды по Spring и станете круче самых крутых гор!

Что почитать?



Не забывайте подписаться на наши ресурсы, там есть ништяки:

  • CodCraft - Youtube-канал от автора этого гайда
  • Оправдания от Олега - Telegram-чат автора (общий, про всё на свете)
  • Javawatch - Telegram-канал про Java
  • Telegram-канал Failover Bar - единственный в Санкт-Петербурге (а может, и в России вообще) бар для разработчиков. Мы здесь постоянно встречаемся и разговариваем про Java.