参考实例
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的容器的功能。
- 拥有Jline的功能
实现
之前不是说了?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的!了解一个东西逐渐了解的感觉十分的不错哦!