自动调用静态块而不显式调用 Class.forName

Automatically call static block without explicitly calling Class.forName

假设以下代码:

public class Main {

    public static final List<Object> configuration = new ArrayList<>();

    public static void main(String[] args) {
        System.out.println(configuration);
    }
}

我现在希望能够,提供"self-configuring"classes。这意味着,他们应该能够简单地提供类似于静态块的东西,它将像这样自动调用:

public class Custom {
    static {
        Main.configuration.add(Custom.class);
    }
}

如果执行此代码,配置列表为空(因为way static blocks are executed). The class is "reachable", but not "loaded"。您可以在 System.out[=18= 之前的 Main class 添加以下内容]

Class.forName("Custom");

列表现在将包含自定义 class 对象(因为 class 尚未初始化,此调用会初始化它)。但是因为控件应该是反向的(Custom 应该知道 Main 而不是相反),这不是一个可用的方法。永远不应直接从 Main 或与 Main 关联的任何 class 调用自定义。

虽然以下是可能的:您可以向 class 添加注释并收集所有带有所述注释的 classes,使用类似 ClassGraph framework 的东西并调用Class.forName 他们每个人。

TL;DR

有没有一种方法可以自动调用静态块而不需要分析所有 classes 和了解具体的需要,"self configuring" class?完美的方法是,在启动应用程序时,自动初始化一个 classes(如果它们用某个注释进行注释)。我考虑过自定义 ClassLoaders,但据我了解,they are lazy 因此不适用于这种方法。

这样做的背景是,我想将它合并到注释处理器中,创建 "self configuring code"。

示例(警告:设计讨论和深入)

为了让这个不那么抽象,想象一下:

你开发了一个框架。我们称它为 Foo。 Foo 有 classes GlobalRepository 和 Repository。 GlobalRepository 遵循单例设计模式(仅限静态方法)。 Repository 和 GlobalRepository 都有一个方法 "void add(Object)" 和“T get(Class)”。如果您在存储库上调用 get 并且找不到 Class,它会调用 GlobalRepository.get(Class).

为了方便起见,您想提供一个名为@Add 的注解。这个注释可以放在类型声明上(又名 Classes)。注释处理器创建一些配置,自动将所有带注释的 classes 添加到 GlobalRepository 中,从而减少样板代码。它应该(在所有情况下)只发生一次。因此生成的代码有一个静态初始化器,其中填充了 GlobalRepository,就像您对本地存储库所做的那样。因为您的配置的名称被设计为尽可能唯一,并且出于某种原因甚至包含创建日期(这有点武断,但请注意),它们几乎不可能被猜到。

因此,您还为这些配置添加了一个注释,称为@AutoLoad。您需要使用开发人员调用 GlobalRepository.load(),然后分析所有 classes 并初始化所有带有此注释的 classes,并因此调用它们各自的静态块。

这是一种可扩展性不强的方法。应用程序越大,搜索的范围越大,时间越长等等。更好的方法是,在启动应用程序时,所有 classes 都会自动初始化。就像通过 ClassLoader。我正在寻找这样的东西。

首先,不要在注册表中保留 Class 对象。这些 Class 对象需要您使用反射来获得实际操作,例如实例化它们或调用某些方法,无论如何您都需要事先知道其签名。

标准方法是使用 interface 来描述动态组件应该支持的操作。然后,有一个实现实例的注册表。如果您将它们分为操作界面和工厂界面,这些仍然允许推迟昂贵的操作。

例如CharsetProvider is not the actual Charset 实现,但按需提供对它们的访问。所以现有的provider registry只要使用common charsets就不会消耗太多内存。

一旦定义了这样的服务接口,就可以使用标准的服务发现机制。对于包含 class 文件的 jar 文件或目录,您可以创建一个子目录 META-INF/services/,其中包含一个文件名作为接口的限定名称,该接口包含实现的限定名称 classes。每个 class 路径条目可能有这样一个资源。

在 Java 模块的情况下,您可以使用

声明这样的实现更加健壮
provides service.interface.name with actual.implementation.class;

模块声明中的语句。

然后,主要class可能会查找实现,只知道接口,如

List<MyService> registered = new ArrayList<>();
for(Iterator<MyService> i = ServiceLoader.load(MyService.class); i.hasNext();) {
    registered.add(i.next());
}

或者,从 Java9

开始
List<MyService> registered = ServiceLoader.load(MyService.class)
    .stream().collect(Collectors.toList());

class documentation of ServiceLoader contains a lot more details about this architecture. When you go through the package list of the standard API 寻找名称以 .spi 结尾的包,您会知道,这种机制在 JDK 本身中使用的频率。但是,接口不需要位于具有此类名称的包中,例如java.sql.Driver 的实现也通过此机制进行搜索。

从 Java 9 开始,您甚至可以使用它来执行诸如“为所有具有特定注释的 class 查找 Class 对象”之类的操作,例如

List<Class<?>> configuration = ServiceLoader.load(MyService.class)
    .stream()
    .map(ServiceLoader.Provider::type)
    .filter(c -> c.isAnnotationPresent(MyAnnotation.class))
    .collect(Collectors.toList());

但由于这仍然需要 classes 实现服务接口并被声明为接口的实现,因此最好使用接口声明的方法与模块交互。