Class 重新转换不适用于 Java 11 上的动态代理
Class retransformation doesn't work for dynamic agent on Java 11
似乎 classes 的动态重新转换仅在 Java 8 上对我有效,但在 Java 11 上无效。在后一种情况下,我从 javassist 那里得到了关于不同的异常没有找到标准 Java classes,例如,我直接引用的那些,甚至是从 method-to-transform 的签名中引用的。
我应该怎么做才能在 Java 11 上解决这个问题?我也想在这里动态转换 classes。
为了便于说明,我创建了一个重现文件。这里我重新改造了两个classes:一个是我自己的,一个是系统的。我创建了 agentmain
和 premain
进行比较。当主要参数传递给应用程序时执行动态变体(我将其仅作为“o”传递)。重新转换后,我调用了两种方法(我自己的 class 和系统的一种)。转换成功后,我会收到额外的日志记录(“hi-”和“scaled!”)。
// MyMain.java
package mypackage;
import com.sun.tools.attach.VirtualMachine;
import javassist.*;
import javassist.bytecode.AccessFlag;
import sun.java2d.SunGraphics2D;
import sun.java2d.SurfaceData;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.lang.management.ManagementFactory;
import java.security.ProtectionDomain;
public class MyMain {
public static void premain(String args, Instrumentation inst) {
System.out.println("premain start");
inst.addTransformer(new MyFormer(), true);
try {
inst.retransformClasses(MyMain.class);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
System.out.println("premain end");
}
public static void agentmain(String args, Instrumentation inst) {
System.out.println("agentmain start");
inst.addTransformer(new MyFormer(), true);
try {
inst.retransformClasses(MyMain.class);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
System.out.println("agentmain end");
}
public static void main(String[] args) {
if (args.length > 0) {
attachToThisVm();
}
Frame f = new JFrame();
f.setVisible(true);
System.out.println(new MyMain().hi());
SunGraphics2D system = new SunGraphics2D(SurfaceData.getPrimarySurfaceData(new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB)), Color.BLACK, Color.WHITE, Font.getFont("System"));
system.drawRenderedImage(null,new AffineTransform() {
@Override
public void setToScale(double sx, double sy) {
super.setToScale(sx, sy);
System.out.println("scaled!");
}
});
}
public static void attachToThisVm() {
System.out.println("dynamically loading javaagent");
String name = ManagementFactory.getRuntimeMXBean().getName();
int p = name.indexOf('@');
String pid = name.substring(0, p);
try {
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent("javaAgentTest-1.0-SNAPSHOT.jar", null);
vm.detach();
}
catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println("dynamically loaded javaagent");
}
public int hi() {
return 3;
}
public static class MyFormer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
return transformClass(className, classfileBuffer);
}
private byte[] transformClass(String className, byte[] buffer) {
if ("mypackage/MyMain".equals(className)) {
System.out.println(className);
ClassPool cp = ClassPool.getDefault();
String name = className.replace("/", ".");
cp.insertClassPath(new ByteArrayClassPath(name, buffer));
try {
CtClass clazz = cp.get(name);
CtBehavior[] declaredBehaviors = clazz.getDeclaredBehaviors();
for (CtBehavior db : declaredBehaviors) {
if ("hi".equals(db.getName())) {
if ((db.getMethodInfo().getAccessFlags() & AccessFlag.STATIC) != 0) {
System.out.println("bad access flags, skipping...");
return buffer;
}
System.out.println("Forming hi...");
db.insertBefore("System.out.print(\"hi-\");"); // crashes on 11, direct usage case, even referencing to java.lang.Object will crash
}
}
return clazz.toBytecode();
} catch (Throwable e) {
e.printStackTrace();
System.out.println("error");
return buffer;
}
}
if ("sun/java2d/SunGraphics2D".equals(className)) {
System.out.println(className);
ClassPool cp = ClassPool.getDefault();
String name = className.replace("/", ".");
cp.insertClassPath(new ByteArrayClassPath(name, buffer));
try {
CtClass clazz = cp.get(name);
CtBehavior[] declaredBehaviors = clazz.getDeclaredBehaviors();
for (CtBehavior db : declaredBehaviors) {
if ("sun.java2d.SunGraphics2D.drawRenderedImage(java.awt.image.RenderedImage,java.awt.geom.AffineTransform)".equals(db.getLongName())) {
if ((db.getMethodInfo().getAccessFlags() & AccessFlag.STATIC) != 0) {
System.out.println("bad access flags, skipping...");
return buffer;
}
System.out.println("Forming drawRenderedImage...");
db.insertBefore(".setToScale(2.0, 2.0);"); // crashes on 11, signature case
}
}
return clazz.toBytecode();
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
return buffer;
}
}
return buffer;
}
}
}
我通过 Gradle:
构建 jar
// build.gradle, module name is javaAgentTest
plugins {
id 'java'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
def inline = { deps -> deps.collect { it.isDirectory() ? it : zipTree(it) } }
jar {
manifest {
attributes(
"Can-Redefine-Classes": true,
"Can-Retransform-Classes": true,
"Premain-Class": "mypackage.MyMain",
"Agent-Class": "mypackage.MyMain",
)
}
from {
inline(configurations.runtimeClasspath) // fat jar
}
}
dependencies {
implementation "org.javassist:javassist:3.27.0-GA"
}
在 Java 8 上,静态和动态变体都有效:
$ java -version
openjdk version "1.8.0_265"
OpenJDK Runtime Environment (build 1.8.0_265-8u265-b01-0ubuntu2~20.04-b01)
OpenJDK 64-Bit Server VM (build 25.265-b01, mixed mode)
$ java -cp javaAgentTest-1.0-SNAPSHOT.jar -javaagent:javaAgentTest-1.0-SNAPSHOT.jar mypackage.MyMain
premain start
mypackage/MyMain
Forming hi...
premain end
sun/java2d/SunGraphics2D
Forming drawRenderedImage...
hi-3
scaled!
$ java -cp javaAgentTest-1.0-SNAPSHOT.jar:/usr/lib/jvm/java-8-openjdk-amd64/lib/tools.jar mypackage.MyMain o
dynamically loading javaagent
agentmain start
mypackage/MyMain
Forming hi...
agentmain end
dynamically loaded javaagent
sun/java2d/SunGraphics2D
Forming drawRenderedImage...
hi-3
scaled!
在 Java 11 上,动态变体不起作用(如果没有对 System.out
的引用,它将分叉 hi
方法,例如,只有 db.insertBefore("return 22;");
):
$ java -version
openjdk version "11.0.8" 2020-07-14
OpenJDK Runtime Environment (build 11.0.8+10-post-Ubuntu-0ubuntu120.04)
OpenJDK 64-Bit Server VM (build 11.0.8+10-post-Ubuntu-0ubuntu120.04, mixed mode, sharing)
$ java -cp javaAgentTest-1.0-SNAPSHOT.jar -javaagent:javaAgentTest-1.0-SNAPSHOT.jar mypackage.MyMain
premain start
mypackage/MyMain
Forming hi...
premain end
sun/java2d/SunGraphics2D
Forming drawRenderedImage...
hi-3
scaled!
$ java -cp javaAgentTest-1.0-SNAPSHOT.jar -Djdk.attach.allowAttachSelf=true mypackage.MyMain o
dynamically loading javaagent
agentmain start
mypackage/MyMain
Forming hi...
javassist.CannotCompileException: [source error] no such class: System.out
at javassist.CtBehavior.insertBefore(CtBehavior.java:806)
at javassist.CtBehavior.insertBefore(CtBehavior.java:766)
at mypackage.MyMain$MyFormer.transformClass(MyMain.java:112)
at mypackage.MyMain$MyFormer.transform(MyMain.java:91)
at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:246)
at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:563)
at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:167)
at mypackage.MyMain.agentmain(MyMain.java:38)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:513)
at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain(InstrumentationImpl.java:535)
Caused by: compile error: no such class: System.out
at javassist.compiler.MemberResolver.searchImports(MemberResolver.java:479)
at javassist.compiler.MemberResolver.lookupClass(MemberResolver.java:422)
at javassist.compiler.MemberResolver.lookupClassByJvmName(MemberResolver.java:329)
at javassist.compiler.TypeChecker.atCallExpr(TypeChecker.java:711)
at javassist.compiler.JvstTypeChecker.atCallExpr(JvstTypeChecker.java:170)
at javassist.compiler.ast.CallExpr.accept(CallExpr.java:49)
at javassist.compiler.CodeGen.doTypeCheck(CodeGen.java:266)
at javassist.compiler.CodeGen.atStmnt(CodeGen.java:360)
at javassist.compiler.ast.Stmnt.accept(Stmnt.java:53)
at javassist.compiler.Javac.compileStmnt(Javac.java:578)
at javassist.CtBehavior.insertBefore(CtBehavior.java:786)
... 15 more
error
agentmain end
dynamically loaded javaagent
sun/java2d/SunGraphics2D
Forming drawRenderedImage...
javassist.CannotCompileException: cannot find java.awt.image.RenderedImage
at javassist.CtBehavior.insertBefore(CtBehavior.java:803)
at javassist.CtBehavior.insertBefore(CtBehavior.java:766)
at mypackage.MyMain$MyFormer.transformClass(MyMain.java:140)
at mypackage.MyMain$MyFormer.transform(MyMain.java:91)
at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:246)
at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:563)
at java.desktop/sun.java2d.loops.GraphicsPrimitiveMgr.<clinit>(GraphicsPrimitiveMgr.java:56)
at java.desktop/sun.java2d.loops.Blit.<clinit>(Blit.java:114)
at java.desktop/sun.java2d.xr.XRPMBlitLoops.register(XRPMBlitLoops.java:46)
at java.desktop/sun.java2d.xr.XRSurfaceData.initXRSurfaceData(XRSurfaceData.java:106)
at java.desktop/sun.awt.X11GraphicsEnvironment.run(X11GraphicsEnvironment.java:124)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.desktop/sun.awt.X11GraphicsEnvironment.<clinit>(X11GraphicsEnvironment.java:61)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:315)
at java.desktop/java.awt.GraphicsEnvironment$LocalGE.createGE(GraphicsEnvironment.java:101)
at java.desktop/java.awt.GraphicsEnvironment$LocalGE.<clinit>(GraphicsEnvironment.java:83)
at java.desktop/java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment(GraphicsEnvironment.java:129)
at java.desktop/java.awt.Window.initGC(Window.java:487)
at java.desktop/java.awt.Window.init(Window.java:507)
at java.desktop/java.awt.Window.<init>(Window.java:549)
at java.desktop/java.awt.Frame.<init>(Frame.java:423)
at java.desktop/java.awt.Frame.<init>(Frame.java:388)
at java.desktop/javax.swing.JFrame.<init>(JFrame.java:180)
at mypackage.MyMain.main(Unknown Source)
Caused by: javassist.NotFoundException: java.awt.image.RenderedImage
at javassist.ClassPool.get(ClassPool.java:430)
at javassist.bytecode.Descriptor.toCtClass(Descriptor.java:571)
at javassist.bytecode.Descriptor.getParameterTypes(Descriptor.java:424)
at javassist.CtBehavior.getParameterTypes(CtBehavior.java:323)
at javassist.CtBehavior.insertBefore(CtBehavior.java:781)
... 25 more
3
我花了一些时间分析您的代码,发现了几个概念性问题,都在总体主题 bootstrapping 下。简单来说,就是类似于那个老问题:先有鸡还是先有蛋?
您的系统有多个组件:
- Java代理
- class 文件转换器(使用 Javassist)
- 主要 class 做代理 hot-attachment,如有必要。
- 转换目标classes
您没有将它们放在单独的 class 中,而是将所有内容都塞进了一个 class MyMain
中。好吧,transformer 处于静态内部 class,但这并没有改变一般情况。因此,您要做的是启动一个在已经 运行 时自行转换的代理,因为它是自己的转换目标。这是个坏主意。
如果您稍微重构一下意大利面条代码,问题就会消失。抱歉,为了更好的可读性,我忍不住重命名了一些东西,但是转换应用程序 class 和 Java2D class 的两种方法仍然包含很多冗余(重复代码)我没有清理,因为我 运行 没时间了。所以我把它留给你。即使您确切知道目标方法是 non-static,对静态标志的奇怪检查也可能消失,除非您的真实代码更通用并且转换多个方法。为了简单起见,我将它从我的示例代码版本中删除了。
我还建议将 Java 代理 + class 文件转换器放入一个单独的代理 JAR 中,即使如果所有内容都在一个 JAR 中,我的重构版本也可以工作。
还请注意不要在 class 上手动调用 retransformClasses
,这些可能已经被转换,例如在 class-loading.
期间
Class 文件转换器
package mypackage;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
return transformClass(className, classfileBuffer);
}
private byte[] transformClass(String className, byte[] buffer) {
switch (className) {
case "mypackage/MyApplication":
return transformMyApplication(className, buffer);
case "sun/java2d/SunGraphics2D":
return transformSunGraphics2D(className, buffer);
default:
return buffer;
}
}
private byte[] transformMyApplication(String className, byte[] buffer) {
System.out.println(className);
ClassPool cp = ClassPool.getDefault();
String name = className.replace("/", ".");
cp.insertClassPath(new ByteArrayClassPath(name, buffer));
try {
CtClass clazz = cp.get(name);
clazz.defrost();
CtBehavior[] declaredBehaviors = clazz.getDeclaredBehaviors();
for (CtBehavior db : declaredBehaviors) {
if ("hi".equals(db.getName())) {
System.out.println("Transforming hi...");
db.insertBefore("System.out.println(\"Hi!\");");
}
}
return clazz.toBytecode();
}
catch (Throwable e) {
e.printStackTrace();
System.out.println("error");
return buffer;
}
}
private byte[] transformSunGraphics2D(String className, byte[] buffer) {
System.out.println(className);
ClassPool cp = ClassPool.getDefault();
String name = className.replace("/", ".");
cp.insertClassPath(new ByteArrayClassPath(name, buffer));
try {
CtClass clazz = cp.get(name);
CtBehavior[] declaredBehaviors = clazz.getDeclaredBehaviors();
for (CtBehavior db : declaredBehaviors) {
if ("sun.java2d.SunGraphics2D.drawRenderedImage(java.awt.image.RenderedImage,java.awt.geom.AffineTransform)".equals(db.getLongName())) {
System.out.println("Transforming drawRenderedImage...");
db.insertBefore(".setToScale(2.0, 2.0);");
}
}
return clazz.toBytecode();
}
catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
return buffer;
}
}
}
Java代理人:
请注意如何使用 MyAgent.instrumentation
来公开附加代理的信息。稍后我们将看到使用此功能。
package mypackage;
import java.lang.instrument.Instrumentation;
public class MyAgent {
public static Instrumentation instrumentation;
public static void premain(String args, Instrumentation inst) {
System.out.println("premain - start");
instrumentation = inst;
inst.addTransformer(new MyTransformer(), true);
System.out.println("premain - end");
}
public static void agentmain(String args, Instrumentation inst) {
System.out.println("agentmain - start");
premain(args, inst);
System.out.println("agentmain - end");
}
}
主要class做on-demandhot-attachment:
如您所见,我使用 MyAgent.instrumentation
以便 auto-detect 代理是否已连接。因此不再需要为它使用命令行参数。
package mypackage;
import com.sun.tools.attach.VirtualMachine;
import java.lang.instrument.UnmodifiableClassException;
import java.lang.management.ManagementFactory;
public class MyMain {
private static final String AGENT_PATH = "build/libs/SO_Javassist_SystemOutRecognisedAsClass_64340794-1.0-SNAPSHOT.jar";
public static void main(String[] args) {
if (MyAgent.instrumentation == null) {
attachAgent();
// This is only necessary if you want to transform an already loaded class,
// which in this example is not the case
// transform(MyApplication.class, SunGraphics2D.class);
}
MyApplication.main(args);
}
public static void attachAgent() {
System.out.println("Dynamically attaching Java agent - start");
String jvmName = ManagementFactory.getRuntimeMXBean().getName();
String pid = jvmName.substring(0, jvmName.indexOf('@'));
try {
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(AGENT_PATH, null);
vm.detach();
}
catch (Exception e) {
throw new RuntimeException(e);
}
finally {
System.out.println("Dynamically attaching Java agent - end");
}
}
public static void transform(Class<?>... targetClasses) {
try {
MyAgent.instrumentation.retransformClasses(targetClasses);
}
catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
样本目标class:
这只是一个示例 class。在本例中,它包含您要转换的 hi
方法。
package mypackage;
import sun.java2d.SunGraphics2D;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import static javax.swing.WindowConstants.EXIT_ON_CLOSE;
import static sun.java2d.SurfaceData.getPrimarySurfaceData;
public class MyApplication {
public static void main(String[] args) {
System.out.println(new MyApplication().hi());
JFrame jFrame = new JFrame();
jFrame.setDefaultCloseOperation(EXIT_ON_CLOSE);
SunGraphics2D graphics2D = new SunGraphics2D(
getPrimarySurfaceData(new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB)),
Color.BLACK,
Color.WHITE,
Font.getFont("System")
);
graphics2D.drawRenderedImage(
null,
new AffineTransform() {
@Override
public void setToScale(double sx, double sy) {
super.setToScale(sx, sy);
System.out.println("scaled!");
}
}
);
jFrame.setVisible(true);
}
public int hi() {
return 3;
}
}
您还想更新清单文件生成器:
manifest {
attributes(
"Can-Redefine-Classes": true,
"Can-Retransform-Classes": true,
"Premain-Class": "mypackage.MyAgent",
"Agent-Class": "mypackage.MyAgent",
)
}
现在一切都在 Java 11+ 上按预期运行。 Javassist 中不再存在系统 class 路径问题,因为您不再尝试在代理 hot-attached 属于 Attach Listener
线程时转换代理本身=22=]线程组。
XXX> java -cp build/libs/SO_Javassist_SystemOutRecognisedAsClass_64340794-1.0-SNAPSHOT.jar -javaagent:build/libs/SO_Javassist_SystemOutRecognisedAsClass_64340794-1.0-SNAPSHOT.jar mypackage.MyMain
premain - start
premain - end
mypackage/MyApplication
Transforming hi...
Hi!
3
sun/java2d/SunGraphics2D
Transforming drawRenderedImage...
scaled!
XXX> java -cp build/libs/SO_Javassist_SystemOutRecognisedAsClass_64340794-1.0-SNAPSHOT.jar -Djdk.attach.allowAttachSelf=true mypackage.MyMain
Dynamically attaching Java agent - start
agentmain - start
premain - start
premain - end
agentmain - end
Dynamically attaching Java agent - end
mypackage/MyApplication
Transforming hi...
Hi!
3
sun/java2d/SunGraphics2D
Transforming drawRenderedImage...
scaled!
似乎 classes 的动态重新转换仅在 Java 8 上对我有效,但在 Java 11 上无效。在后一种情况下,我从 javassist 那里得到了关于不同的异常没有找到标准 Java classes,例如,我直接引用的那些,甚至是从 method-to-transform 的签名中引用的。
我应该怎么做才能在 Java 11 上解决这个问题?我也想在这里动态转换 classes。
为了便于说明,我创建了一个重现文件。这里我重新改造了两个classes:一个是我自己的,一个是系统的。我创建了 agentmain
和 premain
进行比较。当主要参数传递给应用程序时执行动态变体(我将其仅作为“o”传递)。重新转换后,我调用了两种方法(我自己的 class 和系统的一种)。转换成功后,我会收到额外的日志记录(“hi-”和“scaled!”)。
// MyMain.java
package mypackage;
import com.sun.tools.attach.VirtualMachine;
import javassist.*;
import javassist.bytecode.AccessFlag;
import sun.java2d.SunGraphics2D;
import sun.java2d.SurfaceData;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.lang.management.ManagementFactory;
import java.security.ProtectionDomain;
public class MyMain {
public static void premain(String args, Instrumentation inst) {
System.out.println("premain start");
inst.addTransformer(new MyFormer(), true);
try {
inst.retransformClasses(MyMain.class);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
System.out.println("premain end");
}
public static void agentmain(String args, Instrumentation inst) {
System.out.println("agentmain start");
inst.addTransformer(new MyFormer(), true);
try {
inst.retransformClasses(MyMain.class);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
System.out.println("agentmain end");
}
public static void main(String[] args) {
if (args.length > 0) {
attachToThisVm();
}
Frame f = new JFrame();
f.setVisible(true);
System.out.println(new MyMain().hi());
SunGraphics2D system = new SunGraphics2D(SurfaceData.getPrimarySurfaceData(new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB)), Color.BLACK, Color.WHITE, Font.getFont("System"));
system.drawRenderedImage(null,new AffineTransform() {
@Override
public void setToScale(double sx, double sy) {
super.setToScale(sx, sy);
System.out.println("scaled!");
}
});
}
public static void attachToThisVm() {
System.out.println("dynamically loading javaagent");
String name = ManagementFactory.getRuntimeMXBean().getName();
int p = name.indexOf('@');
String pid = name.substring(0, p);
try {
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent("javaAgentTest-1.0-SNAPSHOT.jar", null);
vm.detach();
}
catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println("dynamically loaded javaagent");
}
public int hi() {
return 3;
}
public static class MyFormer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
return transformClass(className, classfileBuffer);
}
private byte[] transformClass(String className, byte[] buffer) {
if ("mypackage/MyMain".equals(className)) {
System.out.println(className);
ClassPool cp = ClassPool.getDefault();
String name = className.replace("/", ".");
cp.insertClassPath(new ByteArrayClassPath(name, buffer));
try {
CtClass clazz = cp.get(name);
CtBehavior[] declaredBehaviors = clazz.getDeclaredBehaviors();
for (CtBehavior db : declaredBehaviors) {
if ("hi".equals(db.getName())) {
if ((db.getMethodInfo().getAccessFlags() & AccessFlag.STATIC) != 0) {
System.out.println("bad access flags, skipping...");
return buffer;
}
System.out.println("Forming hi...");
db.insertBefore("System.out.print(\"hi-\");"); // crashes on 11, direct usage case, even referencing to java.lang.Object will crash
}
}
return clazz.toBytecode();
} catch (Throwable e) {
e.printStackTrace();
System.out.println("error");
return buffer;
}
}
if ("sun/java2d/SunGraphics2D".equals(className)) {
System.out.println(className);
ClassPool cp = ClassPool.getDefault();
String name = className.replace("/", ".");
cp.insertClassPath(new ByteArrayClassPath(name, buffer));
try {
CtClass clazz = cp.get(name);
CtBehavior[] declaredBehaviors = clazz.getDeclaredBehaviors();
for (CtBehavior db : declaredBehaviors) {
if ("sun.java2d.SunGraphics2D.drawRenderedImage(java.awt.image.RenderedImage,java.awt.geom.AffineTransform)".equals(db.getLongName())) {
if ((db.getMethodInfo().getAccessFlags() & AccessFlag.STATIC) != 0) {
System.out.println("bad access flags, skipping...");
return buffer;
}
System.out.println("Forming drawRenderedImage...");
db.insertBefore(".setToScale(2.0, 2.0);"); // crashes on 11, signature case
}
}
return clazz.toBytecode();
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
return buffer;
}
}
return buffer;
}
}
}
我通过 Gradle:
构建jar
// build.gradle, module name is javaAgentTest
plugins {
id 'java'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
def inline = { deps -> deps.collect { it.isDirectory() ? it : zipTree(it) } }
jar {
manifest {
attributes(
"Can-Redefine-Classes": true,
"Can-Retransform-Classes": true,
"Premain-Class": "mypackage.MyMain",
"Agent-Class": "mypackage.MyMain",
)
}
from {
inline(configurations.runtimeClasspath) // fat jar
}
}
dependencies {
implementation "org.javassist:javassist:3.27.0-GA"
}
在 Java 8 上,静态和动态变体都有效:
$ java -version
openjdk version "1.8.0_265"
OpenJDK Runtime Environment (build 1.8.0_265-8u265-b01-0ubuntu2~20.04-b01)
OpenJDK 64-Bit Server VM (build 25.265-b01, mixed mode)
$ java -cp javaAgentTest-1.0-SNAPSHOT.jar -javaagent:javaAgentTest-1.0-SNAPSHOT.jar mypackage.MyMain
premain start
mypackage/MyMain
Forming hi...
premain end
sun/java2d/SunGraphics2D
Forming drawRenderedImage...
hi-3
scaled!
$ java -cp javaAgentTest-1.0-SNAPSHOT.jar:/usr/lib/jvm/java-8-openjdk-amd64/lib/tools.jar mypackage.MyMain o
dynamically loading javaagent
agentmain start
mypackage/MyMain
Forming hi...
agentmain end
dynamically loaded javaagent
sun/java2d/SunGraphics2D
Forming drawRenderedImage...
hi-3
scaled!
在 Java 11 上,动态变体不起作用(如果没有对 System.out
的引用,它将分叉 hi
方法,例如,只有 db.insertBefore("return 22;");
):
$ java -version
openjdk version "11.0.8" 2020-07-14
OpenJDK Runtime Environment (build 11.0.8+10-post-Ubuntu-0ubuntu120.04)
OpenJDK 64-Bit Server VM (build 11.0.8+10-post-Ubuntu-0ubuntu120.04, mixed mode, sharing)
$ java -cp javaAgentTest-1.0-SNAPSHOT.jar -javaagent:javaAgentTest-1.0-SNAPSHOT.jar mypackage.MyMain
premain start
mypackage/MyMain
Forming hi...
premain end
sun/java2d/SunGraphics2D
Forming drawRenderedImage...
hi-3
scaled!
$ java -cp javaAgentTest-1.0-SNAPSHOT.jar -Djdk.attach.allowAttachSelf=true mypackage.MyMain o
dynamically loading javaagent
agentmain start
mypackage/MyMain
Forming hi...
javassist.CannotCompileException: [source error] no such class: System.out
at javassist.CtBehavior.insertBefore(CtBehavior.java:806)
at javassist.CtBehavior.insertBefore(CtBehavior.java:766)
at mypackage.MyMain$MyFormer.transformClass(MyMain.java:112)
at mypackage.MyMain$MyFormer.transform(MyMain.java:91)
at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:246)
at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:563)
at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:167)
at mypackage.MyMain.agentmain(MyMain.java:38)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:513)
at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain(InstrumentationImpl.java:535)
Caused by: compile error: no such class: System.out
at javassist.compiler.MemberResolver.searchImports(MemberResolver.java:479)
at javassist.compiler.MemberResolver.lookupClass(MemberResolver.java:422)
at javassist.compiler.MemberResolver.lookupClassByJvmName(MemberResolver.java:329)
at javassist.compiler.TypeChecker.atCallExpr(TypeChecker.java:711)
at javassist.compiler.JvstTypeChecker.atCallExpr(JvstTypeChecker.java:170)
at javassist.compiler.ast.CallExpr.accept(CallExpr.java:49)
at javassist.compiler.CodeGen.doTypeCheck(CodeGen.java:266)
at javassist.compiler.CodeGen.atStmnt(CodeGen.java:360)
at javassist.compiler.ast.Stmnt.accept(Stmnt.java:53)
at javassist.compiler.Javac.compileStmnt(Javac.java:578)
at javassist.CtBehavior.insertBefore(CtBehavior.java:786)
... 15 more
error
agentmain end
dynamically loaded javaagent
sun/java2d/SunGraphics2D
Forming drawRenderedImage...
javassist.CannotCompileException: cannot find java.awt.image.RenderedImage
at javassist.CtBehavior.insertBefore(CtBehavior.java:803)
at javassist.CtBehavior.insertBefore(CtBehavior.java:766)
at mypackage.MyMain$MyFormer.transformClass(MyMain.java:140)
at mypackage.MyMain$MyFormer.transform(MyMain.java:91)
at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:246)
at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:563)
at java.desktop/sun.java2d.loops.GraphicsPrimitiveMgr.<clinit>(GraphicsPrimitiveMgr.java:56)
at java.desktop/sun.java2d.loops.Blit.<clinit>(Blit.java:114)
at java.desktop/sun.java2d.xr.XRPMBlitLoops.register(XRPMBlitLoops.java:46)
at java.desktop/sun.java2d.xr.XRSurfaceData.initXRSurfaceData(XRSurfaceData.java:106)
at java.desktop/sun.awt.X11GraphicsEnvironment.run(X11GraphicsEnvironment.java:124)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.desktop/sun.awt.X11GraphicsEnvironment.<clinit>(X11GraphicsEnvironment.java:61)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:315)
at java.desktop/java.awt.GraphicsEnvironment$LocalGE.createGE(GraphicsEnvironment.java:101)
at java.desktop/java.awt.GraphicsEnvironment$LocalGE.<clinit>(GraphicsEnvironment.java:83)
at java.desktop/java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment(GraphicsEnvironment.java:129)
at java.desktop/java.awt.Window.initGC(Window.java:487)
at java.desktop/java.awt.Window.init(Window.java:507)
at java.desktop/java.awt.Window.<init>(Window.java:549)
at java.desktop/java.awt.Frame.<init>(Frame.java:423)
at java.desktop/java.awt.Frame.<init>(Frame.java:388)
at java.desktop/javax.swing.JFrame.<init>(JFrame.java:180)
at mypackage.MyMain.main(Unknown Source)
Caused by: javassist.NotFoundException: java.awt.image.RenderedImage
at javassist.ClassPool.get(ClassPool.java:430)
at javassist.bytecode.Descriptor.toCtClass(Descriptor.java:571)
at javassist.bytecode.Descriptor.getParameterTypes(Descriptor.java:424)
at javassist.CtBehavior.getParameterTypes(CtBehavior.java:323)
at javassist.CtBehavior.insertBefore(CtBehavior.java:781)
... 25 more
3
我花了一些时间分析您的代码,发现了几个概念性问题,都在总体主题 bootstrapping 下。简单来说,就是类似于那个老问题:先有鸡还是先有蛋?
您的系统有多个组件:
- Java代理
- class 文件转换器(使用 Javassist)
- 主要 class 做代理 hot-attachment,如有必要。
- 转换目标classes
您没有将它们放在单独的 class 中,而是将所有内容都塞进了一个 class MyMain
中。好吧,transformer 处于静态内部 class,但这并没有改变一般情况。因此,您要做的是启动一个在已经 运行 时自行转换的代理,因为它是自己的转换目标。这是个坏主意。
如果您稍微重构一下意大利面条代码,问题就会消失。抱歉,为了更好的可读性,我忍不住重命名了一些东西,但是转换应用程序 class 和 Java2D class 的两种方法仍然包含很多冗余(重复代码)我没有清理,因为我 运行 没时间了。所以我把它留给你。即使您确切知道目标方法是 non-static,对静态标志的奇怪检查也可能消失,除非您的真实代码更通用并且转换多个方法。为了简单起见,我将它从我的示例代码版本中删除了。
我还建议将 Java 代理 + class 文件转换器放入一个单独的代理 JAR 中,即使如果所有内容都在一个 JAR 中,我的重构版本也可以工作。
还请注意不要在 class 上手动调用 retransformClasses
,这些可能已经被转换,例如在 class-loading.
Class 文件转换器
package mypackage;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
return transformClass(className, classfileBuffer);
}
private byte[] transformClass(String className, byte[] buffer) {
switch (className) {
case "mypackage/MyApplication":
return transformMyApplication(className, buffer);
case "sun/java2d/SunGraphics2D":
return transformSunGraphics2D(className, buffer);
default:
return buffer;
}
}
private byte[] transformMyApplication(String className, byte[] buffer) {
System.out.println(className);
ClassPool cp = ClassPool.getDefault();
String name = className.replace("/", ".");
cp.insertClassPath(new ByteArrayClassPath(name, buffer));
try {
CtClass clazz = cp.get(name);
clazz.defrost();
CtBehavior[] declaredBehaviors = clazz.getDeclaredBehaviors();
for (CtBehavior db : declaredBehaviors) {
if ("hi".equals(db.getName())) {
System.out.println("Transforming hi...");
db.insertBefore("System.out.println(\"Hi!\");");
}
}
return clazz.toBytecode();
}
catch (Throwable e) {
e.printStackTrace();
System.out.println("error");
return buffer;
}
}
private byte[] transformSunGraphics2D(String className, byte[] buffer) {
System.out.println(className);
ClassPool cp = ClassPool.getDefault();
String name = className.replace("/", ".");
cp.insertClassPath(new ByteArrayClassPath(name, buffer));
try {
CtClass clazz = cp.get(name);
CtBehavior[] declaredBehaviors = clazz.getDeclaredBehaviors();
for (CtBehavior db : declaredBehaviors) {
if ("sun.java2d.SunGraphics2D.drawRenderedImage(java.awt.image.RenderedImage,java.awt.geom.AffineTransform)".equals(db.getLongName())) {
System.out.println("Transforming drawRenderedImage...");
db.insertBefore(".setToScale(2.0, 2.0);");
}
}
return clazz.toBytecode();
}
catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
return buffer;
}
}
}
Java代理人:
请注意如何使用 MyAgent.instrumentation
来公开附加代理的信息。稍后我们将看到使用此功能。
package mypackage;
import java.lang.instrument.Instrumentation;
public class MyAgent {
public static Instrumentation instrumentation;
public static void premain(String args, Instrumentation inst) {
System.out.println("premain - start");
instrumentation = inst;
inst.addTransformer(new MyTransformer(), true);
System.out.println("premain - end");
}
public static void agentmain(String args, Instrumentation inst) {
System.out.println("agentmain - start");
premain(args, inst);
System.out.println("agentmain - end");
}
}
主要class做on-demandhot-attachment:
如您所见,我使用 MyAgent.instrumentation
以便 auto-detect 代理是否已连接。因此不再需要为它使用命令行参数。
package mypackage;
import com.sun.tools.attach.VirtualMachine;
import java.lang.instrument.UnmodifiableClassException;
import java.lang.management.ManagementFactory;
public class MyMain {
private static final String AGENT_PATH = "build/libs/SO_Javassist_SystemOutRecognisedAsClass_64340794-1.0-SNAPSHOT.jar";
public static void main(String[] args) {
if (MyAgent.instrumentation == null) {
attachAgent();
// This is only necessary if you want to transform an already loaded class,
// which in this example is not the case
// transform(MyApplication.class, SunGraphics2D.class);
}
MyApplication.main(args);
}
public static void attachAgent() {
System.out.println("Dynamically attaching Java agent - start");
String jvmName = ManagementFactory.getRuntimeMXBean().getName();
String pid = jvmName.substring(0, jvmName.indexOf('@'));
try {
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(AGENT_PATH, null);
vm.detach();
}
catch (Exception e) {
throw new RuntimeException(e);
}
finally {
System.out.println("Dynamically attaching Java agent - end");
}
}
public static void transform(Class<?>... targetClasses) {
try {
MyAgent.instrumentation.retransformClasses(targetClasses);
}
catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
样本目标class:
这只是一个示例 class。在本例中,它包含您要转换的 hi
方法。
package mypackage;
import sun.java2d.SunGraphics2D;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import static javax.swing.WindowConstants.EXIT_ON_CLOSE;
import static sun.java2d.SurfaceData.getPrimarySurfaceData;
public class MyApplication {
public static void main(String[] args) {
System.out.println(new MyApplication().hi());
JFrame jFrame = new JFrame();
jFrame.setDefaultCloseOperation(EXIT_ON_CLOSE);
SunGraphics2D graphics2D = new SunGraphics2D(
getPrimarySurfaceData(new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB)),
Color.BLACK,
Color.WHITE,
Font.getFont("System")
);
graphics2D.drawRenderedImage(
null,
new AffineTransform() {
@Override
public void setToScale(double sx, double sy) {
super.setToScale(sx, sy);
System.out.println("scaled!");
}
}
);
jFrame.setVisible(true);
}
public int hi() {
return 3;
}
}
您还想更新清单文件生成器:
manifest {
attributes(
"Can-Redefine-Classes": true,
"Can-Retransform-Classes": true,
"Premain-Class": "mypackage.MyAgent",
"Agent-Class": "mypackage.MyAgent",
)
}
现在一切都在 Java 11+ 上按预期运行。 Javassist 中不再存在系统 class 路径问题,因为您不再尝试在代理 hot-attached 属于 Attach Listener
线程时转换代理本身=22=]线程组。
XXX> java -cp build/libs/SO_Javassist_SystemOutRecognisedAsClass_64340794-1.0-SNAPSHOT.jar -javaagent:build/libs/SO_Javassist_SystemOutRecognisedAsClass_64340794-1.0-SNAPSHOT.jar mypackage.MyMain
premain - start
premain - end
mypackage/MyApplication
Transforming hi...
Hi!
3
sun/java2d/SunGraphics2D
Transforming drawRenderedImage...
scaled!
XXX> java -cp build/libs/SO_Javassist_SystemOutRecognisedAsClass_64340794-1.0-SNAPSHOT.jar -Djdk.attach.allowAttachSelf=true mypackage.MyMain
Dynamically attaching Java agent - start
agentmain - start
premain - start
premain - end
agentmain - end
Dynamically attaching Java agent - end
mypackage/MyApplication
Transforming hi...
Hi!
3
sun/java2d/SunGraphics2D
Transforming drawRenderedImage...
scaled!