【问题标题】:Is there any way to modify the value of a `private static final` field in Java from outside the class?有没有办法从类外部修改 Java 中的“private static final”字段的值?
【发布时间】:2010-10-20 12:02:29
【问题描述】:

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

可以使用反射修改 Java 中的常规私有字段,但是当尝试对 final 字段执行相同操作时,Java 会引发安全异常。

我假设这是严格执行的,但我想我还是会问一下,以防万一有人想出了一个黑客来做到这一点。

假设我有一个带有“SomeClass”类的外部库

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

  public static SomeClass getInstance(){ 
      return INSTANCE; 
  }

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

我本质上想对 SomeClass 进行猴子补丁,以便我可以执行我自己的 doSomething() 版本。由于(据我所知)在 java 中没有任何方法可以真正做到这一点,因此我唯一的解决方案是更改 INSTANCE 的值,以便它使用修改后的方法返回我的类版本。

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

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

编辑:澄清一下,getInstance() 是由外部库调用的,而不是我的代码,所以仅仅子类化并不能解决问题。

如果我不能这样做,我能想到的唯一其他解决方案是复制粘贴整个类并修改方法。这并不理想,因为我必须让我的 fork 与库的更改保持同步。如果有人有一些更易于维护的东西,我愿意接受建议。

【问题讨论】:

  • 可惜Java中没有任何扩展方法
  • 我冒昧地在 getInstance() 中添加了“静态”,因为它可能看起来就是这样。
  • 啊,谢谢 Saua,你是对的。我的错。
  • 我相信这是stackoverflow.com/questions/289702/…的完全相同的副本
  • 修复你的代码:private static final SomeClass INSTANCE = new SomeClass();

标签: java monkeypatching


【解决方案1】:

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

【讨论】:

    【解决方案2】:

    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 美分。

    【讨论】:

      【解决方案3】:

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

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

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

      10.9 违反访问控制规则

      JNI 不强制执行类、字段、 和方法访问控制限制 可以在 Java 中表示 通过编程语言级别 使用修饰符,例如 private 和 最终的。可以写原生 访问或修改字段的代码 对象,即使这样做在 Java 编程语言级别将 导致 IllegalAccessException。 JNI 的允许是有意识的 设计决策,鉴于本机 代码可以访问和修改任何内存 无论如何都在堆中。

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

      【讨论】:

      • 你能详细说明一下吗?我认为 JNI 仅用于调用外部(即 C)库。
      • @JamesDavies:JNI 代码还可以返回到 JVM 并访问 Java 对象和类。
      【解决方案4】:

      这是可能的。我用它来猴子补丁阻止 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 周围也有一些缓存,所以如果某些代码在它之前运行过,它可能不一定能工作......

      【讨论】:

      • 如果你在应用程序的早期运行这个线程,它可能会解决缓存问题?
      • 缓存问题并不是真正的问题。我认为只有在另一种方法尝试使用反射设置字段时才会发生这种情况。
      • 谢谢,这正是我一直在寻找的(希望我能给你更多的支持,这很好而且晦涩难懂)。
      • 我得到 java.lang.NoSuchFieldException: modifiers in Field modifiersField = Field.class.getDeclaredField("modifiers");
      【解决方案5】:

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

      我假设您使用 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...
        }
      }
      

      【讨论】:

      • 谢谢,但正如问题中提到的那样,这不起作用,因为我没有直接调用 SomeClass。我想更改外部库使用的 SomeClass 的实例 - 我从不直接从我的代码中调用它。
      【解决方案6】:

      您可以尝试以下方法。注意:它根本不是线程安全的,这不适用于编译时已知的常量原语(因为它们被编译器内联)

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

      【讨论】:

      • 不幸的是,安全管理器不喜欢这样 - 即使使用 setAccessible(true) 它也不喜欢修改最终值。
      • 您可以尝试设置 System.setSecurityManager(null) 并查看是否可以暂时禁用它。 ;)
      【解决方案7】:

      如果没有可用的外部破解(至少我不知道),我会破解课程本身。通过添加所需的安全检查来更改代码。因此,它是一个外部库,您不会定期进行更新,也不会发生太多更新。每当发生这种情况时,我都可以很高兴地重新做,因为这不是一项大任务。

      【讨论】:

        【解决方案8】:

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

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

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

        【讨论】:

        • 好答案。关键不是修改字段,而是拦截调用来检索它。
        • 所以这本质上会使用与 JMockit 相同的字节码重写技巧?
        • 我并不完全是专家,但至少有两种方法可以做到这一点。一个使用 java 1.5 检测 API,另一个使用 ASM 和直接字节码操作
        【解决方案9】:

        我将通过承认这实际上不是对您提出的关于修改私有静态最终字段的问题的回答来作为这个答案的开头。但是,在上面提到的具体示例代码中,我实际上可以制作它以便您可以覆盖 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() 而不是您正在使用的不可修改内容的其他部分时才有效。

        【讨论】:

        • 很遗憾并非如此,getInstance 完全由库的内部代码调用。
        猜你喜欢
        • 2011-07-02
        • 2015-02-22
        • 1970-01-01
        • 2022-08-22
        • 2015-05-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多