參考實例
Java程序員的命令行工具
spring-shell-源碼解析-video
一、起源
1.1 原由
爲什麼要使用spring shell,在公司中,發現同事使用scala 寫了一個交互的命令行程序,其實就是scala自帶的信,註冊了函數,感覺使用起來挺方便的,爲啥Java裏面沒有這樣的使用東西!挺好奇的,我想使用一個接入簡單方便,不要花費太多的時間,且我們要熟悉!最後發現spring shell 比較好!集成了spring的容器機制!這個在今天的Java 後端程序員中,要是不會spring 真的不是Java開發的感覺。因此麼事,就瞭解了一下子如何。
1.2 解決什麼問題
在使用arthas的時候,很多的命令記不住,比如arthas watch 後面需要添加一堆的參數,tarce 需要滿足規範,我只想簡單的使用,不想記住那麼多,不想慢慢的看文檔啊!因此簡單的命令行能不能解決問題?可以的,就是一個簡單的字符串處理,比如更好的給你複製到剪切板中,不是很方便?第二個需求,有些常見的命令無法記住,我想當個筆記本來使用這樣可以?哈哈 !因此寫了一個命令行的工具 https://github.com/WangJi92/wdt 武當山命令行!歡迎收藏起來~。
二、Spring shell 簡述
2.1 簡述
spring的實現比較的簡單,命令key->命令(bean,方法) 這樣的一個大倉庫中保存了命令key對於命令需要執行的方法一個大Map結構,有點像Spring bean 管理容器一樣的方式實現。如下圖所示,有一個死循環的while一直在等待輸入,輸入後解析命令,查找命令,執行命令!總體來說就是這樣。
2.2 web 和命令行 爲何啓動了不掛掉
web是作爲後端服務一直有一個後臺處理前端請求的進程一直在運行着呢?因此spring boot 啓動後會一直運行着,你如果去掉web依賴,發現啓動完了就結束了哈!命令行程序也是一樣的道理,沒有是循環的等待用戶的輸入,一樣的會掛掉的!
2.3 整體模塊
三、Jline 終端
JLine is a Java library for handling console input. https://github.com/jline/jline3 這個就是處理終端的輸入的一個工具集合,spring shell 集成了進來,馬上用有了一些使用的功能tab補全,智能提示等等,配置上mac 上的item2 簡直神了。
需求
實現
之前不是說了?spring shell 功能的實現其實就是一個大Map,spring 在啓動之前,通過spring的生命週期各種操作已經處理完成了這些任務,之後只需要在spring 容器啓動後啓動一個Jline的死循環即可。spring 在啓動之後啓動Jline。(在開發中可能會有這樣的情景。需要在容器啓動的時候執行一些內容。比如讀取配置文件,數據庫連接之類的。SpringBoot給我們提供了兩個接口來幫助我們實現這種需求。這兩個接口分別爲CommandLineRunner和ApplicationRunner。他們的執行時機爲容器啓動完成的時候https://blog.csdn.net/jdd92/article/details/81053404)對Jline 也是這樣實現的!!!通過實現了一個ApplicationRunner的接口。要讓spring 啓動後沒有web的依賴,還不掛掉,死循環是必須的!!!
源碼入口
ApplicationRunner 入口程序
org.springframework.shell.jline.InteractiveShellApplicationRunner<br />入口: org.springframework.shell.jline.InteractiveShellApplicationRunner#run<br /> 如何就是啓動後執行spring shell的入口程序,Jline的reader帶入到一個while循環中等待用戶的輸入信息!
@Override
public void run(ApplicationArguments args) throws Exception {
boolean interactive = isEnabled();
if (interactive) {
InputProvider inputProvider = new JLineInputProvider(lineReader, promptProvider);
shell.run(inputProvider);
}
}
死循環等待用戶輸入
這裏就是Jline 內部調用處理,涉及參數處理,結果處理等等。
public void run(InputProvider inputProvider) throws IOException {
Object result = null;
while (!(result instanceof ExitRequest)) { // Handles ExitRequest thrown from Quit command
Input input;
try {
//等待用戶輸入
input = inputProvider.readInput();
}
catch (Exception e) {
if (e instanceof ExitRequest) { // Handles ExitRequest thrown from hitting CTRL-C
break;
}
resultHandler.handleResult(e);
continue;
}
if (input == null) {
break;
}
//解析用戶輸入,反射調用獲取結果
result = evaluate(input);
if (result != NO_INPUT && !(result instanceof ExitRequest)) {
//結果處理器
resultHandler.handleResult(result);
}
}
}
Jline 配置入口
org.springframework.shell.jline.JLineShellAutoConfiguration,之前上面的Jline reader 配置信息就是這裏,針對顯示的效果進行部分的集成進來,對於用戶輸入集成。
Jline reader 入口
org.springframework.shell.jline.JLineShellAutoConfiguration#lineReader
@Bean
public LineReader lineReader() {
LineReaderBuilder lineReaderBuilder = LineReaderBuilder.builder()
.terminal(terminal())
.appName("Spring Shell")
.completer(completer())
.history(history)....
.parser(parser());
LineReader lineReader = lineReaderBuilder.build();
lineReader.unsetOpt(LineReader.Option.INSERT_TAB); // This allows completion on an empty buffer, rather than inserting a tab
return lineReader;
}
Jline 終端配置
org.springframework.shell.jline.JLineShellAutoConfiguration#terminal
使用的系統終端
@Bean(destroyMethod = "close")
public Terminal terminal() {
try {
return TerminalBuilder.builder().build();
}
catch (IOException e) {
throw new BeanCreationException("Could not create Terminal: " + e.getMessage());
}
}
Jline 官方文檔 入門
https://github.com/jline/jline3/wiki/Using-line-readers
Jline reader
LineReader reader = LineReaderBuilder.builder().build();
String prompt = ...;
while (true) {
String line = null;
try {
line = reader.readLine(prompt);
} catch (UserInterruptException e) {
// Ignore
} catch (EndOfFileException e) {
return;
}
...
}
Jline Terminals
https://github.com/jline/jline3/wiki/Terminals
Terminal terminal = TerminalBuilder.builder()
.system(true)
.build();
or
Terminal terminal = TerminalBuilder.terminal();
Jline 官方的文檔一看是不是很簡單了…自己實現沒有spring的容器功能了,是不是感覺不會寫代碼了;不能這樣子,還是要多看一下一些優秀的代碼。
四、命令收集
spring shell自動配置,除了之前的Jline自動裝配之外,還有spring shell 收集,Command Map !
https://github.com/WangJi92/wdt/blob/master/src/main/java/com/wudang/wdt/command/IpCommand.java 這裏面隨便找一個實現的Command,@ShellComponent, @ShellMethod 就是要收集起來,放置到一個map容器中保存起來!解析命令的時候根據名稱查找相應的實現咯。@ShellComponent 本質就是一個Spring Bean無需特殊處理,只需要找到spring 上下文中所有的帶有註解@ShellComponent,查找所有的方法即可!
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.shell.SpringShellAutoConfiguration,\
org.springframework.shell.jline.JLineShellAutoConfiguration
本質就是這樣的!
applicationContext.getBeansWithAnnotation(ShellComponent.class);
4.1 入口
org.springframework.shell.SpringShellAutoConfiguration#shell
/**
*
* @param resultHandler
* @return
*/
@Bean
public Shell shell(@Qualifier("main") ResultHandler resultHandler) {
//這裏默認註冊了一個分發結果的處理器
return new Shell(resultHandler);
}
解析命令
@PostConstruct
public void gatherMethodTargets() throws Exception {
// 命令註冊工廠...
ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry();
//目標方法註冊工廠
for (MethodTargetRegistrar resolver : applicationContext.getBeansOfType(MethodTargetRegistrar.class).values()) {
resolver.register(registry);
}
methodTargets = registry.listCommands();
//參數校驗
methodTargets.values()
.forEach(this::validateParameterResolvers);
}
4.2 命令註冊工廠
- 獲取註冊工廠所有的命令
- 註冊一個命令key,和命令的相關Bean,Map等調用基本元數據信息
大Map 實現類,管理命令集合
org.springframework.shell.ConfigurableCommandRegistry
public class ConfigurableCommandRegistry implements CommandRegistry {
/**
* 命令的名稱 -- 目標(可執行方法的信息)
*/
private Map<String, MethodTarget> commands = new HashMap<>();
@Override
public Map<String, MethodTarget> listCommands() {
return new TreeMap<>(commands);
}
/**
* 註冊一個命令的信息
* @param name
* @param target
*/
public void register(String name, MethodTarget target) {
MethodTarget previous = commands.get(name);
if (previous != null) {
throw new IllegalArgumentException(
String.format("Illegal registration for command '%s': Attempt to register both '%s' and '%s'", name, target, previous));
}
commands.put(name, target);
}
}
4.2 方法註冊器 MethodTargetRegistrar
- 找到處理的方法,比如ShellComponent,ShellMethod 註冊到命令註冊工廠中
實現核心邏輯
org.springframework.shell.standard.StandardMethodTargetRegistrar#register
- 找到ShellComponent class,找到方法,然後處理命令需要的元數據信息!
- 比如分組、幫助信息等等解析
@Override
public void register(ConfigurableCommandRegistry registry) {
//找到所有的這樣的Bean的信息
Map<String, Object> commandBeans = applicationContext.getBeansWithAnnotation(ShellComponent.class);
for (Object bean : commandBeans.values()) {
Class<?> clazz = bean.getClass();
ReflectionUtils.doWithMethods(clazz, method -> {
ShellMethod shellMapping = method.getAnnotation(ShellMethod.class);
//獲取key
String[] keys = shellMapping.key();
if (keys.length == 0) {
keys = new String[] { Utils.unCamelify(method.getName()) };
}
//獲取分組
String group = getOrInferGroup(method);
for (String key : keys) {
//找到是否可用標識
Supplier<Availability> availabilityIndicator = findAvailabilityIndicator(keys, bean, method);
//註冊命令 已經當前的幫助信息 可用性信息
MethodTarget target = new MethodTarget(method, bean, new Command.Help(shellMapping.value(), group), availabilityIndicator);
registry.register(key, target);
commands.put(key, target);
}
}, method -> method.getAnnotation(ShellMethod.class) != null);
}
}
五、命令解析
從之前的瞭解ApplicationRunner入口程序,Jline的reader等待用戶的輸入,用戶輸入的信息無賴就是
watch -n 10 空格分隔的形式,要反射到具體的方法上,需要將後序的參數進行一一的處理。
5.1 獲取字符串
org.springframework.shell.Shell#run
public void run(InputProvider inputProvider) throws IOException {
Object result = null;
while (!(result instanceof ExitRequest)) {
Input input;
try {
//死循環不斷的等待用戶的輸入
input = inputProvider.readInput();
}
catch (Exception e) {
if (e instanceof ExitRequest) {
break;
}
resultHandler.handleResult(e);
continue;
}
if (input == null) {
break;
}
//解析結果,從input中解析參數信息
result = evaluate(input);
if (result != NO_INPUT && !(result instanceof ExitRequest)) {
//解析結果的處理
resultHandler.handleResult(result);
}
}
}
//這裏就是從input中獲取字符串信息,然後空格分隔
//org/springframework/shell/Shell.java:181
String line = input.words().stream().collect(Collectors.joining(" ")).trim();
5.2 解析命令、解析參數
5.2.1 解析命令
命令解析非常的簡單,從大Map中獲取命令唯一的key即可。因爲字符串在命令分隔的形式就是空格分隔而已,第一個是命令,後序的都是一些參數。
//之前收集好的命令
protected Map<String, MethodTarget> methodTargets = new HashMap<>();
MethodTarget methodTarget = methodTargets.get(command);
5.2.2 解析參數
參數解析十分的複雜,org.springframework.shell.standard.StandardParameterResolver 主要的邏輯在這裏,主要是通過參數的空格區分,參數Str->對象,spring ConversionService;其實就是和具體的參數對應起來,要解析好還是複雜的邏輯。
org.springframework.shell.Shell#resolveArgs
private Object[] resolveArgs(Method method, List<String> wordsForArgs) {
//解析參數
List<MethodParameter> parameters = Utils.createMethodParameters(method).collect(Collectors.toList());
Object[] args = new Object[parameters.size()];
Arrays.fill(args, UNRESOLVED);
for (ParameterResolver resolver : parameterResolvers) {
for (int argIndex = 0; argIndex < args.length; argIndex++) {
MethodParameter parameter = parameters.get(argIndex);
//處理參數的輸入輸出
if (args[argIndex] == UNRESOLVED && resolver.supports(parameter)) {
args[argIndex] = resolver.resolve(parameter, wordsForArgs).resolvedValue();
}
}
}
return args;
}
5.3 調用方法
org.springframework.shell.Shell#resolveArgs
Object[] args = resolveArgs(method, wordsForArgs);
//校驗參數
validateArgs(args, methodTarget);
//調用方法
return ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
六、結果解析
結果解析非常的簡單,不像spring mvc 那麼的複雜,這裏僅僅是真對不同的對象進行響應常用不同的方式進行處理,比如異常、找不到命令、table、各種對象;
6.1 基本結構
根據泛型來區分解析不同的類的信息~比較的簡單
public interface ResultHandler<T> {
/**
* Deal with some method execution result, whether it was the normal return value, or some kind
* of {@link Throwable}.
*/
void handleResult(T result);
}
6.2 入口類
入口類比較的簡單,主要是收集所有的handler,根據具體的類型進行分發!
public class TypeHierarchyResultHandler implements ResultHandler<Object> {
/**
* 註冊了一堆Class 對應的處理策略!
*/
private Map<Class<?>, ResultHandler<?>> resultHandlers = new HashMap<>();
@SuppressWarnings("unchecked")
public void handleResult(Object result) {
if (result == null) { // void methods
return;
}
Class<?> clazz = result.getClass();
//根據類的信息進行分發
ResultHandler handler = getResultHandler(clazz);
handler.handleResult(result);
}
private ResultHandler getResultHandler(Class<?> clazz) {
ResultHandler handler = resultHandlers.get(clazz);
if (handler != null) {
return handler;
}
else {
for (Class type : clazz.getInterfaces()) {
//找接口的處理策略
handler = getResultHandler(type);
if (handler != null) {
return handler;
}
}
//繼續找父類
return clazz.getSuperclass() != null ? getResultHandler(clazz.getSuperclass()) : null;
}
}
@Autowired
public void setResultHandlers(Set<ResultHandler<?>> resultHandlers) {
for (ResultHandler<?> resultHandler : resultHandlers) {
//找到泛型的信息
ResolvableType type = ResolvableType.forInstance(resultHandler).as(ResultHandler.class);
registerHandler(type.resolveGeneric(0), resultHandler);
}
}
private void registerHandler(Class<?> type, ResultHandler<?> resultHandler) {
ResultHandler<?> previous = this.resultHandlers.put(type, resultHandler);
if (previous != null) {
throw new IllegalArgumentException(String.format("Multiple ResultHandlers configured for %s: both %s and %s", type, previous, resultHandler));
}
}
}
6.3 最終的目的調用終端寫入
public class DefaultResultHandler extends TerminalAwareResultHandler<Object> {
@Override
protected void doHandleResult(Object result) {
//輸出數據信息.ToString
terminal.writer().println(String.valueOf(result));
}
}
七、總結
簡單的瞭解spring shell 總體的處理邏輯,雖然看起來很簡單,其實內部的實現邏輯還是蠻多的~除了支持Jline之外還支持Jcommnad,默認是Jline的!瞭解一個東西逐漸瞭解的感覺十分的不錯哦!