原创

Java 15中的隐藏类

温馨提示:
本文最后更新于 2022年11月18日,已超过 18 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

1.概述

Java 15引入了大量 特征。在本文中,我们将在下面讨论一个名为“隐藏类”的新特性JEP-371。此功能是作为 不安全API 的替代项而引入的,在 JDK 之外不推荐使用它.

隐藏类特性对于任何使用动态字节码或JVM语言的人都很有用。

2.什么是隐藏类?

动态生成的类为低延迟应用程序提供了效率和灵活性。它们只在有限的时间内需要。在静态生成类的生命周期内保留它们会增加内存占用。针对这种情况现有的解决方案,如按类加载程序,既麻烦又低效。

自Java15以来,隐藏类已成为生成动态类的标准方法。

隐藏类是字节码或其他类不能直接使用的类。 尽管它被称为类,但应该理解为是隐藏类或接口。它也可以定义为 访问控制嵌套 并且可以独立于其他类卸载。

3.隐藏类的属性

让我们来看看这些动态生成的类的属性:

  • 不可发现-在字节码链接期间,JVM或显式使用类加载器的程序都无法发现隐藏类。反射方法 Class::forName, ClassLoader::findLoadedClass, and Lookup::findClass 都不会找到它们。
  • 我们不能将隐藏类用作超类、字段类型、返回类型或参数类型。
  • 隐藏类中的代码可以直接使用它,而不依赖于类对象。
  • final 隐藏类中声明的字段不可修改,无论其可访问标志如何。
  • 它使用不可发现的类扩展访问控制嵌套。
  • 它可以被卸载,即使它的概念定义类加载器仍然可以访问。
  • 默认情况下,堆栈跟踪不会显示隐藏类的方法或名称,但是,调整JVM选项可以显示它们。

4.创建隐藏类

隐藏类不是由任何类加载器创建的。 它具有与查找类相同的定义类加载器、运行时包和保护域。

首先,让我们创建一个 Lookup 对象:

MethodHandles.Lookup lookup = MethodHandles.lookup();

这个 Lookup::defineHiddenClass 方法创建隐藏类。此方法接受字节数组。

为了简单起见,我们将定义一个名为 HiddenClass(HiddenCass) 具有将给定字符串转换为大写的方法:

public class HiddenClass {
    public String convertToUpperCase(String s) {
        return s.toUpperCase();
    }
}

让我们获取类的路径并将其加载到输入流中。之后,我们将使用 IOUItils.到ByteArray():

Class<?> clazz = HiddenClass.class;
String className = clazz.getName();
String classAsPath = className.replace('.', '/') + ".class";
InputStream stream = clazz.getClassLoader()
    .getResourceAsStream(classAsPath);
byte[] bytes = IOUtils.toByteArray();

最后,我们将这些构造的字节传递到Lookup::defineHiddenClass:

Class<?> hiddenClass = lookup.defineHiddenClass(IOUtils.toByteArray(stream),
  true, ClassOption.NESTMATE).lookupClass();

第二个 boolean参数true 初始化类. 第三个参数ClassOption.NESTMATE 指定创建的隐藏类将作为嵌套对象添加到查找类,以便它可以访问 私有的 同一嵌套中所有类和接口的成员。

假设我们想将隐藏类与其类加载器强绑定,ClassOption.STRONG。这意味着只有在无法访问其定义加载器时,才能卸载隐藏类。

5.使用隐藏类

隐藏类由在运行时生成类并通过反射间接使用它们的框架所使用。

在上一节中,我们讨论了创建一个隐藏类。在本节中,我们将了解如何使用它并创建实例。

指派去获取Lookup.defineHiddenClass 对于任何其他类对象都是不可能的,我们使用 Object 以存储隐藏的类实例。如果我们希望转换隐藏类,我们可以定义一个接口并创建一个实现该接口的隐藏类:

Object hiddenClassObject = hiddenClass.getConstructor().newInstance();

现在,让我们从隐藏类中获取方法。获取该方法后,我们将作为任何其他标准方法调用它:

Method method = hiddenClassObject.getClass()
    .getDeclaredMethod("convertToUpperCase", String.class);
Assertions.assertEquals("HELLO", method.invoke(hiddenClassObject, "Hello"));

现在,我们可以通过调用隐藏类的一些方法来验证它的一些属性:

方法isHidden()将返回 true 对于此类:

Assertions.assertEquals(true, hiddenClass.isHidden());

此外,由于隐藏类没有实际名称,因此其规范名称将为 无效的:

Assertions.assertEquals(null, hiddenClass.getCanonicalName());

隐藏类将具有与执行查找的类相同的定义加载器。由于查找发生在同一类中,以下断言将成功:

Assertions.assertEquals(this.getClass()
    .getClassLoader(), hiddenClass.getClassLoader());

如果我们试图通过任何方法访问隐藏类,它们将抛出 ClassNotFoundException这是显而易见的,因为隐藏的类名非常不寻常,不符合条件,其他类无法看到。让我们检查几个断言,以证明隐藏类是不可发现的:

Assertions.assertThrows(ClassNotFoundException.class, () -> Class.forName(hiddenClass.getName()));
Assertions.assertThrows(ClassNotFoundException.class, () -> lookup.findClass(hiddenClass.getName()));

注意,其他类可以使用隐藏类的唯一方法是通过 Class 对象

6.匿名类与隐藏类

我们在前面的章节中创建了一个隐藏类,并使用了它的一些属性。现在,让我们详细说明匿名类(没有显式名称的内部类)和隐藏类之间的区别:

  • 匿名类有一个动态生成的名称,名称之间有一个$,而一个隐藏类是从 com.baeldung.reflection.identclass.HiddenClass类 衍生为 com.baeldung.reflection.identclass.HiddenClass/1234。
  • 匿名类是用Unsafe::defineAnonymousClass创建的实例,是已弃用的,而 Lookup::defineHiddenClass 是实例化隐藏类的.
  • 隐藏类不支持的常量池补丁。它有助于定义匿名类,其常量池条目已解析为具体值。
  • 与隐藏类不同,匿名类可以访问 protected 宿主类的成员,即使它位于不同的包而不是子类中。
  • 匿名类可以封闭其他类以访问其成员,但隐藏类不能封闭其他类。

虽然隐藏类不能替代匿名类,它们正在取代JDK中匿名类的一些用法。 在Java15中,lambda表达式使用隐藏类.

7.结论

在本文中,我们详细讨论了一个名为隐藏类的新语言特性。一如既往,代码可用 在GitHub上.

正文到此结束