diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 6764f76..2224a59 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,10 +2,16 @@ + + + + - - + + + + @@ -192,6 +209,11 @@ 22 diff --git a/README.md b/README.md index dcf94fb..a466f28 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,10 @@ 本项目基于 Netty+FastJson 实现了一个在线的 Java IDE,可以远程运行客户端发来的 Java 代码的 main 方法,并将程序的标准输出内容、运行时异常信息反馈给客户端,并且会对客户端发来的程序的执行时间进行限制。 -项目中涉及的框架相关知识并不多,主要涉及了许多 Java 基础的知识,如:Java 程序编译和运行的过程、Java 类加载机制、Java 类文件结构、Java 反射等。除此之外,还涉及到了一个简单的并发问题:如何将一个非线程安全的类变为一个线程安全的类。因此,本项目较为适合在比较注重基础的面试中介绍给面试官,可以引出一些 Java 虚拟机,Java 并发相关的问题,较能体现应聘者对于 Java 的一些原理性的知识的掌握程度。在本篇文章中,我们尽可能的将用到的知识简单讲解一下或者给出讲解的链接,以方便大家阅读。 +项目中涉及的框架相关知识并不多,主要涉及了许多 Java 基础的知识,如:Java 程序编译和运行的过程、Java 类加载机制、Java 类文件结构、Java 反射等。除此之外,还涉及到了一个简单的并发问题:如何将一个非线程安全的类变为一个线程安全的类。因此,本项目较为适合在比较注重基础的面试中介绍给面试官,可以引出一些 Java 虚拟机,Java 并发相关的问题,较能体现应聘者对于 Java 的一些原理性的知识的掌握程度。 ## TODO -1. 完善页面 - #### 运行效果 ![项目展示](./doc/pic/demo.png) @@ -66,7 +64,7 @@ Java 类运行的过程可分为两个过程: - 关于类加载器的详细讲解可见:[虚拟机类加载机制](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 反射](doc/Reflec&Introspector.md)。 在了解 Java 程序的实际运行过程之后,我们接下来要考虑的是:如何在运行过程中实现这一流程?也就是说,我们要在服务器端程序运行的过程中完成客户端代码发来的代码的编译和运行。通过对上图中 Java 程序编译和运行流程进行分析,我们得到以下客户端 Java 源代码执行流程: diff --git "a/doc/01-\345\212\250\346\200\201\347\274\226\350\257\221.md" "b/doc/01-\345\212\250\346\200\201\347\274\226\350\257\221.md" index ae59fa8..8ff991e 100644 --- "a/doc/01-\345\212\250\346\200\201\347\274\226\350\257\221.md" +++ "b/doc/01-\345\212\250\346\200\201\347\274\226\350\257\221.md" @@ -13,7 +13,7 @@ -从 JDK 1.6 开始,引入了 Java 代码重写的编译接口,使得我们可以在运行时编译 Java 代码,然后在通过类加载器将编译好的类加载进 JVM,这种在运行时编译代码的操作就叫做动态编译。 +从 JDK 1.6 开始,引入了 Java 代码重写的编译接口,使得我们可以在运行时编译 Java 代码,然后再通过类加载器将编译好的类加载进 JVM,这种在运行时编译代码的操作就叫做动态编译。 通过使用动态编译,可以将源代码的字符串直接编译为字节码,在没有动态编译之前,想要在运行过程中编译 Java 源代码,我们要先将源代码写入一个 .java 文件,通过 javac 编译这个文件,得到 .class 文件,然后将 .class 文件通过 ClassLoader 加载进内存,才能得到 Class 对象。这其中存在两个问题:一是会生成 .java 和 .class 两个文件,运行之后还要把它们删除,以防止污染我们的服务器环境;二是会生成文件也就是说涉及 IO 操作,这个操作比起一切都在内存中运行是十分耗时的。所以我们使用了 Java 的动态编译技术,跳过了这两个文件的生成过程,直接在内存中将源代码字符串编译为字节码的字节数组,这样既不会污染环境,又不会额外的引入 IO 操作,一举两得。 diff --git a/doc/ClassFile.md b/doc/ClassFile.md new file mode 100644 index 0000000..647bdab --- /dev/null +++ b/doc/ClassFile.md @@ -0,0 +1,7 @@ +# 类文件结构 + +![类文件结构](pic/class_code.png) + +常量池的起始位置是10,8-9是常量的个数,下表展示常量项的类型。 + +![常量池数据类型](pic/constant_data_type.png) \ No newline at end of file diff --git a/doc/Reflec&Introspector.md b/doc/Reflec&Introspector.md new file mode 100644 index 0000000..0a1d400 --- /dev/null +++ b/doc/Reflec&Introspector.md @@ -0,0 +1,199 @@ +# 反射与内省 + + + +- [反射与内省](#%E5%8F%8D%E5%B0%84%E4%B8%8E%E5%86%85%E7%9C%81) + - [反射 (Reflect)](#%E5%8F%8D%E5%B0%84-reflect) + - [反射的原理](#%E5%8F%8D%E5%B0%84%E7%9A%84%E5%8E%9F%E7%90%86) + - [使用反射操作属性](#%E4%BD%BF%E7%94%A8%E5%8F%8D%E5%B0%84%E6%93%8D%E4%BD%9C%E5%B1%9E%E6%80%A7) + - [使用反射操作构造函数](#%E4%BD%BF%E7%94%A8%E5%8F%8D%E5%B0%84%E6%93%8D%E4%BD%9C%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0) + - [无参数构造方法](#%E6%97%A0%E5%8F%82%E6%95%B0%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95) + - [有参数构造方法](#%E6%9C%89%E5%8F%82%E6%95%B0%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95) + - [使用反射操作方法](#%E4%BD%BF%E7%94%A8%E5%8F%8D%E5%B0%84%E6%93%8D%E4%BD%9C%E6%96%B9%E6%B3%95) + - [实例方法](#%E5%AE%9E%E4%BE%8B%E6%96%B9%E6%B3%95) + - [静态方法](#%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95) + - [内省 (Introspector)](#%E5%86%85%E7%9C%81-introspector) + - [JavaBean的规范](#javabean%E7%9A%84%E8%A7%84%E8%8C%83) + - [Introspector操作JavaBean](#introspector%E6%93%8D%E4%BD%9Cjavabean) + - [使用BeanUtils](#%E4%BD%BF%E7%94%A8beanutils) + - [设置JavaBean属性:`BeanUtils.setProperty(bean, propertyName, propertyValue)`](#%E8%AE%BE%E7%BD%AEjavabean%E5%B1%9E%E6%80%A7beanutilssetpropertybean-propertyname-propertyvalue) + - [获取JavaBean属性:`BeanUtils.getProperty(bean, propertyName)`](#%E8%8E%B7%E5%8F%96javabean%E5%B1%9E%E6%80%A7beanutilsgetpropertybean-propertyname) + - [封装Map数据到JavaBean对象中:`BeanUtils.populate(bean, map)`](#%E5%B0%81%E8%A3%85map%E6%95%B0%E6%8D%AE%E5%88%B0javabean%E5%AF%B9%E8%B1%A1%E4%B8%ADbeanutilspopulatebean-map) + - [反射和内省的区别](#%E5%8F%8D%E5%B0%84%E5%92%8C%E5%86%85%E7%9C%81%E7%9A%84%E5%8C%BA%E5%88%AB) + + + +## 反射 (Reflect) + +在框架开发中,都是基于配置文件开发的,在配置文件中配置了类,可以通过读取配置文件中的类名,然后通过反射得到类中的所有内容,或是让类中的某个方法来执行。 + +也就是说,反射是在运行时获取一个类的所有信息,可以获取到 .class 的任何定义的信息(包括成员 变量,成员方法,构造器等)可以操纵类的字段、方法、构造器等部分。 + +### 反射的原理 + +我们将通过下图对反射的原理进行说明: + +![反射的原理.png](./pic/反射的原理.png) + +- **得到 class 文件** + - 把 java 文件保存到本地硬盘,得到 .java 文件 + - 编译 java 文件,得到 .class 文件 + +- **通过 class 文件得到 Class 类**,可以通过以下 3 种方式获得 Class 类 + + JVM 把 .class 文件加载到内存中,class 文件在内存中使用 Class 类表示 + + - 通过成员变量获得: `类名.class` + - 通过具体对象获得: `对象.getClass()` + - 通过 Class 的静态方法获取: `Class.forName("classFilePath")` + +- **通过 Class 类获取 class 文件中的内容,包括:成员变量,构造方法,普通方法**,它们都可以用相应的类表示: + + - 属性:`Field` + - 构造方法:`Constructor` + - 普通方法:`Method` + +### 使用反射操作属性 + +```java +public void test3() { + try { + Class c2 = Class.forName("cn.itcast.test09.Person"); // 得到Class类 + Person p11 = (Person) c2.newInstance(); // 得到Person类的对象,返回 + Field[] fields = c2.getDeclaredFields(); // 得到所有的属性,返回一个Field数组 + Field f1 = c2.getDeclaredField("name"); // 得到属性,参数是属性的名称 + // 如果操作的是私有的属性,不让操作,可以通过setAccessible(true)操作私有属性 + f1.setAccessible(true); + f1.set(p11, "wangwu"); // 设置name值,相当于p.name = "wangwu"; + System.out.println(f1.get(p11)); // 相当于 p.name + }catch(Exception e) { + e.printStackTrace(); + } +} +``` + +### 使用反射操作构造函数 + +#### 无参数构造方法 + +通过 Class 对象的 `newInstance()` 方法创建。 + +```java +public void test1() throws Exception { + Class c3 = Class.forName("cn.itcast.test09.Person"); + // 无参数的构造方法就是直接使用newInstance()方法 + Person p = (Person) c3.newInstance(); + p.setName("zhangsan"); + System.out.println(p.getName()); +} +``` + +#### 有参数构造方法 + +不能再通过 Class 对象的 `newInstance()` 方法创建了,要先得到要调用的构造函数的 Consturctor 对象,然后通过 Constructor 对象的 `newInstance()` 方法创建。 + +```java +public void test2() throws Exception { + Class c1 = Class.forName("cn.itcast.test09.Person"); + // 获取所有的构造方法 + Constructor[] css = c1.getConstructors(); + // 获取特定的构造方法:传递是有参数的构造方法里面参数类型,类型使用class的形式传递 + Constructor cs = c1.getConstructor(String.class, String.class); + // 通过有参数的构造方法创建Person实例,而不是通过Class的对象 + Person p1 = (Person) cs.newInstance("lisi","100"); + System.out.println(p1.getId()+" "+p1.getName()); +} +``` + + +### 使用反射操作方法 + +#### 实例方法 + +```java +public void test4() throws Exception { + Class c4 = Class.forName("cn.itcast.test09.Person"); + Person p4 = (Person) c4.newInstance(); + // 得到所有的普通方法 + Method[] mds = c4.getDeclaredMethods(); + // 得到特定的普通方法,传递两个参数:第一个参数:方法名称;第二个参数:方法里面参数的类型 + Method m1 = c4.getDeclaredMethod("setName", String.class); + // 使用invoke执行方法,传递两个参数:第一个参数:person实例;第二个参数:设置的值 + // 在这里要传入person对象的原因是:我们需要知道到底是哪一个对象的setName方法执行了 + // 如果要操作的是私有的方法 ,需要 m1.setAccessible(true); + m1.invoke(p4, "niuqi"); + System.out.println(p4.getName()); +} +``` + +#### 静态方法 + +静态方法调用方式是 `类名.方法名`,不需要类的实例,所以使用反射操作静态方式时候,也是不需要实例的,在 invoke 方法的第一个参数传入 null 即可: `m1.invoke(null, "niuqi");` + + +## 内省 (Introspector) + +内省是基于反射实现的,主要用于操作 JavaBean,相比反射使用起来要方便一些。可以获取 bean 的 getter/setter 方法,也就是说,只要 JavaBean 有 `getXxx()` 方法,不管这个 Bean 有没有 Xxx 属性,使用内省我们都认为它有。 + +为了更好的理解,我们先来介绍一下 JavaBean 的规范。 + +### JavaBean 的规范 + +- 必须要有一个默认构造器。 +- 提供 get/set 方法,如果只有 get 方法,那么这个属性是只读属性。 +- 属性:有 get/set 方法的成员,还可以没有成员,只有 get/set 方法。属性名称由 get/set 方法来决定,而不是成员名称。 +- 方法名称满足一定的规范,它就是属性!boolean 类型的属性,它的读方法可以是 is 开头,也可以是 get 开头。 + +### Introspector 操作 JavaBean + +**操作示例:** + +```java +BeanInfo beanInfo = Introspector.getBeanInfo(User.class); +PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors(); +``` + +每个 PropertyDescriptor 对象对应一个 JavaBean 属性: + +- `String getName()`:获取 JavaBean 属性名称; +- `Method getReadMethod()`:获取属性的读方法; +- `Method getWriteMethod()`:获取属性的写方法。 + +然后再调用`invoke(params...)`就可以操作 JavaBean 了。 + +### 使用 BeanUtils + +我们并不需要真的自己通过 Introspector 来获取 JavaBean 的实例,我们可以通过现成的工具:BeanUtils 来操作 JavaBean。想要使用 BeanUtils,我们需要先导入 commons-beanutils.jar 包,然后,我们便可以通过 BeanUtils 来操纵 JavaBean 了。 + +#### 设置 JavaBean 属性:`BeanUtils.setProperty(bean, propertyName, propertyValue)` + +```java +User user = new User(); +BeanUtils.setProperty(user, "username", "admin"); +BeanUtils.setProperty(user, "password", "admin123"); +``` + +#### 获取 JavaBean 属性:`BeanUtils.getProperty(bean, propertyName)` + +```java +User user = new User("admin", "admin123"); +String username = BeanUtils.getProperty(user, "username"); +String password = BeanUtils.getProperty(user, "password"); +``` + +#### 封装 Map 数据到 JavaBean 对象中:`BeanUtils.populate(bean, map)` + +```java +Map map = new HashMap(); +map.put("username", "admin"); +map.put("password", "admin123"); +User user = new User(); +BeanUtils.populate(user, map); +``` + + +## 反射和内省的区别 + +反射就像给类照镜子,这个的所有信息会毫无保留的反射到镜子中,将这个类的所有信息照出来,能照出来就是有,照不出来就是没有,得到的东西都是客观真实存在的。 + +而内省的目的是找出 bean 的 getter 和 setter 以便操作这个 bean,所以只要看到有 getter 或者 setter 就认为这个类有那么一个字段,比如看到 getName() 内省就会认为这个类中有 name 字段,但事实上并不一定会有 name。 diff --git a/doc/pic/class_code.PNG b/doc/pic/class_code.PNG new file mode 100644 index 0000000..769d2ba Binary files /dev/null and b/doc/pic/class_code.PNG differ diff --git a/doc/pic/constant_data_type.png b/doc/pic/constant_data_type.png new file mode 100644 index 0000000..889faca Binary files /dev/null and b/doc/pic/constant_data_type.png differ diff --git a/src/main/java/cn/codeyourlife/execute/ClassModifier.java b/src/main/java/cn/codeyourlife/execute/ClassModifier.java index ef2a536..a768c4c 100644 --- a/src/main/java/cn/codeyourlife/execute/ClassModifier.java +++ b/src/main/java/cn/codeyourlife/execute/ClassModifier.java @@ -3,18 +3,20 @@ public class ClassModifier { /** * Class文件中常量池的起始偏移 + * cafe babe 0000 0034 + * 魔数 次、主版本号 */ private static final int CONSTANT_POOL_COUNT_INDEX = 8; /** - * CONSTANT_UTF8_INFO常量的tag + * tag为1表示此常量类型为 Utf8字符串。 */ - private static final int CONSTANT_UTF8_INFO = 1; + private static final int CONSTANT_UTF8_TAG = 1; /** - * 常量池中11种常量的长度,CONSTANT_ITEM_LENGTH[tag]表示它的长度 + * tag为2-12对应的常量长度 4,4,8,8,2,2,4,4,4,4 */ - private static final int[] CONSTANT_ITEM_LENGTH = {-1, -1, -1, 5, 5, 9, 9, 3, 3, 5, 5, 5, 5}; + private static final int[] CONSTANT_ITEM_LENGTH = {-1, -1, -1, 4, 4, 8, 8, 2, 2, 4, 4, 4, 4}; /** * 1个和2个字节的符号数,用来在classByte数组中取tag和len @@ -35,6 +37,7 @@ public ClassModifier(byte[] classByte) { /** * 从0x00000008开始向后取2个字节,表示的是常量池中常量的个数 + * * @return 常量池中常量的个数 */ public int getConstantPoolCount() { @@ -43,20 +46,27 @@ public int getConstantPoolCount() { /** * 字节码修改器,替换字节码常量池中 oldStr 为 newStr + * * @param oldStr * @param newStr * @return 修改后的字节码字节数组 */ public byte[] modifyUTF8Constant(String oldStr, String newStr) { + // 获取常量的个数 int cpc = getConstantPoolCount(); - int offset = CONSTANT_POOL_COUNT_INDEX + u2; // 真实的常量起始位置 + // 常量池真实的常量起始位置10 + int offset = CONSTANT_POOL_COUNT_INDEX + u2; for (int i = 1; i < cpc; i++) { int tag = ByteUtils.byte2Int(classByte, offset, u1); - if (tag == CONSTANT_UTF8_INFO) { + if (tag == CONSTANT_UTF8_TAG) { + // Utf8 字符串类型,才可能是对System的调用 + // U2(2字节)表示字符串的长度 int len = ByteUtils.byte2Int(classByte, offset + u1, u2); offset += u1 + u2; + // U1(1字节)*len 的字符串 String str = ByteUtils.byte2String(classByte, offset, len); if (str.equals(oldStr)) { + // 命中需要替换的字符串 byte[] strReplaceBytes = ByteUtils.string2Byte(newStr); byte[] intReplaceBytes = ByteUtils.int2Byte(strReplaceBytes.length, u2); // 替换新的字符串的长度 @@ -68,7 +78,9 @@ public byte[] modifyUTF8Constant(String oldStr, String newStr) { offset += len; } } else { - offset += CONSTANT_ITEM_LENGTH[tag]; + // 另外10种常量类型不用处理 + // 增加tag和index长度的偏移 + offset += u1 + CONSTANT_ITEM_LENGTH[tag]; } } return classByte; diff --git a/src/main/java/cn/codeyourlife/execute/JavaClassExecutor.java b/src/main/java/cn/codeyourlife/execute/JavaClassExecutor.java index 54874be..e9d97b0 100644 --- a/src/main/java/cn/codeyourlife/execute/JavaClassExecutor.java +++ b/src/main/java/cn/codeyourlife/execute/JavaClassExecutor.java @@ -9,7 +9,7 @@ * 1. 清空HackSystem中的缓存 * 2. new ClassModifier,并传入需要被修改的字节数组 * 3. 调用ClassModifier#modifyUTF8Constant修改: - * java/lang/System -> com/jvm/ch9/remoteinvoke/HackSystem + * java/lang/System -> HackSystem * 4. new一个类加载器,把字节数组加载为Class对象 * 5. 通过反射调用Class对象的main方法 * 6. 从HackSystem中获取返回结果 @@ -35,7 +35,8 @@ public static String execute(byte[] classByte, String systemIn) { // 5. 通过反射调用Class对象的main方法 try { - Method mainMethod = clazz.getMethod("main", new Class[] { String[].class }); + // main 函数的参数类型时String类型 + Method mainMethod = clazz.getMethod("main", String[].class); mainMethod.invoke(null, new String[] { null }); } catch (NoSuchMethodException e) { e.printStackTrace(); diff --git a/src/main/java/cn/codeyourlife/service/ExecuteStringSourceService.java b/src/main/java/cn/codeyourlife/service/ExecuteStringSourceService.java index 715bfe3..1ee45f1 100644 --- a/src/main/java/cn/codeyourlife/service/ExecuteStringSourceService.java +++ b/src/main/java/cn/codeyourlife/service/ExecuteStringSourceService.java @@ -46,12 +46,7 @@ public String execute(String source, String systemIn) { } // 运行字节码的main方法 - Callable runTask = new Callable() { - @Override - public String call() throws Exception { - return JavaClassExecutor.execute(classBytes, systemIn); - } - }; + Callable runTask = () -> JavaClassExecutor.execute(classBytes, systemIn); Future res = null; try {