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:一个是我自己的,一个是系统的。我创建了 agentmainpremain 进行比较。当主要参数传递给应用程序时执行动态变体(我将其仅作为“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!