仅使用私有构造函数扩展类



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

由于反射允许我们随时创建此类类的实例(通过获取构造函数并调用 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());

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

这是可能的(但一个糟糕的黑客),如果

  • 您拥有带有私有构造函数的类的源代码,或者您可以从字节码重新构造它
  • 类由应用程序类装入器装入
  • 您可以修改 JVM 的类路径

您可以创建与原始类二进制兼容的修补程序。

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

  1. 获取PrivateConstructorClass的源代码并将其复制到源文件中。不得更改包和类名。
  2. PrivateConstructorClass的构造函数从专用更改为受保护。
  3. 重新编译修改后的源文件 PrivateConstructorClass
  4. 将编译好的类文件打包到jar存档中。 例如,称为"补丁.jar"
  5. 创建一个扩展第一个类的类,并根据修补程序中的类编译它.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 ..patchpatch.jar ExtendedPrivateConstructor.java Main.java

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

如果patch.jarprivate.jar之前,则从patch.jar,装入PrivateConstructor类,因为应用程序类装入器是URLClassLoader

 $ java -cp .;..patchpatch.jar;..privateprivate.jar  Main // This works
 $ java -cp .;..privateprivate.jar;..patchpatch.jar  Main // This will fail

@René Link 的解决方案已经足够好了,但就我而言并非如此:我写道我正在破解一个 Eclipse IDE 插件,这意味着我们正在 OSGi 下工作,这意味着我们无法控制类路径解析顺序(它将在我们的捆绑包中加载我们的"hacked"类,并在另一个捆绑包中加载香草受害者类,它将使用不同的类加载器执行此操作, 然后我们将在将这些对象一个转换为另一个对象时遇到问题)。可能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() 构造函数的子类。或者这会炸掉你的整条腿。

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


根据您的

情况,您可以使用的一种方法可能是使用委派模式,这种方法可能有效,也可能无效。 例如:

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可以执行的所有操作。

最新更新