Как разработать клиентские интерфейсы на React и JavaFX, которые могут работать с серверным приложением Coherence, которое мы создали ранее

Эта статья изначально была опубликована в Java Magazine 9 октября 2020 г.

В первой статье этой серии я показал, как реализовать REST API, который управляет задачами списка дел, хранящимися в Coherence. Мы протестировали его с помощью curl и знаем, что он работает, но давайте посмотрим правде в глаза - curl не самый удобный интерфейс на планете, и если мы хотим, чтобы наше приложение покорило мир, нам нужно добиться большего.

Эта статья будет основываться на внутреннем коде из предыдущей статьи путем реализации двух клиентов: веб-интерфейса на основе React (см. рисунок 1) и пользовательского интерфейса рабочего стола, использующего JavaFX.

Клиентские приложения и Coherence

Во-первых, давайте рассмотрим фундаментальную тему: как клиентские приложения подключаются к Coherence и получают доступ к данным?

Coherence поддерживает два типа клиентов: клиенты-участники кластера и удаленные клиенты.

REST API, реализованный в предыдущей статье, является примером клиента-члена кластера, для которого может быть либо включено хранилище, либо отключено хранилище. Эти типы членов по существу идентичны, за исключением того, что члены с отключенным хранением не хранят никаких данных локально.

Это то, на что я ссылался в прошлой статье, когда упоминал, что этот проект может разделить хранилище данных и сервер приложений: я мог бы запустить веб-серверы Helidon, которые обслуживают REST API, как с отключенным хранилищем. участников, отдельно от участников с возможностью хранения, которые управляют всеми данными. Иногда это имеет смысл, потому что это обеспечивает лучшую изоляцию между ними и позволяет им масштабироваться независимо. Для простоты я решил этого не делать, по крайней мере, пока.

Преимущество использования клиентов-членов кластера состоит в том, что они полностью осведомлены о топологии кластера и назначении разделов. Это позволяет клиентам напрямую обращаться к любому объекту данных, хранящемуся в Coherence, через один сетевой переход. Обратной стороной является то, что эти клиенты должны находиться в той же высокоскоростной сети с малой задержкой, что и остальная часть кластера, и они могут дестабилизировать весь кластер, если они начнут действовать и перестанут отвечать.

Удаленные клиенты, с другой стороны, работают немного иначе. Они подключаются не напрямую ко всем остальным членам кластера, а к прокси-серверу, который обычно является членом кластера с отключенным хранилищем.

Традиционно Coherence поддерживает два типа прокси: Coherence * Extend и Coherence REST-прокси. Теперь есть еще третий тип: Coherence gRPC.

Coherence * Extend - это проприетарный протокол RPC на основе TCP, который используется в существующих клиентских реализациях Oracle, Java, .NET и C ++. Coherence * Extend существует уже давно (с 2006 г.), поддерживается многими версиями Oracle Coherence с обратной и прямой совместимостью, и это было доказано во многих критически важных приложениях. С другой стороны, Coherence * Extend является проприетарным и синхронным, он не всегда хорошо работает с современными облачными развертываниями, а некоторые из клиентов, реализованных поверх него (в частности, .NET-клиенты), немного устарели и не работают. использовать многие из последних функций поддерживаемых языков.

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

Откровенно говоря, хотя прокси-сервер REST имел свое предназначение, я действительно не вижу в нем особой необходимости или пользы. Так же легко, если не проще, реализовать собственный API REST для конкретного приложения, который может бесплатно использовать полный Java API, как в предыдущей статье, и предоставить его либо через интеграцию с Helidon (рекомендуется), либо через встроенный HTTP-сервер.

Coherence gRPC - это третья реализация прокси, представленная в последней версии Coherence CE 20.06.

Coherence gRPC - это реализация прокси, которая использует gRPC в качестве транспорта и является жизнеспособной альтернативой Coherence * Extend. Этот прокси построен на основе Helidon gRPC Server и дает ряд непосредственных преимуществ: он намного лучше работает с современными облачными развертываниями, он поддерживается различными балансировщиками нагрузки HTTP и контроллерами входящего трафика Kubernetes, он асинхронен, а gRPC Сам по себе поддерживается практически всеми соответствующими платформами и языками.

