Skip to content

Commit

Permalink
初步完成README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
TangBean committed Feb 6, 2019
1 parent 49dff32 commit c84a66c
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 7 deletions.
47 changes: 44 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

本项目基于 SpringBoot 实现了一个在线的 Java IDE,可以远程运行客户端发来的 Java 代码的 main 方法,并将程序的标准输出内容、运行时异常信息反馈给客户端,并且会对客户端发来的程序的执行时间进行限制。

项目中涉及的框架相关知识并不多,主要涉及了许多 Java 基础的知识,如:Java 程序编译和运行的过程、Java 类加载机制、Java 类文件结构、Java 反射等。除此之外,还涉及到了一个简单的并发问题:如何将一个非线程安全的类变为一个线程安全的类。因此,本项目较为适合在比较注重基础的面试中介绍给面试官,可以引出一些 Java 虚拟机,Java 并发相关的问题,较能体现应聘者对于 Java 的一些原理性的知识的掌握程度。在本篇文章中,我们尽可能的将用到的知识简单讲解一下或者给出讲解的链接,以方便大家阅读。

**运行效果如下:**

![项目展示](./doc/pic/项目展示.gif)

**涉及技术:**

- Java 动态编译
- Java 类文件的结构
- Java 类加载器
- Java 反射
- Java 类文件的结构
- 如何将一个类变为线程安全类


Expand All @@ -22,20 +24,59 @@

![在线执行Java代码实现流程.jpg](./doc/pic/在线执行Java代码实现流程.jpg)

既然要运行客户端发来的 Java 代码,那么我们首先需要了解 Java 程序编译和运行的过程,然后仿照 Java 程序的真实运行过程来运行客户端发来的 Java 代码。



## Java 程序编译和运行的过程

我们先来看一下 Java 程序编译和运行的过程图:

![Java程序编译和运行的过程.jpg](./doc/pic/Java程序编译和运行的过程.jpg)

如上图所示,要运行一个 Java 程序需要经过以下两个步骤:

- 源文件由编译器编译成字节码;
- 字节码由 Java 虚拟机解释运行。

也正是因为 Java 程序既要编译同时也要经过 JVM 的解释运行,所以说 Java 被称为半解释语言。接下来我们将对以上两个步骤进行详细说明。

### 编译

在运行前,我们首先需要将 .java 源文件编译为 .class 文件。Java 编译一个类时,如果这个类所依赖的类还没有被编译,编译器就会先编译这个被依赖的类,然后引用,否则直接引用,如果 Java 编译器在指定目录下找不到该类所其依赖的类的 .class 文件或者 .java 源文件的话,编译器话报“cant find symbol”的 Error。

### 运行

Java 类运行的过程可分为两个过程:

