SpringShell應(yīng)用啟動時, 會默認(rèn)向IOC容器中注入兩個ApplicationRunner: ScriptShellApplicationRunner 和 InteractiveShellApplicationRunner, 其中ScriptShellApplicationRunner 的優(yōu)先級要高于InteractiveShellApplicationRunner.
1. ApplicationRunner 定義
1.1 InteractiveShellApplicationRunner
InteractiveShellApplicationRunner 指定順序?yàn)?
@Order(InteractiveShellApplicationRunner.PRECEDENCE)
public class InteractiveShellApplicationRunner implements ApplicationRunner {
// ...
}
1.2 ScriptShellApplicationRunner
定義時, 指定順序?yàn)镮nteractiveShellApplicationRunner 的順序-100, 也就是優(yōu)先級大于InteractiveShellApplicationRunner, 通過這種方式限制優(yōu)先注冊ScriptShellApplicationRunner.
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 100) // Runs before InteractiveShellApplicationRunner
public class ScriptShellApplicationRunner implements ApplicationRunner {
// ...
}
1.3 配置類JLineShellAutoConfiguration
配置類JLineShellAutoConfiguration中注入 interactiveApplicationRunner 和 scriptApplicationRunner 組件, 有條件注入, 默認(rèn)為true.
@Configuration
public class JLineShellAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = SPRING_SHELL_INTERACTIVE, value = InteractiveShellApplicationRunner.ENABLED, havingValue = "true", matchIfMissing = true)
public ApplicationRunner interactiveApplicationRunner(Parser parser, Environment environment) {
return new InteractiveShellApplicationRunner(lineReader(), promptProvider, parser, shell, environment);
}
@Bean
@ConditionalOnProperty(prefix = SPRING_SHELL_SCRIPT, value = ScriptShellApplicationRunner.ENABLED, havingValue = "true", matchIfMissing = true)
public ApplicationRunner scriptApplicationRunner(Parser parser, ConfigurableEnvironment environment) {
return new ScriptShellApplicationRunner(parser, shell, environment);
}
}
2. 源碼分析
本文只設(shè)計(jì)ApplicationRunner的運(yùn)行流程, 不涉及bean 初始化等其它流程.
1. 啟動容器, 遍歷執(zhí)行自定義ApplicationRunner
- SpringShell 應(yīng)用通過 SpringApplication.run(SpringShellApplication.class, args)啟動項(xiàng)目, 會調(diào)用org.springframework.boot.SpringApplication#run(java.lang.String...) 方法
- org.springframework.boot.SpringApplication#run(java.lang.String...) 會對容器進(jìn)行初始化, 創(chuàng)建bean 等操作, 然后會調(diào)用所有自定義的ApplicationRunner 的run方法
// 源碼: org.springframework.boot.SpringApplication#callRunners
private void callRunners(ApplicationContext context, ApplicationArguments args) {
// 獲取容器中定義的所有ApplicationRunner
List<Object> runners = new ArrayList();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
Iterator var4 = (new LinkedHashSet(runners)).iterator();
// 遍歷所有ApplicationRunner, 執(zhí)行其run方法.
while(var4.hasNext()) {
Object runner = var4.next();
if (runner instanceof ApplicationRunner) {
// callRunner()方法里就是執(zhí)行runner.run(args) 方法
this.callRunner((ApplicationRunner)runner, args);
}
if (runner instanceof CommandLineRunner) {
this.callRunner((CommandLineRunner)runner, args);
}
}
}
2. 優(yōu)先執(zhí)行ScriptShellApplicationRunner.run(args)方法
- 首先對所有啟動參數(shù)做一遍過濾, 獲取所有的腳本參數(shù), 并創(chuàng)建文件列表. 也就是說, 只要啟動參數(shù)包含腳本參數(shù), 則忽略所有其它參數(shù).
- 如果啟動參數(shù)中,包含腳本參數(shù):
- 關(guān)閉交互式方式, 在執(zhí)行InteractiveShellApplicationRunner.run 方法時, 會先判斷是否
- 遍歷腳本列表, 為每個腳本文件創(chuàng)建一個InputProvider, 每個腳本都作為一個獨(dú)立的輸入源, 執(zhí)行sell.run()方法
- 每個腳本讀到最后都返回null, shell.run()讀到null時, 結(jié)束shell.run()中的循環(huán).
- 如果啟動參數(shù), 不包含腳本參數(shù), 則此ApplicationRunner運(yùn)行結(jié)束, 接下來運(yùn)行下一個ApplicationRunner, 即InteractiveShellApplicationRunner
// org.springframework.shell.jline.ScriptShellApplicationRunner#run**
@Override
public void run(ApplicationArguments args) throws Exception {
# 1.獲取啟動參數(shù)中所有以@開頭的參數(shù), 為每個腳本參數(shù)創(chuàng)建一個File對象, 組成File列表
List<File> scriptsToRun = args.getNonOptionArgs().stream()
.filter(s -> s.startsWith("@"))
.map(s -> new File(s.substring(1)))
.collect(Collectors.toList());
boolean batchEnabled = environment.getProperty(SPRING_SHELL_SCRIPT_ENABLED, boolean.class, true);
# 2.如果腳本參數(shù)不為空
if (!scriptsToRun.isEmpty() && batchEnabled) {
// 3.設(shè)置關(guān)閉交互式方式, 保證運(yùn)行為腳本的所有命令之后, 直接關(guān)閉程序
InteractiveShellApplicationRunner.disable(environment);
// 4.遍歷每個腳本, 為每個腳本創(chuàng)建一個命令提供源, 依次執(zhí)行shell的run方法.
for (File file : scriptsToRun) {
try (Reader reader = new FileReader(file);
FileInputProvider inputProvider = new FileInputProvider(reader, parser)) {
shell.run(inputProvider);
}
}
}
}
3. 其次執(zhí)行 InteractiveShellApplicationRunner.run(args)方法
- 首先判斷, 交互方式是否打開. 即在ScriptShellApplicationRunner.run中有沒有關(guān)閉交互時方式
- 如果交互方式打開, 則創(chuàng)建控制臺命令輸入源, 執(zhí)行shell的run方法. shell 的run 會進(jìn)入無限循環(huán), 當(dāng)用戶輸入為null 時, 則終止循環(huán), 退出程序
// 源碼: org.springframework.shell.jline.InteractiveShellApplicationRunner#run
@Override
public void run(ApplicationArguments args) throws Exception {
// 獲取交互方式是否打開, 如果ScriptShellApplicationRunner 中執(zhí)行了第三步, 此處獲取即為false.
boolean interactive = isEnabled();
// 如果交互方式打開(為true), 則創(chuàng)建控制臺輸入源, 執(zhí)行shell 的run方法
if (interactive) {
InputProvider inputProvider = new JLineInputProvider(lineReader, promptProvider);
shell.run(inputProvider);
}
}
4. 核心方法Shell.run(InputProvider)
- Shell的run方法是一個無限循環(huán), 會循環(huán)地從傳入的InputProvider中獲取命令, 如果獲取為空, 則跳出循環(huán), 結(jié)束run方法; 否則解析命令, 執(zhí)行命令,并將返回值輸出.
- run方法從InputProvider 中是一條命令一條命令讀取的, 且只有當(dāng)返回值為null時, 才會跳出run方法, 結(jié)束方法運(yùn)行. 這一點(diǎn)在自定義Runner時需要注意.
// 源碼: org.springframework.shell.Shell#run
public void run(InputProvider inputProvider) throws IOException {
// 自定義循環(huán)退出碼
Object result = null;
// 無限循環(huán), 知道result為退出嘛
while (!(result instanceof ExitRequest)) {
Input input;
// 從輸入源中讀取一條輸入
try {
input = inputProvider.readInput();
}
catch (Exception e) {
resultHandler.handleResult(e);
continue;
}
// 當(dāng)讀取的輸入為null時, 跳出循環(huán), 結(jié)束此shell的運(yùn)行
if (input == null) {
break;
}
// 執(zhí)行命令, 相應(yīng)結(jié)果
result = evaluate(input);
// 處理結(jié)果, 回顯終端
if (result != NO_INPUT && !(result instanceof ExitRequest)) {
resultHandler.handleResult(result);
}
}
}