Java类装入器可以重写系统类的字节码(只有它们的副本)吗?JavaAgent类文件变压器类加载器未解决的问题

所以我有一个类加载器(MyClassLoader),它在内存中维护一组“特殊”类。这些特殊类是动态编译的,并存储在 MyClassLoader 中的字节数组中。当要求 MyClassLoader 输入类时,它首先检查其字典是否包含该类,然后再委派给 System 类装入器。它看起来像这样:specialClasses

class MyClassLoader extends ClassLoader {
    Map<String, byte[]> specialClasses;

    public MyClassLoader(Map<String, byte[]> sb) {
        this.specialClasses = sb;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (specialClasses.containsKey(name)) return findClass(name);
        else return super.loadClass(name);
    }

    @Override
    public Class findClass(String name) {
        byte[] b = specialClasses.get(name);
        return defineClass(name, b, 0, b.length);
    }    
}

如果我想在 上执行转换(例如检测),我只需在调用它之前修改它即可。specialClassesbyte[]defineClass()

我还想转换 System 类加载器提供的类,但 System 类加载器似乎没有提供任何访问它提供的类的原始值的方法,而是直接为我提供对象。byte[]Class

我可以使用一个工具,所有加载到JVM中的类,但这会增加我不想分析的类的开销;我真的只想对 MyClassLoader 加载的类进行检测。-javaagent

  • 有没有办法检索父类装入器提供的类的原始值,以便我可以在定义自己的副本之前检测它们?byte[]
  • 或者,有没有办法模拟System类加载器的功能,就它从哪里获取它而言,以便MyClassLoader可以检测和定义所有System类(Object,String等)的副本?byte[]

编辑:

所以我尝试了另一种方法:

  • 使用 ,捕获加载的每个类,并将其存储在哈希表中,并由类的名称进行键控。-javaagentbyte[]
  • MyClassLoader 不是将系统类委托给其父类装入器,而是使用类名从此哈希表中加载其字节码并定义它

从理论上讲,这将允许MyClassLoader使用检测定义自己的系统类版本。但是,它失败,并带有

java.lang.SecurityException: Prohibited package name: java.lang

显然,JVM 不喜欢我自己定义类,即使它(理论上)应该来自引导加载类的同一来源。寻找解决方案的工作仍在继续。java.langbyte[]

编辑2:

我为这个问题找到了一个(非常粗略的)解决方案,但是如果一个比我更了解Java类加载/工具的复杂性的人可以想出一些不那么粗略的东西,那就太好了。


答案 1

所以我找到了解决这个问题的方法。这不是一个非常优雅的解决方案,它会在代码审查时引起很多愤怒的电子邮件,但它似乎有效。基本要点是:

JavaAgent

使用 a 和 a 来存储对象以供以后使用java.lang.instrumentation-javaagentInstrumentation

class JavaAgent {
    private JavaAgent() {}

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Agent Premain Start");
        Transformer.instrumentation = inst;
        inst.addTransformer(new Transformer(), inst.isRetransformClassesSupported());
    }    
}

类文件变压器

将 a 添加到 仅适用于标记的类。类似的东西TransformerInstrumentation

public class Transformer implements ClassFileTransformer {
    public static Set<Class<?>> transformMe = new Set<>()
    public static Instrumentation instrumentation = null; // set during premain()
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] origBytes) {


        if (transformMe.contains(classBeingRedefined)) {
            return instrument(origBytes, loader);
        } else {
            return null;
        }
    }
    public byte[] instrument(byte[] origBytes) {
        // magic happens here
    }
}

类加载器

在类装入器中,显式标记每个装入的类(甚至是其装入委托给父级的类),方法是在要求转换之前将其放入transformMeInstrumentation

public class MyClassLoader extends ClassLoader{
    public Class<?> instrument(Class<?> in){
        try{
            Transformer.transformMe.add(in);
            Transformer.instrumentation.retransformClasses(in);
            Transformer.transformMe.remove(in);
            return in;
        }catch(Exception e){ return null; }
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return instrument(super.loadClass(name));
    }
}

...瞧!加载的每个类都会被该方法转换,包括所有系统类,如和友,而默认 ClassLoader 加载的所有类都保持不变。MyClassLoaderinstrument()java.lang.Object

我已经使用内存分析方法尝试了这一点,该方法插入回调钩子以跟踪所检测的字节码中的内存分配,并且可以确认这些类在其方法运行时(甚至是系统类)正在触发回调,而“正常”类则没有。instrument()MyClassLoad

胜利!

当然,这是可怕的代码。无处不在的共享可变状态,非局部副作用,全局,你能想象到的一切。可能也不是线程安全的。但它表明这样的事情是可能的,你确实可以有选择地检测类的字节码,甚至是系统类,作为自定义ClassLoader操作的一部分,同时保持程序的“其余部分”不变。

未解决的问题

如果其他人有任何想法如何使这个代码不那么糟糕,我会很高兴听到它。我无法想出一种方法:

  • 通过按需生成一的仪器类,而不是以其他方式加载仪器类InstrumentationretransformClasses()
  • 在每个对象中存储一些元数据,这将允许告诉它是否应该转换,而无需全局可变哈希表查找。Class<?>Transformer
  • 在不使用该方法的情况下转换系统类。如前所述,由于 ClassLoader 中的硬编码检查,任何动态 a 到类中的尝试都会失败.java。Instrumentation.retransformClass()defineClassbyte[]java.lang.*

如果有人能找到解决这些问题的方法,这将使这个问题变得不那么粗略。无论如何,我猜想能够检测(例如,用于分析)一些子系统(即你感兴趣的子系统),同时保持JVM的其余部分不变(没有检测开销)对除我以外的其他人有用,所以这里就是这样。


答案 2

首先,不带 ClassFileTransformer 的解释:

Oracle JRE/JDK 的许可证包括你不能更改 java.* 包,从你在 java.lang 中尝试更改某些内容的测试中显示的内容来看,它们包含了一个测试,如果你尝试的话,它们会引发一个安全异常。

话虽如此,您可以通过编译替代方案并使用 JRE -Xbootclasspath/p CLI 选项引用它来更改系统类的行为。

在查看了通过该方法可以实现的目标之后,我预计您将不得不进行更多工作并编译OpenJDK的自定义版本。我期望这样做,因为Bootstrap类装入器(从我所读到的)是一个本机实现。

请参阅 http://onjava.com/pub/a/onjava/2005/01/26/classloading.html,了解我最喜欢的类加载器概述。

现在使用 ClassFileTransformer:

如您所展示的,您可以更新方法程序(以及预加载类的一些特定的其他方面)。针对您提出的问题:

按需检测:这里重要的是每个加载的类都有一个与之关联的唯一类实例;因此,如果您想以特定的加载类为目标,则必须注意该实例是什么,这可以通过各种方式找到,包括与每个类名关联的成员“类”,例如Object.class。

是线程安全吗:不,两个线程可以同时更改集合,您可以通过多种方式解决此问题;我建议使用 Set 的并发版本。

Globals等:我认为全局变量是特别必要的(我认为你的实现可以做得更好一些),但很可能不会有问题,你以后会学习如何更好地为Java编写代码(我已经编码了大约12年,你不会相信使用该语言的一些微妙的事情)。

类实例中的元数据:在我使用Java的所有时间中,附加元数据并不自然,可能是有充分理由的;为特定目的保留映射是可以的,请记住,它只是指向实例的指针和元数据之间的映射,因此它并不是真正的内存占用。