ApplicationArguments读取应用程序参数并注入到IOC容器

简介: ApplicationArguments读取应用程序参数并注入到IOC容器

一、介绍

使用springboot开发的同学们,都一定会从配置文件application.yml中读取配置。比如我们常常会在上传文件的功能中,把文件的保存路径写在配置文件中,然后在代码中通过@Value()注解从配置文件读取对应的配置,如下所示:

  • 在配置文件中定义文件路径

    file:
      location: /data/files
    
  • 在代码中获取保存路径

    @Component
    public class upload {
         
         
        @Value("${file.location}")
        private String fileLocation; // 文件路径/data/files
    
        public void upload(File file) {
         
         
            // 将文件保存到fileLocation中。
        }
    }
    

这种读取配置的方式非常方便,但是有一个让人抓狂的缺点

多人协作开发的情况下,同事A在配置文件中修改file.location的值为E:\\后将代码提交到git仓库,这时同事B把最新代码拉下来后由于他的电脑中不存在E盘导致该功能出现bug,很多同学不嫌麻烦,每次拉下最新代码后都会把这种配置重新修改以适合自己电脑的要求。

幸运的是,springboot在读取配置参数方面为我们提供了多种方式,并且不同方式之间存在优先级差异,如命令行配置的优先级大于配置文件的优先级。如下图为springboot官方的描述

官文对配置参数优先级的描述.jpg

从上图可知,命令行配置是在非单元测试环境下优先级最高的。

在我们通过java -jar命令启动项目时,添加额外的参数,就可以解决上面提及的多人协作开发的问题了。

二、通过应用程序参数获取配置

当我们使用IDEA启动springboot项目时,可以对项目的启动设置命令行参数,命令行参数的格式为--name=value--name,如下所示

设置命令行参数.jpg

1. 通过bean获取应用程序参数

启动项目后,我们从IOC容器中获取命令行参数对应的beanspringApplicationArguments,再从该bean中就可以获取到我们在命令行中配置的参数了。

springboot悄悄替我们向IOC容器中注册一个ApplicationArguments类型的bean,beanName为springApplicationArguments,该bean中保存着我们设置的应用程序参数。

@SpringBootApplication
public class ArgumentApplication {
   
   

    public static void main(String[] args) {
   
   
        ConfigurableApplicationContext applicationContext = SpringApplication.run(ArgumentApplication.class, args);

        // 获取应用程序参数
        ApplicationArguments applicationArguments =(ApplicationArguments)applicationContext
                                                                                .getBean("springApplicationArguments");
        // 获取命令行中name的配置
        List<String> name = applicationArguments.getOptionValues("name");
        System.out.println(name);
    }
}

输出如下所示

输出命令行参数1.jpg

当然,你也可以通过@Autowired的方式在类里注入ApplicationArguments实例来获取其中的配置。

2. 通过@Value注解获取

当然我们更常用的方式是通过@Value注解来获取,如下所示

  • 新建一个ComponentA,并用@Component注解标注为springBean,然后为其定义@Value标注的成员变量name

    @Component
    public class ComponentA {
         
         
    
        @Value("${name}")
        private String name;
    
        public ComponentA() {
         
         
        }
    
        public String getName() {
         
         
            return name;
        }
    }
    
  • 项目启动后,从IOC容器中获取ComponentA,并调用getName()方法来验证name的值

    @SpringBootApplication
    public class ArgumentApplication {
         
         
    
        public static void main(String[] args) {
         
         
            ConfigurableApplicationContext applicationContext = SpringApplication.run(ArgumentApplication.class, args);
    
            // 从配置文件中获取
            ComponentA componentA = (ComponentA) applicationContext.getBean("componentA");
            System.out.println(componentA.getName());
        }
    }
    
  • 输出,结果符合预期

    输出命令行参数2.jpg

三、源码解读 - 封装应用程序参数

springboot通过启动类的main()方法接收命令行中以--定义的应用程序参数,将参数按照不同类型以Map<String, List<String>>List<String>保存并封装到CommandLineArgs对象中,然后以name="commandLineArgs",source=CommandLineArgs对象将其封装到Source中,而SourceApplicationArguments内部属性,springboot将ApplicationArguments注入IOC容器。

从上面的例子中我们发现,springboot把我们配置的命令行参数封装到ApplicationArguments了,而ApplicationArguments又被springboot注册到IOC容器中,其对应的beanName为"springApplicationArguments",下面我们通过分析源码来逐步解开它是如何操作的。

