如何在运行时修改java.lang类?

16

我正在寻找一种方法,通过重写字节码并重新加载类,在运行时向线程添加字段,不确定是否有可能。欢迎任何指针。我找到了一些关于修改和加载类的信息,我知道JRebel可以无缝地热交换您的代码,但不确定是否适用于此。

这里的动机是探索理论上更好的线程本地对象替代方案。如果该方法有效,我应该能够用注释替换线程本地对象,并且结果应该优于当前的JDK实现。

PS:请不要给我“万恶之源”的演讲。

澄清用例:

想象一下我有一个带有ThreadLocal的类:


class A {
   ThreadLocal<Counter> counter;
   ...
   counter.get().inc()
}

我想用注释替换它:


class A {
   @ThreadLocal
   Counter counter;
   ...
   counter.inc()
}

但是,我希望通过修改Thread对象,而不是生成上述代码。具体地说,我想要使Thread对象具有一个Acounter字段,并让实际代码变成:


class A {
   // Nothing here, field is now in Thread
   ...
   Thread.currentThread().Acounter.inc()
}

请问您能否添加您目前拥有或正在开发的代码,以便我们更好地帮助您? - grepit
抱歉,这还处于初步研究阶段,目前没有可用的代码。我会编辑问题以澄清意图。 - Nitsan Wakart
8个回答

11

目前不可能在运行时重新定义一个类,以使重新定义将导致新方法或字段。这是由于扫描堆以查找所有现有实例并转换它们及其引用的复杂性所致,以及潜在的Unsafe字段偏移量基本更新器(如AtomicFieldUpdater)。

作为JEP-159的一部分,可以解除此限制,但正如在concurrency-interest邮件组中讨论的那样,这是一种重大影响的变化,因此可能根本不会发生。

使用Javaassist /类似工具将允许将类转换为具有新方法/字段的新类。该类可以由ClassLoader加载并在运行时使用,但其定义不会替换现有实例。因此,将无法将此方法与代理结合使用来重新定义类,因为仪器重新定义受到限制,例如:“重新定义可能更改方法体、常量池和属性。重新定义不得添加、删除或重命名字段…”请参见此处

因此,目前不支持。


5

可能的方法是使用仪器,以及可能会用到像Javassist这样的库来动态修改代码。(但是目前仍无法添加或删除字段、方法或构造函数)

//Modify code using javassist and call CtClass#toBytecode() or load bytecode from file
byte[] nevcode;
Class<?> clz = Class.forName("any.class.Example");
instrumentationInstace.redefineClasses(new ClassDefinition(clz, nevcode));

不要忘记在Java代理的清单中添加Can-Redefine-Classes: true

真实示例 - 使用Javassist优化Java < 9 中的string.replace(CharSequence, CharSequence)

String replace_src = 
    "{String str_obj = this;\n"
    + "char[] str = this.value;\n"
    + "String find_obj = $1.toString();\n"
    + "char[] find = find_obj.value;\n"
    + "String repl_obj = $2.toString();\n"
    + "char[] repl = repl_obj.value;\n"
    + "\n"
    + "if(str.length == 0 || find.length == 0 || find.length > str.length) {\n"
    + "    return str_obj;\n"
    + "}\n"
    + "int start = 0;\n"
    + "int end = str_obj.indexOf(find_obj, start);\n"
    + "if(end == -1) {\n"
    + "    return str_obj;\n"
    + "}\n"
    + "int inc = repl.length - find.length;\n"
    + "int inc2 = str.length / find.length / 512;\ninc2 = ((inc2 < 16) ? 16 : inc);\n"
    + "int sb_len = str.length + ((inc < 0) ? 0 : (inc * inc2));\n"
    + "StringBuilder sb = (sb_len < 0) ? new StringBuilder(str.length) : new StringBuilder(sb_len);\n"
    + "while(end != -1) {\n"
    + "    sb.append(str, start, end - start);\n"
    + "    sb.append(repl);\n"
    + "    start = end + find.length;\n"
    + "    end = str_obj.indexOf(find_obj, start);\n"
    + "}\n"
    + "if(start != str.length) {\n"
    + "    sb.append(str, start, str.length - start);\n"
    + "}\n"
    + "return sb.toString();\n"
    +"}";


