springboot加载命令行参数ApplicationArguments

news2024/11/16 19:43:36

往期文章

  • springcloud整合knike4j聚合微服务接口文档
  • spring源码 - 条件注解@ConditionnalOnClass的原理分析
  • springboot项目实现导出pdf功能,这也太简单了吧

目录

文章目录

  • 往期文章
  • 目录
  • 一、介绍
  • 二、通过应用程序参数获取配置
    • 1. 通过bean获取应用程序参数
    • 2. 通过`@Value`注解获取
  • 三、源码解读 - 封装应用程序参数
    • 1. DefaultApplicationArguments
    • 2. Source类
    • 3. SimpleCommandLinePropertySource
    • 4. SimpleCommandLineArgsParser
    • 5. CommandLinePropertySource
    • 6. PropertySource
  • 四、源码解读 - 为什么可以通过@Value注解获取参数配置
  • 五、源码解读 - 将应用程序参数注册到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官方的描述

在这里插入图片描述

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

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

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

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

在这里插入图片描述

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);
    }
}

输出如下所示

在这里插入图片描述

当然,你也可以通过@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());
        }
    }
    
  • 输出,结果符合预期

    在这里插入图片描述

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

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 ,我们可以通过断点进行验证

在这里插入图片描述

在源码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图

在这里插入图片描述

由于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实例来读取应用程序参数。


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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/410479.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在外web浏览器远程访问jupyter notebook服务器

文章目录前言视频教程1. Python环境安装2. Jupyter 安装3. 启动Jupyter Notebook4. 远程访问4.1 安装配置cpolar内网穿透4.2 创建隧道映射本地端口5. 固定公网地址转载自远控源码文章&#xff1a;公网远程访问jupyter notebook【cpolar内网穿透】 前言 Jupyter Notebook&#…

我现在必须new一个对象!!!

目录 前言 1.new 2.delete 3.底层逻辑 4.定位new 5.对比 前言 &#x1f383;之前在使用C语言的时候&#xff0c;我们便使用 malloc 和 calloc 等函数进行动态内存的开辟。但 C 之中又引入了两个操作符 new 和 delete 来代替C语言中的函数进行动态内存的管理。下面就一起…

苏州源特VPT87DDF01B 隔离变压器 小体积/SMD/3000VDC 隔离

1 产品特点  小体积隔离变压器  隔离电压 3000VDC  工作温度&#xff1a;-40~125℃&#xff08;包含产品温升&#xff09;  存储温度&#xff1a;-55~125℃  SMD 表贴安装  回流焊温度&#xff1a;峰值温度≤250℃&#xff08;10s&#xff09;  回流焊次数&#…

Softing新版HART多路复用器软件支持西门子控制器

用于访问配置和诊断数据的HART多路复用器软件——Softing smartLink SW-HT&#xff0c;现在支持西门子的ET200远程IO和FDT/DTM接口。 smartLink SW-HT是一个基于Docker容器的软件应用。通过该软件&#xff0c;用户可以快速地访问以太网远程IO的HART设备&#xff0c;并且无需额外…

【Linux】进程间通信之管道(pipe)

文章目录前言为什么要进程间通信进程间通信的理论依据管道管道的原理创建匿名管道管道的特点管道的场景利用管道控制子进程命名管道命名管道的打开规则命名管道和匿名管道的区别用命名管道实现server和client通信前言 大家好久不见&#xff0c;今天开始我们将进入进程间通信章…

redis高可用方案:主从复制+哨兵模式,经典案例:一主二从三哨兵,及springboot集成配置

Redis高可用方案&#xff1a;主从复制哨兵模式 经典案例&#xff1a;一主二从三哨兵&#xff0c;及springboot集成配置 第一.配置 1.主节点配置&#xff08;redis.conf&#xff09; # 主节点配置 仅展示必要配置 redis.conf # 保护模式设置为关&#xff0c;方便其他节点访问…

推荐几款炫酷的 MySQL 可视化管理工具!好用到爆!!

MySQL 的管理维护工具非常多&#xff0c;除了系统自带的命令行管理工具之外&#xff0c;还有许多其他的图形化管理工具&#xff0c;工具好用是一方面&#xff0c;个人的使用习惯也很重要&#xff0c;这里介绍 13 款 MySQL 图形化管理工具&#xff0c;供大家参考。 1&#xff1a…

vite .env.test环境使用ant design vue ,打包后a-date-picker控件无法选择日期

前端开发后台管理系统&#xff0c;常用的UI库当属Element UI和 Ant Design Vue&#xff0c;但是前段时间遇到一个奇葩问题&#xff0c;在这里记录一下&#xff0c;防止小伙伴们踩坑。 后台系统&#xff0c;大家肯定都用过时间控件&#xff0c;本期我们使用的是ant design vue&…

2道关于chan的面试题

