自动调用静态块,而无需显式调用Class.forName

时间:2018-10-14 11:51:21

标签: java reflection static classloader

假设以下代码:

public class Main {

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

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

我现在希望能够提供“自我配置”类。这意味着,他们应该能够简单地提供静态块之类的东西,它将像这样被自动调用:

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

如果执行此代码,则配置列表为空(由于way static blocks are executed)。该类为“可达”,but not "loaded"。您可以将以下内容添加到System.out之前的Main类中。

Class.forName("Custom");

,该列表现在将包含Custom类对象(由于尚未初始化该类,因此此调用将对其进行初始化)。但是因为控件应该是逆向的(Custom应该知道Main而不是相反),所以这不是可用的方法。永远不要直接从Main或与Main关联的任何类中调用Custom。

但是可能会发生以下情况:您可以在类中添加一个注释,并使用ClassGraph framework之类的东西并使用上述注释收集所有带有该注释的类,并在每个类上调用Class.forName。 / p>

TL; DR

有没有一种方法可以自动调用静态块,而无需分析所有类并且不需要了解具体的“自配置”类?完美的方法是,在启动应用程序后,自动初始化类(如果使用特定注释对它们进行注释)。我曾考虑过自定义ClassLoader,但据我了解,they are lazy并不适用于这种方法。

其背景是,我想将其合并到注释处理器中,该处理器创建“自配置代码”。

示例(警告:设计对话和深入探讨)

为使这一点不太抽象,请想象以下内容:

您将开发一个框架。我们称之为Foo。 Foo具有GlobalRepository和Repository类。 GlobalRepository遵循Singleton设计模式(仅静态方法)。存储库以及GlobalRepository都有一个方法“ void add(Object)”和“ T get(Class)”。如果您在存储库上调用get并且找不到该类,它将调用GlobalRepository.get(Class)。

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

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

这不是一个非常可扩展的方法。应用程序越大,搜索范围越大,时间越长等等。更好的方法是,在启动应用程序后,将自动初始化所​​有类。就像通过ClassLoader一样。我正在寻找这样的东西。

1 个答案:

答案 0 :(得分:3)

首先,不要在注册表中保存Class个对象。这些Class对象将要求您使用Reflection获得实际的操作,例如将其实例化或调用某些方法,无论如何您都需要事先了解其签名。

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

例如CharsetProvider不是实际的Charset实现,但是可以按需提供对它们的访问。因此,只要仅使用通用字符集,提供程序的现有注册表就不会消耗太多内存。

一旦定义了这样的服务接口,就可以使用标准的服务发现机制。如果jar文件或目录包含类文件,则创建一个子目录META-INF/services/,该子目录包含一个文件名作为包含实现类的合格名称的接口的合格名称。每个类路径条目可能都有这样的资源。

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

声明这种实现更加可靠
provides service.interface.name with actual.implementation.class;

模块声明中的声明。

然后,主类可能只知道接口就查找实现,

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

或者从Java 9开始

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

class documentation of ServiceLoader包含有关此体系结构的更多详细信息。当您遍历package list of the standard API并查找名称以.spi结尾的软件包时,您会发现JDK本身已经使用这种机制的频率。尽管不需要接口具有此类名称的程序包,例如通过该机制还可以搜索java.sql.Driver的实现。

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

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

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