Статьи

Модульные Java-приложения — подход микроядра


Разработка программного обеспечения — это все о
повторном использовании . Поэтому мы, программисты, любим разделять приложения на более мелкие компоненты, чтобы каждое из них можно было повторно использовать или расширять независимо.

Ключевое слово здесь — «
слабая связь ». Немного упрощенный, это означает, что каждый компонент должен иметь как можно меньше зависимостей от других компонентов. Наиболее важно, если у меня есть компонент B, который опирается на компонент A, я не хочу, чтобы A нуждался в информации о B. Компонент A должен просто предоставлять чистый интерфейс, который мог бы использоваться и расширяться B.

В Java есть множество фреймворков, которые предоставляют именно эту функциональность:
JavaEE ,
Spring ,
OSGI, Тем не менее, каждая из этих платформ имеет свой собственный способ выполнения задач и предоставляет множество дополнительных функций — хотите вы этого или нет!

Поскольку мы здесь, в
Scireum, любим модульность (мы строим 4 продукта из набора из 10 независимых модулей), мы создали нашу собственную небольшую платформу. Я выделил наиболее важные части и теперь у меня есть один класс с
менее чем 250 строками кода + комментарии !

Я называю это микроядерным подходом, поскольку он хорошо сравнивается с ситуацией, которую мы имеем с операционными системами: существуют
монолитные ядра, такие как Linux, с примерно 11
430 712 строками кода. И есть концепция под названием
микроядроКак и в одном из Minix с около
6000 строк исполняемого кода ядра. Все еще продолжается дискуссия о том, какой из двух солитонов лучше. Монолитное ядро ​​быстрее, микроядро имеет гораздо менее критичный код (критический код означает: ошибка, которая может привести к сбою всей системы. Если вы этого еще не сделали, вам следует прочитать больше о микроядрах
в Википедии .

Однако можно подумать об операционных системах — когда дело доходит до Java, я предпочитаю меньше зависимостей и, если возможно, никакой черной магии, которую я не понимаю. Особенно, если эта магия включает в себя сложные
структуры
ClassLoader . Следовательно, здесь идет
Nucleus

Как это работает?

Каркас (
Nucleus) решает две проблемы модульных приложений:

  • Я хочу предоставить сервис другим компонентам — но я только хочу показать интерфейс, и они должны быть предоставлены моей реализацией во время выполнения, не зная (ссылаясь) на нее.
  • Я хочу предоставить услугу или обратный вызов для других компонентов. Я предоставляю интерфейс и хочу знать все классы, реализующие его, чтобы я мог их вызывать.

Хорошо, нам, вероятно, нужны примеры для этого. Скажем, мы хотим реализовать простой сервис таймера. Предоставляет интерфейс:

public interface EveryMinute {
    void runTimer() throws Exception;
}

Все классы, реализующие этот интерфейс, должны вызываться каждую минуту. Кроме того, мы предоставляем некоторую информацию, а именно, когда таймер был выполнен последним.

public interface TimerInfo {
    String getLastOneMinuteExecution();
}

Хорошо, теперь нам нужен клиент для наших услуг:

@Register(classes = EveryMinute.class)
public class ExampleNucleus implements EveryMinute {

    private static Part<TimerInfo> timerInfo = 
                                     Part.of(TimerInfo.class);

    public static void main(String[] args) throws Exception {
        Nucleus.init(); 
        while (true) {
            Thread.sleep(10000);
            System.out.println("Last invocation: "
                    + timerInfo.get().getLastOneMinuteExecution());
        }
    }

    @Override
    public void runTimer() throws Exception {
        System.out.println("The time is: "
                + DateFormat.getTimeInstance().format(new Date()));
    }
}

Статическое поле «
Part <TimerInfo> timerInfo » — это простой вспомогательный класс, который извлекает зарегистрированный экземпляр из
Nucleus при первом вызове и загружает его в приватное поле. Таким образом, доступ к этой части практически не влияет на обычный доступ к полям — однако мы ссылаемся только на интерфейс, а не на реализацию.

Метод
main сначала инициализирует
Nucleus (он выполняет сканирование пути к классам и т. Д.), А затем просто переходит в бесконечный цикл, печатая последнее выполнение нашего таймера каждые десять секунд.

Поскольку наш класс носит
аннотацию
@Register , он будет обнаружен специальной
ClassLoadAction (не самой
Nucleus ), созданной и зарегистрированной для
интерфейса EveryMinute . Его метод
runTimer будет затем вызываться нашей службой таймера каждую минуту.

Хорошо, но как будет выглядеть наш TimerService?

@Register(classes = { TimerInfo.class })
public class TimerService implements TimerInfo {

    @InjectList(EveryMinute.class)
    private List<EveryMinute> everyMinute;
    private long lastOneMinuteExecution = 0;
    private Timer timer;

    public TimerService() {
        start();
    }

    public void start() {
          timer = new Timer(true);
          // Schedule the task to wait 60 seconds and then invoke
          // every 60 seconds.
          timer.schedule(new InnerTimerTask(),
                      1000 * 60, 
                      1000 * 60);
    }<span face="'Courier New',Courier,monospace" style="">

    private class InnerTimerTask extends TimerTask {

      @Override
        public void run() {
            // Iterate over all instances registered for
            // EveryMinute and invoke its </span><span face="'Courier New',Courier,monospace" style="">runTimer </span><span face="'Courier New',Courier,monospace" style="">method.
            for (EveryMinute task : everyMinute) {
                    task.runTimer();
            }</span>
            // <span face="'Courier New',Courier,monospace" style="">Update lastOneMinuteExecution 
            lastOneMinuteExecution = System.currentTimeMillis();
        }
    }

    @Override
    public String getLastOneMinuteExecution() {
        if (lastOneMinuteExecution == 0) {
            return "-";
        }
        return DateFormat.getDateTimeInstance().format(
                new Date(lastOneMinuteExecution));
    }
}</span>

Этот класс также носит
аннотацию
@Register, так что он также будет загружен
ClassLoadActionназванный выше (
ServiceLoadAction фактически). Как и выше, он будет создан и помещен в
Nucleus (как реализация
TimerInfo ). Кроме того , он носит
@InjectList аннотацию на
everyMinute поле. Это будет обработано другим классом с именем
Factory, который выполняет простое внедрение зависимостей. Поскольку его конструктор запускает таймер Java
для
InnerTimerTask , с этого момента все экземпляры, зарегистрированные для
EveryMinute, будут вызываться этим таймером — как следует из названия — каждую минуту.

Как это реализовано?

Преимущество
Nucleus в том, что он мощный, с одной стороны, но очень простой и маленький с другой. Как вы могли видеть, здесь нет внутренней части для специальных или привилегированных услуг. Все построено вокруг ядра — класса
Nuclues . Вот что он делает:

  • Он сканирует путь к классам и ищет файлы с именем » component.properties «. Они должны находиться в корневой папке JAR или в папке / src каждого проекта Eclipse соответственно. 
  • Для каждого идентифицированного элемента JAR / project / classpath он собирает все содержащиеся в нем файлы классов и загружает их, используя Class.forName .
  • Для каждого класса он проверяет, реализует ли он ClassLoadAction , если да, он помещается в специальный список.
  • Каждый ClassLoadAction создается, и каждый ранее увиденный класс отправляется ему с помощью: void handle (Class <?> Clazz)
  • Наконец, каждое действие ClassLoadAction уведомляется о том, что ядро ​​завершено, чтобы можно было выполнить последние шаги (например, внедрение зависимостей на основе аннотаций).

Вот и все. Единственное, что
предоставляет
Nucleus, — это реестр, который можно использовать для регистрации и извлечения объектов для класса. (Подробное описание вышеуказанного процесса можно найти здесь:
http://andreas.haufler.info/2012/01/iterating-over-all-classes-with.html ).

Теперь, чтобы сделать этот фреймворк пригодным для использования, как показано выше, есть набор классов вокруг
Nucleus . Наиболее важным является класс
ServiceLoadAction , который создает экземпляр каждого класса, который носит
аннотацию @Register, запускает на нем
Factory.inject (наш инструмент мини-DI) и добавляет его в
Nucleus для перечисленных классов.
Что важно :
ServiceLoadActions не имеет особых прав или привилегий, вы можете легко написать свою реализацию, которая делает вещи умнее.

Помимо некоторых аннотаций, есть три других полезных класса, когда дело доходит до извлечения экземпляров из
Nucleus :
Factory, Part и
Parts . Как отмечалось выше,
Фабрика — это простой инжектор зависимостей. В настоящее время только ServiceLoadAction автоматически использует Factory, поскольку все классы с
аннотацией
@Register сканируются на наличие необходимых инъекций. Однако вы можете использовать эту фабрику для выполнения инъекций в ваших собственных классах или других
ClassLoadActions, чтобы сделать то же самое, что
ServiceLoadAction, Если вы не можете или не хотите полагаться на магию зависимостей на основе аннотаций, вы можете использовать два вспомогательных класса
Part и
Parts . Они используются как обычные поля (см.
ExampleNucleus.timerInfo выше) и автоматически выбирают соответствующий объект или список объектов. Так как результат кэшируется, повторные вызовы почти не имеют издержек по сравнению с обычным полем.

Ядро и приведенный выше пример с открытым исходным кодом (MIT-лицензия) и доступны здесь:

https://github.com/andyHa/scireumOpen/blob/master/src/examples/ExampleNucleus.java

https://github.com/andyHa / scireumOpen / дерево / master / src / com / scireum / open / kernel

Если вы заинтересованы в использовании
NucleusЯ мог бы поместить соответствующие источники в репозиторий разделителей, а также предоставить банку релиза — просто напишите комментарий ниже и дайте мне знать.

Этот пост является четвертой частью моей серии «Предприимчивая Java» — мы делимся нашими советами и приемами, как преодолеть препятствия при попытке создать несколько мультитенантных веб-приложений из набора общих модулей.