只有私有构造函数的类如何扩展?

12
问题是:我有一个只有私有构造函数的类(而且我无法修改它的源代码),但我需要扩展它。
由于反射允许我们在需要时创建这些类的实例(通过获取构造函数并调用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());

但是我没有看到任何构建这样对象的方式,因为其超类的构造函数是私有的。问题是“这是否可能。”


你是需要为测试目的还是生产代码而进行扩展? - René Link
这是生产代码:全局问题是与Eclipse CDT插件交互并拦截所有编译器调用(更可能是所有exec(),甚至是从本地代码调用的那些)。没有标准的方法来解决这个问题,看起来最好的解决方案是覆盖它唯一的弱点:ProcessFactory。 - infthi
那你为什么不对编译后的类进行反编译呢?! - user2511414
由于最终用户将拥有一个带有私有构造函数的类的版本,而我无法对其进行任何操作。 - infthi
实际上,有一种方法可以使用仅具有私有构造函数的类进行扩展,但这只能通过内部类完成。 - Mateusz Dymczyk
你能否发布一个最小的代码示例,展示你想要子类化的类以及在子类中想要实现的功能。基本上是一个SSCCE - Bohemian
3个回答

8

如果您有类的源代码,或者可以从字节码重新构建它;该类是由应用程序类加载器加载的;您可以修改jvm的类路径,则可能(但是很差的黑客):

  • 创建与原始类二进制兼容的补丁;

我将在以下部分中称要扩展的类为 "PrivateConstructorClass"。

  1. 获取 PrivateConstructorClass 的源代码并将其复制到一个源文件中。包名和类名不能更改。
  2. 将 PrivateConstructorClass 的构造函数从 private 更改为 protected。
  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);
    }
}

现在需要将client文件夹中的源文件编译成patch.jar
 $ javac -cp ..\patch\patch.jar ExtendedPrivateConstructor.java Main.java

现在将两个jar包都放到类路径中运行,看看会发生什么。

如果patch.jarprivate.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

5
@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 LED
@Andrei,抱歉回复晚了。由于当您在库中有一个私有构造函数的类时会出现这种情况,因此您可以创建一个修改过的版本的该库 - 通过获取原始库并将具有私有构造函数的类替换为人工创建的类文件,其中构造函数具有相同的签名但实际上是公共的 - 然后在编译阶段使用此修改后的库。 - infthi
感谢您的回复。最终我在编译之前添加了一个步骤(幸运的是,在Gradle中非常容易),它会自动对原始类进行仪器化,并将仪器化的类版本放入编译类路径中,以便在原始库之前使用。 - Andrei LED

0

编辑:这显然不适用于上面编辑到问题中的新发布的代码示例,但我将保留这个答案,以备日后有人需要时使用。


对于您来说,可能可行也可能不可行的一种方法是使用委托模式。例如:

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的所有操作。

3
抱歉,但这个类不会是目标类的扩展版本。全局任务是使用反射在单例中伪造一个实例字段,而您的解决方案由于参数异常无法适用于此情况。 - infthi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接