题目一: 下面关于通道描述正确的是: 1.读nil chan会触发panic 2.写nil chan会触发panic 3.读关闭的chan会触发panic 4.写关闭的chan会触发panic解答&#xff1a;这个提涉及到2个知识点&#xff0c;操作nil的chan会怎么样&#xff0c;操作关闭的chan会怎么样&#xff0c;下面我…

双目相机测距原理

双目相机测距是一种常用的计算机视觉技术&#xff0c;它利用两个摄像头同时拍摄同一场景&#xff0c;通过测量两个摄像头视野中同一物体在图像上的像素差异&#xff0c;从而计算出物体距离的方法。 具体原理如下&#xff1a; 双目相机的构成 双目相机由两个摄像头组成&#…

用ChatGPT怎么赚钱?普通人用这5个方法也能赚到生活费

ChatGPT在互联网火得一塌糊涂&#xff0c;因为它可以帮很多人解决问题。比如&#xff1a;帮编辑人员写文章&#xff0c;还可以替代程序员写代码&#xff0c;帮策划人员写文案策划等等。ChatGPT这么厉害&#xff0c;能否用它来赚钱呢&#xff1f;今天和大家分享用ChatGPT赚钱的5…

Cesium关于3Dtiles的细节分享

介绍 介绍一下Cesium中有关3dTiles的奇淫技巧&#xff0c;存在一些埋坑的地方&#xff0c;以下内容仅为自己摸索的细节和方法&#xff0c;仅供参考&#xff0c;若有更好的办法欢迎讨论 通用快速获取feature中包含的属性信息 有时候需要快速获得3dTiles中的feature中的属性信…

deb文件如何安装到iphone方法分享

Cydia或同类APT管理软件在线安装 Cydia或同类APT管理软件在线安装,这个是最佳的安装方式,因为通常无需考虑依赖关系,但缺点是对网络的要求比较高;命令行中以dpkg-iXXX.deb的形式安装,好处是可以以通配符一次性安装多个deb,而且也可以直接看到脚本的运行状况和安装成功/失…

执行命令行程序测试自动化

这几天有一个小工具需要做测试&#xff0c;是一个命令行工具&#xff0c;这个命令行工具有点类似mdbg等命令行工具&#xff0c;即程序运行后&#xff0c;在命令行等待用户敲入的命令&#xff0c;处理命令并显示结果&#xff0c;再继续等待用户敲入新的命令。 原来的测试用例都…

Vue实现自动化平台(五)--用例编辑页面

上一章&#xff1a;Vue实现自动化平台&#xff08;四&#xff09;--接口管理页面的实现_做测试的喵酱的博客-CSDN博客 github地址&#xff1a;https://github.com/18713341733/vuemiaotest 这个目前只是用来练手的&#xff0c;项目还没成型。等以后我写完了&#xff0c;再更…

公网远程访问局域网SQL Server数据库【无公网IP内网穿透】

目录 1.前言 2.本地安装和设置SQL Server 2.1 SQL Server下载 2.2 SQL Server本地连接测试 2.3 Cpolar内网穿透的下载和安装 2.3 Cpolar内网穿透的注册 3.本地网页发布 3.1 Cpolar云端设置 3.2 Cpolar本地设置 4.公网访问测试 5.结语 转发自CSDN远程穿透的文章&…

不用但一定要懂 ---- iOS 之 响应链、传递链 与 手势识别

iOS 事件的主要由&#xff1a;响应连 和 传递链 构成。一般事件先通过传递链&#xff0c;传递下去。响应链&#xff0c;如果上层不能响应&#xff0c;那么一层一层通过响应链找到能响应的UIResponse。 响应链&#xff1a;由最基础的view向系统传递&#xff0c;first view ->…

c/c++:原码,反码,补码和常见的数据类型取值范围,溢出

c/c&#xff1a;原码&#xff0c;反码&#xff0c;补码和常见的数据类型取值范围&#xff0c;溢出 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;此时学会c的话&#xff0c; 我所知道的周边的会c的同学&#xff0c;可手握…

前脚我的 GPT4 被封,后脚收到了文心一言的邀请账号

大家好&#xff0c;我是二哥呀。 一早醒来&#xff0c;我的 ChatGPT Plus 账号就惨遭封禁&#xff0c;很不幸&#xff0c;我刚冲的 Plus 会员&#xff0c;用了不到一周的时间&#xff08;&#x1f62d;&#xff09;。 我没用亚洲的IP&#xff0c;所以网上传的那些不使用亚洲IP…

vuex中的 mapState, mapMutations

vuex中的 mapState&#xff0c; mapMutations Start 今天使用vuex的过程中&#xff0c;遇到 mapState&#xff0c; mapMutations 这么两个函数&#xff0c;今天学习一下这两个函数。 本文介绍的vuex基于 vuex3.0 1. 官方文档说明 1.1 mapState 官方解释 点击这里&#xff1…