问题 有没有办法从类外部修改Java中的`private static final`字段的值?


我知道这通常是相当愚蠢的,但在阅读这个问题之前不要开枪。我保证我有充分的理由需要这样做:)

可以使用反射修改java中的常规私有字段,但是Java在尝试执行相同操作时会抛出安全异常 final 领域。

我认为这是严格执行的,但我想我无论如何都会问,以防万一有人想出一个黑客来做这件事。

我只想说我有一个带有类的外部库“SomeClass

public class SomeClass 
{
  private static final SomeClass INSTANCE = new SomeClass()

  public static SomeClass getInstance(){ 
      return INSTANCE; 
  }

  public Object doSomething(){
    // Do some stuff here 
  }
} 

我本质上想要Monkey-Patch SomeClass,以便我可以执行我自己的版本 doSomething()。由于没有(据我所知)在java中真正做到这一点,我唯一的解决方案就是改变它的价值 INSTANCE 所以它使用修改后的方法返回我的类版本。

基本上我只想用安全检查包装调用,然后调用原始方法。

外部库总是使用 getInstance() 获取这个类的实例(即它是一个单例)。

编辑:只是为了澄清, getInstance() 由外部库调用,而不是我的代码,所以只是子类化不会解决问题。

如果我不能这样做,我能想到的唯一其他解决方案是复制粘贴整个类并修改方法。这并不理想,因为我必须更新库以更新库。如果某人有更多可维护的东西,我愿意接受建议。


1799
2018-04-20 05:27


起源

太糟糕了,Java中没有任何扩展方法 - Jon Limjap
你需要python:P - Rexsung
我冒昧地将“静态”添加到getInstance(),因为它可能是它的样子。 - Joachim Sauer
啊,谢谢Saua,你是对的。我的错。 - James Davies
修复你的代码:private static final SomeClass INSTANCE = new SomeClass(); - dfa


答案:


有可能的。我已经用它来monkeypatch淘气的threadlocals,它阻止了webapps中的类卸载。你只需要使用反射来删除 final 修饰符,然后您可以修改该字段。

像这样的东西可以解决这个问题:

private void killThreadLocal(String klazzName, String fieldName) {
    Field field = Class.forName(klazzName).getDeclaredField(fieldName);
    field.setAccessible(true);  
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    int modifiers = modifiersField.getInt(field);
    modifiers &= ~Modifier.FINAL;
    modifiersField.setInt(field, modifiers);
    field.set(null, null);
}

周围也有一些缓存 Field#set,所以如果一些代码运行之前它可能不一定工作....


10
2018-04-20 08:49



也许如果您在应用程序的早期运行此线程,它将解决缓存问题? - Sam Barnum
缓存问题不是真正的问题。我认为只有在另一种方法尝试使用反射设置字段时才会发生这种情况。 - benmmurphy
谢谢,这正是我一直在寻找的东西(希望我能更多地投资你,这很好而且模糊不清)。 - James Davies
我得到java.lang.NoSuchFieldException:Field modifiersField = Field.class.getDeclaredField(“modifiers”)中的修饰符; - Ixx


任何AOP框架都能满足您的需求

它允许您为getInstance方法定义运行时覆盖,允许您返回任何适合您需要的类。

Jmockit在内部使用ASM框架来做同样的事情。


5
2018-04-20 06:36



好答案。关键是不要修改字段,而是拦截调用以检索它。 - skaffman
那么这基本上是使用与JMockit相同的字节码重写技巧吗? - James Davies
我不是一个专家,但至少有两种方法可以做到这一点。一个使用java 1.5 instrumentation API,另一个使用ASM和直接字节码操作 - Jean


您可以尝试以下方法。注意:它根本不是线程安全的,这对编译时已知的常量基元不起作用(因为它们由编译器内联)

Field field = SomeClass.class.getDeclareField("INSTANCE");
field.setAccessible(true); // what security. ;)
field.set(null, newValue);

1
2018-04-20 06:57



不幸的是,安全管理器不喜欢这样 - 即使使用setAccessible(true),它也不喜欢修改最终值。 - James Davies
您可以尝试设置System.setSecurityManager(null)并查看是否可以暂时禁用它。 ;) - Peter Lawrey


您应该可以使用JNI更改它...不确定这是否适合您。

编辑:这是可能的,但不是一个好主意。

http://java.sun.com/docs/books/jni/html/pitfalls.html

10.9违反访问控制规则

