В больших приложениях, основанных на Spring (или любом другом IoC фреймворке), может наступить такой день, когда при внедрении зависимостей образуется неоднозначность: одной зависимости удовлетворяет сразу несколько бинов, ведь выбор производится по совместимости типов внедряемого и запрашиваемого бинов. Что же тогда произойдёт?
Подготовка
Нам понадобится многомодульный пустой maven проект с Spring и JUnit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <javaee.version>7.0</javaee.version> <org.springframework.version>4.1.7.RELEASE</org.springframework.version> </properties> <modules> <module>fail</module> <module>qualifier</module> <module>named</module> <module>resource</module> </modules> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${org.springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${org.springframework.version}</version> <scope>test</scope> </dependency> </dependencies> |
В проекте создадим интерфейс и две его реализации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public interface Fine { String whatIsFine(); } @Component public class FineDay implements Fine { @Override public String whatIsFine() { return "A day is fine"; } } @Component public class HeavyFine implements Fine { @Override public String whatIsFine() { return "A fine is heavy"; } } |
Неоднозначные бины
Для начала я бы хотел посмотреть, что жё всё таки будет, если мы объявим конкурс «Два бина на одно место» 🙂 Поскольку Spring поддерживает несколько аннотаций для связывания зависимостей, я решил попробовать их все:
- @Autowired — Spring-specific аннотация, которую можно применять и к полям и к конструкторам и даже к сеттерам.
- @Resource — Аннотация из JSR-250, позволяющая внедрять ресурсы в самом широком смысле: от бинов и до ссылок на jndi
- @Inject — Самая последняя разработка, Dependency Injection for Java. При использовании со Spring можно считать её идентичной @Autowired
Код, который неоднозначен:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @Component public class AmbiguousAutowiredFine { @Autowired private Fine fine; public String getFine() { return fine.whatIsFine(); } } @Component public class AmbiguousInjectFine { @Inject private Fine fine; public String getFine() { return fine.whatIsFine(); } } @Component public class AmbiguousResourceFine { @Resource private Fine fine; public String getFine() { return fine.whatIsFine(); } } |
И тестовый класс к нему. Так как тесты отличаются только именем пакета и именем тестируемого класса, достаточно будет показать лишь один:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @ContextConfiguration @RunWith(SpringJUnit4ClassRunner.class) public class AmbiguousAutowiredFineIT { @Inject private ApplicationContext context; @Configuration @ComponentScan("ru.easyjava.spring.autowired") public static class SpringConfig { @Bean public FineDay fineDay() { return new FineDay(); } @Bean public HeavyFine hardFine() { return new HeavyFine(); } } @Test public void testSpring() { AmbiguousAutowiredFine testedObject = context.getBean(AmbiguousAutowiredFine.class); assertFalse(testedObject.getFine().isEmpty()); } } |
Результат исполнения немного предсказуем:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Tests in error: AmbiguousAutowiredFineIT.testSpring ? IllegalState Failed to load ApplicationC... AmbiguousInjectFineIT.testSpring ? IllegalState Failed to load ApplicationCont... AmbiguousResourceFineIT.testSpring ? IllegalState Failed to load ApplicationCo... Tests run: 3, Failures: 0, Errors: 3, Skipped: 0 Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'ambiguousAutowiredFine': Injection of autowired dependencies failed; nested exception is org.springframework.bean .factory.BeanCreationException: Could not autowire field: private ru.easyjava.spring.Fine ru.easyjava.spring.autowired.AmbiguousAutowiredFine.fine; nested exception is org.springframework.beans.factory.NoUniqueBe nDefinitionException: No qualifying bean of type [ru.easyjava.spring.Fine] is defined: expected single matching bean but found 2: fineDay,hardFine Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'ambiguousInjectFine': Injection of autowired dependencies failed; nested exception is org.springframework.beans.fa ctory.BeanCreationException: Could not autowire field: private ru.easyjava.spring.Fine ru.easyjava.spring.inject.AmbiguousInjectFine.fine; nested exception is org.springframework.beans.factory.NoUniqueBeanDefiniti onException: No qualifying bean of type [ru.easyjava.spring.Fine] is defined: expected single matching bean but found 2: fineDay,hardFine Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'ambiguousResourceFine': Injection of resource dependencies failed; nested exception is org.springframework.beans.f actory.NoUniqueBeanDefinitionException: No qualifying bean of type [ru.easyjava.spring.Fine] is defined: expected single matching bean but found 2: fineDay,hardFine |
Неявный выбор бина по имени
Одно из самых простых решений, это намекнуть Spring’у, какой бин нам нужен, используя имена полей.
Каждый бин в Spring context имеет своё имя. Это име порождается либо из имени класса, либо явно задаётся в xml и grovvy конфигах, либо берётся из имени функции создания бина в java config.
Если мы назовём поле с неоднозначным типом бина по имени бина, Spring сможет самостоятельно сделать правильным выбор:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @Component public class AmbiguousAutowiredFine { @Autowired private Fine fineDay; public String getFine() { return fineDay.whatIsFine(); } } @Component public class AmbiguousInjectFine { @Inject private Fine fineDay; public String getFine() { return fineDay.whatIsFine(); } } @Component public class AmbiguousResourceFine { @Resource private Fine fineDay; public String getFine() { return fineDay.whatIsFine(); } } |
Неявное определение типа по имени работает со всеми аннотациями:
1 2 3 4 5 6 7 8 | @Test public void testSpring() { AmbiguousAutowiredFine testedObject = context.getBean(AmbiguousAutowiredFine.class); assertThat(testedObject.getFine(), is("A day is fine")); } Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 |
Явное указание бина по имени
Для тех, кто не любит ‘convention-over-configuration’, в Spring есть аннотация @Qualifier, позволяющая явно задать имя нужного бина:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Component public class AmbiguousAutowiredFine { @Autowired @Qualifier("fineDay") private Fine fine; public String getFine() { return fine.whatIsFine(); } } @Component public class AmbiguousResourceFine { @Resource @Qualifier("fineDay") private Fine fine; public String getFine() { return fine.whatIsFine(); } } |
Тесты остаются теми же самыми и точно так же проходят:
1 | Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 |
Однако я пропустил в этот раз класс AmbiguousInjectFine Аннотация Spring @Qualifier работает, разумеется, и с Inject, но есть и другой @Qualifier.
@Qualifier и задание бина по типу
JSR-330 тоже определяет аннотацию @Qualifier, но она несовместима с @qualifier из Spring и устроена несколько иначе.
Для использования этой аннотации вначале нужно определить собственную аннотацию, уникальную для каждого бина:
1 2 3 4 | @Qualifier @Retention(RUNTIME) @Target({METHOD, FIELD, PARAMETER, TYPE}) public @interface FineDayQualifier { } |
И применить эту аннотацию и к классу бина и к зависимости:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @Component @FineDayQualifier public class FineDay implements Fine { @Override public String whatIsFine() { return "A day is fine"; } } @Component public class AmbiguousInjectFine { @Inject @FineDayQualifier private Fine fine; public String getFine() { return fine.whatIsFine(); } } |
@Named как другой способ указания бина по имени
JSR-330 помимо нового @Qualifier принёс и аналог старого, который называется @Named и ведёт себя аналогичным образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | @Named public class AmbiguousAutowiredFine { @Autowired @Named("fineDay") private Fine fine; public String getFine() { return fine.whatIsFine(); } } @Named public class AmbiguousInjectFine { @Inject @Named("fineDay") private Fine fine; public String getFine() { return fine.whatIsFine(); } } @Named public class AmbiguousResourceFine { @Resource @Named("fineDay") private Fine fine; public String getFine() { return fine.whatIsFine(); } } |
Кроме задания имени бина для внедрения, @Named можно использовать и как замену Spring’овомоу @Component/@Service/@Repository/@Controller, при этом имя бина можно сразу задать как параметр @Named.
@Resource и всё ещё указание бина по имени
К моему облегчению это последний метод задания типа по имени и он специфичен только для аннотации @Resource. Имя бина передаётся параметром аннотации:
1 2 3 4 5 6 7 | @Component public class AmbiguousResourceFine { @Resource(name = "fineDay") private Fine fine; public String getFine() { return fine.whatIsFine(); } } |
Нужно…больше…бинов…
На случай, если нужны одновременно все бины заданного типа, например когда точное имя бина неизвестно, можно внедрить их в коллекцию:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | @Component public class AmbiguousAutowiredFine { @Autowired private List<Fine> fine; public String getFine() { return "Two options: " + fine.stream() .map(Fine::whatIsFine) .collect(Collectors.joining(" and ")); } } @Component public class AmbiguousInjectFine { @Inject private List<Fine> fine; public String getFine() { return "Two options: " + fine.stream() .map(Fine::whatIsFine) .collect(Collectors.joining(" and ")); } } @Component public class AmbiguousResourceFine { @Resource private List<Fine> fine; public String getFine() { return "Two options: " + fine.stream() .map(Fine::whatIsFine) .collect(Collectors.joining(" and ")); } } |
Порядок внедрения бинов не определён. Внедрение коллекции бинов работает со всеми аннотациями.
Заключение и рекомендации
Использование стандартных аннотаций @Named/@Inject позволяет сделать код независимым от реализации IoC/CDI и в перспективе менять реализации как перчатки. Платой за это будет потеря Spring специфичного функционала, такого как @Required/@Lazy/etc.
С другой стороны, выбор между специализацией бина по имени или по типу упирается в вопрос удобства и безопасности. Специализация по типу, с @Qualified из JSR-330 однозначно связывает требуемый бин с его реализацией, но требует достаточно много подготовительного кода. Специализация по имени не требует код вообще, но может привести к неожиданным результатам, если имя будет не то или не у того бина.
Код примера доступен на github. Тесты модуля fail специально оставлены проваливающимися.