Некоторые тонкости injection’а коллекций в Spring’е

в 10:26, , рубрики: boot, Core, ioc, java, java ee, spring, web-разработка, Блог компании OTUS. Онлайн-образование, Программирование, Промышленное программирование, Разработка веб-сайтов

Всем привет! Меня зовут Владислав Родин. В настоящее время я преподаю на портале OTUS курсы, посвященные архитектуре ПО и архитектуре ПО, подверженного высокой нагрузке. Сейчас в OTUS'е открыт набор на новый поток курса Разработчик на Spring Framework. В преддверии старта курса я решил написать небольшой авторский материал, которым хочу поделиться с вами.


Предыстория

Spring содержит внутри себя много «магии», осуществляя самостоятельно некоторые неочевидные вещи. Незнание или непонимание этого может приводить к side-эффектам, с которыми вы можете столкнуться в процессе написания своего приложения, используя данный framework.

Одной из таких неочевидных вещей является injection интерфейсов Java Collection Framework'а. Самостоятельно наступив на грабли, связанные с этой темой, и услышав очередные вопросы от коллег, я решил с ней разобраться и зафиксировать результаты своих исследований в виде статьи с надеждой, что она кому-то поможет уже в работе или при начальном освоении Spring'а.

Сценарий

Давайте рассмотрим сервис, который будет работать с киногероями. Их будет 3: Рембо, Терминатор и Гендальф. Каждый их них будет представлять отдельный класс, а родитель у них будет общий- Hero.

public class Hero {
}

@Component
public class Rambo extends Hero {

    @Override
    public String toString() {
        return "Rambo";
    }
}

@Component
public class Terminator extends Hero {

    @Override
    public String toString() {
        return "Terminator";
    }
}


@Component
public class Gandalf extends Hero {
    @Override
    public String toString() {
        return "Gandalf";
    }
}

Injection List'а

Давайте предположим, что мы хотим создать сервис, который будет работать с героями боевиков. Наверное, придется в него заинжектить list таких героев.

Не проблема! Создаем сервис и конфигурацию:

@Service
@Getter
public class ActionHeroesService {
    @Autowired
    List<Hero> actionHeroes;
}

@Configuration
public class HeroesConfig {

    @Bean
    public List<Hero> action() {
        List<Hero> result = new ArrayList<>();
        result.add(new Terminator());
        result.add(new Rambo());
        return result;
    }
}

Все прекрасно, однако при проверке можно обнаружить, что в список попал и Гендальф!

Spring, увидев, что надо заинжектить List, обошел bean'ы, расположенные в context'е, нашел среди них все, подходящие под generic type, собрал из них List и заинжектил его, проигнорировав наш List.

Как заставить Spring сделать то, что мы хотим?

Вариант 1. Костыльный

Поскольку проблема именно в попытке заинжектить интерфейс Java Collection Framework'а, можно просто заменить List на ArrayList в сервисе и, конечно же, в конфигурации, чтобы Spring нашел экземпляр нужного нам класса. Тогда все будет работать так, как мы ожидали.

@Configuration
public class HeroesConfig {

    @Bean
    public ArrayList<Hero> action() {
        ArrayList<Hero> result = new ArrayList<>();
        result.add(new Terminator());
        result.add(new Rambo());
        return result;
    }
}

@Service
@Getter
public class ActionHeroesService {
    @Autowired
    ArrayList<Hero> actionHeroes;
}

Вариант 2. Правильный

Еще один способ связать Spring'у руки заключается в том, что можно попросить его заинжектить в сервис не абы какой List, а bean со специальным именем, добавив Qualifier. Таким образом, нам удастся заинжектить именно наш bean.

@Service
@Getter
public class ActionHeroesService {
    @Autowired
    @Qualifier("action")
    List<Hero> actionHeroes;
}

Injection Map'ы

Если про нюанс injection'а List'а многие знают, то вот с Map'ой дела обстоят как правило хуже.
Давайте напишем сервис, который будет работать с главными героями фильмов. Inject'иться в него будет Map'а, содержащая по ключам названия фильмов, а по значениям bean'ы главных героев:

@Service
@Getter
public class MainCharactersService {
    @Autowired
    Map<String, Hero> mainCharactersByFilmName;
}

@Configuration
public class HeroesConfig {

    @Bean
    public Map<String, Hero> mainCharactersByFilmName() {
        Map<String, Hero> result = new HashMap<>();

        result.put("rambo", new Rambo());
        result.put("terminator", new Terminator());
        result.put("LOTR", new Gandalf());

        return result;
    }
}

При запуске же можно увидеть, что ключом Гендальфа является не LOTR, а gandalf, из чего можно сделать вывод, что записалось не название фильма, а имя bean'а, тогда как в случае с Рембо и терминатором просто повезло: имена главных героев совпадают с названиями фильмов.

На самом деле, когда необходимо заинжектить Map'у, ключом которой является String, а значением bean'ы, Spring (как и в случае с List'ом) просто проигнорирует предложенную нами Map'у, пройдется по контексту и соберет все подходящие bean'ы, и создаст Map'у с ними в качестве значений и с их именами в качестве ключей.

Варианты обхода похожи на те, которые работали для List'а:

Вариант 1. Костыльный

Заменяем Map на HashMap:

@Service
@Getter
public class MainCharactersService {
    @Autowired
    HashMap<String, Hero> mainCharactersByFilmName;
}


@Configuration
public class HeroesConfig {

    @Bean
    public HashMap<String, Hero> mainCharactersByFilmName() {
        HashMap<String, Hero> result = new HashMap<>();

        result.put("rambo", new Rambo());
        result.put("terminator", new Terminator());
        result.put("LOTR", new Gandalf());

        return result;
    }
}

Вариант 2. Правильный

Добавляем Qualifier:

@Service
@Getter
public class MainCharactersService {
    @Autowired
    @Qualifier("mainCharactersByFilmName")
    Map<String, Hero> mainCharactersByFilmName;
}

Автор: rodinvv

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js