首頁 > 軟體

springboot載入命令列引數ApplicationArguments的實現

2023-08-28 18:05:33

一、介紹

使用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範例來讀取應用程式引數。
 

到此這篇關於springboot載入命令列引數ApplicationArguments的實現的文章就介紹到這了,更多相關springboot載入命令列引數內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com