На данный момент Oracle предлагает только собственный Java-клиент для Coherence gRPC, который я буду использовать для реализации клиента JavaFX в ближайшее время, но, если повезет, скоро выйдет собственный клиент Node.js / JavaScript, а затем современные клиенты .NET и C ++, а также новые клиенты Python, Golang и Swift.

А если вам нужен клиент для другой платформы, которая официально не поддерживается, вы сможете написать его самостоятельно. Protobuf Определения для служб и сообщений, поддерживаемых Coherence, уже общедоступны, и вы сможете использовать существующие клиентские реализации в качестве руководства при реализации своих собственных.

Преимущества удаленных клиентов заключаются в том, что они не обязательно должны находиться в одной сети, их может быть столько, сколько вам нужно, они могут приходить и уходить в любое время, не влияя на членство в кластере, и они, очевидно, могут быть записаны на языки, отличные от Java. Обратной стороной является то, что каждый запрос от клиента должен проходить через прокси-сервер, который добавляет дополнительный сетевой переход и соответствующую задержку для каждой операции.

Теперь пора внедрять клиентов.

Реализация клиента React

Как я признал в предыдущей статье, на самом деле я не являюсь фронтенд-разработчиком, и выбор React несколько случаен и в основном обусловлен тем, с чем я (едва) знаком. Вы можете легко реализовать аналогичный интерфейс, используя Angular, Vue.js или любой другой популярный интерфейсный фреймворк.

Фактически, мой коллега Тим Миддлтон уже реализовал другой веб-интерфейс с использованием Oracle JavaScript Extension Toolkit (Oracle JET), который использует ту же идею привязки пользовательского интерфейса к модели данных, которая обновляется через события. Если вас это интересует, исходный код клиента Oracle JET доступен на GitHub.

У всех этих фреймворков есть одна общая черта: они позволяют использовать стандартную цепочку инструментов разработки Node.js для создания и тестирования пользовательского интерфейса, и, как только вы будете удовлетворены, вы можете «скомпилировать» приложение в набор статических HTML, JavaScript, и файлы CSS, которые могут обслуживаться любым веб-сервером, способным обслуживать статический контент.

Таким образом, пример приложения может использовать тот же веб-сервер Helidon, который обслуживает REST API, для обслуживания статического внешнего интерфейса. Такой подход немного упрощает приложение; нет отдельного сервера для развертывания и управления, и нет проблем с совместным использованием ресурсов между источниками (CORS), которые нужно решать, потому что и интерфейс, и REST API имеют одно и то же происхождение.

Настройка клиента React

Чтобы создать клиент React, мне нужно ответить на несколько вопросов:

  • Где будет исходник для фронтенда?
  • Как мне создать интерфейс как часть существующей сборки Maven?
  • Как будет упакован «скомпилированный» интерфейс, чтобы Helidon мог его обслуживать?

Я отвечу на эти вопросы в обратном порядке.

Helidon может обслуживать статический контент либо из файловой системы, либо из пути к классам. Последним гораздо проще управлять, поэтому я упакую все статические файлы для внешнего интерфейса в файл JAR сервера и настрою Helidon для обслуживания содержимого оттуда, добавив файл META-INF/microprofile-config.properties в проект:

server.static.classpath.location=/web
server.static.classpath.welcome=index.html

Вот и все - Helidon теперь приказано обслуживать статический контент из веб-каталога в пути к классам и использовать index.html в качестве файла приветствия по умолчанию.

Как я буду создавать интерфейсную часть как часть сборки Maven, чтобы гарантировать, что статический контент для внешнего интерфейса окажется там, где его ожидает Helidon? Для этого я воспользуюсь вашим искренним npm-maven-plugin, который включает сборку на основе npm в сборку Maven:

<plugin>
  <groupId>com.seovic.maven.plugins</groupId>
  <artifactId>npm-maven-plugin</artifactId>
  <version>1.0.4</version>
  <executions>
    <execution>
      <id>build-frontend</id>
      <goals>
        <goal>run</goal>
      </goals>
      <phase>generate-resources</phase>
      <configuration>
        <workingDir>${project.basedir}/src/main/web</workingDir>
        <script>build</script>
      </configuration>
    </execution>
  </executions>
</plugin>

Приведенный выше код будет запускать сценарий сборки, определенный в package.json, на этапе создания ресурсов сборки Maven, и в нем говорится, что исходный код для внешнего интерфейса будет находиться в каталоге src/main/web внутри серверного проекта. См. рисунок 2.

Обратите внимание, что интерфейсное приложение (и его разработчики) совершенно не осведомлены об окружающей структуре Maven. Для них выделенный выше веб-каталог является корнем их проекта JavaScript.

Хотя это облегчает разработчикам внешнего интерфейса выполнение их работы без необходимости изучать Maven, это означает, что после создания внешнего интерфейса необходимо скопировать сгенерированные статические файлы в структуру, понятную Maven. К счастью, это легко сделать с помощью стандартного maven-resources-plugin:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-resources-plugin</artifactId>
  <version>3.1.0</version>
  <executions>
    <execution>
      <id>copy-frontend</id>
      <phase>generate-resources</phase>
      <goals>
        <goal>copy-resources</goal>
      </goals>
      <configuration>
        <outputDirectory>
           ${project.build.directory}/classes/web
        </outputDirectory>
        <resources>
          <resource>
            <directory>
              ${project.basedir}/src/main/web/build
            </directory>
            <filtering>true</filtering>
          </resource>
        </resources>
      </configuration>
    </execution>
  </executions>
</plugin>

Вот и все. Теперь, когда структура проекта сформирована, пора приступить к реализации.

Реализация клиента React

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

Здесь основное внимание уделяется аспекту управления состоянием клиентского приложения и взаимодействию между интерфейсным приложением и внутренним REST API, созданным в предыдущей статье.

Я буду использовать Redux для управления состоянием локально на клиенте. Это не единственный вариант, но он прекрасно вписывается в управляемую событиями архитектуру, которую реализует это приложение. Redux использует действия, которые отправляются в reducer для обновления состояния приложения. Технически Redux на самом деле ничего не обновляет, потому что он рассматривает состояние как неизменное; скорее, он создает новое состояние на основе существующего состояния и полученного действия.

Если все это немного неясно (я знаю, что мне потребовалось время, чтобы разобраться в этом), посмотрите на редуктор для клиентского представления списка дел. Если повезет, это должно прояснить ситуацию.

export default function todos(state = [], action = null) {
  switch (action.type) {
    case INIT_TODOS:
      return action.todos || [];
    case ADD_TODO:
      return [
        {
          id: action.id,
          createdAt: action.createdAt,
          completed: false,
          description: action.description
        },
        ...state
      ];
    case DELETE_TODO:
      return state.filter(todo =>
        todo.id !== action.id
      );
    case UPDATE_TODO:
      return state.map(todo =>
        todo.id === action.id ?
          { ...todo, description: action.description } :
          todo
      );
    case COMPLETE_TODO:
      return state.map(todo =>
        todo.id === action.id ?
          { ...todo, completed: action.completed } :
          todo
      );
    default:
      return state;
  }
}

Вышеупомянутая функция определяет reducer для состояния списка дел. Каждый case в операторе switch обрабатывает свой тип действия и возвращает новое состояние на основе текущего состояния и полученных полезных данных действия. Всякий раз, когда состояние изменяется, пользовательский интерфейс реагирует на него (есть причина, по которой фреймворк называется React), обновляя себя соответствующим образом.

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

  • Инициализировать локальное состояние при загрузке приложения
  • Обновить локальное состояние на основе событий, полученных от сервера

Вот код для выполнения обеих задач в основном App.jsкомпоненте приложения React:

