[From] http://www.liaoxuefeng.com/article/0014617596492474eea2227bf04477e83e6d094683e0536000

廖雪峰 / 编程 / 2016-4-27 20:45 / 阅读: 2636

Java是一门静态语言,通常,我们需要的class在编译的时候就已经生成了,为什么有时候我们还想在运行时动态生成class呢?

因为在有些时候,我们还真得在运行时为一个类动态创建子类。比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢?

User为例:

public class User {
private String id;
private String name; public String getId() {
return id;
} public void setId(String id) {
this.id = id;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
}
}

其实UserProxy实现起来很简单,就是创建一个User的子类,覆写所有setXxx()方法,做个标记就可以了:

public class UserProxy extends User {
private boolean dirty; public boolean isDirty() {
return this.dirty;
} public void setDirty(boolean dirty) {
this.dirty = dirty;
} @Override
public void setId(String id) {
super.setId(id);
setDirty(true);
} @Override
public void setName(String name) {
super.setName(name);
setDirty(true);
}
}

但是这个UserProxy就必须在运行时动态创建出来了,因为编译时ORM框架根本不知道User类。

现在问题来了,动态生成字节码,难度有多大?

如果我们要自己直接输出二进制格式的字节码,在完成这个任务前,必须先认真阅读JVM规范第4章,详细了解class文件结构。估计读完规范后,两个月过去了。

所以,第一种方法,自己动手,从零开始创建字节码,理论上可行,实际上很难。

第二种方法,使用已有的一些能操作字节码的库,帮助我们创建class。

目前,能够操作字节码的开源库主要有CGLibJavassist两种,它们都提供了比较高级的API来操作字节码,最后输出为class文件。

比如CGLib,典型的用法如下:

Enhancer e = new Enhancer();
e.setSuperclass(...);
e.setStrategy(new DefaultGeneratorStrategy() {
protected ClassGenerator transform(ClassGenerator cg) {
return new TransformingGenerator(cg,
new AddPropertyTransformer(new String[]{ "foo" },
new Class[] { Integer.TYPE }));
}});
Object obj = e.create();

比自己生成class要简单,但是,要学会它的API还是得花大量的时间,并且,上面的代码很难看懂对不对?

有木有更简单的方法?

有!

换一个思路,如果我们能创建UserProxy.java这个源文件,再调用Java编译器,直接把源码编译成class,再加载进虚拟机,任务完成!

毕竟,创建一个字符串格式的源码是很简单的事情,就是拼字符串嘛,高级点的做法可以用一个模版引擎。

如何编译?

Java的编译器是javac,但是,在很早很早的时候,Java的编译器就已经用纯Java重写了,自己能编译自己,行业黑话叫“自举”。从Java 1.6开始,编译器接口正式放到JDK的公开API中,于是,我们不需要创建新的进程来调用javac,而是直接使用编译器API来编译源码。

使用起来也很简单:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int compilationResult = compiler.run(null, null, null, '/path/to/Test.java');

这么写编译是没啥问题,问题是我们在内存中创建了Java代码后,必须先写到文件,再编译,最后还要手动读取class文件内容并用一个ClassLoader加载。

有木有更简单的方法?

有!

其实Java编译器根本不关心源码的内容是从哪来的,你给它一个String当作源码,它就可以输出byte[]作为class的内容。

所以,我们需要参考Java Compiler API的文档,让Compiler直接在内存中完成编译,输出的class内容就是byte[]

代码改造如下:

Map<String, byte[]> results;
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null);
try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
JavaFileObject javaFileObject = manager.makeStringSource(fileName, source);
CompilationTask task = compiler.getTask(null, manager, null, null, null, Arrays.asList(javaFileObject));
if (task.call()) {
results = manager.getClassBytes();
}
}

上述代码的几个关键在于:

  1. MemoryJavaFileManager替换JDK默认的StandardJavaFileManager,以便在编译器请求源码内容时,不是从文件读取,而是直接返回String
  2. MemoryOutputJavaFileObject替换JDK默认的SimpleJavaFileObject,以便在接收到编译器生成的byte[]内容时,不写入class文件,而是直接保存在内存中。

最后,编译的结果放在Map<String, byte[]>中,Key是类名,对应的byte[]是class的二进制内容。

为什么编译后不是一个byte[]呢?

因为一个.java的源文件编译后可能有多个.class文件!只要包含了静态类、匿名类等,编译出的class肯定多于一个。

如何加载编译后的class呢?

加载class相对而言就容易多了,我们只需要创建一个ClassLoader,覆写findClass()方法:

class MemoryClassLoader extends URLClassLoader {

    Map<String, byte[]> classBytes = new HashMap<String, byte[]>();

    public MemoryClassLoader(Map<String, byte[]> classBytes) {
super(new URL[0], MemoryClassLoader.class.getClassLoader());
this.classBytes.putAll(classBytes);
} @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] buf = classBytes.get(name);
if (buf == null) {
return super.findClass(name);
}
classBytes.remove(name);
return defineClass(name, buf, 0, buf.length);
}
}