- **类的加载**
- 应用程序运行后,系统就会启动一个 JVM 进程,JVM 进程从 classpath 路径中找到名为 Test.class 的二进制文件(假设客户端发来的类名为 Test),将 Test 的类信息加载到运行时数据区的方法区内,这个过程叫做 Test 类的加载。
- 上一步过程主要通过 ClassLoader 完成,类加载器会将类的字节码文件加载为 Class 对象,存放在 Java 虚拟机的方法区中,之后 JVM 就可以通过这个 Class 对象获取该类的各种信息,或者运行该类的方法。
- 关于类加载器的详细讲解可见:[虚拟机类加载机制](https://github.com/TangBean/understanding-the-jvm/blob/master/Ch2-Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E7%A8%8B%E5%BA%8F%E6%89%A7%E8%A1%8C/01-%E8%99%9A%E6%8B%9F%E6%9C%BA%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6.md)。
- **类的执行**
- JVM 找到 Test 的主函数入口,开始执行 main 函数。
- 本项目主要通过反射来完成这一过程,有关反射的详细讲解可见:[Java 反射](https://github.com/TangBean/SimpleSpring/blob/master/doc/011-%E5%8F%8D%E5%B0%84%E4%B8%8E%E5%86%85%E7%9C%81.md#%E5%8F%8D%E5%B0%84-reflect)。

在了解 Java 程序的实际运行过程之后,我们接下来要考虑的是:如何在运行过程中实现这一流程?也就是说,我们要在服务器端程序运行的过程中完成客户端代码发来的代码的编译和运行。通过对上图中 Java 程序编译和运行流程进行分析,我们得到以下客户端 Java 源代码执行流程:

![客户端程序编译和运行的过程.jpg](./doc/pic/客户端程序编译和运行的过程.jpg)

通过观察上图可以发现,我们的重点在于实现 `StringSourceCompiler``JavaClassExecuter` 两个类。它们的作用分别为:

- `StringSourceCompiler`:将字符串形式的源代码 String source 编译成字节码 byte[] classBytes;
- `JavaClassExecuter`:将字节码 byte[] classBytes 加载进 JVM,执行其入口方法,并收集运行输出结果字符串返回。
- `JavaClassExecuter`:将字节码 byte[] classBytes 加载进 JVM,执行其 main 方法,并收集运行输出结果字符串返回。

> **Note:** 我们只收集 `System.out``System.err` 输出的内容返回给客户端。
接下来,我们将对 `StringSourceCompiler``JavaClassExecuter` 类的实现方式进行详解。



## 实现编译模块:StringSourceCompiler

通过 JDK 1.6 后新加的动态编译实现 `StringSourceCompiler`,使用动态编译,可以直接在内存中将源代码字符串编译为字节码的字节数组,这样既不会污染环境,又不会额外的引入 IO 操作,一举两得。

具体实现以及原理说明详见:[动态编译](./doc/01-动态编译.md)
具体实现以及原理说明详见:[动态编译](./doc/01-动态编译.md)



Expand Down
4 changes: 2 additions & 2 deletions doc/02-执行字节码的入口方法.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

获取到需要运行的代码的字节码后,我们接下来需要考虑的是如何通过我们得到的字节码将这个类的 main 方法运行起来,为了方便理解,我们将这个过程进行进一步拆分,分为以下 2 步:

- 通过类加载器将字节码加载为 Class 对象;
- 通过反射调用 Class 对象的 main 方法。
- 类的加载:通过类加载器将字节码加载为 Class 对象;
- 类的运行:通过反射调用 Class 对象的 main 方法。

接下来,我们将对以上两个操作的具体实现细节进行进一步讲解。

Expand Down
4 changes: 2 additions & 2 deletions doc/03-收集代码执行结果:字节码修改器.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

那么如何将客户端程序中对 System 的调用替换为对 HackSystem 的调用呢?当然不能直接修改客户端发来的程序的源代码字符串了,这既不优雅,操作也十分的繁琐。我们采用了一种“高级”的方法,即直接在字节码中,把要执行的类对 System 的符号引用替换为我们准备的 HackSystem 的符号引用,因此我们需要一个字节码修改器,这个字节码修改器完成如下流程:

- 遍历字节码中的所有符号引用,找到 "java/lang/System";
- 遍历字节码常量池中的所有符号引用,找到 "java/lang/System";
- 将 "java/lang/System" 替换为 “.../HackSystem”。

要想完成以上 2 步操作,首先我们要了解类文件的结构,这样我们才能找到类对 System 的符号引用的位置,并且知道替换的方法;其次,我们还需要一个字节数组修改工具 ByteUtils 帮助我们修改存储字节码的字节数组。
Expand Down Expand Up @@ -56,7 +56,7 @@ Class 文件的头 8 个字节是魔数和版本号,其中头 4 个字节是

常量池的开始的两个字节,也就是第 9、10 个字节,放置一个 u2 类型的数据,标识常量池中常量的数量 cpc (constant_pool_count),这个计数值有一个十分特殊的地方,就是它是从 1 开始而不是从 0 开始的,也就是说如果 cpc = 22,那么代表常量池中有 21 项常量,索引值为 1 ~ 21,第 0 项常量被空出来,为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”时,将让这个索引值指向 0 即可。

常量池主要存放以下两大类常量
常量池中记录的是代码出现过的所有 token(类名,成员变量名等,也是我们接下来要修改的地方)以及符号引用(方法引用,成员变量引用等),主要包括以下两大类常量

- 字面量:接近于 Java 语言层面的常量概念,包括
- 文本字符串
Expand Down
8 changes: 8 additions & 0 deletions doc/04-收集代码执行结果:实现HackSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ private ThreadLocal<ByteArrayOutputStream> out;
private ThreadLocal<Boolean> trouble;
```

> **ThreadLocal 实现原理:**
>
> - 每一个 ThreadLocal 都有一个唯一的的 ThreadLocalHashCode;
> - 每一个线程中有一个专门保存这个 HashCode 的 `Map<ThreadLocalHashCode, 对应变量的值>`
> -`ThreadLocal#get()` 时,实际上是当前线程先拿到这个 ThreadLocal 对象的 ThreadLocalHashCode,然后通过这个 ThreadLocalHashCode 去自己内部的 Map 中去取值。
> - 即每个线程对应的变量不是存储在 ThreadLocal 对象中的,而是存在当前线程对象中的,线程自己保管封存在自己内部的变量,达到线程封闭的目的。
> - 也就是说,ThreadLocal 对象并不负责保存数据,它只是一个访问入口。
在进行了以上的修改之后,我们还需要将 HackPrintStream 的父类 PrintStream 中所有对流进行操作的方法进行重写。我们下面将举几个例子,对如何重写父类的方法进行说明。

### ensureOpen 方法
Expand Down
Binary file added doc/pic/Java程序编译和运行的过程.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit c84a66c

Please sign in to comment.