Как зарегистрировать Java-класс, если статический инициализатор не вызывается, пока на класс не ссылаются

У меня есть интерфейс, реализованный классами, которые выполняют обработку файлов, скажем, поиск или что-то еще.

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 нетронутым.
Мне трудно придумать решение, которое не помещает некоторую форму "знания" реализаций на стороне Утилизатора (и которое остается простым), просто из-за проблемы статического инициализатора, который не вызывается. Как это побороть?

6 ответов

Вы можете посмотреть на библиотеку 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 в ваш путь к классам

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 сейчас.

Вы могли бы...

Используйте ту же концепцию, что и JDBC, для загрузки своих драйверов. Это потребует от вас использования Class#forName инициализировать класс при первой загрузке программы. Хотя это означает, что реализация все еще динамична с точки зрения вашего служебного класса, она определяется приложением во время выполнения...

Это дает вам контроль над тем, какую реализацию вы можете использовать

Вы могли бы...

Используйте ту же концепцию, что-то вроде java.awt.Toolkit использует при инициализации своего экземпляра.

Это в основном ищет ресурс (в этом случае System свойство), а затем загружает класс динамически, используя Class,

Лично я обычно ищу именованный ресурс (обычно файл свойств) и загружаю из него ключ.

Что-то вроде getClass().getResource("/some/gloabl/configFile");, что каждая реализация должна обеспечить.

Затем, если доступно, прочитайте файл свойств и найдите ключ, который мне нужен.

Если более чем одна реализация связана, хотя, нет никакой гарантии, какая из них будет загружена.

Быстро и грязно: вы можете статически инициализировать ваш Utilizer в main() с правильной ассоциацией.
Лучшее решение: вывести на внешний вид в файловой ассоциации типа

txt=path.to.package.TxProcessor

загрузить его в Utilizer и загрузить FileProcessorInterface разработчики с Class.forName()

Вы можете иметь файл реестра (например, некоторый файл XML), который будет содержать список всех поддерживаемых вами классов:

<item type="txt">somepackage.TxtProcessor</item>
<item type="gif">somepackage.GIFProcessor</item>
...

Ваш Utilizer загрузит этот файл в свой реестр.

Вы можете принудительно вызвать статическую инициализацию с помощью Class.forName(fqn, true, classLoader) или краткой формы Class.forName(fqn)

Другие вопросы по тегам