15.SpringShell源碼分析-命令掃描與執(zhí)行

上篇從源碼分析了SpringShell的ApplicationRunner, 本文筆者從源碼分析一下自定義命令的掃描和解析過程.

1. 解析自定義命令

1.1 注入Shell 組件

SpringShell 會在SpringShellAutoConfiguration 配置類中使用@Bean 方式向Spring 容器中注入Shell 組件.

// 源碼:org.springframework.shell.SpringShellAutoConfiguration
@Configuration
@Import(ResultHandlerConfig.class)
public class SpringShellAutoConfiguration {

    // 省略其它bean...

    @Bean
    public Shell shell(@Qualifier("main") ResultHandler resultHandler) {
        return new Shell(resultHandler);
    }

}

1.2 執(zhí)行Shell 初始化方法, 完成自定義命令解析工作

由于Shell 類定義時, 使用JSR250注解@PostConstruct 定義了對象初始化方法, 因此在Spring 容器創(chuàng)建Shell 實(shí)例之后, 會執(zhí)行shell實(shí)例的gatherMethodTargets()方法來完成自定義命令的解析工作.

  1. 創(chuàng)建ConfigurableCommandRegistry, 用于在注冊命令時, 臨時存儲所有自定義命令和方法的映射關(guān)系.
  2. 獲取所有的目標(biāo)方法注冊器, 會獲取到StandardMethodTargetRegistrar 對象, 調(diào)用其register 方法, 此方法會將解析到的所有命令方法注冊到ConfigurableCommandRegistry中
  3. 獲取已注冊的所有命令, 保存在methodTargets 屬性中, 用于后續(xù)使用.
  4. 對所有命令對應(yīng)的方法做參數(shù)校驗(yàn)
// 源碼: org.springframework.shell.Shell#gatherMethodTargets
@PostConstruct
public void gatherMethodTargets() throws Exception {
    // 創(chuàng)建ConfigurableCommandRegistry, 存儲所有命令與方法的映射關(guān)系.
    ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry();

    // 獲取所有的目標(biāo)方法注冊器, 會獲取到StandardMethodTargetRegistrar
    for (MethodTargetRegistrar resolver : applicationContext.getBeansOfType(MethodTargetRegistrar.class).values()) {
        // 調(diào)用StandardMethodTargetRegistrar 的register 方法, 將掃描到的所有命令注冊到 ConfigurableCommandRegistry 中
        resolver.register(registry);
    }
    // 獲取ConfigurableCommandRegistry 中已注冊的所有命令, 存入屬性變量methodTargets 中, 類型為: Map<String, MethodTarget>
    methodTargets = registry.listCommands();

    // 對所有命令做參數(shù)校驗(yàn)
    methodTargets.values().forEach(this::validateParameterResolvers);
}

1.3 解析所有ShellComponent組件, 注冊自定義命令

  1. 從IOC容器中獲取所有使用@ShellComponent定義的組件.
  2. 遍歷解析所有ShellComponent組件, 對每個組件的ShellMehtod 方法進(jìn)行遍歷
  3. 處理命令別名, 將駝峰風(fēng)格修改為-連接方式. 如addInt 別名為add-int
  4. 處理命令組名, 優(yōu)先使用方法定義的組名, 其次是類指定的組名, 再次是包指定的組名, 最后是默認(rèn)的組名
  5. 遍歷所有別名, 對每個別名進(jìn)行注冊
    1. 獲取限制命令是否可用的指示器
    2. 創(chuàng)建命令映射對象MethodTarget
    3. 將命令注冊到ConfigurableCommandRegistry中, 即存儲在ConfigurableCommandRegistry的map中
    4. 同時也將命令添加到屬性commands中, toString中使用.
// 源碼:org.springframework.shell.standard.StandardMethodTargetRegistrar#register
@Override
public void register(ConfigurableCommandRegistry registry) {
    // 通過注解類型方式, 獲取容器中所有使用@ShellComponet 注解修飾的組件
    Map<String, Object> commandBeans = applicationContext.getBeansWithAnnotation(ShellComponent.class);

    // 遍歷所有的ShellComponet 組件
    for (Object bean : commandBeans.values()) {
        Class<?> clazz = bean.getClass();
        // 反射處理ShellComponet 組件中的所有使用@ShellMethod 注解修飾的方法方法
        ReflectionUtils.doWithMethods(clazz, method -> {

            ShellMethod shellMapping = method.getAnnotation(ShellMethod.class);

            // 處理方法別名, 即@ShellMethod 的key屬性
            String[] keys = shellMapping.key();
            if (keys.length == 0) {
                // 對別名做轉(zhuǎn)換, 將駝峰風(fēng)格修改為使用-連接的方式
                keys = new String[] { Utils.unCamelify(method.getName()) };
            }

            // 獲取組名: 會先判斷方法是否指定了組名, 否則判斷類是否定義了組名, 否則判斷包是否定義了組名, 否則返回默認(rèn)組名
            String group = getOrInferGroup(method);

            // 遍歷所有的別名, 每個別名都注冊一次
            for (String key : keys) {
                // 獲取所有的限制是否可用的指示器
                Supplier<Availability> availabilityIndicator = findAvailabilityIndicator(keys, bean, method);

                // 創(chuàng)建命令對應(yīng)的MethodTarget
                MethodTarget target = new MethodTarget(method, bean, new Command.Help(shellMapping.value(), group), availabilityIndicator);

                // 將命令注冊到ConfigurableCommandRegistry 中
                registry.register(key, target);

                // 也將命令添加到屬性commands中, toString中使用
                commands.put(key, target);
            }
        }, method -> method.getAnnotation(ShellMethod.class) != null);
    }
}

