14

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

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

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

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

我可以使用-javaagent加载到 JVM 中的所有类的仪器,但这会增加我不想仪器的类的开销;我只希望对 MyClassLoader 加载的类进行检测。

  • 有没有办法检索byte[]父类加载器提供的原始类,所以我可以在定义自己的副本之前对它们进行检测?
  • 或者,是否有任何方法可以模拟 System 类加载器的功能,就它从哪里抓取它而言byte[],以便 MyClassLoader 可以检测和定义它自己的所有 System 类(对象、字符串等)的副本?

编辑:

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

  • 使用 a -javaagent,捕获byte[]每个加载的类并将其存储在哈希表中,以类的名称为键。
  • MyClassLoader 不是将系统类委托给它的父类加载器,而是使用类名从这个哈希表加载它们的字节码并定义它

从理论上讲,这将让 MyClassLoader 使用仪器定义自己的系统类版本。但是,它失败了

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

很明显,JVM 不喜欢我java.lang自己定义类,即使它(理论上)应该来自与byte[]引导加载的类应该来自同一个来源。继续寻找解决方案。

编辑2:

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

4

4 回答 4

11

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

Java代理

使用 ajava.lang.instrumentation和 a-javaagent存储Instrumentation对象以供以后使用

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

类文件转换器

添加一个仅适用于标记类的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
    }
}

类加载器

transformMe在类加载器中,通过在要求Instrumentation转换它之前将每个加载的类(甚至是其加载委托给父类的类)显式标记

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

……瞧!由该方法加载的每个类MyClassLoader都会被该instrument()方法转换,包括所有系统类likejava.lang.Object和friends,而默认ClassLoader加载的所有类都保持不变。

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

胜利!

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

未解决的问题

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

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

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

于 2012-10-25T01:38:10.667 回答
6

首先是没有 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 等:我认为 globals 是特别必要的(我认为你的实现可以做得更好一点),但很可能不会有问题,你稍后会学习如何更好地为 Java 编码(我已经为大约 12 年了,你不会相信使用这种语言的一些微妙的事情)。

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

于 2012-10-27T17:10:35.620 回答
2

类加载器不提供公共 API 来访问已加载类的字节码。字节码很可能被 VM 缓存在某处(在 Oracle VM 中,这是在本机代码中完成的),但您不能再将它作为字节数组取出。

但是,您可以做的是将类文件作为资源重新读取。除非我忘记了一些明显的事情,否则 ClassLoader#getResource() 或 Class#getResource() 应该使用相同的搜索路径来加载类文件,因为它用于加载资源:

public byte[] getClassFile(Class<?> clazz) throws IOException {     
    InputStream is =
        clazz.getResourceAsStream(
            "/" + clazz.getName().replace('.', '/') + ".class");
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    int r = 0;
    byte[] buffer = new byte[8192];
    while((r=is.read(buffer))>=0) {
        baos.write(buffer, 0, r);
    }   
    return baos.toByteArray();
}
于 2012-10-24T13:57:27.433 回答
0

你想重新定义 Object.class,但是这个类在任何程序运行之前就已经被加载了,包括你的类加载器。即使您创建自己的 Object.class,它也会与已经加载的系统类发生冲突,这会造成混乱。

我看到的唯一方法是离线检测系统类,即获取 rt.jar,检测其中的所有类并写回。

于 2012-10-23T15:14:03.373 回答