使用反射访问不可见的类

65
我正在尝试使用反射获取一个非可见类的实例,也就是包私有类。我想知道是否有一种方法可以将修饰符修改为public,然后使用Class.forName访问它。但是我现在尝试这样做时,会出现错误提示,告诉我不能这样操作。不幸的是,Class类没有setAccessible方法。

setAccessible方法定义在构造函数、方法或字段上,而不是类上。使用getConstructor API来获取其构造函数。 - Shiva Kumar
但问题出在哪里呢?为什么你想要使用反射?难道不能直接使用 new full.package.name.of.YourClass() 吗?所有的构造函数都是包/私有的吗? - Pshemo
2
如果我使用的类在不同的包中,则不能使用“no not”。 - Josh Sobel
为什么你想要这样做呢?那个类很可能是有意设为包可见的。 - Louis Wasserman
5个回答

77

嵌套类 - 在其他类中定义的类(包括静态和非静态类)
内部类 - 非静态嵌套类(内部类的实例只能通过外部类的实例创建)

非嵌套(顶级)类

根据您的问题,我们知道您想要访问的构造函数不是公共的。所以您的类可能看起来像这样(A 类位于与我们不同的包中)

package package1;

public class A {
    A(){
        System.out.println("this is non-public constructor");
    }
}

创建此类的实例,我们需要获取要调用的构造函数,并使其可访问。完成后,我们可以使用 Constructor#newInstance(arguments) 创建实例。
Class<?> c = Class.forName("package1.A");
//full package name --------^^^^^^^^^^
//or simpler without Class.forName:
//Class<package1.A> c = package1.A.class;

//In our case we need to use
Constructor<?> constructor = c.getDeclaredConstructor();
//note: getConstructor() can return only public constructors
//so we needed to search for any Declared constructor

//now we need to make this constructor accessible
constructor.setAccessible(true);//ABRACADABRA!

Object o = constructor.newInstance();

嵌套类和内部类

如果您想使用Class.forName访问嵌套(静态和非静态)类,您需要使用以下语法:

Class<?> clazz = Class.forName("package1.Outer$Nested");

Outer$Nested 表示 Nested 类在 Outer 类中声明。嵌套类与方法非常相似,它们可以访问其外部类的所有成员(包括私有成员)。

但是我们需要记住,内部类的实例需要其外部类的实例才能存在。通常我们通过以下方式创建它们:

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