let initialized = false;
function init(actions) {
    actions.fetchAllTodos();
    // register for server-side events
    let source = new EventSource('/api/tasks/events');
    source.addEventListener("insert", (e) => {
      let todo = JSON.parse(e.data);
      actions.addTodo(todo.id, todo.createdAt, todo.description);
    });
    source.addEventListener("update", (e) => {
      let todo = JSON.parse(e.data);
      actions.updateTodo(todo.id, todo.description, todo.completed);
    });
    source.addEventListener("delete", (e) => {
      let todo = JSON.parse(e.data);
      actions.deleteTodo(todo.id);
    });
    source.addEventListener("end", (e) => {
      console.log("end");
      source.close();
    });
    initialized = true;
}
const App = ({todos, actions}) => {
    if (!initialized) {
      init(actions);
    }
    return (
        <div>
        <Header />
        <TodoInput addTodo={actions.addTodoRequest}/>
        <MainSection todos={todos} actions={actions}/>
        </div>
    )
};

Приведенная выше функция init решает обе эти задачи. Сначала он вызывает действие fetchAllTodos, которое вызывает REST API и отправляет результаты редуктору:

export const initTodos = (todos) => ({type: types.INIT_TODOS, todos});
export function fetchAllTodos() {
  return (dispatch) => {
    request
      .get('/api/tasks')
      .end(function (err, res) {
        console.log(err, res);
        if (!err) {
          dispatch(initTodos(res.body));
        }
      });
  }
}

Затем он регистрирует источник событий с конечной точкой /api/tasks/events, реализованной на сервере, и обрабатывает каждое событие, отправляя его редуктору Redux.

Сами действия разделены на две группы: те, которые обновляют локальное состояние, управляемое Redux, и те, которые делают удаленные вызовы REST API для обновления состояния на стороне сервера в Coherence.

Остальные локальные действия аналогичны действию initTodos выше и просто добавляют соответствующий тип действия к полезной нагрузке, чтобы их можно было применить редуктором. Например, действие addTodo определяется следующим образом:

export const addTodo = (id, createdAt, description) => 
               ({type: types.ADD_TODO, id, createdAt, description});

С другой стороны, удаленные действия просто вызывают REST-вызовы на сервер без непосредственного обновления состояния Redux. Вместо этого они полагаются на зарегистрированные ранее прослушиватели событий для применения любых изменений состояния на стороне сервера к локальному состоянию.

Например, действие addTodoRequest, переданное в TodoInputcomponent выше, просто отправляет запрос и регистрирует ответ:

export function addTodoRequest(text) {
  return (dispatch) => {
    request
      .post('/api/tasks')
      .send({description: text})
      .end(function (err, res) {
        console.log(err, res);
      });
  }
}

Затем фактическая задача в формате JSON принимается и добавляется в состояние Redux обработчиком события вставки, определенным ранее:

source.addEventListener("insert", (e) => {
  let todo = JSON.parse(e.data);
  actions.addTodo(todo.id, todo.createdAt, todo.description);
});

Этот реактивный, управляемый событиями подход имеет два важных последствия:

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

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

На этом завершается реализация интерфейса React. Затем я реализую клиент JavaFX.

Реализация клиента JavaFX

В значительной степени клиент JavaFX (см. рисунок 3) очень похож на серверную реализацию REST API из предыдущей статьи. Он использует тот же NamedMap API, он наблюдает за событиями Coherence, и многие методы доступа к данным точно такие же.

Однако есть несколько отличий, которые я объясню. Но сначала мне нужно настроить проект.

Настройка проекта