首先,大家在写springboot启动类时,有没有注意到其中main()方法的参数String[] args,如下所示

@SpringBootApplication
public class ArgumentApplication {
   
   

    public static void main(String[] args) {
   
   
        SpringApplication.run(ArgumentApplication.class, args);
    }
}

但这个参数想必有很多同学不知道它是干嘛用的,它的作用就是用来接收启动命令中设置的--name=key参数,比如java -jarApplication.jar --name=key,我们可以通过断点进行验证

main方法接收应用程序参数.png

在源码run()方法中我们追踪args这个参数的调用链如下:

public ConfigurableApplicationContext run(String... args) {
   
   
    // ...
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // ...
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    // ...
}

从源码可以看出,参数args可以被用来获取运行监听器构造应用参数,因此我们把注意力放在构造应用参数上来。

1. DefaultApplicationArguments

看一下该类的结构,从它的构造方法我们得知,该类是把我们传入的--应用程序参数封装成一个Source对象,同时也保存一份原始的args参数,当我们需要获取参数时,都是调用Source对象提供的方法获取的,因此Source这个类尤其关键,我们需要弄清楚它是如何分析应用程序参数并将其封装到Source中的。

public class DefaultApplicationArguments implements ApplicationArguments {
   
   

    private final Source source;
    private final String[] args;

    public DefaultApplicationArguments(String... args) {
   
   
        Assert.notNull(args, "Args must not be null");
        this.source = new Source(args);
        this.args = args;
    }
    // ...
    private static class Source extends SimpleCommandLinePropertySource {
   
   

        Source(String[] args) {
   
   
            super(args);
        }
        // ...
    }
}

2. Source类

Source类是DefaultApplicationArguments的内部类,上面已经展示其具体实现的源码,它的构造函数就是把接收的应用程序参数传递给父类的构造函数。

下面我们看一下他的UML图

UML图.png

由于Source的构造函数直接把参数args交给其父类的构造函数,而Source本身没有多余的处理,因此我们直接进入其父类SimpleCommandLinePropertySource

3. SimpleCommandLinePropertySource

public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {
   
   

    public SimpleCommandLinePropertySource(String... args) {
   
   
        super(new SimpleCommandLineArgsParser().parse(args));
    }

    public SimpleCommandLinePropertySource(String name, String[] args) {
   
   
        super(name, new SimpleCommandLineArgsParser().parse(args));
    }
}

在这个类中,又是直接调用父类的构造方法,且没有自身的实现。但不同的,这里将我们设置的应用程序进行转换成CommandLineArgs对象交给父类构造函数。

它是怎么分析我们传入的应用程序参数的,又将其转换成什么样的结构呢?

4. SimpleCommandLineArgsParser

该类只有一个静态方法parse(),从命名也可以看出,该类的功能就是对命令行参数提供简单的转换器

class SimpleCommandLineArgsParser {
   
   

    public CommandLineArgs parse(String... args) {
   
   
        CommandLineArgs commandLineArgs = new CommandLineArgs();
        for (String arg : args) {
   
   
            // 以 -- 开头的应用程序参数
            if (arg.startsWith("--")) {
   
   
                String optionText = arg.substring(2);
                String optionName;
                String optionValue = null;
                int indexOfEqualsSign = optionText.indexOf('=');
                if (indexOfEqualsSign > -1) {
   
   
                    // --key=value这种形式的参数
                    optionName = optionText.substring(0, indexOfEqualsSign);
                    optionValue = optionText.substring(indexOfEqualsSign + 1);
                }
                else {
   
   
                    // --key这种形式的参数
                    optionName = optionText;
                }
                if (optionName.isEmpty()) {
   
   
                    throw new IllegalArgumentException("Invalid argument syntax: " + arg);
                }
                commandLineArgs.addOptionArg(optionName, optionValue);
            }
            else {
   
   
                // 不以 -- 开头的应用程序参数
                commandLineArgs.addNonOptionArg(arg);
            }
        }
        return commandLineArgs;
    }
}

从源码得知,应用程序参数的转换过程非常简单,就是根据--=进行字符串裁剪,然后将这些参数封装到CommandLineArgs里。而在CommandLineArgs中用不同的字段来保存不同类型的应用程序参数。如下

