Всем привет! Меня зовут Владислав Родин. В настоящее время я преподаю на портале OTUS курсы, посвященные архитектуре ПО и архитектуре ПО, подверженного высокой нагрузке. Сейчас в OTUS'е открыт набор на новый поток курса Разработчик на Spring Framework. В преддверии старта курса я решил написать небольшой авторский материал, которым хочу поделиться с вами.
Spring содержит внутри себя много «магии», осуществляя самостоятельно некоторые неочевидные вещи. Незнание или непонимание этого может приводить к side-эффектам, с которыми вы можете столкнуться в процессе написания своего приложения, используя данный framework.
Одной из таких неочевидных вещей является injection интерфейсов Java Collection Framework'а. Самостоятельно наступив на грабли, связанные с этой темой, и услышав очередные вопросы от коллег, я решил с ней разобраться и зафиксировать результаты своих исследований в виде статьи с надеждой, что она кому-то поможет уже в работе или при начальном освоении Spring'а.
Давайте рассмотрим сервис, который будет работать с киногероями. Их будет 3: Рембо, Терминатор и Гендальф. Каждый их них будет представлять отдельный класс, а родитель у них будет общий- Hero.
Давайте предположим, что мы хотим создать сервис, который будет работать с героями боевиков. Наверное, придется в него заинжектить list таких героев.
Не проблема! Создаем сервис и конфигурацию:
Все прекрасно, однако при проверке можно обнаружить, что в список попал и Гендальф!
Spring, увидев, что надо заинжектить List, обошел bean'ы, расположенные в context'е, нашел среди них все, подходящие под generic type, собрал из них List и заинжектил его, проигнорировав наш List.
Как заставить Spring сделать то, что мы хотим?
Поскольку проблема именно в попытке заинжектить интерфейс Java Collection Framework'а, можно просто заменить List на ArrayList в сервисе и, конечно же, в конфигурации, чтобы Spring нашел экземпляр нужного нам класса. Тогда все будет работать так, как мы ожидали.
Еще один способ связать Spring'у руки заключается в том, что можно попросить его заинжектить в сервис не абы какой List, а bean со специальным именем, добавив Qualifier. Таким образом, нам удастся заинжектить именно наш bean.
Если про нюанс injection'а List'а многие знают, то вот с Map'ой дела обстоят как правило хуже.
Давайте напишем сервис, который будет работать с главными героями фильмов. Inject'иться в него будет Map'а, содержащая по ключам названия фильмов, а по значениям bean'ы главных героев:
При запуске же можно увидеть, что ключом Гендальфа является не LOTR, а gandalf, из чего можно сделать вывод, что записалось не название фильма, а имя bean'а, тогда как в случае с Рембо и терминатором просто повезло: имена главных героев совпадают с названиями фильмов.
На самом деле, когда необходимо заинжектить Map'у, ключом которой является String, а значением bean'ы, Spring (как и в случае с List'ом) просто проигнорирует предложенную нами Map'у, пройдется по контексту и соберет все подходящие bean'ы, и создаст Map'у с ними в качестве значений и с их именами в качестве ключей.
Варианты обхода похожи на те, которые работали для List'а:
Заменяем Map на HashMap:
Добавляем Qualifier:
Предыстория
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;
}