
Идея поэкспериментировать с интеграцией Neo4j с Helidon возникла вполне естественно.
Neo4j — графовая система управления базами данных с открытым исходным кодом, реализованная на Java. По состоянию на 2015 год считается самой распространённой графовой СУБД. (Википедия, 21.10.2021)
Neo4j написана на Java и доступна из ПО, написанного на других языках с использованием языка запросов Cypher, через HTTP endpoint или через протокол «bolt». Neo4j в настоящее время является стандартом де-факто для графовых баз данных, используемых во многих отраслях.
Вообще, в се началось с небольшого разговора с Michael Simonis, одним из авторов Spring Data Neo4j 6 и сопровождающим Neo4j-OGM. Мы спросили Михаеля, что он думает о том, как Helidon и Neo4J могут работать вместе. Менее чем через час Михаель прислал мне ссылку на этот репозиторий с полнофункциональным примером для Helidon MP и Neo4j SDN.
Когда я начал читать код, я увидел странную зависимость в файле pom.xml:
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-neo4j</artifactId> </dependency>
Spring? Да ладна! Почему в нашем проекте есть Spring?
Но продолжая изучать код, я был весьма удивлен, увидев расширение CDI 2.0, обрабатывающее инициализацию и подготовку драйвера. Это расширение CDI запустилось без проблем в Helidon, поскольку он полностью поддерживает контейнер CDI - шикарно! Я снова почувствовал красоту и силу стандартов.
На следующий день Михаель прислал мне другое репо. На этот раз он включил свои идеи о том, как использовать Neo4j с Helidon SE. Интеграция тоже получилась очень простой. Было легко использовать Helidon Config для переноса всей конфигурации Neo4j в единую конфигурацию. А поскольку драйвер Neo4j полностью поддерживает native image, все можно скомпилировать в native-image исполняемый файл без каких-либо дополнительных действий с точки зрения программиста.
Это породило дискуссии между командами Helidon и Neo4j о том, какой вид интеграции более подходит для Helidon. В результате мы создали официальную интеграцию!
После некоторых консультаций с представителем Neo4j и моим хорошим другом Михаелем Симонисом мы пришли к выводу, что нам нужно только прдоставлять инициализированный драйвер пользователям Helidon. Метрики и Health Checks из Neo4j должны быть проброшены в метрики Helidon / MicroProfile и должны предоставляться как отдельные модули - отдельные зависимости Maven.
Итак, как же нам написать интеграцию Neo4j с Helidon? существуют две разновидности Helidon - MP и SE. SE представляет собой набор реактивных API, реализованных на чистой Java. Абсолютно никакой «магии» вроде рефлекшана или других хитростей. Helidon MP, по сути, оборачивает SE и приводит его к стандартам MicroProfile. Это означает, что рекомендуется сначала реализовать интеграцию с Neo4j для Helidon SE, а затем обернуть ее как расширения CDI для Helidon MP.
Давайте сделаем это!
Дисклеймер: в этой статье я продемонстрирую только основные фрагменты кода. Поскольку Helidon опенсорс проект под лицензией Apache 2.0, полный код доступен в официальном репозитории Helidon.
В Helidon мы обычно создаем так называемый support объект для реализации интеграции. Этот объект содержит всю информацию о конфигурации и инициализации. Мы следуем Builder pattern для чтения из конфигурации. Это означает, что мы создаем внутренний объект Builder, который должен читать все данные из конфигурации:
public Builder config(Config config) { config.get("authentication.username").asString().ifPresent(this::username); config.get("authentication.password").asString().ifPresent(this::password); config.get("authentication.enabled").asBoolean().ifPresent(this::authenticationEnabled); config.get("uri").asString().ifPresent(this::uri); config.get("encrypted").asBoolean().ifPresent(this::encrypted); //pool config.get("pool.metricsEnabled").asBoolean().ifPresent(this::metricsEnabled); config.get("pool.logLeakedSessions").asBoolean().ifPresent(this::logLeakedSessions); config.get("pool.maxConnectionPoolSize").asInt().ifPresent(this::maxConnectionPoolSize); config.get("pool.idleTimeBeforeConnectionTest").as(Duration.class).ifPresent(this::idleTimeBeforeConnectionTest); config.get("pool.maxConnectionLifetime").as(Duration.class).ifPresent(this::maxConnectionLifetime); config.get("pool.connectionAcquisitionTimeout").as(Duration.class).ifPresent(this::connectionAcquisitionTimeout); //trust config.get("trustsettings.trustStrategy").asString().map(TrustStrategy::valueOf).ifPresent(this::trustStrategy); config.get("trustsettings.certificate").as(Path.class).ifPresent(this::certificate); config.get("trustsettings.hostnameVerificationEnabled").asBoolean().ifPresent(this::hostnameVerificationEnabled); return this; }
Вы можете увидеть строки с ключами, они фактически взяты из файла конфигурации. причем, либо из конфигурации SE, либо из конфигурации MicroProfile. Каждый элемент настраивается следуя шаблону Builder:
... public Builder password(String password) { Objects.requireNonNull(password); this.password = password; return this; } ...
Когда все поля установлены, мы можем сбилдить support объект:
@Override public Neo4j build() { if (driver == null) { driver = initDriver(); } return new Neo4j(this); }
Таким образом мы гарантируем, что все значения не будут нулевыми. Фактическая инициализация драйвера довольно проста (некоторые методы опущены):
private Driver initDriver() { AuthToken authToken = AuthTokens.none(); if (authenticationEnabled) { authToken = AuthTokens.basic(username, password); } org.neo4j.driver.Config.ConfigBuilder configBuilder = createBaseConfig(); configureSsl(configBuilder); configurePoolSettings(configBuilder); return GraphDatabase.driver(uri, authToken, configBuilder.build()); }
Support объект далее просто возвращает драйвер:
public Driver driver() { return driver; }
... и его можно использовать:
Neo4jMetricsSupport.builder() .driver(neo4j.driver()) .build() .initialize(); Driver neo4jDriver = neo4j.driver();
Helidon позаботится о его инициализации из файла application.yaml:
neo4j: uri: bolt://localhost:7687 authentication: username: neo4j password: secret pool: metricsEnabled: true
Собственно все - можно использовать уже в своем коде:
public List<Movie> findAll(){ try (var session = driver.session()) { var query = "" + "match (m:Movie) " + "match (m) <- [:DIRECTED] - (d:Person) " + "match (m) <- [r:ACTED_IN] - (a:Person) " + "return m, collect(d) as directors, collect({name:a.name, roles: r.roles}) as actors"; return session.readTransaction(tx -> tx.run(query).list(r -> { var movieNode = r.get("m").asNode(); var directors = r.get("directors").asList(v -> { var personNode = v.asNode(); return new Person(personNode.get("born").asInt(), personNode.get("name").asString()); }); var actors = r.get("actors").asList(v -> { return new Actor(v.get("name").asString(), v.get("roles").asList(Value::asString)); }); var m = new Movie(movieNode.get("title").asString(), movieNode.get("tagline").asString()); m.setReleased(movieNode.get("released").asInt()); m.setDirectorss(directors); m.setActors(actors); return m; })); } }
И вуаля! Helidon и Neo4j теперь могут работать вместе.
А как же MP?
Нам просто нужно обернуть нашу интеграцию в расширение CDI - это действительно довольно просто:
public class Neo4jCdiExtension implements Extension { private static final String NEO4J_METRIC_NAME_PREFIX = "neo4j"; void afterBeanDiscovery(@Observes AfterBeanDiscovery addEvent) { addEvent.addBean() .types(Driver.class) .qualifiers(Default.Literal.INSTANCE, Any.Literal.INSTANCE) .scope(ApplicationScoped.class) .name(Driver.class.getName()) .beanClass(Driver.class) .createWith(creationContext -> { org.eclipse.microprofile.config.Config config = ConfigProvider.getConfig(); Config helidonConfig = MpConfig.toHelidonConfig(config).get(NEO4J_METRIC_NAME_PREFIX); ConfigValue<Neo4j> configValue = helidonConfig.as(Neo4j::create); if (configValue.isPresent()) { return configValue.get().driver(); } throw new Neo4jException("There is no Neo4j driver configured in configuration under key 'neo4j"); }); } }
Как видите, мы можем прочитать конфигурацию с помощью функций SE:
Config helidonConfig = MpConfig.toHelidonConfig(config).get(NEO4J_METRIC_NAME_PREFIX);
Затем просто повторно используем наш support объект Neo4j SE:
ConfigValue<Neo4j> configValue = helidonConfig.as(Neo4j::create);
… и возвращаем драйвер:
return configValue.get().driver();
И тоже собственно все - просто инжектим драйвер:
@Inject public MovieRepository(Driver driver) { this.driver = driver; }
Конфигурация будет взята из файла microprofile-config.properties:
# Neo4j settings neo4j.uri=bolt://localhost:7687 neo4j.authentication.username=neo4j neo4j.authentication.password: secret neo4j.pool.metricsEnabled: true
Далее можно использовать драйвер как в примере с SE.
Теперь и Helidon MP может работать с Neo4j!
Но это еще не все!
Метрики
Как я уже упоминал, для надлежащей работе в облаках нам также необходимо пробросить метрики и health checks из Neo4j.
Сделаем как и раньше - сначала для SE, а потом все обернем в MP.
Начнем с метрик! Сделаем отдельный модуль для метрик Neo4j.
Как и в Helidon SE, в support объекте Neo4j мы будем следовать шаблону Builder для настройки поддержки метрик. На самом деле нам нужен только драйвер Neo4j, так как мы можем все показатели получить из него:
public static class Builder implements io.helidon.common.Builder<Neo4jMetricsSupport> { private Driver driver; private Builder() { } public Neo4jMetricsSupport build() { Objects.requireNonNull(driver, "Must set driver before building"); return new Neo4jMetricsSupport(this); } public Builder driver(Driver driver) { this.driver = driver; return this; } }
Затем мы должны обернуть counter–ы и gauge–ы Neo4j:
private static class Neo4JCounterWrapper implements Counter { private final Supplier<Long> fn; private Neo4JCounterWrapper(Supplier<Long> fn) { this.fn = fn; } @Override public void inc() { throw new UnsupportedOperationException(); } @Override public void inc(long n) { throw new UnsupportedOperationException(); } @Override public long getCount() { return fn.get(); } } private static class Neo4JGaugeWrapper<T> implements Gauge<T> { private final Supplier<T> supplier; private Neo4JGaugeWrapper(Supplier<T> supplier) { this.supplier = supplier; } @Override public T getValue() { return supplier.get(); } }
Далее нужно зарегистрировать эти счетчики в MetricsRegistry:
private void registerCounter(MetricRegistry metricRegistry, ConnectionPoolMetrics cpm, String poolPrefix, String name, Function<ConnectionPoolMetrics, Long> fn) { String counterName = poolPrefix + name; if (metricRegistry.getCounters().get(new MetricID(counterName)) == null) { Metadata metadata = Metadata.builder() .withName(counterName) .withType(MetricType.COUNTER) .notReusable() .build(); Neo4JCounterWrapper wrapper = new Neo4JCounterWrapper(() -> fn.apply(cpm)); metricRegistry.register(metadata, wrapper); } } private void registerGauge(MetricRegistry metricRegistry, ConnectionPoolMetrics cpm, String poolPrefix, String name, Function<ConnectionPoolMetrics, Integer> fn) { String gaugeName = poolPrefix + name; if (metricRegistry.getGauges().get(new MetricID(gaugeName)) == null) { Metadata metadata = Metadata.builder() .withName(poolPrefix + name) .withType(MetricType.GAUGE) .notReusable() .build(); Neo4JGaugeWrapper<Integer> wrapper = new Neo4JGaugeWrapper<>(() -> fn.apply(cpm)); metricRegistry.register(metadata, wrapper); } }
И мы практически готовы:
private void reinit() { Map<String, Function<ConnectionPoolMetrics, Long>> counters = Map.ofEntries( entry("acquired", ConnectionPoolMetrics::acquired), entry("closed", ConnectionPoolMetrics::closed), entry("created", ConnectionPoolMetrics::created), entry("failedToCreate", ConnectionPoolMetrics::failedToCreate), entry("timedOutToAcquire", ConnectionPoolMetrics::timedOutToAcquire), entry("totalAcquisitionTime", ConnectionPoolMetrics::totalAcquisitionTime), entry("totalConnectionTime", ConnectionPoolMetrics::totalConnectionTime), entry("totalInUseCount", ConnectionPoolMetrics::totalInUseCount), entry("totalInUseTime", ConnectionPoolMetrics::totalInUseTime)); Map<String, Function<ConnectionPoolMetrics, Integer>> gauges = Map.ofEntries( entry("acquiring", ConnectionPoolMetrics::acquiring), entry("creating", ConnectionPoolMetrics::creating), entry("idle", ConnectionPoolMetrics::idle), entry("inUse", ConnectionPoolMetrics::inUse) ); for (ConnectionPoolMetrics it : lastPoolMetrics.get()) { String poolPrefix = NEO4J_METRIC_NAME_PREFIX + "-"; counters.forEach((name, supplier) -> registerCounter(metricRegistry.get(), it, poolPrefix, name, supplier)); gauges.forEach((name, supplier) -> registerGauge(metricRegistry.get(), it, poolPrefix, name, supplier)); // we only care about the first one metricsInitialized.set(true); break; } }
Всегда полезно обновлять вовремя метрики, и для этого у нас есть функция:
private void refreshMetrics(ScheduledExecutorService executor) { Collection<ConnectionPoolMetrics> currentPoolMetrics = driver.metrics().connectionPoolMetrics(); if (!metricsInitialized.get() && currentPoolMetrics.size() >= 1) { lastPoolMetrics.set(currentPoolMetrics); reinit(); if (metricsInitialized.get()) { reinitFuture.get().cancel(false); executor.shutdown(); } } }
Нам нужно только предоставить драйвер Neo4j для предоставления нам информации о метриках в нашем файле application.yaml:
neo4j: pool: metricsEnabled: true
… Или в нашем microprofile-config.properties :
neo4j.pool.metricsEnabled = true
Теперь, если вы пойдете по «/health», вы также получите показания от Neo4j.
Для MP, нам нужно только обернуть регистрацию метрик как расширение CDI. Событие это должно произойти после того, как драйвер уже инициализирован:
public class Neo4jMetricsCdiExtension implements Extension { private void addMetrics(@Observes @Priority(PLATFORM_AFTER + 101) @Initialized(ApplicationScoped.class) Object event) { Instance<Driver> driver = CDI.current().select(Driver.class); Neo4jMetricsSupport.builder() .driver(driver.get()) .build() .initialize(); } }
Вот и все! Метрики Neo4j теперь доступны и в Helidon MP!
И теперь, последнее, но не менее важное, проверка работоспособности!
Health Checks
И опять же, это должен быть отдельный модуль, чтобы все было чисто и красиво.
На этот раз мы начнем с MP:
@Readiness @ApplicationScoped public class Neo4jHealthCheck implements HealthCheck { private static final String CYPHER = "RETURN 1 AS result"; private static final SessionConfig DEFAULT_SESSION_CONFIG = SessionConfig.builder() .withDefaultAccessMode(AccessMode.WRITE) .build(); private final Driver driver; @Inject //will be ignored outside of CDI Neo4jHealthCheck(Driver driver) { this.driver = driver; } public static Neo4jHealthCheck create(Driver driver) { return new Neo4jHealthCheck(driver); } private static HealthCheckResponse buildStatusUp(ResultSummary resultSummary, HealthCheckResponseBuilder builder) { ServerInfo serverInfo = resultSummary.server(); builder.withData("server", serverInfo.version() + "@" + serverInfo.address()); String databaseName = resultSummary.database().name(); if (!(databaseName == null || databaseName.trim().isEmpty())) { builder.withData("database", databaseName.trim()); } return builder.build(); } @Override public HealthCheckResponse call() { HealthCheckResponseBuilder builder = HealthCheckResponse.named("Neo4j connection health check").up(); try { ResultSummary resultSummary; // Retry one time when the session has been expired try { resultSummary = runHealthCheckQuery(); } catch (SessionExpiredException sessionExpiredException) { resultSummary = runHealthCheckQuery(); } return buildStatusUp(resultSummary, builder); } catch (Exception e) { return builder.down().withData("reason", e.getMessage()).build(); } } private ResultSummary runHealthCheckQuery() { // We use WRITE here to make sure UP is returned for a server that supports // all possible workloads if (driver != null) { Session session = this.driver.session(DEFAULT_SESSION_CONFIG); Result run = session.run(CYPHER); return run.consume(); } return null; } }
Технически для health check-a, мы выполняем простой запрос на Cypher, и если он работает, значит, Neo4j жив, этого достаточно!
Нам нужно только добавить Maven зависимость в наш проект:
<dependency> <groupId>io.helidon.integrations.neo4j</groupId> <artifactId>helidon-integrations-neo4j-health</artifactId> </dependency>
И снова - Вуаля, все работает!
Что касается SE, поскольку это чистая Java, нам просто нужно все инициализировать:
Neo4j neo4j = Neo4j.create(config.get("neo4j")); Neo4jHealthCheck healthCheck = Neo4jHealthCheck.create(neo4j.driver()); Driver neo4jDriver = neo4j.driver(); HealthSupport health = HealthSupport.builder() .addLiveness(HealthChecks.healthChecks()) // Adds a convenient set of checks .addReadiness(healthCheck) .build(); return Routing.builder() .register(health) // Health at "/health" //other services .build(); }
Теперь наше Helidon приложение, как MP, так и SE, может работать с Neo4j, читать его метрики и выполнять health checks.
Кстати, поскольку драйвер Neo4j полностью поддерживает GraalVM native-image, приложения на Helidon MP или SE могут быть в него скомпилированы!
В этой статье было продемонстрировано, как создать интеграцию разных технологий с Helidon. Что касается Neo4j, мы уже официально сделали это за вас!
Просто нужно включить в свои Maven проект следующие зависимости:
<dependency> <groupId>io.helidon.integrations.neo4j</groupId> <artifactId>helidon-integrations-neo4j</artifactId> </dependency> <dependency> <groupId>io.helidon.integrations.neo4j</groupId> <artifactId>helidon-integrations-neo4j-metrics</artifactId> </dependency> <dependency> <groupId>io.helidon.integrations.neo4j</groupId> <artifactId>helidon-integrations-neo4j-health</artifactId> </dependency>
… И это все, что вам нужно для начала работы с Helidon и Neo4j!
Заключение
Как видите, интеграция с Helidon довольно проста. Стандартный способ сделать это - сначала написать supportобъект Helidon SE, следуя шаблону Builder для его инициализации, а затем просто обернуть его в CDI расширение, чтобы MicroProfile мог воспользоваться этой "магией"!
Вы можете поиграть с примерами Helidon ин Neo4j в нашем официальном репозитории интеграции Helidon Neo4j .
Что касается Neo4j, вас также может заинтересовать CypherDsl и пример с ним.
Следующие шаги
В этой статье я хотел показать вам не только, как Neo4j работает с Helidon, но и как вы можете писать свои собственные расширения. Поскольку Helidon опенсорсный продукт, мы приглашаем вас в него контрибьютить!
