Java 可以打猴子补丁吗?
-
22-08-2019 - |
题
我不想讨论这种方法的优点,只是如果可能的话。我相信答案是“不”。但也许有人会让我感到惊讶!
假设您有一个核心小部件类。它有一个方法 calculateHeight()
, ,返回高度。高度太大——这会导致按钮太大。您可以扩展DefaultWidget来创建您自己的NiceWidget,并实现您自己的 calculateHeight()
返回更好的尺寸。
现在,库类 WindowDisplayFactory 用相当复杂的方法实例化 DefaultWidget。您希望它使用您的 NiceWidget。工厂类的方法看起来像这样:
public IWidget createView(Component parent) {
DefaultWidget widget = new DefaultWidget(CONSTS.BLUE, CONSTS.SIZE_STUPIDLY);
// bunch of ifs ...
SomeOtherWidget bla = new SomeOtherWidget(widget);
SomeResultWidget result = new SomeResultWidget(parent);
SomeListener listener = new SomeListener(parent, widget, flags);
// more widget creation and voodoo here
return result;
}
就是这样。结果是 DefaultWidget 深入到其他对象的层次结构中。问题 - 如何让这个工厂方法使用我自己的 NiceWidget?或者至少得到我自己的 calculateHeight()
在那里。理想情况下,我希望能够对 DefaultWidget 进行猴子修补,以便它的calculateHeight 做正确的事情......
public class MyWindowDisplayFactory {
public IWidget createView(Component parent) {
DefaultWidget.class.setMethod("calculateHeight", myCalculateHeight);
return super.createView(parent);
}
}
这就是我可以用 Python、Ruby 等来做的事情。我发明了这个名字 setMethod()
尽管。我可以选择的其他选项是:
- 复制并粘贴代码
createView()
方法到我自己的继承自工厂类的类中 - 与太大的小部件一起生活
工厂类无法更改 - 它是核心平台 API 的一部分。我尝试对返回的结果进行反射以获取(最终)添加的小部件,但它是几个小部件层,并且在某个地方它被用来初始化其他东西,从而导致奇怪的副作用。
有任何想法吗?到目前为止,我的解决方案是复制粘贴工作,但这是一种逃避,需要在升级到较新版本的平台时跟踪父工厂类中的更改,我有兴趣听到其他选项。
解决方案
也许您可以使用面向方面的编程来捕获对该函数的调用并返回您自己的版本?
Spring 提供了一些 AOP 功能,但还有其他库也可以做到这一点。
其他提示
一种丑陋的解决方案是将您自己的 DefaultWidget 实现(具有相同的 FQCN)比正常实现更早地放在类路径上。这是一个可怕的黑客,但我能想到的所有其他方法都更糟糕。
只是我的概念想法,
可以使用AOP,通过字节码工程方式,将aspect注入到calculateHeight方法中。
然后,您可以通过 ThreadLocal 或其他变量启用修补程序。
程序库 是一个 Java 库,可以做一些类似于猴子修补的事情——它可以在运行时操纵字节码来改变某些行为。我不确定它是否能完全满足您的需要,但值得一看......
使用 Unsafe.putObject 和类查找器在 Java 中进行 Monkeypatch 是完全可能的。在这里写了一篇博文:
https://tersesystems.com/blog/2014/03/02/monkeypatching-java-classes/
面向对象的方法是创建一个实现 IWidget 的包装器,将所有调用委托给实际的小部件(除了calculateHeight),如下所示:
class MyWidget implements IWidget {
private IWidget delegate;
public MyWidget(IWidget d) {
this.delegate = d;
}
public int calculateHeight() {
// my implementation of calculate height
}
// for all other methods: {
public Object foo(Object bar) {
return delegate.foo(bar);
}
}
为此,您需要拦截要替换的小部件的所有创建,这可能意味着为 WidgetFactory 创建类似的包装器。并且您必须能够配置要使用的 WidgetFactory。
它还取决于没有客户端尝试将 IWidget 转换回 DefaultWidget...
我能想到的唯一建议:
深入研究库 API,看看是否有某种方法可以覆盖默认值和大小调整。在 swing 中,大小调整可能会令人困惑(至少对我来说),setMinimum,setMaximum,setdefault,setDefaultOnThursday,...。或许有办法。如果您可以联系库设计者,您可能会找到一个答案,从而减少对令人不快的黑客攻击的需要。
也许扩展工厂只覆盖一些默认的大小调整参数?取决于工厂,但也许是可能的。
创建一个具有相同名称的类可能是唯一的其他选择,正如其他人指出的那样,它很丑陋,并且当您更新 api 库或部署在不同的环境中并忘记为什么要使用它时,您很容易忘记它并破坏内容。类路径就是这样设置的。
您可以尝试使用 PowerMock/Mockito 等工具。如果您可以在测试中进行模拟,那么您也可以在生产中进行模拟。
然而,这些工具并不是真正设计用于这种方式,因此您必须自己准备环境,并且无法像在测试中那样使用 JUnit 运行器...
好吧,我一直在尝试发布建议,然后我发现它们不起作用,或者您已经提到过您尝试过它们。
我能想到的最好的解决方案是子类化 WindowDisplayFactory ,然后在子类的 createView() 方法中,首先调用 super.createView() ,然后修改返回的对象以完全抛出该窗口小部件并将其替换为子类的实例做你想做的事。但该小部件用于初始化内容,因此您必须更改所有这些内容。
然后我想到对 createView() 返回的对象使用反射并尝试以这种方式修复问题,但同样,这很麻烦,因为太多的东西是用小部件初始化的。不过,我想我会尝试使用这种方法,如果它足够简单,足以证明它比复制和粘贴更合理。
我会关注这个,并思考是否能想出任何其他想法。Java 反射确实很好,但它无法击败我在 Perl 和 Python 等语言中看到的动态内省。