2. Shell 交互式命令運(yùn)行邏輯

從筆者上一篇博客中, 我們知道SpringShell啟動之后, 會調(diào)用ApplicationRunner的run方法來啟動應(yīng)用, 而run方法核心就是調(diào)用Shell.run()方法.Shell的run方法是一個循環(huán), 會一直從ApplicationRunner 提供的InputProvider中獲取命令, 并執(zhí)行, 直到獲取的命令為null時, 終止循環(huán), 結(jié)束程序運(yùn)行.

2.1 核心循環(huán)流程

  1. 創(chuàng)建變量result, 保存命令執(zhí)行結(jié)果
  2. 啟動循環(huán), 循環(huán)終止條件為:
    1. 返回結(jié)果為ExitRequest實(shí)例時, 當(dāng)執(zhí)行quite/exit命令時, 返回ExitRequest. 參閱源碼: org.springframework.shell.standard.commands.Quit#quit
    2. 當(dāng)從InputProvider中讀取返回值為null 時, 通過break跳出循環(huán)
  3. 從InputProvider中獲取一條輸入命令
  4. 判斷InputProvider.readInput()返回值是否為null, 是的話直接跳出循環(huán)
  5. 調(diào)用evaluate方法, 執(zhí)行命令, 獲取命令返回值
  6. 判斷返回結(jié)果, 如果返回結(jié)果正常, 則處理返回結(jié)果.
public void run(InputProvider inputProvider) throws IOException {
    // 自定義保存命令執(zhí)行結(jié)果
    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í)行命令, 返回執(zhí)行結(jié)果
        result = evaluate(input);

        // 結(jié)果不是new Object 或 ExitRequest 實(shí)例時, 處理結(jié)果
        if (result != NO_INPUT && !(result instanceof ExitRequest)) {
            resultHandler.handleResult(result);
        }
    }
}

2.2 執(zhí)行命令邏輯

  1. 輸入命令為空, 則返回new Object
  2. 格式化命令, 將命令中多余的空白符,都替換為單個空格
  3. 提取輸入命令中的命令(不帶參數(shù)的)
  4. 將輸入命令按空格分隔成數(shù)組
  5. 如果命令不為空:
    1. 從shell.methodTargets 屬性中獲取命令的映射關(guān)系
    2. 獲取命令的限制可用條件, 用于判斷當(dāng)前命令是否可執(zhí)行
      1. 如果命令可執(zhí)行:
        1. 獲取命令的參數(shù)
        2. 獲取命令對應(yīng)的方法
        3. 解析并校驗(yàn)命令傳入的參數(shù)
        4. 反射調(diào)用命令對應(yīng)的方法, 并返回方法返回值
      2. 命令不可執(zhí)行, 則返回命令不可用異常信息
  6. 命令找不到, 返回?zé)o此命令的異常信息.
// 源碼:org.springframework.shell.Shell#evaluate
// 假定傳入?yún)?shù)為  add    2    3
public Object evaluate(Input input) {
    // 如果input 為空, 返回New Object()
    if (noInput(input)) {
        return NO_INPUT;
    }

    // 格式化命令, 將多個空格轉(zhuǎn)換為單個空格. 轉(zhuǎn)化后line為add 2 3
    String line = input.words().stream().collect(Collectors.joining(" ")).trim();

    // 從命令中提取命令的key, command為add
    String command = findLongestCommand(line);

    // 將輸入的命令按空白符分隔為列表, words為: ["add","2","3"]
    List<String> words = input.words();
    if (command != null) {
        // 從shell.methodTarget 中獲取命令對應(yīng)的方法信息
        MethodTarget methodTarget = methodTargets.get(command);
        // 獲取限制命令是否可用的條件定義
        Availability availability = methodTarget.getAvailability();

        // 判斷命令是否可用
        if (availability.isAvailable()) {
            // 獲取命令參數(shù), wordsForArgs 為["2","3"]
            List<String> wordsForArgs = wordsForArguments(command, words);

            // 獲取命令對應(yīng)的方法
            Method method = methodTarget.getMethod();

            try {
                // 解析校驗(yàn)參數(shù)
                Object[] args = resolveArgs(method, wordsForArgs);
                validateArgs(args, methodTarget);

                // 反射調(diào)用方法, 并返回方法返回值
                return ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
            }
            catch (Exception e) {
                return e;
            }
        } else {
            //命令不可用, 返回命令不可用異常. CommandNotCurrentlyAvailable 繼承了 RuntimeException
            return new CommandNotCurrentlyAvailable(command, availability);
        }
    } else {
        // 命令找不到, 返回命令找不到異常.CommandNotFound 繼承了 RuntimeException
        return new CommandNotFound(words);
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 官網(wǎng) 中文版本 好的網(wǎng)站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,727評論 0 5
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,632評論 4 61
  • Laravel 學(xué)習(xí)交流 QQ 群:375462817 本文檔前言Laravel 文檔寫的很好,只是新手看起來會有...
    Leonzai閱讀 8,721評論 2 12
  • 親愛的同學(xué)們,火是人類的朋友,它帶給我們光明,推動著人類社會走向文明。但是,火一旦失去控制就會造成災(zāi)難...
    英子_826d閱讀 1,159評論 0 0
  • 分享:主題:如何讓孩子順從吃飯 今天帶左左右右在萬達(dá)茂寶貝王玩,到了中午該吃飯的點(diǎn)小朋友還不想出來,還想繼續(xù)玩不愿...
    謝謝左右閱讀 470評論 0 2

友情鏈接更多精彩內(nèi)容