【问题标题】:Extending class with only private constructors仅使用私有构造函数扩展类
【发布时间】:2013-10-21 10:36:45
【问题描述】:

问题是:我有一个只有私有构造函数可用的类(我不能修改它的源代码),我需要扩展它。

由于反射允许我们在需要时创建此类的实例(通过获取构造函数并调用 newInstance()),有没有办法创建此类的扩展版本的实例(我的意思是,真的任何方式,即使它是针对 OOP 的)?

我知道,这是一个不好的做法,但看起来我别无选择:我需要拦截对一个类的一些调用(它是一个单例,它不是接口实现,所以动态代理在这里不起作用) .

最小示例(根据要求):

public class Singleton {
static private Singleton instance;

private Singleton() {
}

public static Singleton getFactory() {
    if (instance == null)
        instance = new Singleton();
    return instance;
}

public void doWork(String arg) {
    System.out.println(arg);
}}

我想做的就是构建自己的包装器(比如这个)

class Extension extends Singleton {
@Override
public void doWork(String arg) {
    super.doWork("Processed: " + arg);
}}

并使用反射将其注入工厂:

Singleton.class.getField("instance").set(null, new Extension());

但我看不到任何构造此类对象的方法,因为它的超类的构造函数是私有的。问题是“这有可能吗”。

【问题讨论】:

  • 您是否需要扩展它以用于测试目的或生产代码?
  • 这是生产代码:全局问题是与 Eclipse CDT 插件交互并拦截它的所有编译器调用(更有可能是所有 exec(),甚至是从本机代码调用的那些)。没有标准的方法可以做到这一点,看起来最好的解决方案是覆盖它唯一的弱点:ProcessFactory。
  • 那为什么不反编译编译好的类呢?!
  • 因为最终用户将拥有一个带有私有构造函数的此类版本,而我对此无能为力。
  • 实际上一种仅使用私有构造函数来扩展类的方法,但这只能由内部类完成。

标签: java reflection


【解决方案1】:

@René Link 的解决方案足够好,但在我的情况下不是这样:我写道我正在破解一个 Eclipse IDE 插件,这意味着我们在 OSGi 下工作,这意味着我们无法控制类路径解析顺序(它会在我们的包中加载我们的“被黑”类,在另一个包中加载普通的受害者类,它会使用不同的类加载器来执行此操作,然后我们会遇到将这些对象转换为另一个的问题)。可能 OSGi 有一些工具可以解决这个问题,但我不太了解,也没有找到这方面的信息。

所以我们发明了另一种解决方案。它比上一个更糟糕,但至少它在我们的案例中有效(因此更灵活)。

解决方案很简单:javaagent。它是一个标准工具,允许在加载字节码时对其进行操作。所以任务通过使用它和java ASM库解决了:修改了受害者的字节码,使其构造函数公开,剩下的很容易。

    public class MyAgent {
        public static void premain(String agentArguments, Instrumentation instrumentation) {
            instrumentation.addTransformer(new ClassFileTransformer() {

                @Override
                public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
                    throws IllegalClassFormatException {
                    if (className.equals("org/victim/PrivateClass")) { //name of class you want to modify
                        try {
                            ClassReader cr = new ClassReader(classfileBuffer);
                            ClassNode cn = new ClassNode();
                            cr.accept(cn, 0);

                            for (Object methodInst : cn.methods) {
                                MethodNode method = (MethodNode) methodInst;
                                if (method.name.equals("<init>") && method.desc.equals("()V")) { //we get constructor with no arguments, you can filter whatever you want
                                    method.access &= ~Opcodes.ACC_PRIVATE;
                                    method.access |= Opcodes.ACC_PUBLIC; //removed "private" flag, set "public" flag
                                }
                            }
                            ClassWriter result = new ClassWriter(0);
                            cn.accept(result);
                            return result.toByteArray();
                        } catch (Throwable e) {
                            return null; //or you can somehow log failure here
                        }
                    }
                    return null;
                }
            });
        }
    }

接下来,必须使用 JVM 标志激活这个 javaagent,然后一切正常:现在您可以拥有可以毫无问题地调用 super() 构造函数的子类。否则这会炸掉你的整条腿。