正如您所看到的,每个内部类实例都有关于其外部类的一些信息(对该外部实例的引用存储在this$0字段中,更多信息:在调试Java时,如果一个变量的名称为“this$0”,在IntelliJ IDEA中是什么意思?

因此,在使用Constructor#newInstance()创建Inner类实例时,您需要将外部类实例的引用作为第一个参数传递(以模拟outer.new Inner()的行为)。

这里是一个例子。

在package1中

package package1;

public class Outer {
    class Inner{
        Inner(){
            System.out.println("non-public constructor of inner class");
        }
    }
}

在package2中

package package2;

import package1.Outer;
import java.lang.reflect.Constructor;

public class Test {
    public static void main(String[] args) throws Exception {

        Outer outerObject = new Outer();

        Class<?> innerClazz = Class.forName("package1.Outer$Inner");

        // constructor of inner class as first argument need instance of
        // Outer class, so we need to select such constructor
        Constructor<?> constructor = innerClazz.getDeclaredConstructor(Outer.class);

        //we need to make constructor accessible 
        constructor.setAccessible(true);

        //and pass instance of Outer class as first argument
        Object o = constructor.newInstance(outerObject);
        
        System.out.println("we created object of class: "+o.getClass().getName());

    }
}

静态嵌套类

静态嵌套类的实例不需要外部类的实例(因为它们是静态的)。因此,在它们的情况下,我们不需要查找第一个参数为 Outer.class 的构造函数。我们也不需要将外部类的实例作为第一个参数传递。换句话说,代码与非嵌套(顶级)类相同(也许除了在使用 Class.forName() 引用嵌套类时需要添加使用 $ 而不是 . 在类名中,例如 Class.forName("some.package.Outer$Nested1$NestedNested"))。


如果A类默认为public/package private,那么我们可以在不同的包中使用Class.forName加载它吗? - Mahesh Bhuva
3
@MaheshBhuva 是的(至少在标准设置下,我不是反射专家,因此无法确定是否有方法可以防止它)。Class.forName返回类字面值,它是包含关于指定类的元信息的Class实例。但是如果我们想要访问它中的一些方法/字段,我们可能需要对这些MethodField的实例调用setAccessible(true) - Pshemo

3

Class.forName应该可以正常工作。如果该类在“package.access”安全属性的包层次结构列表中,则您需要使用适当的特权(通常是所有权限;或者没有安全管理器)执行操作。

如果您正在尝试使用Class.newInstance,请不要这样做。 Class.newInstance处理异常不好。相反,获取一个Constructor并在其上调用newInstance。如果没有异常跟踪,很难看出您遇到了什么问题。

像往常一样,大多数但不是所有反射使用都是坏主意。


1
我们最近发布了一个库,它可以通过反射非常方便地访问私有字段、方法和内部类: BoundBox 对于像下面这样的类:
public class Outer {
    private static class Inner {
        private int foo() {return 2;}
    }
}

它提供了类似于以下语法的功能:

Outer outer = new Outer();
Object inner = BoundBoxOfOuter.boundBox_new_Inner();
new BoundBoxOfOuter.BoundBoxOfInner(inner).foo();

创建BoundBox类的唯一要做的事情就是编写@BoundBox(boundClass=Outer.class),然后BoundBoxOfOuter类将立即生成。


0

我有一个需求,如果最新版本的对象中某个字段的值为空,则需要从旧版本的对象中复制该字段的值。我们有以下两个选项。

核心Java:

for (Field f : object.getClass().getSuperclass().getDeclaredFields()) {
    f.setAccessible(true);
  System.out.println(f.getName());
  if (f.get(object) == null){
    f.set(object, f.get(oldObject));
  }
}

使用 Spring [org.springframework.beans.BeanWrapper] :

BeanWrapper bw = new BeanWrapperImpl(object);
PropertyDescriptor[] data = bw.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : data) {
  System.out.println(propertyDescriptor.getName());
  Object propertyValue = bw.getPropertyValue(propertyDescriptor.getName());
  if(propertyValue == null )
    bw.setPropertyValue( new PropertyValue(propertyDescriptor.getName(),"newValue"));
}

0

在Android上不得不做类似的事情,这是我想出来的:

/**
 * To fix issues with wrong edge-to-edge bottom sheet top padding and status bar icon color…
 * …we need to call [BottomSheetDialog.EdgeToEdgeCallback.setPaddingForPosition] which is a private function from a private class.
 * See: https://github.com/material-components/material-components-android/issues/2165
 */
fun adjustBottomSheet(aDialog : BottomSheetDialog) {
    // Get our private class
    val classEdgeToEdgeCallback = Class.forName("com.google.android.material.bottomsheet.BottomSheetDialog\$EdgeToEdgeCallback")
    // Get our private method
    val methodSetPaddingForPosition: Method = classEdgeToEdgeCallback.getDeclaredMethod("setPaddingForPosition", View::class.java)
    methodSetPaddingForPosition.isAccessible = true
    // Get private field containing our EdgeToEdgeCallback instance
    val fieldEdgeToEdgeCallback = BottomSheetDialog::class.java.getDeclaredField("edgeToEdgeCallback")
    fieldEdgeToEdgeCallback.isAccessible = true
    // Get our bottom sheet view field
    val fieldBottomField = BottomSheetDialog::class.java.getDeclaredField("bottomSheet")
    fieldBottomField.isAccessible = true
    // Eventually call setPaddingForPosition from EdgeToEdgeCallback instance passing bottom sheet view as parameter
    methodSetPaddingForPosition.invoke(fieldEdgeToEdgeCallback.get(aDialog),fieldBottomField.get(aDialog))
}

你也许需要确保ProGuard没有丢弃你要访问的方法:

# Needed for now to fix our bottom sheet issue from 
com.google.android.material:material:1.4.0-alpha02
-keep class com.google.android.material.bottomsheet.BottomSheetDialog$EdgeToEdgeCallback {
    private void setPaddingForPosition(android.view.View);
}

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