java - Java Classloader 能否重写系统类(仅它们的副本)的字节码?

标签 java class metaprogramming classloader bytecode

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

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()在上面。

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

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

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

编辑:

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

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

理论上,这会让 MyClassLoader 定义它自己的系统类版本,并带有检测。但是,它失败了

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

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

编辑2:

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

最佳答案

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

Java代理

使用 java.lang.instrumentation和一个 -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
    }
}

类加载器

在类加载器中,显式标记每个加载的类(甚至是加载委托(delegate)给父类的类),方法是将其放置在 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() 转化方法,包括所有系统类,如 java.lang.Object和 friend ,而所有由默认 ClassLoader 加载的类都保持不变。

我已经尝试使用内存分析 instrument()方法,它插入回调 Hook 以跟踪检测字节码中的内存分配,并可以确认 MyClassLoad类在它们的方法运行时触发回调(甚至系统类),而“普通”类则没有。

胜利!

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

开放问题

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

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

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

关于java - Java Classloader 能否重写系统类(仅它们的副本)的字节码?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13032918/

相关文章:

具有单例用法的 JavaScript 命名空间?我难住了!

java - 如何在 Google Chrome 浏览器中使用 Java

java - 如何接收电话事件

java - 什么是NullPointerException,我该如何解决?

java - Android TextView 计时器

java - 引用匿名类?

Ruby Case 类检查

julia - 获取文件/复杂代码的整个 CAST

c++ - 如何确定编译器对元程序做了什么? (对于 boost.proto)

ruby - 在 ruby​​ 中加载/卸载/更新类