ClassPool cp = new ClassPool(true);
CtClass clz = cp.get("java.lang.String");
CtClass charseq = cp.get("java.lang.CharSequence");

clz.getDeclaredMethod("replace", new CtClass[] {
        charseq, charseq
}).setBody(replace_src);

instrumentationInstance.redefineClasses(new ClassDefinition(Class.forName(clz.getName(), false, null), clz.toBytecode()));

5
如果您想在运行时更改“class”的行为,则可以尝试使用javassist。API在这里

我正在寻找一种方法来改变已经加载和分配的类。我可以找到足够的文档来支持在加载时更改类,但不知道如何用我的新版本替换正在运行的线程。 - Nitsan Wakart

5
我见过一种自定义类加载解决方案,可以动态重新加载JAR文件 - 你为每个JAR文件定义一个ClassLoader并使用它从该JAR加载类;要重新加载整个JAR,只需“杀死”其ClassLoader实例并创建另一个(在替换JAR文件后)。我认为这种方法不可能在Java的内部Thread类上进行调整,因为你无法控制System ClassLoader。一个可能的解决方案是有一个CustomThreadWeaver类,它将生成一个新的类来扩展Thread,并使用自定义DynamicWeavedThreadClassLoader来加载它们。祝你好运,并在成功时向我们展示你的“怪物”。

2
这似乎是使用正确的工具来完成工作的问题。这里曾经有过类似的问题:另一个Stack Overflow问题,Javaassist字节码操作库是可能的解决方案。
但是如果没有更多关于此尝试的原因的细节,似乎真正的答案是使用正确的工具来完成工作。例如,使用Groovy 动态添加方法到语言中

添加新方法并不像添加新字段那样简单,所以这并没有帮助。如果您知道如何使用Javaassist在运行时添加新字段,请告诉我。这是一种智力锻炼的动机,目前只是一个想法/思路。 - Nitsan Wakart
啊,我明白了。我自己没有使用过Javaassist,但是我找到了一个声称可以添加字段的教程:http://www.csg.is.titech.ac.jp/~chiba/javassist/tutorial/tutorial2.html#add - droozen

1

遗憾的是,文档声称这是不可能的:“重新定义可能会更改方法体、常量池和属性。重新定义__不能添加、删除或重命名字段__…”请参见此处 - Nitsan Wakart

0
要实现您想要的功能,更简单的方法是使用Thread的子类,运行它,然后在该线程内执行您示例中的代码(同时将currentThread()强制转换为您的子类)。

请注意,这种方法也适用于通过 javassist 动态生成的子类。 - kutschkem
但这不是我想要的...我想要动态添加字段。如果不是动态添加,那就像你所描述的那样。 - Nitsan Wakart
但是针对您所描述的情况,我认为您应该使用动态子类。如果您有多个线程,其中一个使用A,第二个使用B。您真的想要为同一Thread类生成两个字段吗?此外,我看到的问题是ThreadLocals是实例的成员。因此,您应该为每个A实例生成一个对象,而不是为A类生成一个字段(好吧,我假设示例只是用来演示问题)。 - kutschkem
没关系,既然ThreadLocal需要在所有线程中存在,我想你是正确的,你需要以某种方式将你的字段走私到Thread类中。 - kutschkem

-2

您试图做的事情是不可能的。

既然您已经了解了ThreadLocal,您也知道建议的解决方案是什么。

另外,您可以子类化Thread并添加自己的字段;但是,只有您明确创建的那些类的线程才会具有这些字段,因此您仍然必须能够“回退”到使用线程本地。

真正的问题是“为什么?”,即“为什么线程本地对您的要求不足?”


那是你真正的问题,不是我的 :). 通过在Thread上拥有一个字段和使用ThreadLocal之间的性能差异来衡量,这就是为什么。为什么不可能呢?像JRebel这样的产品声称能够在运行时热交换您的代码,那么为什么不能热交换Thread呢? - Nitsan Wakart
但是答案哪里不正确呢?顺便说一下,JRebel通过在加载时更改类来工作,在任何需要修改的类中添加一个额外的字段。如果您不喜欢合理的建议,可以自己更改rt.jar中的Thread.class。 - cpurdy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接