如何注册一个Java类,如果静态初始化程序直到引用类时才被调用?

11

我有一个由类实现的接口,用于执行文件处理,比如搜索或其他操作。

public interface FileProcessorInterface {

    public void processFile(String fileName);

}

然后我针对每种文件类型都有不同的实现:

public class TxtProcessor implements FileProcessorInterface {

    @Override public void processFile(String fileName) { //do the work }

}

因此我拥有处理器的Utilizer,它有一个方法可以注册每个类,类似于以下内容:

class Utilizer {

Map <String, Class> registered = new HashMap<>();

public void registerClass(String fileExt, Class clazz) {

    registered.put(fileExt, clazz);

}

public void processFile(String fileName) {

    //1) get the registered class from registered map (omitted because easy and not relevant)

    //2) create an instance of the class using reflection (omitted because easy and not relevant)
    FileProcessorInterface p = ....

    p.processFile(fileName);             

}

到目前为止还好。

现在,我提供了很多我的接口实现。

我很想为每个实现类提供一个静态初始化程序,在Utilizer中注册自己,在之前的TxtProcessor中它是这样的:

class TxtProcessor implements FileProcessorInterface {

    //previous code

    static {
        Utilizer.registerClass("txt", TxtProcessor.class);
    }

}
问题在于这个静态方法永远不会被调用,因为在应用程序的“静态可达”代码中没有引用到我的TxtProcessor类,因为它是通过反射实例化的。因此,JVM不会调用静态初始化程序。
假设我有两个部分:通用代码是Utilizer,实现是另一部分;它必须被视为动态提供的东西,因此Utilizer部分并不知道它。事实上,想法正是每个类都会注册自己,使Utilizer保持不变。
对于我来说,很难构思出一种不需要在Utilizer端放置某些形式的实现“知识”(并且保持简单)的解决方案,正是由于静态初始化程序未被调用的问题。如何克服这个问题?
6个回答

5
使用 reflections 应该是最适合的。它就像专为这个而设计的。
你只需要在 Utilizer 中添加一个小的 静态 块,如下:
static {

    Reflections reflections = new Reflections(
                new ConfigurationBuilder()
               .setUrls(ClasspathHelper.forPackage("path.to.all.processors.pkg"))
               .setScanners(new SubTypesScanner())
           );

    reflections.getSubTypesOf(path.to.all.processors.pkg.FileProcessor.class);
}

如果您不想依赖第三方库,只需将 FileProcessors.properties 文件添加到您的 classpath 中。
txt=path.to.all.processors.pkg.TxtProcessor
doc=path.to.all.processors.pkg.DocProcessor
pdf=path.to.all.processors.pkg.PdfProcessor

然后将Utilizer中列出的所有类注册为

static {
    Properties processors = new Properties();
    try {
        processors.load(Utilizer.class
                  .getResourceAsStream("FileProcessors.properties"));
    } catch (IOException e) {
        e.printStackTrace();
    }
    for (String ext : processors.stringPropertyNames()) {
        Utilizer.registerClass(ext, Class.forName(processors.getProperty(ext));
    }
}

现在不再需要在每个FileProcessor中使用静态块。


如何在不知道包名称的情况下指定包名称(例如“path.to.all.processors.pkg”)? 代码可以添加到任何项目中,我们不能让开发人员需要开始编辑代码或配置文件。 我们需要能够找到所有相关的类。 - Ian Boyd

5
你可以查看 Reflections 库。它允许你查找所有实现了一个接口、具有注释或扩展了一个类的类。

1
+1,Reflections +注释似乎很适合这里,并且如果它不是真正可配置的内容,则避免了配置/资源文件。 - Trevor Freeman

3

你可以...

使用与JDBC相同的概念来加载驱动程序。这将要求您使用Class#forName在程序首次加载时初始化类。虽然这意味着从你的实用程序类的角度来看,实现仍然是动态的,但它由应用程序在运行时指定...

这使您可以控制要使用哪个实现。

你可以...

使用类似于java.awt.Toolkit在初始化其实例时使用的概念。

它基本上查找资源(在这种情况下是一个System属性),然后使用Class动态加载类。

通常,我会寻找命名资源(通常是属性文件)并从中加载密钥。

例如:getClass().getResource("/some/gloabl/configFile");,每个实现都需要提供。

然后,如果可用,读取属性文件并查找我需要的键。

如果链接了多个实现,就无法保证加载哪个实现。


1

快速而简单的方法:您可以在main()中静态地初始化Utilizer并正确关联它。
更好的解决方案:将关联内容外部化到资源文件中,例如

txt=path.to.package.TxProcessor

将其加载到Utilizer中,并使用Class.forName()加载FileProcessorInterface实现程序。


这对于纯第三方类并不起作用。 - RecursiveExceptionException

1
你可以通过Class.forName(fqn, true, classLoader)或简写形式Class.forName(fqn)强制进行静态初始化。

0
你可以拥有一个注册表文件(例如,某个XML文件),其中包含您支持的所有类的列表:
<item type="txt">somepackage.TxtProcessor</item>
<item type="gif">somepackage.GIFProcessor</item>
...

你的 Utilizer 会将这个文件加载到它的注册表中。

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