【问题标题】:override java final methods via reflection or other means?通过反射或其他方式覆盖java final 方法?
【发布时间】:2010-10-19 09:37:34
【问题描述】:

在尝试编写测试用例时会出现这个问题。 Foo 是框架库中的一个类,我没有源代码访问权限。

public class Foo{
  public final Object getX(){
  ...
  }
}

我的申请将

public class Bar extends Foo{
  public int process(){
    Object value = getX();
    ...
  }
}

单元测试用例无法初始化,因为由于其他依赖关系,我无法创建 Foo 对象。 BarTest 抛出一个空指针,因为值为空。

public class BarTest extends TestCase{
  public testProcess(){
    Bar bar = new Bar();        
    int result = bar.process();
    ...
  }
}

有没有办法我可以使用反射 api 将 getX() 设置为非最终的?或者我应该如何进行测试?

【问题讨论】:

    标签: java reflection methods final


    【解决方案1】:

    因为这是 google 中“覆盖最终方法 java”的最佳结果之一。我想我会留下我的解决方案。这个类展示了一个使用示例“Bagel”类和一个免费使用的 javassist 库的简单解决方案:

    /**
     * This class shows how you can override a final method of a super class using the Javassist's bytecode toolkit
     * The library can be found here: http://jboss-javassist.github.io/javassist/
     * 
     * The basic idea is that you get the super class and reset the modifiers so the modifiers of the method don't include final.
     * Then you add in a new method to the sub class which overrides the now non final method of the super class.
     * 
     * The only "catch" is you have to do the class manipulation before any calls to the class happen in your code. So put the
     * manipulation as early in your code as you can otherwise you will get exceptions.
     */
    
    package packagename;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    import javassist.CtNewMethod;
    import javassist.Modifier;
    
    /** 
     * A simple class to show how to use the library
     */
    public class TestCt {
    
        /** 
         * The starting point for the application
         */
        public static void main(String[] args) {
    
            // in order for us to override the final method we must manipulate the class using the Javassist library.
            // we need to do this FIRST because once we initialize the class it will no longer be editable.
            try
            {
                // get the super class
                CtClass bagel = ClassPool.getDefault().get("packagename.TestCt$Bagel");
    
                // get the method you want to override
                CtMethod originalMethod = bagel.getDeclaredMethod("getDescription");
    
                // set the modifier. This will remove the 'final' modifier from the method.
                // If for whatever reason you needed more than one modifier just add them together
                originalMethod.setModifiers(Modifier.PUBLIC);
    
                // save the changes to the super class
                bagel.toClass();
    
                // get the subclass
                CtClass bagelsolver = ClassPool.getDefault().get("packagename.TestCt$BagelWithOptions");
    
                // create the method that will override the super class's method and include the options in the output
                CtMethod overrideMethod = CtNewMethod.make("public String getDescription() { return super.getDescription() + \" with \" + getOptions(); }", bagelsolver);
    
                // add the new method to the sub class
                bagelsolver.addMethod(overrideMethod);
    
                // save the changes to the sub class
                bagelsolver.toClass();
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
    
            // now that we have edited the classes with the new methods, we can create an instance and see if it worked
    
            // create a new instance of BagelWithOptions
            BagelWithOptions myBagel = new BagelWithOptions();
    
            // give it some options
            myBagel.setOptions("cheese, bacon and eggs");
    
            // print the description of the bagel to the console.
            // This should now use our new code when calling getDescription() which will include the options in the output.
            System.out.println("My bagel is: " + myBagel.getDescription());
    
            // The output should be:
            // **My bagel is: a plain bagel with cheese, bacon and eggs**
        }
    
        /**
         * A plain bagel class which has a final method which we want to override
         */
        public static class Bagel {
    
            /**
             * return a description for this bagel
             */
            public final String getDescription() {
                return "a plain bagel";
            }
        }
    
        /**
         * A sub class of bagel which adds some extra options for the bagel.
         */
        public static class BagelWithOptions extends Bagel {
    
            /**
             * A string that will contain any extra options for the bagel
             */
            String  options;
    
            /**
             * Initiate the bagel with no extra options
             */
            public BagelWithOptions() {
                options = "nothing else";
            }
    
            /**
             * Set the options for the bagel
             * @param options - a string with the new options for this bagel
             */
            public void setOptions(String options) {
                this.options = options;
            }
    
            /**
             * return the current options for this bagel
             */
            public String getOptions() {
                return options;
            }
        }
    }
    

    【讨论】:

    • 你是如何让它工作的?这些类已经定义,并在 ClassLoader 中加载。每次我尝试在已经存在的类上简单地 toClass() 时,都会收到如下错误:Caused by: java.lang.LinkageError: loader (instance of sun/misc/Launcher$AppClassLoader): attempted duplicate class definition for name:"com/package/ClassName"
    • @XaeroDegreaz 我做了一个JDoodle。仅当您将 JDK 更改为 1.8 时才有效。
    • 注意:使用最新版本的 javassist 可能适用于 JDK 9 或 10。我并没有真正尝试让它在这些环境中工作。
    • 添加新的百吉饼();到 main 的开头,这种方法就失败了。总体来说还是不错的尝试 +1
    • @AmirAfghani 我在类的顶部评论中解释了为什么会发生这种情况:The only "catch" is you have to do the class manipulation before any calls to the class happen in your code. So put the manipulation as early in your code as you can otherwise you will get exceptions. 所以是的,如果你在进行类操作之前实例化类,它会爆炸,因为 JVM 已经加载班上。如果你想让它工作,你必须首先进行类操作。
    【解决方案2】:
    public class Bar extends Foo{
      public int process(){
        Object value = getX();
        return process2(value);
      }
      public int process2(Object value){
      ...
      }
    }
    
    public class BarTest extends TestCase{
      public testProcess(){
        Bar bar = new Bar();   
        Mockobj mo = new Mockobj();     
        int result = bar.process2(mo);
        ...
      }
    }
    

    我最终所做的就是上述内容。有点难看……詹姆斯的解决方案肯定比这个好很多……

    【讨论】:

      【解决方案3】:

      如果getX() 返回的变量不是final,您可以使用What’s the best way of unit testing private methods? 中介绍的技术通过Reflection 更改private 变量的值。

      【讨论】:

        【解决方案4】:

        您可以创建另一个可以在测试中覆盖的方法:

        public class Bar extends Foo {
          protected Object doGetX() {
            return getX();
          }
          public int process(){
            Object value = doGetX();
            ...
          }
        }
        

        那么,您可以在 BarTest 中覆盖 doGetX。

        【讨论】:

          【解决方案5】:

          Seb 是正确的,只是为了确保您得到问题的答案,没有在本机代码中做某事(我很确定这不会起作用)或在运行时修改类的字节码,并创建在运行时覆盖该方法的类,我看不到改变方法“最终性”的方法。反射在这里对你没有帮助。

          【讨论】:

          • 你指的是面向切面的又名 AOP 吗?知道 AOP 是否可以将访问权限更改为非最终版本?
          • 我不是真的意思 AOP,不确定它是否能做到,我想更多的是沿着jakarta.apache.org/bcel 的思路。但是,如果某些事情很难做到,那可能是有原因的......而且通常不应该做困难的事情:-)
          【解决方案6】:

          如果您的单元测试用例由于其他依赖关系而无法创建 Foo,这可能表明您一开始就没有正确地进行单元测试。

          单元测试旨在在生产代码运行的相同环境下进行测试,因此我建议在您的测试中重新创建相同的生产环境。否则,您的测试将无法完成。

          【讨论】:

          • 这不是单元测试的用途。单元测试适用于极其精细的代码单元。您所描述的内容听起来更像是集成测试,或者可能是功能测试。
          • 不一定;我不知道他工作的具体情况(即其他类、DB 等),但是您可以一次对许多类进行某些测试 - 例如,如果您使用的是外部库,它是可以假设它工作正常并在您的测试中使用它的所有类。
          • 是的,有“某些测试可以同时对多个课程进行”。这些不是单元测试。仍然有用且重要,但它们不是单元测试。
          • “单位”、“烟雾”、“积分”、“黑盒”、“白盒”等都是对原问题的干扰。开发人员想弄清楚他们是否可以用替代方法代替框架的最终方法。我认为这暴露了一个有缺陷的依赖关系,需要进行一些重构。
          • gregturn> 实际上,依赖关系是有意表达的,因为框架开发人员不希望任何人覆盖它。是的,你很好地总结了我的意图。谢谢 8)
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-04-07
          • 2013-08-19
          • 1970-01-01
          • 2023-04-10
          相关资源
          最近更新 更多