@Profile注解 -【Spring底层原理】

一、注解用法

在我们开发开发测试部署当中,有不同的环境,比如有:开发环境、测试环境、上产环境,不同的环境有不同的组件,这听着怎么那么像springboot中的多环境配置呢?今天,咱们来看看在spring中是如何实现的。

为了多环境开发,Spring为我们提供的可以根据当前环境,动态的激活和切换一系列组件的功能,比如数据源组件的配置,不同开发环境连接的数据源可能会不同,就可以使用@Profile注解进行配置,根据环境动态切换数据源组件。

@Profile:指定组件在哪个环境下才能被注册到容器中,不指定则任何环境都能注册这个组件,加了环境标识的bean,只有这个环境被激活的时候才能注册到容器

二、实例分析

就以数据源配置为例,不同环境数据源的配置往往不同,如何使用@Profile注解在不同环境下进行数据源的注册呢,通过实例来进行分析。

【1】@Profile环境搭建

// 启动测试类
@Test
public void TestMain() {
    // 创建IOC容器
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
    String[] namesForType = applicationContext.getBeanNamesForType(DataSource.class);
    for (String string : namesForType) {
        System.out.println(string);
    }
    // applicationContext.close();
}

// 配置类
@Configuration
public class AppConfig {

    // 测试环境
    // @Profile("test")
    @Bean("testDataSource")
    public DataSource dataSourceTest() throws PropertyVetoException {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("806188");
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/vhr");
        dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }

    // 开发环境
    // @Profile("dev")
    @Bean("devDataSource")
    public DataSource dataSourceDev() throws PropertyVetoException {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("806188");
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/vhr");
        dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }

    // 生产环境
    // @Profile("pro")
    @Bean("proDataSource")
    public DataSource dataSourcePro() throws PropertyVetoException {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("806188");
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/vhr");
        dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }
}

可以看到,在配置类中,有不同的数据源配置,分别是测试环境、开发环境、生产环境,我们可以通过@Profile注解来指定注入哪种环境,当不指定则任何环境都能注册这个组件,也就是上面代码,运行测试类,输出结果如下,三个数据源组价都进行了注入:

image-20210316111408626

将数据源使用@Profile注解进行标识,也就是将上面代码三个@Profile注解打开,此时因为没有激活注册环境,所以这个三个数据源都不能被注入。下面来进行激活。

【2】激活注册环境

  • default默认环境:@Profile("default")
  • 使用命令行动态参数:-Dspring.profiles.active=dev
  • 使用代码手动激活指定环境:要使用无参构造方法
  • 配置在配置类上:只有在指定环境的时候,整个配置类的所有配置才能生效
  1. default默认环境,使用@Profile("default"),标识默认当前环境
// 测试环境
@Profile("default")
// @Profile("test")
@Bean("testDataSource")
public DataSource dataSourceTest() throws PropertyVetoException {
    ComboPooledDataSource dataSource = new ComboPooledDataSource();
    dataSource.setUser("root");
    dataSource.setPassword("806188");
    dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/vhr");
    dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
    return dataSource;
}

image-20210316114109583

  1. 使用命令行动态参数,编辑运行配置的VM options,参数为:-Dspring.profiles.active=dev,通过该配置来标注是何种环境

image-20210316114355679

运行启动类,可以看到devDataSource被注入:

image-20210316114612732

  1. 使用代码手动激活指定环境,使用这种方法不能让有参构造器代码执行,因为执行有参构造器加载配置类的时候,执行refresh()方法容器就启动刷新了,就将配置写死了,所以这里要用无参构造器,手动激活指定环境。修改启动类:
@Test
public void TestMain() {
    // 创建IOC容器
    // AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
    // 设置需要激活的环境
    applicationContext.getEnvironment().setActiveProfiles("pro");
    // 注册主配置类
    applicationContext.register(AppConfig.class);
    // 启动刷新容器
    applicationContext.refresh();
    String[] namesForType = applicationContext.getBeanNamesForType(DataSource.class);
    for (String string : namesForType) {
        System.out.println(string);
    }
}

修改后,手动设置需要激活的环境,运行启动类,输出结果如下:

image-20210316142824671

  1. 在配置类上加@Profile注解,则只有在指定环境的时候,整个配置类的所有配置才能生效,如下:
@Profile("test")
@Configuration
public class AppConfig {...}

运行启动类,此时启动类是手动代码配置了pro环境,因为配置类上是@Profile("test"),只有在test环境下该配置类才会生效,所以没有输出:

image-20210316144725462

三、源码追踪

查看@Profile注解源码,我们可以看到,@Profile实际上是一个@Conditional注解:

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

@Conditional注解在前面的文章讲解过,可以进行参考:@Conditional注解 -【Spring底层原理】,这里简单分析一下

@Conditinal是一个条件注解,参数是一个class,这个class都要实现Condition接口,重写matches()方法。例如,上面示例代码中的ProfileCondition.class

class ProfileCondition implements Condition {
    ProfileCondition() {
    }
	// Spring从ConditionContext中拿到激活的Profile和注解上的字符串进行比对,以决定是否实例化这个bean
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            Iterator var4 = ((List)attrs.get("value")).iterator();

            Object value;
            do {
                if (!var4.hasNext()) {
                    return false;
                }

                value = var4.next();
            } while(!context.getEnvironment().acceptsProfiles(Profiles.of((String[])((String[])value))));

            return true;
        } else {
            return true;
        }
    }
}
  • mathces()方法的返回值是一个布尔值,返回true时,spring就会创建这个bean,返回false时就不会创建
  • mathes()方法上有两个参数,分别是ConditionContextAnnotatedTypeMetadata,这连个参数中包含了大量的信息,ConditionContext中有Environment, ClassLoader等信息,AnnotatedTypeMetadata可以获得注解的信息
  • Spring从ConditionContext中拿到激活的Profile和注解上的字符串进行比对,以决定是否实例化这个bean

四、总结

@Profile注解是用来指定组件在哪个环境下才能被注册到容器中的,只有这个环境被激活的时候才能注册到容器,激活总结为以下几种方式:

  • default默认环境:@Profile("default")
  • 使用命令行动态参数:-Dspring.profiles.active=dev
  • 使用代码手动激活指定环境:要使用无参构造方法
  • 配置在配置类上:只有在指定环境的时候,整个配置类的所有配置才能生效
end
  • 作者:ONESTAR(联系作者)
  • 更新时间:2021-03-17 09:12
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转载栈主转载的文章,请附上原文链接
  • 公众号转载:请在文末添加作者公众号二维码(公众号二维码见右边,欢迎关注)
  • 评论

    新增邮件回复功能,回复将会通过邮件形式提醒,请填写有效的邮件!