-
java谜题--java运行时修改引用类的静态常量
2016-04-25 13:19:29下面这个谜题测试了你关于二进制兼容性(binary compatibility)的知识:当你改变了某个类所依赖的另外一个类时,第一个类的行为会发生什么改变呢?更特殊的是,假设你编译的是如下的2个类。第一个作为一个客户端,...下面这个谜题测试了你关于二进制兼容性(binary compatibility)的知识:当你改变了某个类所依赖的另外一个类时,第一个类的行为会发生什么改变呢?更特殊的是,假设你编译的是如下的2个类。第一个作为一个客户端,第二个作为一个库类,会怎么样呢:
- public class PrintWords {
- public static void main(String[] args) {
- System.out.println(Words.FIRST + " " +
- Words.SECOND + " " +
- Words.THIRD);
- }
- }
- public class Words {
- private Words() { }; // Uninstantiable
- public static final String FIRST = "the";
- public static final String SECOND = null;
- public static final String THIRD = "set";
- }
现在假设你像下面这样改变了那个库类并且重编译了这个类,但并不重编译客户端的程序:
- public class Words {
- private Words() { }; // Uninstantiable
- public static final String FIRST = "physics";
- public static final String SECOND = "chemistry";
- public static final String THIRD = "biology";
- }
此时,客户端的程序会打印出什么呢?
简单地看看程序,你会觉得它应该打印 physics chemistry biology;毕竟Java是在运行期对类进行装载的,所以它总是会访问到最新版本的类。但是更深入一点的分析会得出不同的结论。对于常量域的引用会在编译期被转化为它们所表示的常量的值[JLS 13.1]。这样的域从技术上讲,被称作常量变量(constant variables),这可能在修辞上显得有点矛盾。一个常量变量的定义是:一个在编译期被常量表达式初始化的final的原始类型或String类型的变量[JLS 4.12.4]。在知道了这些知识之后,我们有理由认为客户端程序会将初始值Words.FIRST, Words.SECOND, Words.THIRD编译进class文件,然后无论Words类是否被改变,客户端都会打印the null set。
这种分析可能是有道理的,但是却是不对的。如果你运行了程序,你会发现它打印的是the chemistry set。这看起来确实太奇怪的了。它为什么会做出这种事情呢?答案可以在编译期常量表达式(compile-time constant expression)[JLS 15.28]的精确定义中找到。它的定义太长了,就不在这里写出来了,但是理解这个程序的行为的关键是null不是一个编译期常量表达式。
由于常量域将会编译进客户端,API的设计者在设计一个常量域之前应该深思熟虑。如果一个域表示的是一个真实的常量,例如π或者一周之内的天数,那么将这个域设为常量域没有任何坏处。但是如果你想让客户端程序感知并适应这个域的变化,那么就不能让这个域成为一个常量。有一个简单的方法可以做到这一点:如果你使用了一个非常量的表达式去初始化一个域,甚至是一个final域,那么这个域就不是一个常量。你可以通过将一个常量表达式传给一个方法使得它变成一个非常量,该方法将直接返回其输入参数。
如果我们使用这种方法来修改Word类,在Words类被重新修改和编译之后,PrintWords类将打印出physics chemistry biology:
- public class Words {
- private Words() {}; // Uninstantiable
- public static final String FIRST = ident("the");
- public static final String SECOND = ident(null);
- public static final String THIRD = ident("set");
- private static String ident(String s) {
- return s;
- }
- }
在5.0版本中引入的枚举常量(enum constants),虽然有这样一个名字,但是它们并不是常量变量。你可以在枚举类型中加入枚举常量,对它们重新排序,甚至可以移除没有用的枚举常量,而且并不需要重新编译客户端。
总之,常量变量将会被编译进那些引用它们的类中。一个常量变量就是任何被常量表达式初始化的原始类型或字符串变量。令人惊讶的是,null不是一个常量表达式。
对于语言设计者来说,在一个动态链接的语言中,将常量表达式编译进客户端可能并不是一个好主意。这让很多程序员大吃一惊,并且很容易产生一些难以查出的缺陷:当缺陷被侦测出来的时候,那些定义常量的源代码可能已经不存在了。另外一方面,将常量表达式编译进客户端使得我们可以使用if语句来模拟条件编译(conditional compilation)[JLS 14.21]。为了正当目的可以不择手段的做法是需要每个人自己来判断的。
-
OpenRasp Java运行时修改字节码技术
2020-04-20 18:25:06Java运行时修改字节码技术 Java运行时动态修改字节码技术,常用的有javassist asm来实现。不过最近在分析openrasp-java这块时,程序使用的javassist来动态插桩关键类,达到监控某些程序的行为,OpenRasp使用这个技术...Java运行时修改字节码技术
Java运行时动态修改字节码技术,常用的有
javassist asm
来实现。不过最近在分析openrasp-java
这块时,程序使用的javassist
来动态插桩关键类,达到监控某些程序的行为,OpenRasp
使用这个技术来实现了监控程序的行为。为了分析OpenRasp
和理解其使用的技术原理,先做一个java动态修改指令基础知识的补充。第一个程序
有如下程序
package com.company; import java.net.URL; import java.io.File; import java.net.URLDecoder; import java.util.Set; public class Test1 { private String aa="heh"; public Test1(){} @Override public String toString() { return "Test1{" + "aa='" + aa + '\'' + '}'; } }
正常情况下调用
toString()
方法,会得到如下InsertCode:Test1{aa='heh'}
如果想要在
toString()
方法前插入某一个方法块,输出如下内容方法调用前 ----->>aa 方法调用前 ----->>aaa 方法调用后 ---->> bbbb InsertCode:Test1{aa='heh'}
可以借用
javassist
工具类操作对应的字节码。动态修改字节码–常用javassist类(这里是根据写的样例记录的,不是针对所有情况)
要想在动态修改程序行为,则需要使用
javassist
内的三个主要类ClassPool --> 是CtClass的一个容器 要想获得一个类对象,必须 CtClass -->和java的Class类似 CtMethod -->和java的Method类似
- ClassPool --> 是CtClass的一个容器 要想获得一个类对象,必须通过这个对象获取
CtClass ctClzz =classPool.get("完整的类名,例如com.test.demn.A"); 如果当前的classPool内没有这个类,则会报javassist.NotFoundException: com.company.Test1 后续会提到 #参考 http://javadox.com/org.javassist/javassist/3.18.1-GA/javassist/ClassPool.html
- CtClass -->和java的Class类似代表了一个类对象,从
ClassPool
内获取到
参考 http://javadox.com/org.javassist/javassist/3.18.1-GA/javassist/CtClass.html
- CtMethod -->和java的Method类似代表的一个方法对象,继承自CtBeHavior
参考 http://javadox.com/org.javassist/javassist/3.18.1-GA/javassist/CtMethod.html
在知道了这几个类之后,接着就是动手尝试修改
com.company.Test1
的toString()
方法。插入代码到toString()方法
编写
TestInsetOPs类
package com.company; import javassist.*; import org.junit.Test; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.LinkedList; //测试插入程序 public class TestInsertOps { private static ClassPool insertCode() { try { //todo 两个方式回去ClassPool 1)ClassPool.getDefault(); 内部会自动调用appendSystemPath方法 2)可以直接new,不过要手动 appendSystemPath ClassPool pool = new ClassPool();// ClassPool.getDefault(); pool.appendSystemPath();//如果添加到系统环境中内程序可以执行,否则会javassist.NotFoundException: com.company.Test1 CtClass clazz = pool.get("com.company.Test1"); CtMethod method = clazz.getDeclaredMethod("toString"); method.insertBefore("{ System.out.println(\"方法调用前 ----->>aaa \"); }"); method.insertAfter("{ System.out.println(\"方法调用后 ---->> bbbb\"); }"); return pool; } catch (NotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (CannotCompileException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } public static void testInsertCode() throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException { ClassPool classPool = insertCode(); if (classPool == null) { System.out.println("程序添加失败 --> classPool is NullPointer"); return; } // ClassPool pool = new ClassPool() Test1 test1 = (Test1) classPool.get("com.company.Test1").toClass().newInstance(); System.out.println("InsertCode:" + test1.toString()); } }
首先,我们需要获取一个
ClassPool
对象,通常有两个方法获取- 第一种
ClassPool.getDefault()
这个方法的好处是调用getDefault()
方法,其内部封装单例了方法,如下
public static synchronized ClassPool getDefault() { if (defaultPool == null) { defaultPool = new ClassPool((ClassPool)null); defaultPool.appendSystemPath(); } return defaultPool; }
可以看到其内部调用了
new ClassPool()
方法,因此,第二种方法就是- 第二种
new ClassPool()
,构造函数如下
public ClassPool() { this((ClassPool)null); } --> public ClassPool(ClassPool parent) { this.childFirstLookup = false; this.cflow = null; this.classes = new Hashtable(191); this.source = new ClassPoolTail(); this.parent = parent; if (parent == null) { CtClass[] pt = CtClass.primitiveTypes; for(int i = 0; i < pt.length; ++i) { this.classes.put(pt[i].getName(), pt[i]); } } this.cflow = null; this.compressCount = 0; this.clearImportedPackages(); }
两个不同的点在于,
getDefault
自动调用了defaultPool.appendSystemPath();
如果这个方法不调用,则就是报错误
javassist.NotFoundException:
。如果使用的是第二种方法获取ClassPool
对象,需要调用这个方法。其次调用
ClassPool.get(String className)
获取一个CtClass
对象,CtClass clazz = pool.get("com.company.Test1");
接着获取一个
CtMethod
,可以通过getMethods()和getDeclareMethod()
来获取,和java的反射类似使用,只是Method-->CtMethod
。这里获取
toString
方法,使用getDeclareMethod
方法并判断是否是预期的方法CtMethod method = clazz.getDeclaredMethod("toString");
在得到了方法体之后,调用
insertBefore(String method)
在方法入口处插入第一段方法接着调用
insertAfter(String method)
在return之前添加程序method.insertAfter("{ System.out.println(\"方法调用后 ---->> bbbb\"); }");
insertAfter和insertBefore的 method参数是一个字符串,并且是一个完整的方法调用,这个在openrasp中有体现
最后调用toString方法
Test1 test1 = (Test1) classPool.get("com.company.Test1").toClass().newInstance(); System.out.println("InsertCode:" + test1.toString());
输入结果为
方法调用前 ----->>aaa 方法调用后 ---->> bbbb InsertCode:Test1{aa='heh'}
验证程序写入的位置
为了验证程序添加到正确的位置,这里修改了
TestInsertOps
的程序,在InsertCode
内添加了如下内容package com.company; import javassist.*; import org.junit.Test; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.LinkedList; //测试插入程序 public class TestInsertOps { private static void save(byte[] data,String filename) throws IOException { String path = System.getProperty("user.dir"); File cache = new File(path+File.separator+"cache"); if(!cache.isDirectory()){ cache.mkdirs(); } path = cache+File.separator+filename; File file = new File(path); if(file.exists()){ file.delete(); } FileOutputStream fileOutputStream = new FileOutputStream(file); fileOutputStream.write(data); fileOutputStream.flush(); fileOutputStream.close(); System.out.println("保存成功 "+file.getAbsolutePath()); } private static ClassPool insertCode() { try { //todo 两个方式回去ClassPool 1)ClassPool.getDefault(); 内部会自动调用appendSystemPath方法 2)可以直接new,不过要手动 appendSystemPath ClassPool.getDefault(); ClassPool pool = new ClassPool();// ClassPool.getDefault(); pool.appendSystemPath();//如果添加到系统环境中内程序可以执行,否则会javassist.NotFoundException: com.company.Test1 CtClass clazz = pool.get("com.company.Test1"); // byte[] origin_clzz=clazz.toBytecode(); save(origin_clzz,"origin_clzz.class"); if (clazz.isFrozen()) { System.out.println("Frozen ..."); clazz.defrost(); } CtMethod method = clazz.getDeclaredMethod("toString"); method.insertBefore("{ System.out.println(\"方法调用前 ----->>aaa \"); }"); method.insertAfter("{ System.out.println(\"方法调用后 ---->> bbbb\"); }"); // clazz.writeFile(); byte[] mode_clzz=clazz.toBytecode(); save(mode_clzz,"mode_clzz.class"); return pool; } catch (NotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (CannotCompileException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } public static void testInsertCode() throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException { ClassPool classPool = insertCode(); if (classPool == null) { System.out.println("程序添加失败 --> classPool is NullPointer"); return; } // ClassPool pool = new ClassPool() Test1 test1 = (Test1) classPool.get("com.company.Test1").toClass().newInstance(); System.out.println("InsertCode:" + test1.toString()); } }
程序运行后可以得到两个类
origin_clzz.class和mode_clzz.class
。使用Idea的反编译功能,可以看到被修改的类Test1
前后差别Before :
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.company; import java.io.File; import java.net.URL; import java.net.URLDecoder; import java.util.Set; public class Test1 { private String aa = "heh"; public Test1() { } public String getLocalPath() { URL url = this.getClass().getProtectionDomain().getCodeSource().getLocation(); String decode = URLDecoder.decode(url.getFile()); System.out.println("Before " + url.getFile() + "decodeUrl " + decode + " replace " + decode.replace("+", "%2B") + "parent " + (new File(decode)).getParent()); return decode; } public void testParam(Set<String> result) { String[] objec = new String[]{"AAA", "BBB", "CCC", "DDD"}; String[] var3 = objec; int var4 = objec.length; for(int var5 = 0; var5 < var4; ++var5) { String a = var3[var5]; result.add(a); } } public String toString() { return "Test1{aa='" + this.aa + '\'' + '}'; } }
After:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.company; import java.io.File; import java.net.URL; import java.net.URLDecoder; import java.util.Set; public class Test1 { private String aa = "heh"; public Test1() { } public String getLocalPath() { URL url = this.getClass().getProtectionDomain().getCodeSource().getLocation(); String decode = URLDecoder.decode(url.getFile()); System.out.println("Before " + url.getFile() + "decodeUrl " + decode + " replace " + decode.replace("+", "%2B") + "parent " + (new File(decode)).getParent()); return decode; } public void testParam(Set<String> result) { String[] objec = new String[]{"AAA", "BBB", "CCC", "DDD"}; String[] var3 = objec; int var4 = objec.length; for(int var5 = 0; var5 < var4; ++var5) { String a = var3[var5]; result.add(a); } } public String toString() { System.out.println("方法调用前 ----->>aaa "); String var2 = "Test1{aa='" + this.aa + '\'' + '}'; System.out.println("方法调用后 ---->> bbbb"); return var2; } }
完。
总结
java可以在运行时修改程序,通过
javaasist
工具类来实现,通过ClassPool,CtClass,CtMethod
来实现。 -
如何使用 Java Agent 在运行时修改 Java 类(以魔改 Jenkins 为例)
2020-05-13 12:05:41深夜更新 Jenkins 插件的时候,遇到了一个问题:插件下载速度太慢了,并且有大概率失败。 因此我研究了一下 Jenkins 的插件升级机制,研究是否可以使用镜像站点加速。 初步研究 Jenkins 的 升级信息(包含本体和插件...遇到问题
起因
深夜更新 Jenkins 插件的时候,遇到了一个问题:插件下载速度太慢了,并且有大概率失败。
因此我研究了一下 Jenkins 的插件升级机制,研究是否可以使用镜像站点加速。
初步研究
Jenkins 的
升级信息
(包含本体和插件)来自于http://mirrors.jenkins-ci.org/updates/update-center.json
这个 URL。这个 JSON 文件描述了所有可用插件的
版本信息
和下载地址
,并且 Jenkins 在插件中心
的高级
选项卡里提供了修改这个地址的功能。我找到了
jenkins-zh
社区提供的升级信息
镜像地址https://updates.jenkins-zh.cn/update-center.json
,他们所提供的 JSON 文件内部,将所有下载地址
替换为等价的 清华 TUNA 镜像站 的地址。遭遇困境
但是当我把镜像地址填入 Jenkins,点击
更新
时,界面突然弹出来巨长一串超出屏幕的报错,提示签名验证失败
。为什么
Jenkins 在这个 JSON 文件内做了签名认证。
也就是说,Jenkins 允许你从另外的 URL 获取到
update-center.json
这个文件,但是 JSON 文件内部描述的下载地址
等信息是无法修改
的,否则会破坏签名认证。这也就意味着,插件下载的过程仍然无法使用镜像站点加速。
怎么办
两个方法:
- 重新签名
update-center.json
并替换 Jenkins 内的 CA 文件,让修改后的update-center.json
变得合法 - 修改代码重新编译 Jenkins,关掉这个签名认证 (既然允许自定义
升级信息
地址,又何必非要做签名认证
呢?)
方法 1 的问题
如果有一天我不使用这个镜像站了,我还得想办法恢复 CA 文件。
并且每次
update-center.json
有变动,我都得重新签名。尽管可以制作或者寻找现有的自动化工具,但是终究需要额外部署一套工具。方法 2 的问题
每次 Jenkins 版本更新都需要重新编译 Jenkins,着实麻烦。
更好的办法
我想到了一个更好的办法,使用
Java Agent
机制,在运行时覆盖掉 Jenkins 签名验证方法,让它跳过实际的签名验证,永远返回正确。这样即便是 Jenkins 版本更新,只要
升级信息
签名验证代码流程不变,这个方法就可以一直有效。实施流程
什么是 Java Agent
Java Agent 通过在 Java 命令上添加参数
-javaagent:XXXXXX.jar
来启动,它允许你在主类之外,额外加载一个 Jar 包,并提供机制,允许你在类加载时,修改类的字节码。通过 Java Agent 机制,可以在不修改源代码的情况下,在运行时修改 Java 程序。
定位 Jenkins 代码
简要查阅了 Jenkins 的源代码,定位到了 Jenkins 中,负责校验升级信息签名的方法来自
hudson.model.UpdateSite
类的verifySignature
方法/** * Verifies the signature in the update center data file. */ private FormValidation verifySignature(JSONObject o) throws IOException { return getJsonSignatureValidator().verifySignature(o); }
我们的目标是,将这个方法的代码体,修改成如下内容
return FormValidation.ok();
这样,修改后的方法,永远会返回校验正常。
编写 Java Agent
知道了要修改什么,就可以开始编写 Java Agent 了。
我们选用
javassist
这个库,这是一个 IBM 推出的库,可以便捷地修改 Java 类字节码。创建 Maven 工程
首先我们创建一个 Maven 工程,和常见的工程类似,但是 Pom 文件的设置有些许区别:
<!-- 略去了不重要的内容 --> <dependencies> <!-- 引入 javassist 包 --> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.27.0-GA</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <!-- Java Agent 的 MANIFEST 里面不是 Main-Class--> <!-- 我们使用 premain 模式,因此要写 Premain-Class --> <archive> <manifestEntries> <Premain-Class>net.landzero.jenkins.tune.Agent</Premain-Class> </manifestEntries> </archive> </configuration> </plugin> <!-- 使用 shade 插件,把 javassist 直接包含在成品 Jar 包内部 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> <configuration> <filters> <!-- 过滤掉 javassist 自己的 META-INF 文件(可能多此一举了) --> <filter> <artifact>org.javassist:*</artifact> <excludes> <exclude>META-INF/license/**</exclude> <exclude>META-INF/*</exclude> <exclude>META-INF/maven/**</exclude> <exclude>LICENSE</exclude> <exclude>NOTICE</exclude> <exclude>/*.txt</exclude> <exclude>build.properties</exclude> </excludes> </filter> </filters> </configuration> </plugin> </plugins> </build> <!-- 略去了不重要的内容 -->
编写 Agent 主类
package net.landzero.jenkins.tune; // 此处略却一大堆 import public class Agent { // Java Agent 的启动入口不是 main,而是 premain,并且有特定的方法签名 public static void premain(String agentArgs, Instrumentation inst) { // 注册一个类转换器 inst.addTransformer(new UpdateSiteTransformer()); } private static class UpdateSiteTransformer implements ClassFileTransformer { private static final String CLASS_NAME = "hudson.model.UpdateSite"; private static final String CLASS_NAME_INTERNAL = CLASS_NAME.replace('.', '/'); public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (className == null) { return null; } // 如果不是 hudson.model.UpdateSite 就忽略掉,不做修改 // 这个方法传入的 className 是使用 “/” 分割的类名,而不是标准的 "." 分割的类名 if (!CLASS_NAME_INTERNAL.equals(className)) { return null; } try { // 加载 javassist 类池 ClassPool cp = ClassPool.getDefault(); // 因为 Jenkins 是 WAR 包,还需要把 loader 补充进去,不然下面 javassist 解析字节码会报告找不到 import 的类 cp.appendClassPath(new LoaderClassPath(loader)); // 把字节码载入进去,生成 CtClass CtClass cc = cp.makeClass(new ByteArrayInputStream(classfileBuffer)); // 定位到方法 verifySignature CtMethod cm = cc.getDeclaredMethod("verifySignature"); // 修改方法代码体 cm.setBody("{ return hudson.util.FormValidation.ok(); }"); // 返回重新编译的字节码 return cc.toBytecode(); } catch (Exception e) { return null; } } } }
打包
直接
mvn clean package
打包,得到jenkins-tune-1.0-SNAPSHOT.jar
文件,这里面包含了我们写的Agent
类和依赖项javassist
。部署 Java Agent
把
jenkins-tune-1.0.0-SNAPSHOT.jar
复制到服务器上,假设复制到/usr/local/lib/jenkins-tune.jar
这个位置。编辑
/etc/init.d/jenkins
,寻找JAVA_CMD=...
那一行,添加一句-javaagent:/usr/local/lib/jenkins-tune.jar
。(如果是
yum
包安装的Jenkins
,也可以修改/etc/default/jenkins
中的JAVA_ARGS
字段)(如果使用的是
WAR
包手动部署,则需要修改Tomcat
的启动脚本)systemctl restart jenkins
重新启动 Jenkins 就大功告成了。结果
我重新填入了
jenkins-zh
提供的镜像地址,这次就再也没有提示签名错误了,也可以正常地从 清华 TUNA 镜像站 拉取插件更新,速度自然是飞快。代码地址
https://github.com/guoyk93/jenkins-tune
如果你不想自己编译 Jar 包,可以在 Release 页面找到预先编译好的 Jar 包
- 重新签名
-
动态类java_Java运行时动态生成类几种方式
2021-03-07 08:19:47最近一个项目中利用规则引擎,提供用户拖拽式的灵活定义规则。这就要求根据数据库数据动态生成对象处理特定规则的...那就着手从Java如何根据字符串模板在运行时动态生成对象。Java是一门静态语言,通常,我们需要...最近一个项目中利用规则引擎,提供用户拖拽式的灵活定义规则。这就要求根据数据库数据动态生成对象处理特定规则的逻辑。如果手写不仅每次都要修改代码,还要每次测试发版,而且无法灵活根据用户定义的规则动态处理逻辑。所以想到将公共逻辑写到父类实现,将特定逻辑根据字符串动态生成子类处理。这就可以一劳永逸解决这个问题。
那就着手从Java如何根据字符串模板在运行时动态生成对象。
Java是一门静态语言,通常,我们需要的class在编译的时候就已经生成了,为什么有时候我们还想在运行时动态生成class呢?
经过一番网上资料查找,由繁到简的方式总结如下:
一、利用JDK自带工具类实现
现在问题来了,动态生成字节码,难度有多大?
如果我们要自己直接输出二进制格式的字节码,在完成这个任务前,必须先认真阅读JVM规范第4章,详细了解class文件结构。估计读完规范后,两个月过去了。
所以,第一种方法,自己动手,从零开始创建字节码,理论上可行,实际上很难。
第二种方法,使用已有的一些能操作字节码的库,帮助我们创建class。
目前,能够操作字节码的开源库主要有CGLib和Javassist两种,它们都提供了比较高级的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还是得花大量的时间,并且,上面的代码很难看懂对不对?
有木有更简单的方法?
有!
Java的编译器是javac,但是,在很早很早的时候,Java的编译器就已经用纯Java重写了,自己能编译自己,行业黑话叫“自举”。从Java 1.6开始,编译器接口正式放到JDK的公开API中,于是,我们不需要创建新的进程来调用javac,而是直接使用编译器API来编译源码。
使用起来也很简单:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int compilationResult = compiler.run(null, null, null, ‘/path/Test.java‘);
这么写编译是没啥问题,问题是我们在内存中创建了Java代码后,必须先写到文件,再编译,最后还要手动读取class文件内容并用一个ClassLoader加载。
有木有更简单的方法?
有!
其实Java编译器根本不关心源码的内容是从哪来的,你给它一个String当作源码,它就可以输出byte[]作为class的内容。
所以,我们需要参考Java Compiler API的文档,让Compiler直接在内存中完成编译,输出的class内容就是byte[]。
Map 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();
}
}
上述代码的几个关键在于:
用MemoryJavaFileManager替换JDK默认的StandardJavaFileManager,以便在编译器请求源码内容时,不是从文件读取,而是直接返回String;
用MemoryOutputJavaFileObject替换JDK默认的SimpleJavaFileObject,以便在接收到编译器生成的byte[]内容时,不写入class文件,而是直接保存在内存中。
最后,编译的结果放在Map中,Key是类名,对应的byte[]是class的二进制内容。
为什么编译后不是一个byte[]呢?
因为一个.java的源文件编译后可能有多个.class文件!只要包含了静态类、匿名类等,编译出的class肯定多于一个。
如何加载编译后的class呢?
加载class相对而言就容易多了,我们只需要创建一个ClassLoader,覆写findClass()方法:
class MemoryClassLoader extends URLClassLoader {
Map classBytes = new HashMap();
public MemoryClassLoader(Map 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);
}
}
总结以上,那么我们来编写一个Java脚本引擎吧:
二、利用三方Jar包实现
利用三方包com.itranswarp.compiler来实现:
1. 引入Maven依赖包:
2. 编写工具类
public class StringCompiler {
public static Object run(String source, String...args) throws Exception {
// 声明类名
String className = "Main";
String packageName = "top.fomeiherz";
// 声明包名:package top.fomeiherz;
String prefix = String.format("package %s;", packageName);
// 全类名:top.fomeiherz.Main
String fullName = String.format("%s.%s", packageName, className);
// 编译器
JavaStringCompiler compiler = new JavaStringCompiler();
// 编译:compiler.compile("Main.java", source)
Map results = compiler.compile(className + ".java", prefix + source);
// 加载内存中byte到Class>对象
Class> clazz = compiler.loadClass(fullName, results);
// 创建实例
Object instance = clazz.newInstance();
Method mainMethod = clazz.getMethod("main", String[].class);
// String[]数组时必须使用Object[]封装
// 否则会报错:java.lang.IllegalArgumentException: wrong number of arguments
return mainMethod.invoke(instance, new Object[]{args});
}
}
3. 测试执行
public class StringCompilerTest {
public static void main(String[] args) throws Exception {
// 传入String类型的代码
String source = "import java.util.Arrays;public class Main" +
"{" +
"public static void main(String[] args) {" +
"System.out.println(Arrays.toString(args));" +
"}" +
"}";
StringCompiler.run(source, "1", "2");
}
}
三、利用Groovy脚本实现
以上两种方式尝试过,后来发现Groovy原生就支持脚本动态生成对象。
1. 引入Groovy maven依赖
org.codehaus.groovy
groovy-all
2.4.13
2. 直接上测试代码
@Test
public void testGroovyClasses() throws Exception {
//groovy提供了一种将字符串文本代码直接转换成Java Class对象的功能
GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
//里面的文本是Java代码,但是我们可以看到这是一个字符串我们可以直接生成对应的Class>对象,而不需要我们写一个.java文件
Class> clazz = groovyClassLoader.parseClass("package com.xxl.job.core.glue;\n" +
"\n" +
"public class Main {\n" +
"\n" +
" public int age = 22;\n" +
" \n" +
" public void sayHello() {\n" +
" System.out.println(\"年龄是:\" + age);\n" +
" }\n" +
"}\n");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sayHello");
method.invoke(obj);
Object val = method.getDefaultValue();
System.out.println(val);
}
-
动态生成java类_Java 运行时动态生成class
2021-02-12 17:01:09因为在有些时候,我们还真得在运行时为一个类动态创建子类。比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢?以User为例:public classUser {privateString id;privateString name;publicS..... -
java 动态创建类_Java运行时动态生成类几种方式
2021-02-12 10:23:51最近一个项目中利用规则引擎,提供用户拖拽式的灵活定义规则。这就要求根据数据库数据动态生成对象处理特定规则的...那就着手从Java如何根据字符串模板在运行时动态生成对象。Java是一门静态语言,通常,我们需要... -
java 修改系统属性_Java:通过运行时修改系统属性
2021-02-26 09:47:32您可以使用命令行在系统属性上定义-DpropertyName=propertyValue所以你可以写java -jar selenium-rc.jar -Dhttp.proxyHost=YourProxyHost -Dhttp.proxyPort=YourProxyPort编辑:您可以编写一个作为应用程序启动器的... -
java 运行 class_Java运行时动态生成class的方法
2021-02-12 12:51:59因为在有些时候,我们还真得在运行时为一个类动态创建子类。比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢?以User为例:public class User {private String id;private String name;publ..... -
一种Java运行时动态生成class的方法
2017-05-11 09:21:56Java运行时动态生成class的方法Java是一门静态语言,通常,我们需要的class在编译的时候就已经生成了,为什么有时候我们还想在运行时动态生成class呢?因为在有些时候,我们还真得在运行时为一个类动态创建子类。... -
java如何运行一个继承了不同包下类的.class文件。
2021-02-18 14:08:46第一个类是base.Base,存在./path1/base/路径下,第二个类是sub.Sub,存放在./path2/sub/路径下,Sub继承了Base。源代码如下: <code class="language-java">package base; public ... -
java动态新增方法,【Java】Java运行时动态生成class的方法
2021-03-25 09:34:56因为在有些时候,我们还真得在运行时为一个类动态创建子类。比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢?以User为例:public class User {private String id;private String name;publ... -
Java运行时生成Class的方法
2017-12-02 12:02:16因为在有些时候,我们还真得在运行时为一个类动态创建子类。比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢? 以User为例: public class User { private String id; priv -
java加载时初始化_java – 加载,链接和初始化 – 什么时候加载一个类?
2021-03-16 03:29:43我对类加载的理解是一个类在第一次需要时被加载(以非常简单的方式放置).使用-verbose:class和Iterators类的修改版本运行以下示例,该类在调用其clinit时打印消息我观察到了一些我无法解释的内容:public class ... -
Java运行于手机的一个联系人管理小程序.rar
2019-07-10 09:42:26Java运行于手机的一个联系人管理小程序,RMS记录读取和修改程序, Display display = null; // 设备的显示器 List list = null; TextField nameField; // 姓名文本域 TextField honeField; //电话号码... -
类运行时报错:java.lang.OutOfMemoryError: Java heap space 解决方法
2020-09-14 16:14:36今天写了一个工具类,导入Excel文件,根据文件批量生成SQL,但是运行的时候,一直报错,报错如下: java.lang.OutOfMemoryError: Java heap space 调整idea中setting中的compiler的Build process heap size(Mbytes)... -
java 运行时类型识别(RTTI) - 2 - 反射
2012-07-30 12:55:34本文将叙述如何运行时查看类信息,其中包括变量,方法,以及通过反射修改变量,执行方法等 包括如何反射匿名内部类及如何执行其方法,但是笔者强烈不建议这么做,这里只是演示反射而已 下面是一个测试类... -
java动态生成class_[转] Java运行时动态生成class的方法
2021-03-10 07:33:30因为在有些时候,我们还真得在运行时为一个类动态创建子类。比如,编写一个ORM框架,如何得知一个简单的JavaBean是否被用户修改过呢?以User为例:public class User {private String id;private String name;publ..... -
java 运行jar包时 找不到jdk 中的类
2017-10-10 18:01:02今天打了个jar 包,总是会报错,找不到 jdk 中的类 java.lang.NoClassDefFoundError: javax/xml/ws/Service 这个Service 明明是 jdk 中 rt.jar 中的 类,怎么会找不到呢? 开始不停地修改环境变量中···· ... -
java 动态对象_Java运行时动态生成对象几种方式
2021-02-12 14:12:11最近一个项目中利用规则引擎,提供用户拖拽式的灵活定义规则。这就要求根据数据库数据动态生成对象处理特定规则的...那就着手从Java如何根据字符串模板在运行时动态生成对象。Java是一门静态语言,通常,我们需要... -
修改java.lang.String类时出错?
2011-10-12 10:30:34本人想修改java.lang.String类,然后导出成jar,代替原来虚拟机的String类使用,但是在添加一个布尔数组后,导出成jar并使用时出现下面的错误: Error occurred during initialization of VM java.lang.... -
java类只运行一次_java程序启动的时候,是不是一次性加载所有类-Go语言中文社区...
2021-03-18 09:41:56前言:首先我们来思考一个问题,我们进程在项目开发中,仅仅修改了几个类的代码,之后将这些类的class文件拷贝到web容器对应位置中,如果不重启服务时能否保住我们修改的代码也被加载?同是也考虑一下,是否是在web... -
字节预算:Java虚拟机的运行时代码生成-源码
2021-02-03 17:25:20Byte Buddy是一个代码生成和操作库,用于在Java应用程序运行时创建和修改Java类,而无需编译器的帮助。 除了的代码生成实用程序外,Byte Buddy还允许创建任意类,并且不限于实现用于创建运行时代理的接口。 此外,... -
weblogic修改java重启_Weblogic中的热部署——修改JSP、java类无需重启(转)
2021-03-14 19:05:17因为java classloader没有任何一种机制来卸下一系列存在的类,也不能用类的新版本来替换老版本,为了在一个运行的虚拟机中更新相关的类,classloader必须被替换掉。当它被替换时,它所装载的所有类以及衍生的子... -
java中的反射 2.2——类:检查一个类的声明信息@译自Oracle官方文档
2015-07-22 16:44:56一个类可以在声明时使用一些修饰符,这些修饰符会影响其运行时行为: 访问修饰符:public,protected和private要求重写(override):abstract限定为一个实例:static禁止修改:final强制精确浮点 -
idea 查看一个类的子类_实现一个分布式调用链路追踪Java探针你可能会遇到的问题
2020-11-28 17:21:59Java探针可以在Java应用运行时毫无感知的切入应用代码,是一种用于监听代码行为或改变代码行为的工具。分布式调用链路追踪的实现无非两种方式,代码侵入式和非代码侵入式,基于Java探针实现的属于非代码侵入式。运行... -
java代码编译时修改行为_GitHub - niuzhihua/AST_demo: 利用JavaParser框架在编译时修改语法树(源码)的 ...
2021-03-16 22:14:30本例子展示了如下功能:1、删除方法中的 打印日志代码。2、删除指定的 方法 和 成员变量。3、检测方法中 是否有 new Thread 代码。...7、在现有类 中生成一个方法 ,并在运行时验证。环境:gradle...