Я буду реализовывать клиент JavaFX как еще один модуль в рамках проекта Maven, поэтому лучше всего начать с клиентского POM-файла:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.oracle.coherence.examples</groupId>
  <artifactId>todo-list-client</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <properties>
    <coherence.groupId>com.oracle.coherence.ce</coherence.groupId>
    <coherence.version>20.06</coherence.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>${coherence.groupId}</groupId>
      <artifactId>coherence-java-client</artifactId>
      <version>${coherence.version}</version>
    </dependency>
    <dependency>
      <groupId>${coherence.groupId}</groupId>
      <artifactId>coherence-json</artifactId>
      <version>${coherence.version}</version>
    </dependency>
    <!-- JavaFX dependencies -->
    <dependency>
      <groupId>org.openjfx</groupId>
      <artifactId>javafx-controls</artifactId>
      <version>14.0.2.1</version>
    </dependency>
    <dependency>
      <groupId>org.openjfx</groupId>
      <artifactId>javafx-fxml</artifactId>
      <version>14.0.2.1</version>
    </dependency>
    <!-- CDI support -->
    <dependency>
      <groupId>de.perdoctus.fx</groupId>
      <artifactId>javafx-cdi-bootstrap</artifactId>
      <version>2.0.0</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.weld.se</groupId>
      <artifactId>weld-se-core</artifactId>
      <version>3.1.4.Final</version>
    </dependency>
  </dependencies>
</project>

В приведенном выше коде нет ничего удивительного или очень интересного. Обратите внимание, что он включает поддержку Coherence Java Client и сериализации JSON, а также зависимости, необходимые для JavaFX, а также для поддержки контекстов и внедрения зависимостей (CDI).

Однако этого недостаточно, потому что у клиента еще нет прокси, который позволил бы ему взаимодействовать со стороной сервера. Чтобы исправить это, добавьте следующие зависимости в файл POM server:

<dependency>
  <groupId>${coherence.groupId}</groupId>
  <artifactId>coherence-grpc-proxy</artifactId>
  <version>${coherence.version}</version>
</dependency>
<dependency>
  <groupId>${coherence.groupId}</groupId>
  <artifactId>coherence-json</artifactId>
  <version>${coherence.version}</version>
</dependency>

Подводя итог, есть две новые зависимости:

  • Прокси-сервер Coherence gRPC, содержащий реализацию службы gRPC, необходимую клиенту gRPC.
  • Coherence JSON, который является той же зависимостью, что и от клиента, и обеспечивает поддержку сериализации JSON, которую я хочу использовать между клиентом и сервером.

Вот и все. Прокси-сервер Coherence gRPC построен поверх Helidon gRPC Server, который будет добавлен в качестве транзитивной зависимости. Как и веб-сервер Helidon, сервер Helidon gRPC будет загружаться с помощью CDI при запуске, если он присутствует в пути к классам, и любые обнаруженные службы gRPC будут развернуты автоматически. Больше нечего делать, поскольку конфигурация Helidon gRPC Server по умолчанию отлично подходит для целей этого проекта.

Помимо создания проекта Maven, мне нужно включить CDI, создав файл META-INF/beans.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                           http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
        version="2.0"
        bean-discovery-mode="annotated"/>

Теперь, когда есть клиентский проект и необходимые зависимости добавлены в серверный проект, я могу начать реализацию клиента.

Реализация

Как и в случае с клиентом React, я не буду обсуждать реализацию пользовательского интерфейса; Вместо этого я сосредоточусь на коде, который взаимодействует с Coherence. Первым шагом является создание класса модели данных для задач, который практически идентичен классу на сервере:

package com.oracle.coherence.examples.todo.client;
public class Task
    {
    private String id;
    private long createdAt;
    private String description;
    private Boolean completed;
    /**
     * Construct Task instance.
     *
     * @param description  task description
     */
    public Task(String description)
        {
        this.id = UUID.randomUUID().toString().substring(0, 6);
        this.createdAt = System.currentTimeMillis();
        this.description = description;
        this.completed = false;
        }
    // accessors omitted for brevity
    }

На самом деле существует только два различия между реализацией класса Task на стороне сервера и на стороне клиента. Во-первых, клиентский класс не реализует интерфейс Serializable, который на самом деле не имеет большого значения. Однако второе отличие важно и требует обсуждения.

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