除了写ORM用之外,还能干什么?

可以用它来做一个Java脚本引擎。实际上本文的代码主要就是参考了Scripting项目的源码。

完整的源码呢?

在这里:https://github.com/michaelliao/compiler,连Maven的包都给你准备好了!

也就200行代码吧!动态创建class不是梦!

[转] Java运行时动态生成class的方法的更多相关文章

  1. Java 运行时动态生成class

    转载 http://www.liaoxuefeng.com/article/0014617596492474eea2227bf04477e83e6d094683e0536000 Java是一门静态语言 ...

  2. 如何获取java运行时动态生成的class文件?

    查看运行时生成的文件,以更清楚运行情况. 查看动态生成的类,一般有两个方法: 1. 使用据说是jdk自带包sa-jdi.jar里的工具. 其中,不想自己搞,当然就利用下,sa-jdi.jar 里自带的 ...

  3. Java运行时动态加载类之ClassLoader

    https://blog.csdn.net/fjssharpsword/article/details/64922083 *************************************** ...

  4. 理解JVM之JAVA运行时内存区域

    java运行时内存区域划分为方法区,堆区,虚拟机栈区,本地方法栈,程序计数器.其中方法区跟堆区是线程共享的数据区,其他的是线程私有的数据区. 1.程序计数器 程序计数器(PC)是一块较小的内存,他是存 ...

  5. 使用javassist运行时动态重新加载java类及其他替换选择

    在不少的情况下,我们需要对生产中的系统进行问题排查,但是又不能重启应用,java应用不同于数据库的存储过程,至少到目前为止,还不能原生的支持随时进行编译替换,从这种角度来说,数据库比java的动态性要 ...

  6. 读书笔记-浅析Java运行时数据区

    作为一个 Java 为主语言的程序员,我偶尔也需要 用 C/C++ 写程序,在使用时让我很烦恼的一件事情就是需要对 new 出来的对象进行 delete/free 操作,我老是担心忘了这件事情,从而导 ...

  7. 【转】Java运行时数据区简介及堆与栈的区别

    理解JVM运行时的数据区是Java编程中的进阶部分.我们在开发中都遇到过一个很头疼的问题就是OutOfMemoryError(内存溢出错误),但是如果我们了解JVM的内部实现和其运行时的数据区的工作机 ...

  8. JVM发展史和Java运行时内存区域

    目前三大主流JVM: Sun HotSpot:Sun于1997年收购Longview Technologies公司所得.Sun于2009年被Oracle收购. BEA JRockit:BEA于2002 ...

  9. java运行时内存模式学习

    学习java运行时内存模式: 各区介绍: 方法区(线程共享):用于存放被虚拟机加载的类的元数据:静态变量,常量,以及编译和的代码(字节码),也称为永久代(所有该类的实例被回收,或者此类classLoa ...

随机推荐

  1. ROS编译:catkin简析

    博客转载自:https://blog.csdn.net/zyh821351004/article/details/50388429 Catkin tutorials: http://wiki.ros. ...

  2. UOJ 176 新年的繁荣

    挺妙的解法. 发现边权很小,我们可以考虑从大到小枚举边权来进行$kruskal$算法,这样子对于每一个边权$i$,我们只要枚举$0 \leq j < m$,找到一个点使它的点权为$i | 2^j ...

  3. readfile()

    readfile()将一个文件写入到输出缓存参数1:文件名

  4. Classification and Prediction

    # coding: utf-8 # In[128]: get_ipython().magic(u'matplotlib inline') import pandas as pd from pandas ...

  5. 《the art of software testing》 第三章 人工测试

    在深入研究较为传统的计算机测试技术之前,要先进行"人工测试". 代码检查与走查是两种主要的人工测试方法. 代码检查与走查是对过去桌面检查过程(在提交测试前由程序员阅读自己程序的过程 ...

  6. 希尔伯特空间(Hilbert Space)是什么?

    希尔伯特空间是老希在解决无穷维线性方程组时提出的概念, 原来的线性代数理论都是基于有限维欧几里得空间的, 无法适用, 这迫使老希去思考无穷维欧几里得空间, 也就是无穷序列空间的性质. 大家知道, 在一 ...

  7. python 单双引号交替的json串

    单双引号交替的json串 1.常见的json串,类似于这种{"isSucess":true, "name":"yoyo", "st ...

  8. C# 的 Task、Thread、ThreadPool 之间有什么异同?

    Thread就是Thread,需要自己调度,适合长跑型的操作. ThreadPool是Thread基础上的一个线程池,目的是减少频繁创建线程的开销.线程很贵,要开新的stack,要增加CPU上下文切换 ...

  9. Log--检查各数据库日志的使用情况

    -- Recovery model, log reuse wait description, log file size,-- log usage size and compatibility lev ...

  10. SpringMVC+Hibernate 项目开发之二 (STS整合Maven)

    为什么用STS不用Eclipse,主要是Eclipse集成Maven把我整疯了,最后估计原因除在网速上了. 其实用了STS以后发现还真比Eclipse好用点. STS本身集成有Maven的,但是默认的 ...