【SpringBoot】注入SpringBoot容器的相关注解及其注意事项 ()

条件注解,Spring Boot 的基石! - Spring Boot2 教程合集 (javaboy.org)

一、基础环境

这一节主要想给大家分享一下SpringBoot项目中组件(实体对象)注入Spring容器的用法及其注意事项。

首先要准备以下测试环境,在SpringBoot项目框架基础上创建两个实体类User与Pet。

在实体类中都手动生成无参与有参构造,get与set方法。

图片

二、组件(实体对象)注入Spring容器

xml文件注入

在SpringMVC中,如果想要往Spring容器中注入对象,通常是在xml文件中配置该对象。

如下图所示,先在resources目录下新建BeanTest.xml文件。并在该文件中采用bean标签手动配置两个对象,其中子标签property用于给实体类的各个属性赋值。

图片

例如上图中注入User类对象时id为对象名,给这个对象的name属性赋值张三,给age字段赋值26。

那么除了使用xml文件形式的注入对象方法,当然还可以使用注解形式。

@Configuration

我在config目录下新建MyConfig实体类,并在实体类上标注@Configuration注解。

图片

此时这个实体类中就可以加入你想要注入的实体对象了,具体的注入方法如下。

在实体类MyConfig中,我们生成user1方法,并且在该方法上使用@Bean注解注入Spring容器中。

可以对比一下xml注入和注解注入方式的联系。

在注解注入中@Bean注解就相当于xml方式中bean标签,返回值类型就相当于xml方式中class="com.ndzssj.boot.bean.User",方法名user1就相当于xml方式中的id值,new对象就相当于xml方式中的子标签property给属性赋值。

下面就来测试获取这个注入的组件。

图片

我们发现在SpringBoot启动以后,发现打印出了刚刚在MyConfig类中注入的组件。

另外值得一提的是,使用@Configuration注入的组件默认是单例的。

从上图所示,两次从容器中获取的user2的对象是相等的,所以是单例的。

@Conditional

为了满足更加复杂场景的需要,可以进行条件式装配(注入对象)。

下图展示了@Conditional的派生注解。

图片

我们来拿@ConditionalOnBean这个注解举例如何进行条件式装配。

图片

这个注解被我加在了user2方法上,意思是只有Spring 容器中包含名字为pet的容器,我才将user2方法返回的对象放入Spring容器中,这就是所谓的根据条件装配。

值得一提的是,这个注解放的位置不同效果也会不同,现在我放在user2方法上,那么它只会影响到user2方法的注入。

如果我把这个注解放在MyConfig类上面,那么也就是说如果Spring容器中没有pet对象,整个MyConfig类中声明的对象都不会被注入到容器中。

@ImportResource

这个注解的作用是为了配合很多老系统中已经使用了xml注解的方式装配了对象,如果想要在SpringBoot中继续发挥作用就可以如下图所示引入该配置文件。

图片

@ConfigurationProperties

这个注解的作用是将配置文件中的值绑定到相应的实体类中。

SpringBoot项目都会有个配置文件application.properties,其中通常会放置一些全局的值。

例如我想给User对象的属性值放在application.properties中,如下图所示。

图片

那么如果想要生成的User对象都采用这两个值就需要在User实体类上加注解@ConfigurationProperties

图片

在这里要注意两点,第一点是@ConfigurationProperties注解中要加入prefix,也就是赋给它前缀值,这个值和application.properties中的userhh相同,帮助实体对象找到对应的属性值。

另外,实体对象上还需要加上注解@Component以将User类放入容器中,这样SpringBoot才能托管这个类,将其属性赋值。

三、细说条件注解

@Conditional

Spring4 中提供了更加通用的条件注解,让我们可以在满足不同条件时创建不同的 Bean,这种配置方式在 Spring Boot 中得到了广泛的使用,大量的自动化配置都是通过条件注解来实现的。

有的小伙伴可能没用过条件注解,但是开发环境、生产环境切换的 Profile 多多少少都有用过吧?实际上这就是条件注解的一个特例。

抛开 Spring Boot,我们来单纯的看看在 Spring 中条件注解的用法。

首先我们来创建一个普通的 Maven 项目,然后引入 spring-context,如下:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>
</dependencies>

然后定义一个 Food 接口,Food 接口有一个 showName 方法

