В больших приложениях, основанных на 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 специально оставлены проваливающимися.