【问题标题】:Accessing non-visible classes with reflection使用反射访问不可见的类
【发布时间】:2013-02-07 13:39:10
【问题描述】:

我正在尝试使用反射获取一个不可见类的实例,即 AKA 包私有类。我想知道是否有办法切换修饰符以使其公开,然后使用Class.forName 访问它。当我现在尝试这样做时,它会阻止我说我做不到。不幸的是,Class 类没有 setAccesible 方法。

【问题讨论】:

  • setAccesible 是在构造函数、方法或字段上定义的,而不是在类上定义的。在类上使用 getConstructor API 来获取其构造函数
  • 但是问题出在哪里?为什么要使用反射?你不能用new full.package.name.of.YourClass()吗?所有构造函数都是包/私有的吗?
  • 如果我使用的类在不同的包中,则不会
  • 您为什么要这样做?出于某种原因,该类可能是包私有的。

标签: java class reflection package-private


【解决方案1】:

嵌套类 - 在其他类中定义的类(包括静态和非静态类)
内部类 - 非静态嵌套类(内部类的实例需要外部类的实例存在)

##非嵌套(顶级)类

根据您的问题,我们知道您要访问的构造函数不是公开的。所以你的班级可能看起来像这样(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 字段中,更多信息:What does it mean if a variable has the name "this$0" in IntelliJ IDEA while debugging Java?

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

这是一个例子。

在包 1 中

package package1;

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

在包2中

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 作为第一个参数的构造函数。而且我们不需要将外部类的实例作为第一个参数传递。换句话说,代码将与非嵌套(顶级)类相同(可能除了在引用 @987654342 中的嵌套类时,您需要在类名中添加 use $ 而不是 . @喜欢Class.forName("some.package.Outer$Nested1$NestedNested"))。

【讨论】:

  • 如果 A 类是默认的公共/包私有怎么办?那么我们也可以在不同的包中使用 Class.forName 加载它吗?
  • @MaheshBhuva 是的(至少在标准设置下,我不是反射大师,所以我不知道是否有办法防止它)。 Class.forName 返回类 literal,它是 Class 的实例,包含 about 指定类的元信息。但是,如果我们想从中访问一些方法/字段,我们可能需要在 Methods 或 Fields 的实例上调用 setAccessible(true)
【解决方案2】:

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

如果您尝试使用Class.newInstance,请不要使用。 Class.newInstance 处理异常很差。取而代之的是获得Constructor 并为此致电newInstance。如果没有异常跟踪,很难看出您遇到了什么问题。

与以往一样,大多数但并非所有反射的使用都是坏主意。

【讨论】:

    【解决方案3】:

    我们最近发布了一个库,它对通过反射访问私有字段、方法和内部类有很大帮助: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 类。

    【讨论】:

      【解决方案4】:

      您可以使用 Manifold's @Jailbreak 进行直接、类型安全的 Java 反射:

      Foo foo = new @Jailbreak Foo();
      
      public class Foo {
          Foo() {...}
      
          private void yodog() {...}
      }
      

      @Jailbreak 使编译器能够像 public 一样安全地解析构造函数,而 Manifold 会在后台为您生成高效的反射代码。

      此外,您还可以使用@Jailbreak 来访问和构造不可见的类:

      com.abc. @Jailbreak Bar bar = new com.abc. @Jailbreak Bar();
      
      package com.abc;
      // package-private class
      class Bar {
          Bar() {...}
      }
      

      对于隐藏的类访问,Java 的注解语法要求类在其包之外被注解。

      更一般地,您可以将@Jailbreak 用于任何类型的反射:

      @Jailbreak Foo foo = new Foo();
      foo.yodog();
      

      @Jailbreak 解锁编译器中的 foo 局部变量,以便直接访问 Foo 层次结构中的所有成员。

      同样你可以使用 jailbreak() 扩展方法一次性使用:

      foo.jailbreak().yodog();
      

      通过jailbreak() 方法,您可以访问Foo 层次结构中的任何成员。

      发现更多关于Manifold的信息。

      【讨论】:

        【解决方案5】:

        如果在最新版本中值为 null,我需要从旧版本的对象中复制字段的值。我们有这两个选项。

        核心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"));
        }
        

        【讨论】:

          【解决方案6】:

          我想在 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);
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2011-11-03
            • 2010-11-18
            • 2011-06-30
            • 2015-09-24
            • 1970-01-01
            • 2018-07-09
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多