Skip to content

Commit fa5710d

Browse files
committed
Allow configuring multiple higher level Elasticsearch clients
1 parent e5ba988 commit fa5710d

File tree

9 files changed

+321
-85
lines changed

9 files changed

+321
-85
lines changed

docs/src/main/asciidoc/elasticsearch.adoc

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ public class FruitService {
607607
// ...
608608
}
609609
----
610-
<1> We specify the name of the client through the `@Identifier` when injecting a `RestClient` inside the service.
610+
<1> We specify the name of the client through the `@Identifier` when injecting a `RestClient` within the service.
611611

612612
Named Elasticsearch REST clients in this case can be configured as follows:
613613

@@ -617,6 +617,29 @@ Named Elasticsearch REST clients in this case can be configured as follows:
617617
quarkus.elasticsearch."second-elasticsearch-cluster".hosts=second-elasticsearch:9200
618618
----
619619

620+
It is also possible to use the named higher level Elasticsearch clients.
621+
The name of such client *must* match an existing, configured lower-level REST client.
622+
623+
[source,java]
624+
----
625+
626+
import jakarta.enterprise.context.ApplicationScoped;
627+
import jakarta.inject.Inject;
628+
629+
import co.elastic.clients.elasticsearch.ElasticsearchClient;
630+
import io.smallrye.common.annotation.Identifier;
631+
632+
@ApplicationScoped
633+
public class FruitService {
634+
@Inject
635+
@Identifier("second-elasticsearch-cluster") // <1>
636+
ElasticsearchClient client;
637+
638+
// ...
639+
}
640+
----
641+
<1> We specify the name of the client through the `@Identifier` when injecting a `ElasticsearchClient` within the service.
642+
620643
== Hibernate Search Elasticsearch
621644

622645
Quarkus supports Hibernate Search with Elasticsearch via the `quarkus-hibernate-search-orm-elasticsearch`

extensions/elasticsearch-java-client/deployment/src/main/java/io/quarkus/elasticsearch/javaclient/deployment/ElasticsearchJavaClientProcessor.java

Lines changed: 153 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,56 @@
11
package io.quarkus.elasticsearch.javaclient.deployment;
22

3-
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
3+
import java.lang.annotation.Annotation;
4+
import java.util.HashSet;
5+
import java.util.List;
6+
import java.util.Set;
7+
import java.util.function.Supplier;
8+
9+
import jakarta.enterprise.inject.Default;
10+
import jakarta.inject.Singleton;
11+
12+
import org.elasticsearch.client.RestClient;
13+
import org.jboss.jandex.AnnotationInstance;
14+
import org.jboss.jandex.ClassType;
15+
import org.jboss.jandex.DotName;
16+
17+
import com.fasterxml.jackson.databind.ObjectMapper;
18+
19+
import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient;
20+
import co.elastic.clients.elasticsearch.ElasticsearchClient;
21+
import co.elastic.clients.transport.ElasticsearchTransport;
22+
import io.quarkus.arc.ActiveResult;
23+
import io.quarkus.arc.BeanDestroyer;
24+
import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem;
25+
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
26+
import io.quarkus.arc.processor.DotNames;
427
import io.quarkus.deployment.Feature;
28+
import io.quarkus.deployment.annotations.BuildProducer;
529
import io.quarkus.deployment.annotations.BuildStep;
30+
import io.quarkus.deployment.annotations.ExecutionTime;
31+
import io.quarkus.deployment.annotations.Record;
32+
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
633
import io.quarkus.deployment.builditem.FeatureBuildItem;
734
import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem;
835
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
936
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
10-
import io.quarkus.elasticsearch.javaclient.runtime.ElasticsearchJavaClientProducer;
37+
import io.quarkus.elasticsearch.javaclient.runtime.ElasticsearchJavaClientRecorder;
38+
import io.quarkus.elasticsearch.restclient.common.deployment.ElasticsearchClientProcessorUtil;
39+
import io.quarkus.elasticsearch.restclient.common.runtime.ElasticsearchClientBeanUtil;
40+
import io.quarkus.elasticsearch.restclient.lowlevel.deployment.ConfiguredElasticsearchLowLevelClientBuildItem;
41+
import io.quarkus.elasticsearch.restclient.lowlevel.deployment.ElasticsearchBuildTimeConfig;
42+
import io.quarkus.elasticsearch.restclient.lowlevel.deployment.ElasticsearchLowLevelClientReferenceBuildItem;
43+
import io.smallrye.common.annotation.Identifier;
1144

