Java 扩展或替换最终类 “不,不行”

Java 扩展或替换最终类 “不,不行”,java,java-8,final,Java,Java 8,Final,这件事已经讨论了很多次,唯一的回答是“不行,不行”。但伙计们,我们做代码。理论上,一切都是有可能的 问题 基本上,我试图扩展最后一个类来更改单个行为。这不能用传统的方法来完成——在编译时使用extend关键字。我已经找到了其他一些方法: 创建一个代理-仅适用于接口 编辑类的修饰符,删除最终的修饰符,并在运行时扩展类-这可以对字段执行,但不能对类执行 使用自定义的ClassLoader加载新版本的类-它不起作用,因为我要扩展的类是java包的一部分。幸运的是,ClassLoader无法加载此包的

这件事已经讨论了很多次,唯一的回答是“不行,不行”。但伙计们,我们做代码。理论上,一切都是有可能的

问题 基本上,我试图扩展最后一个类来更改单个行为。这不能用传统的方法来完成——在编译时使用
extend
关键字。我已经找到了其他一些方法:

  • 创建一个代理-仅适用于接口
  • 编辑类的
    修饰符
    ,删除
    最终的
    修饰符,并在运行时扩展类-这可以对字段执行,但不能对类执行
  • 使用自定义的
    ClassLoader
    加载新版本的类-它不起作用,因为我要扩展的类是
    java
    包的一部分。幸运的是,
    ClassLoader
    无法加载此包的类,因为出现了
    SecurityException
    。否则,这将搞乱许多证券
老师发给我们的代码覆盖率报告显示,他没有测试这个模块,所以我们不必测试。但我不信任Java,我可以肯定这个模块能像预期的那样工作。那我得测试一下

[注意:由于它不是必需的,所以它只是发现新Java黑客的一种手段,甚至可能不会计入最终分数。]


我唯一可以访问的变量是
字符串
规则
。然后我应该处理它,使
load
调用抛出IOException。调用
load
抛出IOException的唯一原因是当StringReader的流关闭时。这通过
重新打开
方法进行检查:

/** Check to make sure that the stream has not been closed */
private void ensureOpen() throws IOException {
    if (str == null)
        throw new IOException("Stream closed");
}
因此,只有null
字符串
才能使此方法抛出
IOException
。然后将空
字符串
插入
规则
,就很容易了。坏消息是
StringReader
构造函数:

public StringReader(String s) {
    this.str = s;
    this.length = s.length();
}
它在传递
null
字符串时抛出一个NPE。呃

我的最后一个想法是更改
字符串的值
,使其内部
字段
为空
——在使用该字段的
修饰符时变得容易。这不起作用,因为引用而不是值本身应该是
null
;由于调用了
StringReader
上的
read
方法,这只会抛出一个NPE:

public int read() throws IOException {
    synchronized (lock) {
        ensureOpen();
        if (next >= length)
            return -1;
        return str.charAt(next++);
    }
}

你知道吗?一个能为我做到这一点的框架?我可以探索一些途径来解决我无用的问题吗


这个问题与代码测试无关。它是关于如何破解Java及其最终类的。

可能的解决方案之一是使用Aspect

您可以在执行最终类方法时创建方面和切入点。您可以在方面内部实现您的行为。

有一个方面,它允许Java软件,即所谓的Java代理,在运行时修改类。这就是模拟框架执行工作的方式之一。将其用于单元测试并非不合理

但是请注意,代码注入的具体目标是错误的。在代码中

Properties props = new Properties();
try
{
    props.load(new StringReader(rules));
} catch (IOException e)
{
    e.printStackTrace();
    throw e;
}
永远不会抛出
IOException
。正如您自己所指出的,
StringReader
的构造函数不会抛出它,这同样适用于它的
read
方法。
Properties
load
方法声明了它,因为它必须处理任意的
Reader
实现,但它本身不会产生这样的异常

因此,在这个场景中,如果您使用新构造的
StringReader
字符串中读取,它将永远不会被抛出,并且注入一个实际上不存在的行为来测试一个永远不会发生的场景是没有意义的


正如您自己所指出的,它可以抛出
IOException
的唯一点是方法
ensureOpen()
,顾名思义,该方法检查读卡器是否仍处于打开状态。因此,您可以通过在读卡器上简单地调用
close()
来强制执行抛出行为,但是,正如前面所说的,这与被测代码的实际行为不匹配,也没有必要引发一个实际上永远不会发生的被测异常。

您说修改或替换JRE不是您的选择。那么唯一的解决方案就是修改字节码。有很多方法可以做到这一点,其他答案中也提到了一些方法。我建议一些,从简单到更复杂(在我看来)

1) 使用PowerMock。它的一个重要部分是修改字节码的类加载器MockClassLoader。PowerMock的优点是您可以从原始类获得所有方法签名,这就是为什么您可以在IDE中使用自动完成(Eclipse、IDEA) 因此,与AspectJ、Spring AOP或ASM相比,您可以更轻松地重写您的方法,并且包名、类名和方法名中的错误更少,这些都是其他工具中的字符串,因此更容易出错

2) 加载前修改字节码。例如,从目标库中提取所需的类,根据需要对其进行修改,例如使该类不是最终类,重新打包库,然后在应用程序中使用它。为此有不同的工具:AspectJ(当您有源代码并编译它时)、ASM(您也可以修改现有的字节码)。与其他方法的一个本质区别是,当您在编译期间在应用程序中使用这种修改过的类时,编译器不知道修改过的类的原始字节码,因此您可以使用以前的类的正常继承

3) 在加载过程中修改字节码。您可以使用asppecj、Spring AOP、ASM。最强大的是ASM,但它也需要更多的努力。AspectJ
public int read() throws IOException {
    synchronized (lock) {
        ensureOpen();
        if (next >= length)
            return -1;
        return str.charAt(next++);
    }
}
Properties props = new Properties();
try
{
    props.load(new StringReader(rules));
} catch (IOException e)
{
    e.printStackTrace();
    throw e;
}