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

时间:2013-10-21 10:36:46

标签: java reflection

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

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

并使用反射将其注入Factory:

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

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

3 个答案:

答案 0 :(得分:7)

如果

,有可能(但是糟糕的黑客
  • 您拥有私有构造函数的类的源代码,或者您可以从字节码重构它
  • 该类由应用程序类加载器
  • 加载
  • 你可以修改jvm的classpath

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

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

  1. 获取PrivateConstructorClass的源代码并将其复制到源文件中。不得更改包名和类名。
  2. PrivateConstructorClass的构造函数从private更改为protected。
  3. 重新编译PrivateConstructorClass
  4. 的修改后的源文件
  5. 将已编译的类文件打包到jar存档中。例如。叫做“patch.jar”
  6. 创建一个扩展第一个类的类,并针对patch.jar中的类进行编译。
  7. 更改jvm的类路径,以便patch.jar是类路径中的第一个条目。
  8. 现在有一些示例代码可以让您检查它是如何工作的:

    期待以下文件夹结构

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

    PrivateConstructor文件夹

    中创建private课程
    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.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
    

答案 1 :(得分:3)

@RenéLink的解决方案已经足够了,但不是我的情况:我写的是我正在攻击Eclipse IDE插件,这意味着我们在OSGi下工作,这意味着我们无法控制类路径解析顺序(它将在我们的bundle中加载我们的“hacked”类,在另一个bundle中加载vanilla受害者类,并且它将使用不同的类加载器来执行此操作,然后我们会遇到将这些对象一个接一个地转换的问题)。可能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()构造函数而没有任何问题的子类。或者这可能会让你的整条腿受伤。

答案 2 :(得分:0)

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


根据您的具体情况,您可以使用或不使用的一种方法是使用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所能做的所有事情。