public interface Food {
    String showName();
}

定义两个Food 接口的实现类,分别是 RiceNoodles,两个类实现了 showName 方法,然后分别返回不同值。

public class Rice implements Food {
    public String showName() {
        return "米饭";
    }
}
public class Noodles implements Food {
    public String showName() {
        return "面条";
    }
}

接下来再分别创建 Rice 和 Noodles 的条件类,如下:

public class NoodlesCondition implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("people").equals("北方人");
    }
}
public class RiceCondition implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("people").equals("南方人");
    }
}

在 matches 方法中做条件属性判断,当系统属性中的 people 属性值为 ‘北方人’ 的时候,NoodlesCondition 的条件得到满足,当系统中 people 属性值为 ‘南方人’ 的时候,RiceCondition 的条件得到满足,换句话说,哪个条件得到满足,一会就会创建哪个 Bean 。

接下来我们来配置 Rice 和 Noodles :

@Configuration
public class JavaConfig {
    @Bean("food")
    @Conditional(RiceCondition.class)
    Food rice() {
        return new Rice();
    }
    @Bean("food")
    @Conditional(NoodlesCondition.class)
    Food noodles() {
        return new Noodles();
    }
}

这个配置类,大家重点注意两个地方:

  • 两个 Bean 的名字都为 food,这不是巧合,而是有意取的。两个 Bean 的返回值都为其父类对象 Food。
  • 每个 Bean 上都多了 @Conditional 注解,当 @Conditional 注解中配置的条件类的 matches 方法返回值为 true 时,对应的 Bean 就会生效。

配置完成后,我们就可以在 main 方法中进行测试了:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.getEnvironment().getSystemProperties().put("people", "南方人");
        ctx.register(JavaConfig.class);
        ctx.refresh();
        Food food = (Food) ctx.getBean("food");
        System.out.println(food.showName());
    }
}

首先我们创建一个 AnnotationConfigApplicationContext 实例用来加载 Java 配置类,然后我们添加一个 property 到 environment 中,添加完成后,再去注册我们的配置类,然后刷新容器。容器刷新完成后,我们就可以从容器中去获取 food 的实例了,这个实例会根据 people 属性的不同,而创建出来不同的 Food 实例。

这个就是 Spring 中的条件注解。

进阶版 @Profile

条件注解还有一个进化版,那就是 Profile。我们一般利用 Profile 来实现在开发环境和生产环境之间进行快速切换。其实 Profile 就是利用条件注解来实现的。

还是刚才的例子,我们用 Profile 来稍微改造一下:

首先 Food、Rice 以及 Noodles 的定义不用变,条件注解这次我们不需要了,我们直接在 Bean 定义时添加 @Profile 注解,如下:

@Configuration
public class JavaConfig {
    @Bean("food")
    @Profile("南方人")
    Food rice() {
        return new Rice();
    }
    @Bean("food")
    @Profile("北方人")
    Food noodles() {
        return new Noodles();
    }
}

这次不需要条件注解了,取而代之的是 @Profile 。然后在 Main 方法中,按照如下方式加载 Bean:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.getEnvironment().setActiveProfiles("南方人");
        ctx.register(JavaConfig.class);
        ctx.refresh();
        Food food = (Food) ctx.getBean("food");
        System.out.println(food.showName());
    }
}

效果和上面的案例一样。

这样看起来 @Profile 注解貌似比 @Conditional 注解还要方便,那么 @Profile 注解到底是什么实现的呢?

我们来看一下 @Profile 的定义:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
	String[] value();
}

可以看到,它也是通过条件注解来实现的。条件类是 ProfileCondition ,我们来看看:

class ProfileCondition implements Condition {
	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
		if (attrs != null) {
			for (Object value : attrs.get("value")) {
				if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
					return true;
				}
			}
			return false;
		}
		return true;
	}
}

看到这里就明白了,其实还是我们在条件注解中写的那一套东西,只不过 @Profile 注解自动帮我们实现了而已。

@Profile 虽然方便,但是不够灵活,因为具体的判断逻辑不是我们自己实现的。而 @Conditional 则比较灵活。

两个例子向大家展示了条件注解在 Spring 中的使用,它的一个核心思想就是当满足某种条件的时候,某个 Bean 才会生效,而正是这一特性,支撑起了 Spring Boot 的自动化配置。