Java9不出意外的又跳票了,传闻Java9加入了JEP 222,所以用OpenJDK体验了一把JShell并分析了下其实现。

JShell简述

JShell是一个能够执行Java代码片段、表达式的Shell,从外表看上去像是一个Java的解释器,实际上它是一个Read-Eval-Print Loop,接受命令、代码片段输入,输出运算结果或者一个变更状态。

JShell简易使用指北

  1. 通过${JAVA_HOME}/bin/jshell便可以启动一个JShell。
  2. 如其他Java工具一样,JShell已自带普通话补丁,在其OepnJDK源码资源文件也可以看到有l10n_zh_CN.properties的多语言支持。
  3. 执行命令
    类似于IPython,JShell除了可以执行Java代码,还内置了部分命令,键入\help便可以获得命令列表。
  4. 表达式执行
    JShell可以直接执行一个算术表达式,输出计算值或者函数的返回值。对于单条语句,可以省略句尾句号
  5. import
    JShell同时可以支持import外部包,使用Tab进行自动补全等功能。这里import还可以使用\evn指令导入的合法classpath下的任意jar。也就是说导入
  6. 类创建初始化
    JShell同时可以创建初始化类实例,调用实例方法。

JShell基础对象

MemoryFileManager

JShell编译器内存文件API的管理器,同时具有一个OutputMemoryJavaFileObject的Map用于类文件的缓存。

TaskFactory

编译器用于解析、分析、在内存编译class文件的API基本接口,其中CompileTask、AnalyzeTask、ParseTask均继承自BaseTask,这几个Task均为TaskFactory的内部类,通过TaskFactory的工厂方法产生对应的实例。

Eval

赋值引擎,将source 封装成方法、属性等等,在外部封装了imports和class,为整个JShell的核心部分

编译 声明 重定义 替换 执行

Snippet

代表传递的Java代码片段,同一个片段是不可变的,这一特性也就意味着它的任意方法都会有相同的返回,并且是线程安全的。
Snippet为抽象类,只有继承终点为实现类,其中派生Snippet结构如图所示。
ExpressionSnippet为表达式片段,StatementSnippet为声明片段,ErroneousSnippet为非法片段,PersistentSnippet为被存储影响后续代码结果的片段。DeclarationSnippet为值片段,其下属值、类型、函数都是其实现类。

SnippetEvent

直接/间接通过 JShell.eval(String)或者JShell.drop(Snippet), 或者Snippet被直接生成的关于这个变化描述的片段

ReplParserFactory

继承自com.sun.tools.javac.parser.ParserFactory解析器工厂,通过com.sun.tools.javac.util.Context初始化了,com.sun.tools.javac.parser.ScannerFactory用于产生Scanner。

DiagList

一个存放Snippet诊断信息的向量,这些包含是否有不可达、未声明、无法解析等错误。

MaskCommentsAndModifiers

对一个输入的String,通过Context隐藏其注释和修饰符细节。

Warp

将源输入包装成方法,成员变量,等等。

JShell源码分析

由于时间有限,这里先把程序的主要执行逻辑写在这里,以后有时间绘成流程图。

JShell.eval(String input) throws IllegalStateException
-》Eval.eval(String userSource) throws IllegalStateException
//初始化
-》List<SnippetEvent> allEvents
-》Eval sourceToSnippets(String userSource)
-》分析语句、通过MaskCommentsAndModifiers隐藏修饰符细节,通过工具类分析行尾,组成可以被编译的语句
-》创建ParseTask进行语义分析获得分析树List<? extends Tree> units
-》分析units首语句类型,进入对应执行模块
-》解剖unit Eval.processVariables(String userSource, List<? extends Tree> units, String compileSource, ParseTask pt)
-》TreeDissector.createByFirstClass(TaskFactory.BaseTask bt)
-》static TreeDissector createByFirstClass(TaskFactory.BaseTask bt) {
Pair<CompilationUnitTree, ClassTree> pair = classes(bt.firstCuTree())
.findFirst().orElseGet(() -> new Pair<>(bt.firstCuTree(), null));
return new TreeDissector(bt, pair.first, pair.second);
}
-》根据遍历units,获取Variable基本类型、变量名、基类
VariableTree vt = (VariableTree) unitTree;
String name = vt.getName().toString();
String typeName = EvalPretty.prettyExpr((JCTree) vt.getType(), false);
Tree baseType = vt.getType();
-》扫描语义依赖TreeDependencyScanner.scan(Tree node)
-》通过VariableTree.getInitializer();获得ExpressionTree
-》Wrap.varWrap(String source, Range rtype, String brackets, Range rname, Range rinit)
-》allEvents.addAll(declare(snip, snip.syntheticDiags()));

JShell eval

通过以上源码分析,我们我们可以通过建立一个JShell对象来尝试执行一个简单的Java程序块。

public class JShellTry {
public static void main(String[] args) {
List<SnippetEvent> evals = JShell.create().eval("System.out.print(\"xdsjsd\");");
evals.forEach(evals-> System.out.println(evals.value()));
}
}

虽然执行过程稍微长了一点,但是程序成功输出了xdsjsd,实验成功。

总结

JShell为开发者提供了一个稳定的交互式终端,程序开发者可以通过JShell进行一些简单的程序执行结果验证,同时它还赋予了Java应用程序动态执行Java代码的能力,如果不出意外接下来会有很多基于JShell机制的Virtual Runtime,OJ平台出现,大家拭目以待吧!