如何在Java中热交换java.lang类

时间:2017-08-08 11:47:30

标签: java jvm javassist hotswap

我知道这不是一个好的需求。但我现在需要这样做。

我需要替换Class.getResourceAsStream(String)

的方法

但是ClassLoader.preDefineClass(String , ProtectionDomain)检查全名的类名是否以java.开头,如果是,则抛出异常并且不允许加载。

有没有办法绕过运行时安全检查,而不是编译时间

更新

真正的要求是许多旧项目需要迁移到新环境,因为其中一些项目可能已过时,其中一些项目可能无法找到源代码。我需要在新环境中模拟旧环境,以便这些古老的项目能够继续发挥作用。

Upadte(GhostCat)

我尝试了你给出的方式。

ClassLoader Class

public class ClassLoaderTest extends ClassLoader {
    @Override
    public InputStream getResourceAsStream(String name) {
        System.out.println("getResourceAsStream " + name);
        return new ByteArrayInputStream(new byte[]{1, 3, 5, 7, 9});
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        System.out.println("Load class " + name);
        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
        InputStream is = getClass().getResourceAsStream(fileName);

        if (is == null) {
            return super.loadClass(name);
        }

        byte[] b = new byte[0];
        try {
            b = new byte[is.available()];
            is.read(b);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return defineClass(name, b, 0, b.length);
    }
}

调用类

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
    ClassLoaderTest classLoaderTest = new ClassLoaderTest();
    Class clazz = Class.forName("com.Client", true, classLoaderTest);
    Object client = clazz.newInstance();
    Method method = clazz.getDeclaredMethod("method");
    method.invoke(client);
}

客户等级

public class Client {
    public void method() {
        System.out.println(this.getClass().getResourceAsStream("aaa"));
        System.out.println(Class.class.getResourceAsStream("bbb"));
        System.out.println("".getClass().getResourceAsStream("ccc"));
    }
}

它真的适用于this.getClass().getResourceAsStream(String),但Class.class.getResourceAsStream(String) / "".getClass().getResourceAsStream(String)。因为我无法使用我的自定义ClassLoader加载类  名称以java.

开头

3 个答案:

答案 0 :(得分:3)

你做不到。

此类由bootstrap类加载器加载。你无法改变这个人正在做的事情 - 它是用本地代码编写的。换句话说:这是JVM的核心 - 你不能篡改它。

这就是java安全概念的“整体思路”:用户无法“进入”JVM平台保证的事物。

鉴于OP的编辑,这是关于遗留代码的:似乎你想要挂钩Class.getResourceAsStream(String)不是 JVM的一部分 - 但属于您的拥有代码。

如果是这种情况,则“答案”就是:您必须确保所有 类都由您的类加载器加载。 getResourceAsStreeam()的javadoc明确指出:

  

查找具有给定名称的资源。搜索与给定类关联的资源的规则由类的定义类加载器实现。此方法委托给该对象的类加载器。

因此:学习如何使用自定义类加载器。例如,以here开头。

答案 1 :(得分:2)

尝试定义具有以java.开头的限定名称的类的异常是the contract of defineClass的一部分,但这是无关紧要的。任何在ClassLoader上定义具有现有类名称的类的尝试只能有两种可能的结果之一:

  1. 如果现有类已由同一ClassLoader定义,则尝试再次定义它将被拒绝并抛出异常。 defineClass不会更改现有的类,这根本不在其功能范围内。

  2. 如果现有的类已由不同的ClassLoader(或引导加载程序)加载,则可以在此ClassLoader上定义具有相同限定名称的类,但它将是一个完全不同的类,与具有相同名称但不同的定义类加载器的类无关。

  3. 您无法为java.*类定义具有相同名称和不同加载器的类,这只是一个额外的限制,但无论如何您的尝试都无法正常工作。

    如果要在加载后替换类ClassLoader,则只能使用代理,例如使用Instrumentation API的Java代理或调试器。或者,您可以使用-Xbootclasspath/p:…命令行选项将自己的版本添加到引导类路径中。

    当然,将其替换为特定版本仅适用于从中派生此版本的环境。此类应用程序也不允许公开部署。

    更通用的解决方案是使用当前版本ClassLoader的Instrumentation Agent并在运行中对其进行修改,但是,如果您必须选择开发字节码转换代理,那么它会很多更简单,更可靠地转换应用程序代码,只需用调用自定义方法替换Class.getResourceAsStream(String)的所有调用,而无需触及不需要这样调整的代码。

答案 2 :(得分:1)

我在这里写了一个关于如何实现运行时类转换的长篇答案:how to retransform a class at runtime

更简单的方法是在https://github.com/nickman/retransformer重新构建项目,它将转换java。*类,包括本机方法。 (我用它来修改Runtime.availableProcessors)