类重新转换不适用于 Java 11 上的动态代理



类的动态重新转换似乎只适用于Java 8,而不适用于Java 11。在后一种情况下,我从javassist获得了关于不同的未找到的标准Java类的异常,例如,我直接引用的类,甚至是要转换的方法的签名。

我应该怎么做才能在Java 11上解决这个问题?我也想在这里动态地转换类。

为了便于说明,我创建了一个repro文件。在这里,我重新转换了两个类:一个是我自己的,另一个是系统。我创建了agentmainpremain进行比较。当一个主参数被传递到应用程序时,动态变量就会被执行(我把它作为"o"传递(。在重新转换后,我调用了两个方法(属于我自己的类和系统的一个(。当转换成功时,我会收到额外的日志记录("hi-"one_answers"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("$2.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!

在Java11上,动态变体不起作用(如果没有引用System.out,例如,只有db.insertBefore("return 22;");,它将派生为hi方法(:

$ 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$1.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

我花了一些时间分析您的代码,发现了一些概念性问题,这些问题都在总体主题引导下。简单地说,这类似于老问题:首先是什么,鸡还是蛋?

您的系统有多个组件:

  • Java代理
  • 类文件转换器(使用Javassist(
  • 主课做代理热连接,如有必要
  • 转换目标类

您没有将它们放在单独的类中,而是将所有内容都塞进一个单独的类MyMain中。好的,transformer在一个静态的内部类中,但这并不能改变一般情况。因此,您要做的是启动一个代理,它在已经运行的情况下对自己进行转换,因为它是自己的转换目标。这是个坏主意。

如果你只是稍微重构一下你的意大利面条代码,问题就会消失。很抱歉,为了更好的可读性,我忍不住重命名了一些东西,但转换应用程序类和Java2D类的两个方法仍然包含大量冗余(重复代码(,我没有清理这些冗余,因为时间不够了。所以我把这件事留给你。即使你确切地知道你的目标方法是非静态的,对静态标志的奇怪检查也可能会消失,除非你的真实代码更通用并转换多个方法。为了简单起见,我从您的示例代码版本中删除了它。

我还建议将Java代理+类文件转换器放入一个单独的代理JAR中,即使如果所有内容都在一个JAR中,我的重构版本也可以工作。

还请注意不要在可能已经转换的类上手动调用retransformClasses,例如在类加载期间。

类文件转换器

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("$2.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");
}
}

执行按需热连接的主要类:

正如您所看到的,我使用MyAgent.instrumentation来自动检测代理是否已经连接。因此不再需要使用命令行参数

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();
}
}
}

样本目标类别:

这只是一个示例类。在这种情况下,它包含要转换的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中不再存在系统类路径问题,因为在属于system线程组的Attach Listener线程中热附加代理时,您不再尝试转换代理本身。

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!

最新更新