class CommandLineArgs {
   
   
    // 保存 --key=value  和 --key这两种类型的应用程序参数
    private final Map<String, List<String>> optionArgs = new HashMap<>();
    // 保存 key 这一种类型的应用程序参数
    private final List<String> nonOptionArgs = new ArrayList<>();
}


回到上一节SimpleCommandLinePropertySource,它的构造函数就是将应用程序参数转换为CommandLineArgs然后交给父类构造函数,那下面我们看其父类CommandLinePropertySource

5. CommandLinePropertySource

CommandLinePropertySource中,我们主要看其构造函数。

public abstract class CommandLinePropertySource<T> extends EnumerablePropertySource<T> {
   
   

    public static final String COMMAND_LINE_PROPERTY_SOURCE_NAME = "commandLineArgs";

    public CommandLinePropertySource(T source) {
   
   
        super(COMMAND_LINE_PROPERTY_SOURCE_NAME, source);
    }
}

很显然,又是直接调用父类的构造函数,而且向其父类构造函数传入的是"commandLineArgs"字符串 和 CommandLineArgs对象。那我们继续,进入父类EnumerablePropertySource,然后又将这两个参数继续传递给父类PropertySource

public abstract class EnumerablePropertySource<T> extends PropertySource<T> {
   
   

    public EnumerablePropertySource(String name, T source) {
   
   
        super(name, source);
    }
}

6. PropertySource

通过前面一系列对父类构造函数的调用,最终将name初始化为"commandLineArgs"字符串 ,将source初始化为 CommandLineArgs对象。

public abstract class PropertySource<T> {
   
   

    protected final String name;

    protected final T source;

    public PropertySource(String name, T source) {
   
   
        Assert.hasText(name, "Property source name must contain at least one character");
        Assert.notNull(source, "Property source must not be null");
        this.name = name;
        this.source = source;
    }
}

四、源码解读 - 为什么可以通过@Value注解获取参数配置

在前面我们将应用程序参数封装到ApplicationArguments对象中后,springboot又将这些应用程序参数添加到environment对象中,并且对已存在的配置进行覆盖,因此与配置文件中定义的参数类似,都可以通过@Value注解获取。

在下面的源码中,主要表达的是应用程序参数在各个方法调用中的传递,最关键的部分我们要看configurePropertySources()方法。该方法将应用程序参数配置到运行环境environment

public ConfigurableApplicationContext run(String... args) {
   
   
    // ...
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
    // ...
}

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
   
   
    // Create and configure the environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
}

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
   
   
    // ...
    configurePropertySources(environment, args);
    // ...
}

// 将应用程序设置到environment对象中,与配置文件中的参数处于同一environment对象中,因此可以通过@Value注解获取参数配置
protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
   
   
    MutablePropertySources sources = environment.getPropertySources();
    DefaultPropertiesPropertySource.ifNotEmpty(this.defaultProperties, sources::addLast);
    if (this.addCommandLineProperties && args.length > 0) {
   
   
        String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
        if (sources.contains(name)) {
   
   
            // 环境中已存在相同的配置,则进行覆盖
            PropertySource<?> source = sources.get(name);
            CompositePropertySource composite = new CompositePropertySource(name);
            composite.addPropertySource(
                new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
            composite.addPropertySource(source);
            sources.replace(name, composite);
        }
        else {
   
   
            sources.addFirst(new SimpleCommandLinePropertySource(args));
        }
    }
}

五、源码解读 - 将应用程序参数注册到IOC容器

在前面的章节,我们通过源码分析得出结论,springboot将应用程序参数封装到ApplicationArguments和运行环境Environment中。接下来我们看它是如何注册到IOC容器的。

public ConfigurableApplicationContext run(String... args) {
   
   
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
    // ...
    prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
    // ...
}

private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
            ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments, Banner printedBanner) {
   
   
    // ...
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    // ...
}

springboot将应用程序参数ApplicationArguments直接通过beanFactory.registerSingleton()方法手动地注册到IOC容器中,beanName为springApplicationArguments

六、总结

springboot将我们配置的命令行参数封装到ApplicationArguments,并使用"springApplicationArguments"作为beanName将其注册到IOC容器。

  • 设置应用程序参数时,符合要求的设置为:--key=value--key 以及 key
  • 可以通过@Value注解直接获取应用程序参数。
  • 可以通过@Autowired依赖注入一个ApplicationArguments实例来读取应用程序参数。




纸上得来终觉浅,绝知此事要躬行。