1245
class ElasticsearchJavaClientProcessor {
1346

47+
private static final DotName ELASTICSEARCH_CLIENT = DotName.createSimple(ElasticsearchClient.class.getName());
48+
1449
@BuildStep
1550
FeatureBuildItem feature() {
1651
return new FeatureBuildItem(Feature.ELASTICSEARCH_JAVA_CLIENT);
1752
}
1853

19-
@BuildStep
20-
AdditionalBeanBuildItem build() {
21-
return AdditionalBeanBuildItem.unremovableOf(ElasticsearchJavaClientProducer.class);
22-
}
23-
2454
@BuildStep
2555
ServiceProviderBuildItem serviceProvider() {
2656
return new ServiceProviderBuildItem("jakarta.json.spi.JsonProvider",
@@ -38,4 +68,120 @@ NativeImageFeatureBuildItem enableElasticsearchJavaClientFeature() {
3868
"io.quarkus.elasticsearch.javaclient.runtime.graalvm.ElasticsearchJavaClientFeature");
3969
}
4070

71+
@BuildStep
72+
public void collectJavaClientReferences(
73+
CombinedIndexBuildItem indexBuildItem,
74+
BeanRegistrationPhaseBuildItem registrationPhase,
75+
List<ElasticsearchLowLevelClientReferenceBuildItem> elasticsearchLowLevelClientReferenceBuildItems,
76+
BuildProducer<ElasticsearchJavaClientReferenceBuildItem> references) {
77+
Set<String> clientNames = new HashSet<>();
78+
for (String name : ElasticsearchClientProcessorUtil.collectReferencedClientNames(indexBuildItem, registrationPhase,
79+
Set.of(ELASTICSEARCH_CLIENT),
80+
Set.of())) {
81+
references.produce(new ElasticsearchJavaClientReferenceBuildItem(name));
82+
clientNames.add(name);
83+
}
84+
// Because we may have not discovered all the clients with ^ we just create for any lower level client we have:
85+
for (ElasticsearchLowLevelClientReferenceBuildItem item : elasticsearchLowLevelClientReferenceBuildItems) {
86+
if (clientNames.add(item.getName())) {
87+
references.produce(new ElasticsearchJavaClientReferenceBuildItem(item.getName()));
88+
}
89+
}
90+
}
91+
92+
@Record(ExecutionTime.RUNTIME_INIT)
93+
@BuildStep
94+
void generateElasticsearchClientBeans(
95+
ElasticsearchJavaClientRecorder recorder,
96+
ConfiguredElasticsearchLowLevelClientBuildItem lowLevelClientBuildItem,
97+
ElasticsearchBuildTimeConfig config,
98+
List<ElasticsearchJavaClientReferenceBuildItem> elasticsearchLowLevelClientReferenceBuildItems,
99+
BuildProducer<SyntheticBeanBuildItem> producer) {
100+
for (ElasticsearchJavaClientReferenceBuildItem buildItem : elasticsearchLowLevelClientReferenceBuildItems) {
101+
String clientName = buildItem.getName();
102+
if (!lowLevelClientBuildItem.getNames().contains(clientName)) {
103+
throw new IllegalStateException("Unable to locate the low-level client [" + clientName
104+
+ "]. Impossible to create the transport for higher level Elasticsearch clients. " +
105+
"Make sure that the low-level client is correctly configured.");
106+
}
107+
produceTransportBean(clientName, recorder, producer);
108+
produceBlockingClientBean(clientName, recorder, producer);
109+
produceAsyncClientBean(clientName, recorder, producer);
110+
}
111+
}
112+
113+
private void produceTransportBean(String clientName, ElasticsearchJavaClientRecorder recorder,
114+
BuildProducer<SyntheticBeanBuildItem> producer) {
115+
producer.produce(createSyntheticBean(
116+
clientName,
117+
ElasticsearchTransport.class,
118+
Singleton.class,
119+
ElasticsearchClientBeanUtil.isDefault(clientName),
120+
recorder.checkActiveElasticsearchTransportSupplier(clientName))
121+
.addInjectionPoint(ClassType.create(DotName.createSimple(RestClient.class)), qualifier(clientName))
122+
.addInjectionPoint(ClassType.create(DotName.createSimple(ObjectMapper.class)))
123+
.createWith(recorder.elasticsearchTransportSupplier(clientName))
124+
.destroyer(BeanDestroyer.AutoCloseableDestroyer.class)
125+
.done());
126+
}
127+
128+
private void produceBlockingClientBean(String clientName, ElasticsearchJavaClientRecorder recorder,
129+
BuildProducer<SyntheticBeanBuildItem> producer) {
130+
producer.produce(createSyntheticBean(
131+
clientName,
132+
ElasticsearchClient.class,
133+
Singleton.class,
134+
ElasticsearchClientBeanUtil.isDefault(clientName),
135+
recorder.checkActiveElasticsearchTransportSupplier(clientName))
136+
.createWith(recorder.blockingClientSupplier(clientName))
137+
.addInjectionPoint(ClassType.create(DotName.createSimple(ElasticsearchTransport.class)), qualifier(clientName))
138+
.destroyer(BeanDestroyer.AutoCloseableDestroyer.class)
139+
.done());
140+
}
141+
142+
private void produceAsyncClientBean(String clientName, ElasticsearchJavaClientRecorder recorder,
143+
BuildProducer<SyntheticBeanBuildItem> producer) {
144+
producer.produce(createSyntheticBean(
145+
clientName,
146+
ElasticsearchAsyncClient.class,
147+
Singleton.class,
148+
ElasticsearchClientBeanUtil.isDefault(clientName),
149+
recorder.checkActiveElasticsearchTransportSupplier(clientName))
150+
.createWith(recorder.asyncClientSupplier(clientName))
151+
.addInjectionPoint(ClassType.create(DotName.createSimple(ElasticsearchTransport.class)), qualifier(clientName))
152+
.destroyer(BeanDestroyer.AutoCloseableDestroyer.class)
153+
.done());
154+
}
155+
156+
private static <T> SyntheticBeanBuildItem.ExtendedBeanConfigurator createSyntheticBean(String clientName,
157+
Class<T> type,
158+
Class<? extends Annotation> scope,
159+
boolean defaultBean,
160+
Supplier<ActiveResult> checkActiveSupplier) {
161+
SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem
162+
.configure(type)
163+
.scope(scope)
164+
.unremovable()
165+
.setRuntimeInit()
166+
.checkActive(checkActiveSupplier)
167+
.startup();
168+
169+
if (defaultBean) {
170+
configurator.defaultBean();
171+
configurator.addQualifier(Default.class);
172+
}
173+
174+
configurator.addQualifier().annotation(DotNames.IDENTIFIER).addValue("value", clientName).done();
175+
176+
return configurator;
177+
}
178+
179+
private static AnnotationInstance qualifier(String clientName) {
180+
if (clientName == null || ElasticsearchClientBeanUtil.isDefault(clientName)) {
181+
return AnnotationInstance.builder(Default.class).build();
182+
} else {
183+
return AnnotationInstance.builder(Identifier.class).value(clientName).build();
184+
}
185+
}
186+
41187
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.quarkus.elasticsearch.javaclient.deployment;
2+
3+
import io.quarkus.builder.item.MultiBuildItem;
4+
5+
/**
6+
* Represents a reference (direct or indirect) to an Elasticsearch Java client detected at compile time.
7+
* <p>
8+
* Used in particular to determine which clients should have associated beans.
9+
*/
10+
public final class ElasticsearchJavaClientReferenceBuildItem extends MultiBuildItem {
11+
12+
private final String name;
13+
14+
public ElasticsearchJavaClientReferenceBuildItem(String name) {
15+
this.name = name;
16+
}
17+
18+
public String getName() {
19+
return name;
20+
}
21+
22+
}

extensions/elasticsearch-java-client/runtime/src/main/java/io/quarkus/elasticsearch/javaclient/runtime/ElasticsearchJavaClientProducer.java

Lines changed: 0 additions & 68 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.quarkus.elasticsearch.javaclient.runtime;
2+
3+
import java.util.function.Function;
4+
import java.util.function.Supplier;
5+
6+
import org.elasticsearch.client.RestClient;
7+
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
10+
import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient;
11+
import co.elastic.clients.elasticsearch.ElasticsearchClient;
12+
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
13+
import co.elastic.clients.transport.ElasticsearchTransport;
14+
import co.elastic.clients.transport.rest_client.RestClientTransport;
15+
import io.quarkus.arc.ActiveResult;
16+
import io.quarkus.arc.SyntheticCreationalContext;
17+
import io.quarkus.runtime.annotations.Recorder;
18+
19+
@Recorder
20+
public class ElasticsearchJavaClientRecorder {
21+
22+
public Function<SyntheticCreationalContext<ElasticsearchTransport>, ElasticsearchTransport> elasticsearchTransportSupplier(
23+
String clientName) {
24+
return new Function<SyntheticCreationalContext<ElasticsearchTransport>, ElasticsearchTransport>() {
25+
@Override
26+
public ElasticsearchTransport apply(SyntheticCreationalContext<ElasticsearchTransport> context) {
27+
RestClient client = context.getInjectedReference(RestClient.class);
28+
ObjectMapper objectMapper = context.getInjectedReference(ObjectMapper.class);
29+
return new RestClientTransport(client, new JacksonJsonpMapper(objectMapper));
30+
}
31+
};
32+
}
33+
34+
public Function<SyntheticCreationalContext<ElasticsearchClient>, ElasticsearchClient> blockingClientSupplier(
35+
String clientName) {
36+
return new Function<SyntheticCreationalContext<ElasticsearchClient>, ElasticsearchClient>() {
37+
@Override
38+
public ElasticsearchClient apply(SyntheticCreationalContext<ElasticsearchClient> context) {
39+
ElasticsearchTransport transport = context.getInjectedReference(ElasticsearchTransport.class);
40+
return new ElasticsearchClient(transport);
41+
}
42+
};
43+
}
44+
45+
public Function<SyntheticCreationalContext<ElasticsearchAsyncClient>, ElasticsearchAsyncClient> asyncClientSupplier(
46+
String clientName) {
47+
return new Function<SyntheticCreationalContext<ElasticsearchAsyncClient>, ElasticsearchAsyncClient>() {
48+
@Override
49+
public ElasticsearchAsyncClient apply(SyntheticCreationalContext<ElasticsearchAsyncClient> context) {
50+
ElasticsearchTransport transport = context.getInjectedReference(ElasticsearchTransport.class);
51+
return new ElasticsearchAsyncClient(transport);
52+
}
53+
};
54+
}
55+
56+
public Supplier<ActiveResult> checkActiveHealthCheckSupplier(String clientName) {
57+
return ActiveResult::active;
58+
}
59+
60+
public Supplier<ActiveResult> checkActiveElasticsearchTransportSupplier(String clientName) {
61+
return ActiveResult::active;
62+
}
63+
}

extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchClientProcessorUtil.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ private ElasticsearchClientProcessorUtil() {
2828
/**
2929
* Collect referenced names for a given type of Elasticsearch client:
3030
* <ul>
31-
* <li>All injected clients with the @Default, @Named or @ElasticsearchClientName qualifiers</li>
31+
* <li>All injected clients with the @Default or @Identifier qualifiers</li>
3232
* <li>All configuration classes that are expected to target a given client,
3333
* e.g. @ElasticsearchClientConfig</li>
3434
* </ul>
@@ -37,6 +37,8 @@ public static Set<String> collectReferencedClientNames(CombinedIndexBuildItem in
3737
BeanRegistrationPhaseBuildItem registrationPhase,
3838
Set<DotName> clientTypeNames, Set<DotName> configAnnotationNames) {
3939
Set<String> referencedNames = new HashSet<>();
40+
// Always start with the default:
41+
referencedNames.add(ElasticsearchClientBeanUtil.DEFAULT_ELASTICSEARCH_CLIENT_NAME);
4042
IndexView indexView = indexBuildItem.getIndex();
4143
for (DotName annotationName : configAnnotationNames) {
4244
for (AnnotationInstance annotation : indexView.getAnnotations(annotationName)) {

0 commit comments

Comments
 (0)