Java的反射与动态编译


JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。这种技术在一些开源框架中使用非常广泛,但Java为什么能够支持反射,而在其他语言如C++,Go中却没有反射的说法。这要从JVM的动态编译技术说起。

动态编译与静态编译

  • 静态编译:在编译时,把所有模块都编译进可执行文件里,当启动这个可执行文件时,所有模块都被加载进来。即所有的代码,类的关系在写代码时都是确定的,
  • 动态编译:在程序运行时可以动态的加载一些在编译阶段完全未知的代码,我们可以在运行阶段动态生成新的代码,然后编译运行

动态编译是从Java 6开始支持的,主要是通过一个JavaCompiler接口来完成的。通过这种方式我们可以在程序运行中直接编译一个已经存在的java文件,也可以在内存中动态生成Java代码,动态编译执行。正是由于这种技术的存在,所有运行于JVM上的语言(如Kotlin,Groovy,Scala)都可以和Java语言混合运行

什么是解释器?什么是编译器?

HotSpot包括一个解释器和两个编译器(client和server, 实际运行中选择其一即可, 大多选择server), 解释与编译混合执行模式, 默认启动解释执行

server模式下应用程序启动较慢, 占用内存多, 但执行效率高, 其适用于服务器端需要长期执行的应用;
client模式下应用程序启动较快, 占用内存小, 但执行效率较低, 默认情况下不进行动态编译, 适用于桌面应用程序.

查看 JVM 的启动模式:

# 使用解释与编译混合的模式
java -version
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode)
# mixed mode 解释与编译混合的模式, 是默认使用的模式.


# 使用纯解释模式
java -Xint -version
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
# interpreted mode 纯解释模式, 即禁用JIT编译.


# 使用纯编译模式
java -Xcomp -version
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, compiled mode)
# compiled mode 纯编译模式, 如果方法无法编译, 则回退到解释模式执行无法编译的方法.

解释器

解释器是一种计算机程序,它直接执行由编程语言或脚本语言编写的代码,并不会把源代码预编译成机器码,解释器会一行一行的读取源代码,解释,然后立即执行。JVM启动时,会根据预定义的规范对字节码采用逐行解释的方式执行,也就是将字节码文件中的每条内容都翻译成系统能识别的指令并执行。注意这里是直接执行,并没有编译出什么文件(就算有也是部分临时的),相当于实时翻译并执行。

编译器

编译器负责把一种编程语言(通常为高级语言)“翻译”成另外一种语言(通常为低级语言),后者往往是二进制的形式的机器语言,被称为目标代码(object code),这个转换的过程通常的目的是生成可执行的程序。在Java中,将.java编译为.class文件就是编译器的一种操作。而如果我们又将.class编译成计算机CPU可以直接执行的机器语言,也是编译操作。注意这里并没有执行,只负责完整、彻底的翻译,然后生成翻译后的文件,并没有任何执行的操作。

由于JVM可以动态编译,在此技术之上衍生出了许多框架,例如反射和动态代理。

动态编译代码

首先写一个动态创建编译的方法,测试类如下

package com.build;

import javax.tools.*;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.lang.reflect.Method;

/**
 * @Auther: Lushunjian
 * @Date: 2022/6/4 14:59
 * @Description:
 */
public class MyApp {

    public static void main(String args[]){

        build("测试动态编译输出");
    }

    static void build(String param) {
        //构建一个类的源代码
        StringBuilder sourceCode = new StringBuilder();
        sourceCode.append("public class Temp {").append("\r\n")
                .append("public static String call(String args) {").append("\r\n")
                .append("System.out.println(\"").append(param).append("\");").append("\r\n")
                .append("return \"Hello, ").append(param).append("\";").append("\r\n")
                .append("}").append("\r\n")
                .append("}");
        try {
            //将源文件写入到磁盘中
            String javaFileName = "Temp.java";
            //生成的Java源文件存放到指定目录
            File sourceDir = new File("F:\\test\\java-demo\\source");
            if (!sourceDir.exists()) {
                sourceDir.mkdirs();
            }
            File javaFile = new File(sourceDir, javaFileName);
            PrintWriter writer = new PrintWriter(new FileWriter(javaFile));
            writer.write(sourceCode.toString());
            writer.flush();
            writer.close();

            //动态编译磁盘中的代码
            //生成的字节码文件存放到<module>F:\test\java-demo\classes目录下
            File distDir = new File("F:\\test\\java-demo\\classes");
            if (!distDir.exists()) {
                distDir.mkdirs();
            }
            JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
            //JavaCompiler最核心的方法是run, 通过这个方法编译java源文件, 前3个参数传null时,
            //分别使用标准输入/输出/错误流来 处理输入和编译输出. 使用编译参数-d指定字节码输出目录.
            int compileResult = javac.run(null, null, null, "-d", distDir.getAbsolutePath(), javaFile.getAbsolutePath());
            //run方法的返回值: 0-表示编译成功, 否则表示编译失败
            if(compileResult != 0) {
                System.err.println("编译失败!!");
                return;
            }

            //动态执行 (反射执行)

            // 加载本地磁盘上的包
            MyClassLoader loader = new MyClassLoader();
            //加载类
            Class<?> klass = loader.findClass("Temp");

            Method evalMethod = klass.getMethod("call", String.class);
            String result = (String)evalMethod.invoke(klass.newInstance(), param);
            System.out.println("eval(" + param + ") = " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

再写一个类加载器,用来加载动态创建的类

package com.build;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * @Auther: Lushunjian
 * @Date: 2022/6/4 15:26
 * @Description:
 */
public class MyClassLoader extends ClassLoader {

    public MyClassLoader(){}

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String myPath = "file:///F:/test/java-demo/classes/" + name.replace(".","/") + ".class";
        //System.out.println(myPath);
        byte[] cLassBytes = null;
        Path path = null;
        try {
            path = Paths.get(new URI(myPath));
            cLassBytes = Files.readAllBytes(path);
        } catch (IOException | URISyntaxException e) {
            e.printStackTrace();
        }
        assert cLassBytes != null;
        Class clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);
        return clazz;
    }

}

Java的反射

Java反射是Java被视为动态(或准动态)语言的一个关键性质。这个机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息,包括其modifiers(诸如public、static等)、superclass(例如Object)、实现之interfaces(例如Cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤起methods。

Reflection可以在运行时加载、探知、使用编译期间完全未知的classes。即Java程序可以加载一个运行时才得知名称的class,获取其完整构造,并生成其对象实体、或对其fields设值、或唤起其methods。

反射(reflection)允许静态语言在运行时(runtime)检查、修改程序的结构与行为。
在静态语言中,使用一个变量时,必须知道它的类型。在Java中,变量的类型信息在编译时都保存到了class文件中,这样在运行时才能保证准确无误;换句话说,程序在运行时的行为都是固定的。如果想在运行时改变,就需要反射这东西了。


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
select与epoll select与epoll
select与epoll是操作系统提供的两种I/O多路复用的机制,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。select是比较早出现的技术,但是
2022-06-05
Next 
对于类加载的一点理解 对于类加载的一点理解
当我们写好一个Java程序之后,不是管是CS还是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以
2022-06-02
  TOC