springboot04:条件注入与装配
条件注入
多个满足条件时的自动注入
在之前使用spring开发时,我们通常使用自动注入@Autowired来对配置类中注入依赖,有些场景下仅仅靠这个注解不足以让Spring知道到底要注入哪个 bean。默认情况下,@Autowired 按类型装配 Spring Bean。如果容器中有多个相同类型的 bean,则框架将抛出 NoUniqueBeanDefinitionException,以提示有多个满足条件的 bean 进行自动装配。程序无法正确做出判断使用哪一个,如下面的例子:
@Component("fish")
public class Fish implements Animal { // 鱼
public String format() {
return "fish";
}
}
@Component("bear")
public class Bear implements Animal { // 熊
public String format() {
return "bear";
}
}
@Component
public class FoodService {
@Autowired
private Animal animal; // 此时spring不知道要注入哪一个类? 鱼 or 熊
//todo
}
此时将会报错,Spring 框架将抛出 NoUniqueBeanDefinitionException。这是因为 Spring 不知道要注入哪个 bean。通常对于这种情况,我们会使用@Qualifier来指定特定的Spring bean来进行注入,例子如下:
@Component
public class FoodService {
@Autowired
@Qualifier("bear") // 指定Bear Bean注入
private Animal animal;
}
@Qualifier
通过将 @Qualifier 注解与我们想要使用的特定 Spring bean 的名称一起进行装配,Spring 框架就能从多个相同类型并满足装配要求的 bean 中找到我们想要的,避免让Spring脑裂。我们需要做的是@Component或者@Bean注解中声明的value属性以确定名称。 其实我们也可以在 Animal 实现类上使用 @Qualifier 注释,而不是在 @Component 或者 @Bean 中指定名称,也能达到相同的效果:
@Component
@Qualifier("fish")
public class Fish implements Animal { // 鱼
public String format() {
return "fish";
}
}
@Component
@Qualifier("bear")
public class Bear implements Animal { // 熊
public String format() {
return "bear";
}
}
@Primary
还有另一个名为 @Primary 的注解,我们也可以用来发生依赖注入的歧义时决定要注入哪个 bean。当存在多个相同类型的 bean 时,此注解定义了首选项。除非另有说明,否则将使用与 @Primary 注释关联的 bean 。 我们来看一个例子:
@Bean
public Employee tomEmployee() {
return new Employee("Tom");
}
@Bean
@Primary
public Employee johnEmployee() {
return new Employee("john");
}
在此示例中,两个方法都返回相同的 Employee类型。Spring 将注入的 bean 是方法 johnEmployee 返回的 bean。这是因为它包含 @Primary 注解。当我们想要指定默认情况下应该注入特定类型的 bean 时,此注解很有用。
如果我们在某个注入点需要另一个 bean,我们需要专门指出它。我们可以通过 @Qualifier 注解来做到这一点。例如,我们可以通过使用 @Qualifier 注释来指定我们想要使用 tomEmployee 方法返回的 bean 。 值得注意的是,如果 @Qualifier 和 @Primary 注释都存在,那么 @Qualifier 注释将具有优先权。
基本上,@Primary 是定义了默认值,而 @Qualifier 则非常具体。 当然@Component 也可以使用@Primary 注解。例子如下:
@Component
@Primary
public class Fish implements Animal { // 鱼
public String format() {
return "fish";
}
}
@Component
public class Bear implements Animal { // 熊
public String format() {
return "bear";
}
}
通过名称来自动注入
在使用 @Autowired 进行自动装配时,如果 Spring 没有其他提示,将会按照需要注入的变量名称来寻找合适的 bean。也可以解决依赖注入歧义的问题。让我们看一些基于我们最初的例子的代码:
@Component
public class FoodService {
@Autowired
private Animal fish; // 通过名称来注入
//todo
}
在这种情况下,Spring 将确定要注入的 bean 是 fish,因为字段名称与我们在该 bean 的 @Component或者 @Bean 注解中使用的值(默认 @Bean 使用方法名)相匹配。
条件装配
@Profile
Spring为应用程序准备了Profile这一概念,用来表示不同的环境。例如,我们分别定义开发、测试和生产这3个环境:
- native
- test
- production
创建某个Bean时,Spring容器可以根据注解@Profile来决定是否创建。例如,以下配置:
@Configuration
@ComponentScan
public class AppConfig {
@Bean
@Profile("!test")
ZoneId createZoneId() {
return ZoneId.systemDefault();
}
@Bean
@Profile("test")
ZoneId createZoneIdForTest() {
return ZoneId.of("America/New_York");
}
}
如果当前的Profile设置为test,则Spring容器会调用createZoneIdForTest()创建ZoneId,否则,调用createZoneId()创建ZoneId。注意到@Profile("!test")表示非test环境。
在运行程序时,加上JVM参数-Dspring.profiles.active=test就可以指定以test环境启动。
实际上,Spring允许指定多个Profile,例如:
-Dspring.profiles.active=test,master
可以表示test环境,并使用master分支代码。
要满足多个Profile条件,可以这样写:
@Bean
@Profile({ "test", "master" }) // 满足test或master
ZoneId createZoneId() {
...
}
Spring Boot对Profiles的支持在于,可以在application.yml中为每个环境进行配置。下面是一个示例配置:
spring:
application:
name: ${APP_NAME:unnamed}
datasource:
url: jdbc:hsqldb:file:testdb
username: sa
password:
dirver-class-name: org.hsqldb.jdbc.JDBCDriver
hikari:
auto-commit: false
connection-timeout: 3000
validation-timeout: 3000
max-lifetime: 60000
maximum-pool-size: 20
minimum-idle: 1
pebble:
suffix:
cache: false
server:
port: ${APP_PORT:8080}
---
spring:
profiles: test
server:
port: 8000
---
spring:
profiles: production
server:
port: 80
pebble:
cache: true
注意到分隔符---,最前面的配置是默认配置,不需要指定Profile,后面的每段配置都必须以spring.profiles: xxx开头,表示一个Profile。上述配置默认使用8080端口,但是在test环境下,使用8000端口,在production环境下,使用80端口,并且启用Pebble的缓存。
@Conditional
使用Profile能根据不同的Profile进行条件装配,但是Profile控制粒度比较糙,如果想要精细控制,例如,配置本地存储,AWS存储和阿里云存储,将来很可能会增加Azure存储等,用Profile就很难实现。
根据条件创建bean
除了根据@Profile条件来决定是否创建某个Bean外,Spring还可以根据@Conditional决定是否创建某个Bean。@Conditional 通常作用于配置类上(@Configuration),作用于配置类的@Bean方法。
下面来自定义一个根据Condition来实现自动装配bean的实例。
首先创建一个基础的bean对象,在Configuration类中建立两个名字不同bean方法,编写一个继承Condition的类,然后在Configuration类中进行条件配置。
public class Object01 {
private Integer i1;
private String s1;
... setter、getter、toString 方法
}
@Configuration
public class MyConfiguration {
@Bean("Object01_1")
public Object01 object011(){
return new Object01();
}
//如果满足ConditionOnMissingBean的条件,才会创建Object01_2这个Bean。
@Bean("Object01_2")
@Conditional({ConditionOnMissingBean.class})
public Object01 object012(){
return new Object01();
}
}
// 当Object01_1已经创建则不创建Object01_2这个bean
public class ConditionOnMissingBean implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
BeanDefinitionRegistry registry = context.getRegistry();
// 如果Object01_1这个bean已经创建,则matches返回false
return !registry.isBeanNameInUse("Object01_1");
}
}
// 控制类,检验结果
@Controller
public class MyController {
@Autowired
private ApplicationContext Ioc;
@RequestMapping("/r1")
@ResponseBody
public String r1(){
System.out.println(Ioc.containsBean("Object01_1"));//查看容器是否创建了Object01_1
System.out.println(Ioc.containsBean("Object01_2"));//查看容器是否创建了Object01_2
return "r1";
}
}
常见的Condition类
在org.springframework.boot.autoconfigure包下
- @ConditionalOnProperty:如果配置文件中有指定的配置,条件生效;
- name为key,havingValue为value
- prefix为属性名的前缀部分,value为属性名最后一部分,matchIfMissing默认为false
- 当matchIfMissing设定为true,则无论属性名是否设定都返回true(不影响havingValue)
- @ConditionalOnBean:如果有指定的Bean,条件生效;
- @ConditionalOnClass:如果有指定的Class,条件生效;
- @ConditionalOnMissingBean:如果没有指定的Bean,条件生效;
- @ConditionalOnMissingClass:如果没有指定的Class,条件生效;
- @ConditionalOnWebApplication:在Web环境中条件生效;
- @ConditionalOnExpression:根据表达式判断条件是否生效。
以上使用方法见这:使用Conditional