【讨论】:

  • 我了解 javaagent 如何允许您在运行时将私有构造函数更改为公共,但是您如何设法诱使 Java 认为提供构造函数在编译期间是公共的?我看不出 javaagent 在编译期间如何提供帮助。
  • @Andrei,抱歉回答迟了。由于在库中有一个带有私有构造函数的类时会发生这种情况,因此您可以创建该库的修改版本 - 通过获取原始库并将私有构造函数替换为人工创建的类文件,其中构造函数具有相同的签名,但实际上是公开的 - 然后在编译阶段使用这个修改后的库。
  • 感谢您的回复。我最终在编译之前添加了一个步骤(幸好在 Gradle 中非常简单),它会自动检测原始类并将检测的类版本放入原始库之前的编译类路径中。
【解决方案2】:

如果

有可能(但不好的hack
  • 您拥有带有私有构造函数的类的源代码,或者您可以从字节码重构它
  • 类由应用程序类加载器加载
  • 你可以修改jvm的类路径

您可以创建一个与原始类二进制兼容的补丁。

我将在下一节中调用您要扩展 PrivateConstructorClass 的类。

  1. 获取PrivateConstructorClass 的源代码并将其复制到源文件中。包名和类名不得更改。
  2. PrivateConstructorClass 的构造函数从私有更改为受保护。
  3. 重新编译修改后的PrivateConstructorClass源文件。
  4. 将编译好的类文件打包成一个jar包。例如。称为“patch.jar”
  5. 创建一个扩展第一个类的类,并针对 patch.jar 中的类进行编译
  6. 更改 jvm 的类路径,使 patch.jar 成为类路径中的第一个条目。

现在一些示例代码可以让您检查它是如何工作的:

期望以下文件夹结构

+-- workspace
  +- private
  +- patch
  +- client

private 文件夹中创建PrivateConstructor

public class PrivateConstructor {


    private String test;

    private PrivateConstructor(String test){
        this.test = test;
    }

    @Override
    public String toString() {
        return test;
    }
}

private文件夹中打开命令提示符,编译打包。

$ javac PrivateConstructor.java
$ jar cvf private.jar PrivateConstructor.class

现在在patch 文件夹中创建补丁文件:

    public class PrivateConstructor {


    private String test;

    protected PrivateConstructor(String test){
        this.test = test;
    }

    @Override
    public String toString() {
        return test;
    }
}

编译打包

$ javac PrivateConstructor.java
$ jar cvf patch.jar PrivateConstructor.class

现在是有趣的部分。

在客户端文件夹中创建一个扩展 PrivateConstructor 的类。

public class ExtendedPrivateConstructor extends PrivateConstructor {


    public ExtendedPrivateConstructor(String test){
        super(test);
    }
}

和一个主要的类来测试它

public class Main {

    public static void main(String str[])  {
       PrivateConstructor privateConstructor = new ExtendedPrivateConstructor("Gotcha");
       System.out.println(privateConstructor);
    }
}

现在针对patch.jar 编译client 文件夹的源文件

 $ javac -cp ..\patch\patch.jar ExtendedPrivateConstructor.java Main.java

现在用类路径上的两个 jar 运行它,看看会发生什么。

如果patch.jar 出现在private.jar 之前,则PrivateConstructor 类将从patch.jar, 加载,因为应用程序类加载器是URLClassLoader

 $ java -cp .;..\patch\patch.jar;..\private\private.jar  Main // This works
 $ java -cp .;..\private\private.jar;..\patch\patch.jar  Main // This will fail

【讨论】:

    【解决方案3】:

    编辑:这显然不适用于编辑到上述问题中的新发布的代码示例,但如果它对其他人有帮助,我会将答案保留在这里以供将来的后代使用。


    根据您的情况,一种可能有效也可能无效的方法是使用Delegation pattern。例如:

    public class PrivateClass {
        private PrivateClass instance = new PrivateClass();
    
        private PrivateClass() {/*You can't subclass me!*/
    
        public static PrivateClass getInstance() { return instance; }
        public void doSomething() {}
    }
    
    public class WrapperClass {
        private PrivateClass privateInstance = PrivateClass.getInstance();
        public void doSomething() {
             //your additional logic here
             privateInstance.doSomething();
        }
    }
    

    您现在有一个类WrapperClass,它具有与 PrivateClass 相同的 API,但将所有功能委托给 PrivateClass(在完成一些前期或后期工作之后)。显然,WrapperClassPrivateClass 的类型层次结构无关,但可以设置为 PrivateClass 可以做的所有事情。

    【讨论】:

    • 抱歉,这个类不会是目标类的扩展版本。全局任务是在单例中伪造一个实例字段(使用反射),由于 IllegalArgumentException,您的解决方案不适用于这种情况
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-29
    • 2016-10-22
    • 2013-11-15
    相关资源
    最近更新 更多