JNI不强制执行class,field,   和方法访问控制限制   可以在Java上表达   编程语言水平通过   使用修饰符如private和   最后。可以写原生   用于访问或修改字段的代码   即使这样做也是对象   Java编程语言水平会   导致IllegalAccessException。   JNI的放纵是有意识的   设计决定,鉴于原生   代码可以访问和修改任何内存   无论如何,在堆中的位置。

绕过的本机代码   源语言级访问检查   可能会产生不良影响   程序执行。例如,一个   如果a,可能会产生不一致   native方法修改final字段   在即时(JIT)编译器之后   已经内联访问该字段。   同样,本机方法不应该   修改不可变对象,如   实例中的字段   java.lang.String或java.lang.Integer。   这样做可能会导致破损   Java平台中的不变量   实现。


1
2018-04-20 06:29



你能详细说说吗?我认为JNI只用于调用外部(即C)库。 - James Davies
@JamesDavies:JNI代码也可以返回到JVM并访问Java对象和类。 - mhsmith


如果你真的必须(虽然对于我们的问题,我建议你使用CaptainAwesomePants的解决方案)你可以看看 JMockIt。虽然这有意在单元测试中使用,但是如果允许您重新定义任意方法。这是通过在运行时修改字节码来完成的。


1
2018-04-20 05:47





我将通过承认这实际上不是对您所声明的关于修改私有静态最终字段的问题的答案来解释这个答案。但是,在上面提到的具体示例代码中,我实际上可以使它可以覆盖doSomething()。你可以做的是利用getInstance()是一个公共方法和子类这一事实:

public class MySomeClass extends SomeClass
{
   private static final INSTANCE = new MySomeClass();

   public SomeClass getInstance() {
        return INSTANCE;
   }

   public Object doSomething() {
      //Override behavior here!
   }
}

现在只需调用MySomeClass.getInstance()而不是SomeClass.getInstance(),你就可以了。当然,这只有在你调用getInstance()而不是你正在使用的不可修改的东西的其他部分时才有效。


0
2018-04-20 05:41



不幸的是,情况并非如此,getInstance完全由库的内部代码调用。 - James Davies
啊,抱歉,然后:( - Brandon Yarbrough


的Mockito 很简单:

import static org.mockito.Mockito.*;

public class SomeClass {

    private static final SomeClass INSTANCE = new SomeClass();

    public static SomeClass getInstance() {
        return INSTANCE;
    }

    public Object doSomething() {
        return "done!";
    }

    public static void main(String[] args) {
        SomeClass someClass = mock(SomeClass.getInstance().getClass());
        when(someClass.doSomething()).thenReturn("something changed!");
        System.out.println(someClass.doSomething());
    }
}

这段代码打印出“改变了一些东西!”;您可以轻松替换您的单例实例。我0.02美分。


0
2018-04-20 17:15





如果没有可用的外部黑客攻击(至少我不知道),我会攻击这个类本身。通过添加所需的安全检查来更改代码。因此它是一个外部库,你不会定期更新,也不会发生很多更新。无论何时发生这种情况,我都可以愉快地重新做到这一点,因为无论如何这不是一件大事。


-1
2018-04-20 06:45





在这里,您的问题是古老的依赖注入(又名反转控制)。你的目标应该是注入你的实现 SomeClass 而不是monkeypatching它。是的,这种方法需要对现有设计进行一些更改,但出于正确的原因(在此命名您最喜欢的设计原则) - 尤其是同一个对象不应该负责创建和使用其他对象。

我假设你正在使用的方式 SomeClass 看起来有点像这样:

public class OtherClass {
  public void doEverything() {
    SomeClass sc = SomeClass.geInstance();
    Object o = sc.doSomething();

    // some more stuff here...
  }
}

相反,你应该做的是首先创建实现相同接口或扩展的类 SomeClass 然后将该实例传递给 doEverything() 所以你的班级变得不可知实施 SomeClass。在这种情况下调用的代码 doEverything 负责传递正确的实施 - 无论是实际的 SomeClass 或者你的monkeypatched MySomeClass

public class MySomeClass() extends SomeClass {
  public Object doSomething() {
    // your monkeypatched implementation goes here
  }
}

public class OtherClass {
  public void doEveryting(SomeClass sc) {
    Object o = sc.doSomething();

    // some more stuff here...
  }
}

-1
2018-04-20 16:32



谢谢,但正如问题中所提到的,这不起作用,因为我不是直接调用SomeClass。我想更改外部库使用的SomeClass实例 - 我从不直接从我的代码中调用它。 - James Davies