转载

http://www.liaoxuefeng.com/article/0014617596492474eea2227bf04477e83e6d094683e0536000

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的方法

    [From] http://www.liaoxuefeng.com/article/0014617596492474eea2227bf04477e83e6d094683e0536000 廖雪峰 / 编 ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  9. Jvm基础(1)-Java运行时数据区

    最近在看<深入理解Java虚拟机>,里面讲到了Java运行时数据区,这是Jvm基本知识,把读书笔记记录在此.这些知识属于常识,都能查到的,如果我有理解不对的地方,还请指出. 首先把图贴上来 ...

随机推荐

  1. Spring及tomcat初始化日志

    Tomcat StandardContext初始化过程 //org.apache.catalina.core.StandardContext#startInternal // 子容器启动 for (C ...

  2. spring aop 源码分析(三) @Scope注解创建代理对象

    一.源码环境的搭建: @Component @Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON,proxyMode = ScopedP ...

  3. 使用IBMMQ时的那些事

    之前项目一直使用的是ActiveMQ,突然要变更成IBMMQ,整理了一些搭建MQ和代码连接MQ是遇到的问题,记录一下. (1)三个系统之内部使用IBMMQ交互,所以应设立两个本地接收队列和两个发送队列 ...

  4. django 3.1 序列化讲述

    序列化Django对象¶ Django的序列化框架提供了一种将Django模型"翻译"为其他格式的机制.通常,这些其他格式将基于文本,并用于通过电线发送Django数据,但是序列化 ...

  5. 音频数据增强及python实现

    博客作者:凌逆战 博客地址:https://www.cnblogs.com/LXP-Never/p/13404523.html 音频时域波形具有以下特征:音调,响度,质量.我们在进行数据增强时,最好只 ...

  6. Python字符编码和二进制不得不说的故事

    二进制 核心思想: 冯诺依曼 + 图灵机 电如何表示状态,才能稳定? 计算机开始设计的时候并不是考虑简单,而是考虑能自动完成任务与结果的可靠性, 简单始终是建立再稳定.可靠基础上 经过尝试10进制,但 ...

  7. 【题解】[SDOI2010]捉迷藏

    题目链接:https://www.luogu.com.cn/problem/P2479 题目大意:求平面\(n\)个点中,到其它\(n-1\)个点的曼哈顿距离最大和最小距离之差最小的点,求出这个这个距 ...

  8. STM32之旅4——USART

    STM32之旅4--USART 串口也是用的比较多的,在STM32CubeMX中生成代码后,需要添加一些代码才可以用. drv_usart.h: #ifndef __DRV_USART_H #defi ...

  9. OAth 2.0 的白话讲解

    一.OAuth2.0是什么,主要做什么用的? 官方注解 简单说,OAuth 就是一种授权机制.数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据.系统从而产生一个短期的进入令牌(token ...

  10. node_modules 文件夹需要管理员权限才能删除问题

    方法一:以管理员权限运行IDE ,然后在IDE里面删除该文件夹 方法二:以管理员身份运行cmd,使用命令行来删除该文件夹 找到要删除文件夹的位置,使用命令行 rmdir /s/q 文件夹位置 /s 是 ...