是否可以在 Java 中进行猴子补丁?

Posted

技术标签:

【中文标题】是否可以在 Java 中进行猴子补丁?【英文标题】:Is it possible to monkey patch in Java? 【发布时间】:2010-09-27 17:31:00 【问题描述】:

我不想讨论这种方法的优点,只要可能。我相信答案是“不”。但也许有人会让我大吃一惊!

假设您有一个核心小部件类。它有一个方法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 的一部分。我尝试对返回的结果进行反射以获取(最终)添加的小部件,但它向下有几个小部件层,并且在某个地方它被用来初始化其他东西,导致奇怪的副作用。

有什么想法吗?到目前为止,我的解决方案是复制粘贴工作,但这是一种逃避,需要在升级到新版本平台时跟踪父工厂类的更改,我很想听听其他选项。

【问题讨论】:

【参考方案1】:

也许您可以使用面向方面的编程来捕获对该函数的调用并返回您自己的版本?

Spring 提供了一些 AOP 功能,但也有其他库也可以。

【讨论】:

听起来不错,可能是最接近实际的解决方案。可能有点矫枉过正,添加一个 Spring 依赖项,但只是为了这个修复,应用程序不存在这个依赖项。自定义类加载器可能无法很好地与 Eclipse RCP 配合使用,我也使用它... 你不需要Spring来做AOP,只要得到AspectJ。还有一个eclipse插件:eclipse.org/aspectj【参考方案2】:

一个丑陋的解决方案是将您自己的 DefaultWidget 实现(具有相同的 FQCN)放在 Classpath 上,而不是正常实现。这是一个可怕的 hack,但我能想到的所有其他方法都更糟糕。

【讨论】:

遗憾的是,我最终想要使用 99% 的 real DefaultWidget 却无法访问它。【参考方案3】:

只是我的概念想法,

可以使用 AOP,通过字节码工程的方式,将切面注入到 calculateHeight 方法中。

然后,您可以通过 ThreadLocal 或其他变量启用补丁。

【讨论】:

【参考方案4】:

cglib 是一个 Java 库,它可以做一些类似于猴子补丁的事情——它可以在运行时操纵字节码来改变某些行为。我不确定它是否能完全满足您的需求,但值得一看...

【讨论】:

其他处理字节码的工具:jakarta.apache.org/bcelasm.objectweb.org【参考方案5】:

使用 Unsafe.putObject 和类查找器完全可以在 Java 中进行猴子补丁。在这里写了一篇博文:

https://tersesystems.com/blog/2014/03/02/monkeypatching-java-classes/

【讨论】:

这不是真正的猴子补丁。它(只是)通过打破抽象来改变字段的值。现在,如果您要添加字段/添加方法/更改方法在运行时(不是加载时)......那就是猴子补丁。 来自***:“猴子补丁是程序在本地扩展或修改支持系统软件的一种方式(仅影响程序的运行实例)。”方法更改时 JVM 正在运行,所以这是猴子补丁。 根据您对该定义的解释,执行任何赋值语句也将是猴子补丁。像修改不可变字符串这样的“棘手”事情也会如此。无论如何,大多数 Java 程序员都会不同意您的解释。这就是为什么所有其他建议都是针对 AOP 和字节码工程以及模拟框架之类的。 > 诸如修改不可变字符串之类的“棘手”事情也会如此。是的,根据定义。从您链接的 URL 中:“不,它不像任何这些东西。它只是在运行时动态替换属性。”属性不限于方法。像tersesystems.com/blog/2016/01/19/… 这样的东西当然也是monkeypatch。【参考方案6】:

这样做的面向对象的方法是创建一个实现 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...

【讨论】:

这是策略设计模式,也是经典的Java工具。与猴子补丁无关。你可以喜欢或不喜欢猴子补丁,但它是一个非常特殊问题的答案。 当然,我只是将其发布为一个可能的选项。这里有很多替代方案,最适合取决于实际场景:相关 API 的扩展可能性、灵活性、平台、需要向前/向后兼容等。【参考方案7】:

只有我能想到的建议:

    挖掘库 API 以查看是否有某种方法可以覆盖默认值和大小。在摇摆(至少对我而言),setMinimum,setMaximum,setdefault,setDefaultOnThursday,...时,尺寸可能会令人困惑。可能有办法。如果您可以联系图书馆设计者,您可能会找到一个可以减轻令人不快的黑客行为的答案。

    也许扩展工厂只覆盖一些默认大小参数?取决于工厂,但有可能。

创建一个具有相同名称的类可能是唯一的其他选择,因为其他人指出它很丑陋,当您更新 api 库或部署在不同的环境中并忘记原因时,您可能会忘记它并破坏东西你已经以这种方式设置了类路径。

【讨论】:

我一直在寻找“setMinimumNoReallyIMeanItStopIgnoringMe”方法。 嘿 - 实际上比这更糟糕 - 有问题的代码甚至不是 Swing。查看 API,它请求依赖注入,但我可以看到为什么它没有这个(为了简单起见)。哼哼!【参考方案8】:

您可以尝试使用 PowerMock/Mockito 等工具。如果您可以在测试中进行模拟,那么您也可以在生产中进行模拟。

但是,这些工具并非真正设计为以这种方式使用,因此您必须自己准备环境,并且无法像在测试中那样使用 JUnit 运行器...

【讨论】:

【参考方案9】:

嗯,我一直在尝试发布建议,然后我发现它们不起作用,或者您已经提到过您尝试过它们。

我能想到的最好的解决办法就是继承WindowDisplayFactory,然后在子类的createView()方法中,先调用super.createView(),然后修改返回的对象,彻底扔掉widget,换成一个实例做你想做的事的子类。但是这个小部件是用来初始化东西的,所以你必须去改变所有这些。

然后我想到对从 createView() 返回的对象使用反射并尝试以这种方式修复问题,但同样,这很麻烦,因为使用小部件初始化了很多东西。不过,我想我会尝试使用这种方法,如果它足够简单,可以证明复制和粘贴是合理的。

我会看这个,并想看看我是否能想出任何其他的想法。 Java 反射确实不错,但它无法击败我在 Perl 和 Python 等语言中看到的动态自省。

【讨论】:

是的,在 createView 中额外的新类中完成了相当多的工作 - 最后反射 + 修复可能会更加脆弱(使用新版本的WidgetFactory),而不仅仅是复制粘贴。

以上是关于是否可以在 Java 中进行猴子补丁?的主要内容,如果未能解决你的问题,请参考以下文章

猴子补丁(monkey patch)

猴子补丁的应用,猴子补丁来改变日志。

如何绕过 Angular 使用自己的猴子补丁来撤消我的猴子补丁?

php中的猴子补丁

Python之猴子补丁

“猴子补丁”真的那么糟糕吗? [关闭]