如何在Java中从不同的类读取私有字段的值?

560

我有一个设计不良的第三方JAR中的类,我需要访问它的一个私有字段。例如,为什么需要选择私有字段,这是必要的吗?

class IWasDesignedPoorly {
    private Hashtable stuffIWant;
}

IWasDesignedPoorly obj = ...;

我该如何使用反射来获取stuffIWant的值?

14个回答

748
为了访问私有字段,你需要从类的“声明”字段中获取它们,然后使其可访问:
Field f = obj.getClass().getDeclaredField("stuffIWant"); //NoSuchFieldException
f.setAccessible(true);
Hashtable iWantThis = (Hashtable) f.get(obj); //IllegalAccessException

编辑:正如aperkins所评论的那样,访问该字段、将其设置为可访问并检索值都可能抛出Exception,尽管你需要注意的唯一已检查异常在上面有注释。

NoSuchFieldException会在你按名称请求一个未对应于声明字段的字段时抛出。

obj.getClass().getDeclaredField("misspelled"); //will throw NoSuchFieldException

如果一个字段不可访问(例如,它是私有的并且没有通过省略f.setAccessible(true)代码行来使其可访问)则会抛出IllegalAccessException异常。

可能会抛出的RuntimeException有两种情况:一种是SecurityException(如果JVM的SecurityManager不允许更改字段的可访问性),另一种是IllegalArgumentException,如果你尝试在不属于字段类类型的对象上访问该字段:

f.get("BOB"); //will throw IllegalArgumentException, as String is of the wrong type

4
请问您能否解释一下“异常注释”?这段代码会运行但可能会抛出异常,还是说这段代码有可能会抛出异常? - Nir Levy
1
@Nir - 不会,很可能代码会正常运行(因为默认的SecurityManager允许更改字段的可访问性),但是你必须处理已检查的异常(要么捕获它们,要么声明它们需要被重新抛出)。我稍微修改了我的答案。写一个小的测试用例来玩一下并看看会发生什么可能对你有好处。 - oxbow_lakes
3
抱歉,这个答案对我来说有些令人困惑。也许您可以展示一个典型的异常处理示例。似乎当类的连接不正确时会发生异常。代码示例使得异常好像会在相应的行上抛出。 - LaFayette
2
如果一个字段是在父类中定义的,getDeclaredField()就找不到它 -- 你必须通过遍历父类层次结构并在每个类上调用getDeclaredField()来查找匹配项(之后可以调用setAccessible(true)),或者直到达到Object - Luke Hutchison
2
@legend 你可以安装一个安全管理器来禁止这样的访问。自Java 9以来,这种访问不应该跨越模块边界工作(除非显式打开一个模块),尽管在执行新规则方面存在过渡阶段。此外,像反射这样需要额外努力的方法总是对非“private”字段产生影响。它可以防止意外访问。 - Holger
显示剩余8条评论

209

107
我相信,通过组合使用 commons-lang3 中的几种方法,你可以解决大部分世界问题。 - Cameron
1
@yegor256 我仍然可以访问C#和C++的私有成员..!!那又怎样?所有语言的创建者都是弱的吗? - Asif Mushtaq
5
Java本身并不允许在范围之外使用私有字段或更改final引用,因为Java语言和Java虚拟机是两个不同的实体。后者是基于字节码操作的,并且有一些库可用于操作它。但是,使用工具可以做到这一点,而这个工具恰好在标准库中。 - evgenii
@EvgeniiMorozov 再次提出问题,留下了为什么。为什么Java允许使用或创建这些工具? - Asif Mushtaq
4
Java没有"friend"访问控制,这就是Java不能像DI、ORM或XML/JSON序列化器这样的基础框架一样仅允许访问字段的一个原因。这些框架需要访问对象字段以正确地序列化或初始化对象的内部状态,但您的业务逻辑仍然需要适当的编译时封装强制执行。 - Ivan Gammel
显示剩余8条评论

28

反射不是解决您的问题的唯一方法(即访问类/组件的私有功能/行为)。

另一种解决方案是从.jar文件中提取类,使用(比如)JodeJad对其进行反编译,更改字段(或添加访问器),并针对原始的 .jar文件重新编译它。然后将新的.class文件放在classpath的前面,或重新插入到.jar文件中。(jar实用程序允许您从现有的.jar文件中提取和重新插入)

如下所述,这解决了访问/更改私有状态而不仅仅是访问/更改字段的广泛问题。

当然,这需要.jar文件没有签名。


44
这种方法对于一个简单领域来说会相当痛苦。 - Valentin Rocher
2
我不同意。这样做不仅允许您访问字段,而且如果访问字段不足够,还可以更改类。 - Brian Agnew
5
那么你必须再次这样做。如果这个jar包得到更新,而你使用反射读取一个不存在的字段,会发生什么?这正是同样的问题,你只需要处理它即可。 - Brian Agnew
4
我很惊讶这个提议会被投票踩得如此之多,因为a)它被强调为一种实际的选择 b)它适用于改变字段可见性不足以解决问题的情况。 - Brian Agnew
2
@BrianAgnew也许这只是语义问题,但如果我们坚持问题(使用反射读取私有字段),不使用反射就是自相矛盾的。但我同意你提供了对该字段的访问权限...但是该字段仍然不再是私有的,因此我们仍然没有坚持问题“读取私有字段”。从另一个角度来看,.jar修改在某些情况下可能无法正常工作(签名的jar),需要每次更新jar时进行操作,需要仔细操作类路径(如果您在应用程序容器中执行,则可能无法完全控制)等。 - Remi Morin
显示剩余5条评论

