Skip to content

Commit 71eecc8

Browse files
authored
Reactive routing via cdi provider (helidon-io#3050)
* Reactive routing via cdi provider Signed-off-by: Daniel Kec <daniel.kec@oracle.com>
1 parent 97a0da0 commit 71eecc8

10 files changed

Lines changed: 396 additions & 47 deletions

File tree

docs/mp/guides/38_se_services.adoc

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
///////////////////////////////////////////////////////////////////////////////
2+
3+
Copyright (c) 2021 Oracle and/or its affiliates.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
17+
///////////////////////////////////////////////////////////////////////////////
18+
19+
= Reusing Helidon SE services
20+
:h1Prefix: MP
21+
:description: Helidon Reactive Routing
22+
:keywords: helidon, guide, routing
23+
:helidon-uc-flavor: MP
24+
:helidon-lc-flavor: mp
25+
26+
This guide shows how reuse Helidon SE Service in your Helidon MP application.
27+
28+
== What You Need
29+
30+
|===
31+
|About 10 minutes
32+
|<<about/03_prerequisites.adoc,Helidon Prerequisites>>
33+
|<<mp/guides/02_quickstart.adoc,A Helidon MP application, such as Helidon MP Quickstart>>
34+
|===
35+
36+
Helidon MP supports <<mp/jaxrs/10_reactive-routing.adoc, Reactive routing>> which brings possibility for reusing
37+
`io.helidon.webserver.Service` implementations in Helidon MP. Such feature can be quite useful for common
38+
solutions for filtering, auditing, logging or augmenting REST endpoints in hybrid Helidon SE/MP environment.
39+
40+
Let's define simple Helidon SE Service for adding special header to every REST response:
41+
42+
[source,java]
43+
----
44+
public class CoolingService implements Service, Handler {
45+
46+
public static final String COOL_HEADER_NAME = "Cool-Header";
47+
public static final String COOLING_VALUE = "This is way cooler response than ";
48+
49+
@Override
50+
public void update(Routing.Rules rules) {
51+
rules.any(this);
52+
}
53+
54+
@Override
55+
public void accept(ServerRequest req, ServerResponse res) {
56+
res.headers().add(COOL_HEADER_NAME, COOLING_VALUE);
57+
req.next();
58+
}
59+
}
60+
----
61+
62+
Its easy to use it with Helidon SE:
63+
64+
[source,java]
65+
----
66+
WebServer.builder(Routing.builder()
67+
// register service with routing path
68+
.register("/cool", new CoolingService())
69+
.build())
70+
.config(config)
71+
.addMediaSupport(JsonpSupport.create())
72+
.build()
73+
.start();
74+
----
75+
76+
And not much harder to use it with Helidon MP:
77+
78+
[source,java]
79+
----
80+
@ApplicationScoped
81+
public class MyBean {
82+
83+
@Produces
84+
@ApplicationScoped
85+
@RoutingPath("/cool")
86+
public Service coolService() {
87+
return new CoolingService();
88+
}
89+
90+
}
91+
----
92+
93+
You can leverage annotations:
94+
95+
* @RoutingPath - path of the WebServer service
96+
* @RoutingName - select routing when <<mp/jaxrs/02_server-configuration.adoc#conf-additional-ports,serving requests on multiple ports>>

docs/mp/jaxrs/02_server-configuration.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
///////////////////////////////////////////////////////////////////////////////
22

3-
Copyright (c) 2018, 2020 Oracle and/or its affiliates.
3+
Copyright (c) 2018, 2021 Oracle and/or its affiliates.
44

55
Licensed under the Apache License, Version 2.0 (the "License");
66
you may not use this file except in compliance with the License.
@@ -100,7 +100,7 @@ server:
100100
resource-path: "keystore.p12"
101101
----
102102
103-
== Configuring additional ports
103+
== Configuring additional ports [[conf-additional-ports]]
104104
105105
Helidon MP can expose multiple ports, with the following limitations:
106106

docs/mp/jaxrs/10_reactive-routing.adoc

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
///////////////////////////////////////////////////////////////////////////////
22

3-
Copyright (c) 2019, 2020 Oracle and/or its affiliates.
3+
Copyright (c) 2019, 2021 Oracle and/or its affiliates.
44

55
Licensed under the Apache License, Version 2.0 (the "License");
66
you may not use this file except in compliance with the License.
@@ -51,7 +51,7 @@ You can assign a reactive service to a named routing (and as a result to a named
5151
either an annotation or configuration (or both to override the value from annotation).
5252
5353
===== Annotation `@RoutingName`
54-
You can annotated a service with this annotation to assign it to a specific named routing,
54+
You can annotate a service bean with this annotation to assign it to a specific named routing,
5555
that is (most likely) going to be bound to a specific port.
5656
5757
The annotation has two attributes:
@@ -74,10 +74,11 @@ is not configured.
7474
7575
===== Configuration override of routing name
7676
77-
For each service class you can define the routing name and its required flag by specifying a configuration
78-
option `class-name.routing-name.name` and `class-name.routing-name.required`.
77+
For each service bean you can define the routing name and its required flag by specifying a configuration
78+
option `bean-class-name.routing-name.name` and `bean-class-name.routing-name.required`.
79+
For service beans produced with producer method replace `bean-class-name` with `class-name.producer-method-name`.
7980
80-
Example (YAML) configuration for a class `io.helidon.examples.AdminService` that changes the
81+
Example (YAML) configuration for a service bean `io.helidon.examples.AdminService` that changes the
8182
routing name to `management` and its required flag to `false`:
8283
8384
[source,yaml]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2021 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.microprofile.server;
18+
19+
import javax.enterprise.inject.spi.Annotated;
20+
21+
import io.helidon.config.Config;
22+
23+
class RoutingConfiguration {
24+
private String routingName = RoutingName.DEFAULT_NAME;
25+
private String routingPath = null;
26+
private boolean required = false;
27+
private final String contextConfigKey;
28+
29+
RoutingConfiguration(Annotated annotated, String contextConfigKey) {
30+
RoutingName rn = annotated.getAnnotation(RoutingName.class);
31+
RoutingPath rp = annotated.getAnnotation(RoutingPath.class);
32+
this.contextConfigKey = contextConfigKey;
33+
if (rn != null) {
34+
this.routingName = rn.value();
35+
this.required = rn.required();
36+
}
37+
if (rp != null) {
38+
this.routingPath = rp.value();
39+
}
40+
}
41+
42+
String routingName(Config config) {
43+
return config.get(contextConfigKey)
44+
.get(RoutingName.CONFIG_KEY_NAME)
45+
.asString()
46+
.orElse(routingName);
47+
}
48+
49+
String routingPath(Config config) {
50+
return config.get(contextConfigKey)
51+
.get(RoutingPath.CONFIG_KEY_PATH)
52+
.asString()
53+
.orElse(routingPath);
54+
}
55+
56+
boolean required(Config config) {
57+
return config.get(contextConfigKey)
58+
.get(RoutingName.CONFIG_KEY_REQUIRED)
59+
.asBoolean()
60+
.orElse(required);
61+
}
62+
63+
String configContext() {
64+
return contextConfigKey;
65+
}
66+
}

microprofile/server/src/main/java/io/helidon/microprofile/server/RoutingName.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.lang.annotation.Retention;
2020
import java.lang.annotation.Target;
2121

22+
import static java.lang.annotation.ElementType.FIELD;
23+
import static java.lang.annotation.ElementType.METHOD;
2224
import static java.lang.annotation.ElementType.TYPE;
2325
import static java.lang.annotation.RetentionPolicy.RUNTIME;
2426

@@ -54,7 +56,7 @@
5456
* required: false
5557
* </pre>
5658
*/
57-
@Target(TYPE)
59+
@Target({TYPE, METHOD, FIELD})
5860
@Retention(RUNTIME)
5961
@Documented
6062
public @interface RoutingName {

microprofile/server/src/main/java/io/helidon/microprofile/server/RoutingPath.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.lang.annotation.Retention;
2020
import java.lang.annotation.Target;
2121

22+
import static java.lang.annotation.ElementType.FIELD;
23+
import static java.lang.annotation.ElementType.METHOD;
2224
import static java.lang.annotation.ElementType.TYPE;
2325
import static java.lang.annotation.RetentionPolicy.RUNTIME;
2426

@@ -56,7 +58,7 @@
5658
* {@link javax.enterprise.context.ApplicationScoped} and {@link javax.enterprise.context.Dependent} scopes</li>
5759
* </ul>
5860
*/
59-
@Target(TYPE)
61+
@Target({TYPE, METHOD, FIELD})
6062
@Retention(RUNTIME)
6163
@Documented
6264
public @interface RoutingPath {
@@ -67,6 +69,7 @@
6769

6870
/**
6971
* Path of this WebServer service. Use the same path as would be used with {@link io.helidon.webserver.Routing.Rules}.
72+
*
7073
* @return path to register the service on.
7174
*/
7275
String value();

microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
package io.helidon.microprofile.server;
1818

1919
import java.lang.management.ManagementFactory;
20+
import java.lang.reflect.Field;
21+
import java.lang.reflect.Method;
2022
import java.nio.file.Path;
2123
import java.util.ArrayList;
24+
import java.util.Collections;
2225
import java.util.HashMap;
26+
import java.util.IdentityHashMap;
2327
import java.util.LinkedList;
2428
import java.util.List;
2529
import java.util.Map;
@@ -44,6 +48,9 @@
4448
import javax.enterprise.inject.spi.BeanManager;
4549
import javax.enterprise.inject.spi.DeploymentException;
4650
import javax.enterprise.inject.spi.Extension;
51+
import javax.enterprise.inject.spi.ProcessManagedBean;
52+
import javax.enterprise.inject.spi.ProcessProducerField;
53+
import javax.enterprise.inject.spi.ProcessProducerMethod;
4754

4855
import io.helidon.common.Prioritized;
4956
import io.helidon.common.configurable.ServerThreadPoolSupplier;
@@ -95,6 +102,9 @@ public class ServerCdiExtension implements Extension {
95102
private volatile boolean started;
96103
private final List<JerseySupport> jerseySupports = new LinkedList<>();
97104

105+
private final Map<Bean<?>, RoutingConfiguration> serviceBeans
106+
= Collections.synchronizedMap(new IdentityHashMap<>());
107+
98108
private void buildTime(@Observes @BuildTimeStart Object event) {
99109
// update the status of server, as we may have been started without a builder being used
100110
// such as when cdi.Main or SeContainerInitializer are used
@@ -119,6 +129,23 @@ private void registerKpiMetricsDeferrableRequestHandlers(
119129
jaxRsApplications.forEach(it -> registerKpiMetricsDeferrableRequestContextSetterHandler(jaxRs, it));
120130
}
121131

132+
private void recordMethodProducedServices(@Observes ProcessProducerMethod<? extends Service, ?> ppm) {
133+
Method m = ppm.getAnnotatedProducerMethod().getJavaMember();
134+
String contextKey = m.getDeclaringClass().getName() + "." + m.getName();
135+
serviceBeans.put(ppm.getBean(), new RoutingConfiguration(ppm.getAnnotated(), contextKey));
136+
}
137+
138+
private void recordFieldProducedServices(@Observes ProcessProducerField<? extends Service, ?> ppf) {
139+
Field f = ppf.getAnnotatedProducerField().getJavaMember();
140+
String contextKey = f.getDeclaringClass().getName() + "." + f.getName();
141+
serviceBeans.put(ppf.getBean(), new RoutingConfiguration(ppf.getAnnotated(), contextKey));
142+
}
143+
144+
private void recordBeanServices(@Observes ProcessManagedBean<? extends Service> pmb) {
145+
Class<? extends Service> cls = pmb.getAnnotatedBeanClass().getJavaClass();
146+
serviceBeans.put(pmb.getBean(), new RoutingConfiguration(pmb.getAnnotated(), cls.getName()));
147+
}
148+
122149
private void registerKpiMetricsDeferrableRequestContextSetterHandler(JaxRsCdiExtension jaxRs,
123150
JaxRsApplication applicationMeta) {
124151
Optional<String> contextRoot = jaxRs.findContextRoot(config, applicationMeta);
@@ -392,9 +419,8 @@ private void registerWebServerServices(BeanManager beanManager) {
392419

393420
for (Bean<?> bean : beans) {
394421
Bean<Object> objBean = (Bean<Object>) bean;
395-
Class<?> aClass = objBean.getBeanClass();
396422
Service service = (Service) objBean.create(context);
397-
registerWebServerService(aClass, service);
423+
registerWebServerService(serviceBeans.remove(bean), service);
398424
}
399425
}
400426

@@ -413,40 +439,15 @@ private static int priority(Class<?> aClass) {
413439
return (null == prio) ? Prioritized.DEFAULT_PRIORITY : prio.value();
414440
}
415441

416-
private void registerWebServerService(Class<?> serviceClass,
417-
Service service) {
442+
private void registerWebServerService(RoutingConfiguration routingConf, Service service) {
418443

419-
RoutingPath rp = serviceClass.getAnnotation(RoutingPath.class);
420-
RoutingName rn = serviceClass.getAnnotation(RoutingName.class);
444+
String path = routingConf.routingPath(config);
445+
String routingName = routingConf.routingName(config);
446+
boolean routingNameRequired = routingConf.required(config);
421447

422-
String path = (null == rp) ? null : rp.value();
423-
String routingName = (null == rn) ? null : rn.value();
424-
boolean routingNameRequired = (null != rn) && rn.required();
425-
426-
// can override routing path from configuration
427-
path = config.get(serviceClass.getName()
428-
+ "."
429-
+ RoutingPath.CONFIG_KEY_PATH)
430-
.asString()
431-
.orElse(path);
432-
433-
// can override routing name from configuration
434-
routingName = config.get(serviceClass.getName()
435-
+ "."
436-
+ RoutingName.CONFIG_KEY_NAME)
437-
.asString()
438-
.orElse(routingName);
439-
440-
// also whether the routing name is required can be overridden
441-
routingNameRequired = config.get(serviceClass.getName()
442-
+ "."
443-
+ RoutingName.CONFIG_KEY_REQUIRED)
444-
.asBoolean()
445-
.orElse(routingNameRequired);
446-
447-
Routing.Rules routing = findRouting(serviceClass.getName(),
448-
routingName,
449-
routingNameRequired);
448+
Routing.Rules routing = findRouting(routingConf.configContext(),
449+
routingName,
450+
routingNameRequired);
450451

451452
if ((null == path) || "/".equals(path)) {
452453
routing.register(service);
@@ -500,7 +501,7 @@ public Routing.Builder serverRoutingBuilder() {
500501

501502
/**
502503
* Helidon webserver routing builder that can be used to add routes to a named socket
503-
* of the webserver.
504+
* of the webserver.
504505
*
505506
* @param name name of the named routing (should match a named socket configuration)
506507
* @return builder for routing of the named route
@@ -520,6 +521,7 @@ public void defaultExecutorService(Supplier<? extends ExecutorService> defaultEx
520521

521522
/**
522523
* Current host the server is running on.
524+
*
523525
* @return host of this server
524526
*/
525527
public String host() {

0 commit comments

Comments
 (0)