相关文章推荐
冲动的鸵鸟  ·  Using String Replace ...·  2 年前    · 
寂寞的炒粉  ·  windows subsystem for ...·  2 年前    · 
小眼睛的鼠标垫  ·  C# ...·  2 年前    · 

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();
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的!了解一个东西逐渐了解的感觉十分的不错哦!

在一文中有提到adb shell的指令在/system/bin路径下,现在本文以am指令为例,解析一条指令的执行流程以 adb shell am broadcast -a zhihe.factorytest.action.PASS -f 0x1000000 为例在android11中用adb发送广播不加后面的 -f 0x1000000,应用中接受不到广播,也顺便追溯一下 -f 0x1000000带表的什么意思。 本文对bash的源码(版本:4.2.46(1)-release)进行简要分析。 bash是用C语言写成的,其源码中只使用了少量的数据结构:数组,树,单向链表,双向链表和哈希表。几乎所有的bash结构都是用这些基本结构实现的。 源码中最主要的结构都定义在根目录下头文件command.h中。 bash在不同阶段传输信息并处理数... 这个系列是对仿unix基本接口和内部设计的简化教学操作系统xv6的文档及源码阅读理解,仅个人学习笔记理解,有误见谅哈哈。本文提供用vsCode连接WSL使用GDB调试xv6教程,及用户源码调试教程,及xv6中文文档“Chapter 1 Operating system interfaces”详解,最后对xv6的shell与命令解析相关源码进行详细解析(流程图),可视化地展示各类命令在xv6的解析过程。内含优质相关博文推荐。 Shell 快速入门(零):目录 Shell 快速入门定位于快速帮助初学者掌握 Shell 的语法,快速编写 Shell 脚本。对于 Shell 学习者来说,应该将 Shell 当成是工具,掌握了基础的语法之后就直接使用。当后面需要一些更高级的语法或者功能时,再去查找相应的资料。 这个系列与其他 Shell 入门教程不同的是:这个系列专注于用最少的知识,让你快速掌握必须的语法,最终实现快速上手的目... SpringBoot Shell基于JLine库实现了REPL(READ EVAL PRINT LOOP) 模式的命令行工具,给我们提供了方便的使用命令行的工具。 通过https://start.spring.io/创建SpringBoot项目,并添加依赖。 <dependency> <groupId>org.springframework.shel... 大家好,我是王老狮,今天开始开新坑。作为JAVA程序员,Spring基本上是必备的技能,也是面试经常考核的技能,特别是大厂,Spring源码基本是必问的题目。但是很多同学看到源码就头疼,根本不知道源码该无法下手。今天开始,就带着大家全面剖析下SpringSpringBoot,以及SpringCloud的源码,欢迎大家来交流。 一、Spring的前世今生 我们学习一款框架,不仅要学习他的作用以及是如何使用的,更应该了解他的过去,这样有助于我们思考为什么要有这么一款产品,以及他带给我们什么样的价值。可以说,S 一、什么是Spring Shell ? Spring Shell允许人们轻松创建这样的可运行应用程序,用户将在其中输入文本命令,这些命令将被执行直到程序终止。Spring Shell项目提供了创建此类REPL(读取,评估,打印循环)的基础结构,从而使开发人员可以使用熟悉的Spring编程模型来专注于命令实现。 诸如解析,TAB完成,输出着色,精美的ascii-art表显示,输入转换和验证之类的高级... 并非所有应用程序都需要精美的Web用户界面!有时,使用交互式终端与应用程序交互是完成工作的最合适方式。SpringShell允许人们轻松地创建这样一个可运行的应用程序,用户将在其中输入将执行的文本命令,直到程序终止。SpringShell项目提供了创建这样一个REPL(读取、评估、打印循环)的基础设施,允许开发人员使用熟悉的Spring编程模型专注于命令实现。默认情况下,不需要为您的命令指定键(**即在shell中应该用来调用它的单词)。sayHello()即将变为)。}... 首先看下trap结构体: 1272 typedef struct trap { 1273 const char *name; /* short name */ 1274 const char *mess;