Это одна из причин использования JSON для маршалинга данных между клиентом и сервером: он позволяет мне десериализовать совместимые полезные данные в разные классы на обоих концах конвейера. То же самое было бы, если бы я выбрал Portable Object Format (POF), который мог бы быть лучшим выбором с точки зрения производительности, но меня это не особо беспокоит в этом примере приложения.

Однако остается еще одна проблема: в отличие от реализованного ранее REST API, который может определять класс, который будет использоваться при десериализации полезной нагрузки JSON из строго типизированных сигнатур методов JAX-RS, NamedMap<K,V> Coherence является универсальным interface, что делает невозможным определение типа. Чтобы решить эту проблему, Coherence JSON по умолчанию включает информацию о типе в полезные данные JSON во время сериализации через метасвойство @class.

Например, полезная нагрузка JSON для сериализованного экземпляра на стороне сервера может выглядеть примерно так:

{
  "@class": "com.oracle.coherence.examples.todo.server.Task",
  "id": "a3f764",
  "completed": true,
  "createdAt": 1596105656378,
  "description": "Write an article"
}

Вы видите проблему? Имя класса не существует на клиенте, встроенном в полезные данные JSON. К счастью, в отличие от сериализации Java, Coherence JSON обеспечивает поддержку псевдонима типов. Это позволяет регистрировать разные классы на сервере и клиенте под одним и тем же псевдонимом, чтобы полезная нагрузка JSON была совместима с обоими.

Для этого реализуйте GensonBundleProvider как на клиенте, так и на сервере:

public class JsonConfig
        implements GensonBundleProvider
    {
    @Override
    public GensonBundle provide()
        {
        return new GensonBundle()
            {
            public void configure(GensonBuilder builder)
                {
                builder.addAlias("Task", Task.class);
                }
            };
        }
    }

За исключением имени пакета, которое не показано выше, классы идентичны и используют ту реализацию, которая Task доступна в пути к классам.

Затем добавьте файл com.oracle.coherence.io.json.GensonBundleProvider в каталог META-INF/services, чтобы пользовательские поставщики могли быть обнаружены загрузчиком служб и настроены с использованием содержимого com.oracle.coherence.examples.todo.server.JsonConfig на сервере и com.oracle.coherence.examples.todo.client.JsonConfig на клиенте.

После того, как необходимая конфигурация JSON будет на месте, полезная нагрузка станет

{
  "@class": "Task",
  "id": "a3f764",
  "completed": true,
  "createdAt": 1596105656378,
  "description": "Write an article"
}

И его можно десериализовать на клиенте и на сервере, успешно используя локально зарегистрированную реализацию класса Task.

Обратите внимание, что Coherence JSON использует встроенную, сильно настроенную версию сериализатора Genson JSON. Встроенный сериализатор Genson находится в другом пакете, чтобы предотвратить конфликты в случае, если официальная версия Genson также используется приложением.

По большей части вам не следует об этом заботиться. Пока вы аннотируете свои классы данных с помощью аннотаций JSON-B или Jackson (при необходимости), все должно работать, и вы можете использовать другие реализации JSON для других целей. Фактически, как только это будет сделано, REST API будет использовать эталонную реализацию JSON-B, Eclipse Yasson, предложенную Helidon.

Разобравшись с этим, пора вернуться к реализации клиента. Вся важная логика находится внутри класса TaskManager:

@ApplicationScoped
public class TaskManager
    {
    /**
     * A {@link Filter} to retrieve completed tasks.
     */
    private static final Filter<Task> COMPLETED = 
            Filters.equal("completed", true);
    /**
     * A {@link Filter} to retrieve active tasks.
     */
    private static final Filter<Task> ACTIVE = 
            Filters.equal("completed", false);
    /**
     * Tasks map.
     */
    @Inject
    @Remote
    private NamedMap<String, Task> tasks;
    ...
    }

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

Однако есть одно важное отличие. Чтобы внедрить NamedMap, полученный клиентом Coherence Java gRPC, мне нужно добавить квалификатор @Remote в точку внедрения. Если бы я этого не сделал, расширение Coherence CDI внедрило бы реализацию по умолчанию NamedMap, и клиент JavaFX попытался бы присоединиться к кластеру, что не, что я хочу.