18

还有一种未被提及的选项:使用Groovy。Groovy允许您访问私有实例变量,这是语言设计的副作用。无论是否有该字段的getter,您都可以直接使用。

def obj = new IWasDesignedPoorly()
def hashTable = obj.getStuffIWant()

9
OP特别要求使用Java。 - Jochen
3
如今很多Java项目都包含Groovy。只要该项目使用Spring的Groovy DSL,就会将Groovy添加到类路径中。在这种情况下,这个答案是有用的,虽然并没有直接回答问题,但对许多访问者来说都会有好处。 - Amir Abiri

13

使用Java中的反射,可以访问一个类的所有private/public字段和方法,并在另一个类中使用。但是根据Oracle 文档中的缺点部分建议:

"由于反射允许代码执行非反射代码中不合法的操作,例如访问私有字段和方法,因此使用反射可能会导致意外的副作用,这可能使代码失效并破坏可移植性。反射代码打破了抽象层,并且因此可能会随着平台升级而改变行为"

以下代码片段演示了反射的基本概念

Reflection1.java

public class Reflection1{

    private int i = 10;

    public void methoda()
    {

        System.out.println("method1");
    }
    public void methodb()
    {

        System.out.println("method2");
    }
    public void methodc()
    {

        System.out.println("method3");
    }

}

Reflection2.java

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;


public class Reflection2{

    public static void main(String ar[]) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
    {
        Method[] mthd = Reflection1.class.getMethods(); // for axis the methods 

        Field[] fld = Reflection1.class.getDeclaredFields();  // for axis the fields  

        // Loop for get all the methods in class
        for(Method mthd1:mthd)
        {

            System.out.println("method :"+mthd1.getName());
            System.out.println("parametes :"+mthd1.getReturnType());
        }

        // Loop for get all the Field in class
        for(Field fld1:fld)
        {
            fld1.setAccessible(true);
            System.out.println("field :"+fld1.getName());
            System.out.println("type :"+fld1.getType());
            System.out.println("value :"+fld1.getInt(new Reflaction1()));
        }
    }

}

希望它能够有所帮助。


6

Java 9引入了变量句柄(Variable Handles)。您可以使用它们访问类的私有字段。

示例代码如下所示:

var lookup = MethodHandles.lookup();
var handle = MethodHandles
    .privateLookupIn(IWasDesignedPoorly.class, lookup)
    .findVarHandle(IWasDesignedPoorly.class, "stuffIWant", Hashtable.class);
var value = handle.get(obj);

建议将 LookupVarHandle 对象作为 static final 字段使用。


5
如oxbow_lakes所说,您可以使用反射来绕过访问限制(假设您的SecurityManager允许)。 话虽如此,如果这个类设计得如此糟糕以至于让您采用这样的hackery,也许您应该寻找一个替代方法。当然,现在这个小技巧可能会为您节省几个小时,但是未来它会花费您多少呢?

3
实际上,我比那还要幸运,我只是在使用这段代码来提取一些数据,然后就可以把它扔进回收站了。 - Frank Krueger
2
那么,在这种情况下,尽管放手去干吧。 :-) - Laurence Gonsalves
3
这并没有回答问题。如果想批评或请求作者进行澄清,请在他们的帖子下留言。 - Mureinik
@Mureinik - 这确实回答了问题,使用了“你可以使用反射”的话语。虽然缺少示例或更详细的解释,但这是一个答案。如果您不喜欢它,请给它点个踩。 - ArtOfWarfare

4

如果使用Spring:

测试环境中,ReflectionTestUtils提供了一些方便的工具,可以在最小的努力下帮助解决问题。它被描述为"用于单元和集成测试场景"

非测试环境中,也有一个类似的类名为ReflectionUtils,但这被描述为"仅供内部使用" - 请参见this answer以获得对此含义的良好解释。

针对原始帖子中的示例进行如下处理:

Hashtable iWantThis = (Hashtable)ReflectionTestUtils.getField(obj, "stuffIWant");

1
如果您决定使用Spring的Utils类,则除非您实际上是在进行单元测试,否则应该使用非测试类(https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/ReflectionUtils.html#handleReflectionException-java.lang.Exception)。 - Sync

4
使用Soot Java优化框架直接修改字节码。 http://www.sable.mcgill.ca/soot/ Soot完全使用Java编写,并且可以与新版本的Java一起使用。

3
您需要按照以下步骤进行操作:
private static Field getField(Class<?> cls, String fieldName) {
    for (Class<?> c = cls; c != null; c = c.getSuperclass()) {
        try {
            final Field field = c.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field;
        } catch (final NoSuchFieldException e) {
            // Try parent
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    "Cannot access field " + cls.getName() + "." + fieldName, e);
        }
    }
    throw new IllegalArgumentException(
            "Cannot find field " + cls.getName() + "." + fieldName);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,