————————我是万万岁,我们下期再见————————

相关文章
|
3月前
|
XML Java 数据格式
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
这篇文章是Spring5框架的实战教程,主要介绍了如何在Spring的IOC容器中通过XML配置方式使用外部属性文件来管理Bean,特别是数据库连接池的配置。文章详细讲解了创建属性文件、引入属性文件到Spring配置、以及如何使用属性占位符来引用属性文件中的值。
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
|
19天前
|
XML Java 数据格式
Spring IOC容器的深度解析及实战应用
【10月更文挑战第14天】在软件工程中,随着系统规模的扩大,对象间的依赖关系变得越来越复杂,这导致了系统的高耦合度,增加了开发和维护的难度。为解决这一问题,Michael Mattson在1996年提出了IOC(Inversion of Control,控制反转)理论,旨在降低对象间的耦合度,提高系统的灵活性和可维护性。Spring框架正是基于这一理论,通过IOC容器实现了对象间的依赖注入和生命周期管理。
46 0
|
2月前
|
XML Java 开发者
经典面试---spring IOC容器的核心实现原理
作为一名拥有十年研发经验的工程师,对Spring框架尤其是其IOC(Inversion of Control,控制反转)容器的核心实现原理有着深入的理解。
99 3
|
3月前
|
XML Java 数据格式
Spring5入门到实战------3、IOC容器-Bean管理XML方式(一)
这篇文章详细介绍了Spring框架中IOC容器的Bean管理,特别是基于XML配置方式的实现。文章涵盖了Bean的定义、属性注入、使用set方法和构造函数注入,以及如何注入不同类型的属性,包括null值、特殊字符和外部bean。此外,还探讨了内部bean的概念及其与外部bean的比较,并提供了相应的示例代码和测试结果。
Spring5入门到实战------3、IOC容器-Bean管理XML方式(一)
|
3月前
|
XML Java 数据格式
Spring5入门到实战------5、IOC容器-Bean管理(三)
这篇文章深入探讨了Spring5框架中IOC容器的高级Bean管理,包括FactoryBean的使用、Bean作用域的设置、Bean生命周期的详细解释以及Bean后置处理器的实现和应用。
Spring5入门到实战------5、IOC容器-Bean管理(三)
|
3月前
|
XML Java 数据格式
Spring5入门到实战------4、IOC容器-Bean管理XML方式、集合的注入(二)
这篇文章是Spring5框架的实战教程,主题是IOC容器中Bean的集合属性注入,通过XML配置方式。文章详细讲解了如何在Spring中注入数组、List、Map和Set类型的集合属性,并提供了相应的XML配置示例和Java类定义。此外,还介绍了如何在集合中注入对象类型值,以及如何使用Spring的util命名空间来实现集合的复用。最后,通过测试代码和结果展示了注入效果。
Spring5入门到实战------4、IOC容器-Bean管理XML方式、集合的注入(二)
|
3月前
|
XML Java 数据格式
Spring5入门到实战------6、IOC容器-Bean管理XML方式(自动装配)
这篇文章是Spring5框架的入门教程,详细讲解了IOC容器中Bean的自动装配机制,包括手动装配、`byName`和`byType`两种自动装配方式,并通过XML配置文件和Java代码示例展示了如何在Spring中实现自动装配。
Spring5入门到实战------6、IOC容器-Bean管理XML方式(自动装配)
|
3月前
|
XML Java 数据格式
Spring5入门到实战------2、IOC容器底层原理
这篇文章深入探讨了Spring5框架中的IOC容器,包括IOC的概念、底层原理、以及BeanFactory接口和ApplicationContext接口的介绍。文章通过图解和实例代码,解释了IOC如何通过工厂模式和反射机制实现对象的创建和管理,以及如何降低代码耦合度,提高开发效率。
Spring5入门到实战------2、IOC容器底层原理
|
3月前
|
Kubernetes 负载均衡 数据中心
在K8S中,在主机和容器上部署应用程序有什么区别?
在K8S中,在主机和容器上部署应用程序有什么区别?
|
3月前
|
XML Java 数据格式
Spring5入门到实战------8、IOC容器-Bean管理注解方式
这篇文章详细介绍了Spring5框架中使用注解进行Bean管理的方法,包括创建Bean的注解、自动装配和属性注入的注解,以及如何用配置类替代XML配置文件实现完全注解开发。
Spring5入门到实战------8、IOC容器-Bean管理注解方式