Чтобы приведенный выше код работал, я должен настроить клиент gRPC для использования правильного сеанса.

Сеанс определяет, какой канал gRPC использовать для подключения к серверу, а также какой сериализатор использовать. Итак, давайте добавим следующий application.yaml файл в каталог src/main/resources клиентского модуля:

coherence:
  sessions:
    - name: default
      serializer: json
      channel: default

Этот файл настраивает клиента на использование сериализатора JSON и канала gRPC по умолчанию, поэтому клиент попытается подключиться к серверу gRPC на localhost:1408.

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

Хорошая новость заключается в том, что, поскольку Helidon MP Config используется для конфигурации, я могу легко переопределить любое из приведенных выше значений (или добавить новые), используя системные свойства или переменные среды. Но оставим это и для следующей статьи.

Обратите внимание, что клиент явно настроен на использование сериализатора JSON, но я не делал ничего подобного на прокси. Хорошая новость в том, что мне не нужно. Прокси-сервер поддерживает все доступные форматы сериализации, которые обнаруживаются либо CDI, либо загрузчиком служб, и он будет использовать тот сериализатор, который клиент указывает ему использовать.

Теоретически клиент может использовать другой формат для каждого запроса, но на практике каждый клиент, скорее всего, будет использовать тот же формат, который был настроен в сеансе выше, на время своего подключения к прокси.

Теперь продолжим реализацию TaskManager и рассмотрим некоторые методы доступа к данным:

public void addTodo(String description)
    {
    Task todo = new Task(description);
    tasks.put(todo.getId(), todo);
    }
public Collection<Task> getAllTasks()
    {
    return tasks.values();
    }
public Collection<Task> getActiveTasks()
    {
    return tasks.values(ACTIVE);
    }
public Collection<Task> getCompletedTasks()
    {
    return tasks.values(COMPLETED);
    }
public void removeTodo(String id)
    {
    tasks.remove(id);
    }
public void removeCompletedTasks()
    {
    tasks.invokeAll(COMPLETED, Processors.remove(Filters.always()));
    }
public void updateCompleted(String id, Boolean completed)
    {
    tasks.invoke(id, Processors.update("setCompleted", completed));
    }
public void updateText(String id, String description)
    {
    tasks.invoke(id, Processors.update("setDescription", description));
    }

Все вышеперечисленное очень похоже на код, реализованный в REST API. Для реализации базовых операций CRUD с tasksmap в коде используются стандартные Map API, такие как put, remove и values, а также NamedMap API, такие как invoke, invokeAll и values перегрузки, которые принимают фильтр.

Агрегаторы Coherence. В TaskManager есть два дополнительных метода, которые используют функцию Coherence, о которой я еще не говорил: агрегаторы.

public int getActiveCount()
    {
    return tasks.aggregate(ACTIVE, Aggregators.count());
    }
public int getCompletedCount()
    {
    return tasks.aggregate(COMPLETED, Aggregators.count());
    }

Агрегаторы Coherence позволяют выполнять параллельные агрегаты в стиле MapReduce в кластере.

В приведенном выше примере используется очень простой агрегатор Count, который просто возвращает количество записей, удовлетворяющих указанному фильтру, но есть много других встроенных агрегаторов, которые позволяют вам находить минимальные, максимальные и средние значения определенного атрибута или даже для группировки записей по некоторому атрибуту и ​​выполнения другого агрегирования внутри каждой группы записей. Вы также можете реализовать свои собственные агрегаторы.

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

В приведенном выше примере каждый член будет определять количество локальных записей, удовлетворяющих указанным критериям, и возвращать частичный результат корневому агрегатору, выполняющемуся на клиенте (или в этом case на прокси-сервере gRPC), который затем объединит эти частичные результаты в окончательный результат.

В этом случае конечный результат - это скалярное значение, но это может быть буквально что угодно.

Кстати, Coherence также предоставляет настраиваемую реализацию Stream API, представленного в Java 8, который построен на основе агрегаторов.

Когда вы используете Coherence Remote Stream API, определение потокового конвейера будет отправлено всем членам кластера с помощью настраиваемого агрегатора, и оно будет выполняться параллельно, не в одной JVM и на нескольких ядрах ЦП, но, возможно, через сотни JVM и тысячи ядер.

Наконец, так же, как мне пришлось наблюдать события Coherence в REST API и преобразовывать их в события, отправленные сервером (SSE), которые может использовать веб-интерфейс, мне нужно сделать что-то подобное здесь. Разница в том, что вместо преобразования Coherence MapEvents в события SSE, код преобразует их в стандартные события CDI. Таким образом, реализация пользовательского интерфейса JavaFX может оставаться полностью независимой от Coherence и просто наблюдать за событиями CDI по мере их публикации:

@Inject
private Event<TaskEvent> taskEvent;
/**
 * Convert Coherence map events to CDI events.
 */
void onTaskEvent(@Observes @MapName("tasks") 
                 MapEvent<String, Task> event)
    {
    taskEvent.fire(
        new TaskEvent(event.getOldValue(), event.getNewValue()));
    }

На этом завершается реализация клиента JavaFX. Пришло время проверить, работают ли новые клиенты должным образом, что позволит вам избавиться от curl раз и навсегда.

Запуск клиентов

Если вы еще этого не сделали, вы должны получить код для примера с GitHub, потому что в этой статье я не охватил весь код, необходимый для запуска приложения.

Чтобы получить доступ к клиенту React, вы должны создать и запустить сервер, который отвечает за обслуживание интерфейсного приложения и REST API, от которого зависит интерфейс.

В предыдущей статье мне удалось просто запустить сервер в среде IDE. Но теперь вам нужно построить сервер с помощью Maven, чтобы создать интерфейс и упаковать его в файл JAR сервера. Если у вас установлена ​​последняя версия Node.js и npm, это можно легко сделать, просто запустив mvn install в каталоге сервера.

Сначала вам нужно запустить npm install в каталоге server/src/main/web, чтобы установить необходимые клиентские зависимости. Сделать это нужно только один раз.

Затем вы можете запустить сервер в среде IDE, как и раньше, или из командной строки, запустив mvn exec:exec. Если все пойдет хорошо, вы должны увидеть запуск сервера, а через несколько секунд вы должны увидеть то же сообщение журнала Helidon, которое упоминалось в первой статье:

2020.08.11 03:16:00 INFO io.helidon.microprofile.server.ServerCdiExtension Thread[main,5,main]: Server started on http://localhost:7001 (and all other host addresses) in 11967 milliseconds (since JVM startup).

Теперь вы можете получить доступ к интерфейсу React (см. Рисунок 4), просто перейдя к http: // localhost: 7001 /, как следует из сообщения журнала выше.

Не стесняйтесь играть с приложением и создавать задания. Если вы хотите сделать его более интересным, откройте приложение в нескольких окнах браузера и посмотрите, как все они синхронизируются с помощью событий, когда вы вносите изменения.

Наконец, запустите клиент JavaFX, запустив mvn install в каталоге клиента, а затем запустите mvn javafx:run. Вы должны увидеть пользовательский интерфейс, аналогичный изображенному на рисунке 5.

Очевидно, что первоначальный список задач будет зависеть от задач, которые вы создали ранее с помощью клиента React. Как и раньше, добавьте несколько задач и измените существующие и посмотрите, как оба приложения остаются синхронизированными при внесении изменений.

Или же, если вам не хочется запускать код самостоятельно, вы можете посмотреть это видео.

Заключение

На этом статья была длинная. Результатом является приложение To Do List, которое можно запускать локально. Это хорошо, но я первым признаю, что это не так уж и полезно.

В третьей и последней статье я превращу это игрушечное демонстрационное приложение в высокодоступное производственное приложение, которое развертывается в кластере Kubernetes, может быть легко масштабировано и отслеживаться с помощью Prometheus, Grafana и Jaeger.