-
Notifications
You must be signed in to change notification settings - Fork 88
guide service client spring
This guide is about consuming (calling) services from other applications (micro-services) in devon4j-spring.
You need to add (at least one of) these dependencies to your application:
<!-- Starter for asynchronous consuming REST services via Jaca HTTP Client (Java11+) -->
<dependency>
<groupId>com.devonfw.java.starters</groupId>
<artifactId>devon4j-starter-http-client-rest-async</artifactId>
</dependency>
<!-- Starter for synchronous consuming REST services via Jaca HTTP Client (Java11+) -->
<dependency>
<groupId>com.devonfw.java.starters</groupId>
<artifactId>devon4j-starter-http-client-rest-sync</artifactId>
</dependency>
<!-- Starter for synchronous consuming REST services via Apache CXF (Java8+)
NOTE: This is an alternative to devon4j-starter-http-client-rest-sync
-->
<!--
<dependency>
<groupId>com.devonfw.java.starters</groupId>
<artifactId>devon4j-starter-cxf-client-rest</artifactId>
</dependency>
-->
<!-- Starter for synchronous consuming SOAP services via Apache CXF (Java8+) -->
<dependency>
<groupId>com.devonfw.java.starters</groupId>
<artifactId>devon4j-starter-cxf-client-ws</artifactId>
</dependency>
When invoking a service, you need to consider many cross-cutting aspects. You might not think about them in the very first place and you do not want to redundantly implement them multiple times. Therefore, you should consider using this approach. The following sub-sections list the covered features and aspects:
Assuming you already have a Java interface MyService
of the service you want to invoke:
package com.company.department.foo.mycomponent.service.api.rest;
...
@Path("/myservice")
public interface MyService extends RestService {
@POST
@Path("/getresult")
MyResult getResult(MyArgs myArgs);
@DELETE
@Path("/entity/{id}")
void deleteEntity(@PathParam("id") long id);
}
Then, all you need to do is this:
@Named
public class UcMyUseCaseImpl extends MyUseCaseBase implements UcMyUseCase {
@Inject
private ServiceClientFactory serviceClientFactory;
...
private void callSynchronous(MyArgs myArgs) {
MyService myService = this.serviceClientFactory.create(MyService.class);
// call of service over the wire, synchronously blocking until result is received or error occurred
MyResult myResult = myService.myMethod(myArgs);
handleResult(myResult);
}
private void callAsynchronous(MyArgs myArgs) {
AsyncServiceClient<MyService> client = this.serviceClientFactory.createAsync(MyService.class);
// call of service over the wire, will return when request is send and invoke handleResult asynchronously
client.call(client.get().myMethod(myArgs), this::handleResult);
}
private void handleResult(MyResult myResult) {
...
}
...
}
As you can see, both synchronous and asynchronous invocation of a service is very simple and type-safe. However, it is also very flexible and powerful (see following features). The actual call of myMethod
will technically call the remote service over the wire (e.g. via HTTP), including marshalling the arguments (e.g. converting myArgs
to JSON) and unmarshalling the result (e.g. converting the received JSON to myResult
).
If you want to call a service method with void
as the return type, the type-safe call
method cannot be used as void
methods do not return a result. Therefore you can use the callVoid
method as following:
private void callAsynchronousVoid(long id) {
AsyncServiceClient<MyService> client = this.serviceClientFactory.createAsync(MyService.class);
// call of service over the wire, will return when request is send and invoke resultHandler asynchronously
Consumer<Void> resultHandler = r -> { System.out.println("Response received")};
client.callVoid(() -> { client.get().deleteEntity(id);}, resultHandler);
}
You may also provide null
as resultHandler
for "fire and forget". However, this will lead to the result being ignored, so even in the case of an error you will not be notified.
This solution allows a very flexible configuration on the following levels:
-
Global configuration (defaults)
-
Configuration per remote service application (microservice)
-
Configuration per invocation.
A configuration on a deeper level (e.g. 3) overrides the configuration from a higher level (e.g. 1).
The configuration on Level 1 and 2 are configured via application.properties
(see configuration guide).
For Level 1, the prefix service.client.default.
is used for properties.
Further, for level 2, the prefix service.client.app.«application».
is used where «application»
is the
technical name of the application providing the service. This name will automatically be derived from
the java package of the service interface (e.g. foo
in MyService
interface before) following our
packaging conventions.
In case these conventions are not met, it will fall back to the fully qualified name of the service interface.
Configuration on Level 3 has to be provided as a Map
argument to the method
ServiceClientFactory.create(Class<S> serviceInterface, Map<String, String> config)
.
The keys of this Map
will not use prefixes (such as the ones above). For common configuration
parameters, a type-safe builder is offered to create such a map via ServiceClientConfigBuilder
.
E.g. for testing, you may want to do:
this.serviceClientFactory.create(MyService.class,
new ServiceClientConfigBuilder().authBasic().userLogin(login).userPassword(password).buildMap());
Here is an example of a configuration block for your application.properties
:
service.client.default.url=https://api.company.com/services/${type}
service.client.default.timeout.connection=120
service.client.default.timeout.response=3600
service.client.app.bar.url=https://bar.company.com:8080/services/rest
service.client.app.bar.auth=basic
service.client.app.bar.user.login=user4711
service.client.app.bar.user.password=ENC(jd5ZREpBqxuN9ok0IhnXabgw7V3EoG2p)
service.client.app.foo.url=https://foo.company.com:8443/services/rest
# authForward: simply forward Authorization header (e.g. with JWT) to remote service
service.client.app.bar.auth=authForward
You do not want to hardwire service URLs in your code, right? Therefore, different strategies might apply
to discover the URL of the invoked service. This is done internally by an implementation of the interface
ServiceDiscoverer
. The default implementation simply reads the base URL from the configuration.
You can simply add this to your application.properties
as in the above configuration example.
Assuming your service interface has the fully qualified name
com.company.department.foo.mycomponent.service.api.rest.MyService
, then the URL would be resolved to
https://foo.company.com:8443/services/rest
, as the «application»
is foo
.
Additionally, the URL might use the following variables that will automatically be resolved:
-
${app}
to«application»
(useful for default URL) -
${type}
to the type of the service. E.g.rest
in case of a REST service andws
for a SOAP service. -
${local.server.port}
for the port of your current Java servlet container running the JVM. Should only be used for testing with spring-boot random port mechanism (technically spring cannot resolve this variable, but we do it for you here).
Therefore, the default URL may also be configured as:
service.client.default.url=https://api.company.com/${app}/services/${type}
A very common demand is to tweak (HTTP) headers in the request to invoke the service. May it be for security (authentication data) or for other cross-cutting concerns (such as the Correlation ID). This is done internally by implementations of the interface ServiceHeaderCustomizer
.
We already provide several implementations such as:
-
ServiceHeaderCustomizerBasicAuth
for basic authentication (auth=basic
). -
ServiceHeaderCustomizerOAuth
for OAuth: passes a security token from security context such as a JWT via OAuth (auth=oauth
). -
ServiceHeaderCustomizerAuthForward
forwards theAuthorization
HTTP header from the running request to the request to the remote service as is (auth=authForward
). Be careful to avoid security pitfalls by misconfiguring this feature, as it may also contain sensitive credentials (e.g. basic auth) to the remote service. Never use as default. -
ServiceHeaderCustomizerCorrelationId
passed the Correlation ID to the service request.
Additionally, you can add further custom implementations of ServiceHeaderCustomizer
for your individual requirements and additional headers.
You can configure timeouts in a very flexible way. First of all, you can configure timeouts to establish the connection (timeout.connection
) and to wait for the response (timeout.response
) separately. These timeouts can be configured on all three levels as described in the configuration section above.
Whilst invoking a remote service, an error may occur. This solution will automatically handle such errors and map them to a higher level ServiceInvocationFailedException
. In general, we separate two different types of errors:
-
Network error
In such a case (host not found, connection refused, time out, etc.), there is not even a response from the server. However, in advance to a low-level exception you will get a wrappedServiceInvocationFailedException
(with codeServiceInvoke
) with a readable message containing the service that could not be invoked. -
Service error
In case the service failed on the server-side, the error result will be parsed and thrown as aServiceInvocationFailedException
with the received message and code.
This allows to catch and handle errors when a service-invocation failed. You can even distinguish business errors from the server-side from technical errors and implement retry strategies or the like. Further, the created exception contains detailed contextual information about the service that failed (service interface class, method, URL), which makes it much easier to trace down errors. Here is an example from our tests:
While invoking the service com.devonfw.test.app.myexample.service.api.rest.MyExampleRestService#businessError[http://localhost:50178/app/services/rest/my-example/v1/business-error] the following error occurred: Test of business error. Probably the service is temporary unavailable. Please try again later. If the problem persists contact your system administrator.
2f43b03e-685b-45c0-9aae-23ff4b220c85:BusinessErrorCode
You may even provide your own implementation of ServiceClientErrorFactory
instead to provide an own exception class for this purpose.
In case of a synchronous service invocation, an error will be immediately thrown so you can surround the call with a regular try-catch block:
private void callSynchronous(MyArgs myArgs) {
MyService myService = this.serviceClientFactory.create(MyService.class);
// call of service over the wire, synchronously blocking until result is received or error occurred
try {
MyResult myResult = myService.myMethod(myArgs);
handleResult(myResult);
} catch (ServiceInvocationFailedException e) {
if (e.isTechnical()) {
handleTechnicalError(e);
} else {
// error code you defined in the exception on the server side of the service
String errorCode = e.getCode();
handleBusinessError(e, errorCode;
}
} catch (Throwable e) { // you may not handle this explicitly here...
handleTechnicalError(e);
}
}
If you are using asynchronous service invocation, an error can occurr in a separate thread. Therefore, you may and should define a custom error handler:
private void callAsynchronous(MyArgs myArgs) {
AsyncServiceClient<MyService> client = this.serviceClientFactory.createAsync(MyService.class);
Consumer<Throwalbe> errorHandler = this::handleError;
client.setErrorHandler(errorHandler);
// call of service over the wire, will return when request is send and invoke handleResult asynchronously
client.call(client.get().myMethod(myArgs), this::handleResult);
}
private void handleError(Throwalbe error) {
...
}
}
The error handler consumes Throwable
, and not only RuntimeException
, so you can get notified even in case of an unexpected OutOfMemoryError
, NoClassDefFoundError
, or other technical problems. Please note that the error handler may also be called from the thread calling the service (e.g. if already creating the request fails). The default error handler used if no custom handler is set will only log the error and do nothing else.
By default, this solution will log all invocations including the URL of the invoked service, success or error status flag and the duration in seconds (with decimal nano precision as available). Therefore, you can easily monitor the status and performance of the service invocations. Here is an example from our tests:
Invoking service com.devonfw.test.app.myexample.service.api.rest.MyExampleRestService#greet[http://localhost:50178/app/services/rest/my-example/v1/greet/John%20Doe%20%26%20%3F%23] took PT20.309756622S (20309756622ns) and succeded with status 200.
Resilience adds a lot of complexity, which typically means that addressing this here would most probably result in not being up-to-date and not meeting all requirements. Therefore, we recommend something completely different: the sidecar approach (based on sidecar pattern). This means that you use a generic proxy app that runs as a separate process on the same host, VM, or container of your actual application. Then, in your app, you call the service via the sidecar proxy on localhost
(service discovery URL is e.g. http://localhost:8081/${app}/services/${type}
) that then acts as proxy to the actual remote service. Now aspects such as resilience with circuit breaking and the actual service discovery can be configured in the sidecar proxy app, independent of your actual application. Therefore, you can even share and reuse configuration and experience with such a sidecar proxy app even across different technologies (Java, .NET/C#, Node.JS, etc.). Further, you do not pollute the technology stack of your actual app with the infrastructure for resilience, throttling, etc. and can update the app and the sidecar independently when security-fixes are available.
Various implementations of such sidecar proxy apps are available as free open source software. Our recommendation in devonfw is to use istio. This not only provides such a side-car, but also an entire management solution for service-mesh, making administration and maintenance much easier. Platforms like OpenShift support this out of the box.
However, if you are looking for details about side-car implementations for services, you can have a look at the following links:
-
Netflix Sidecar - see Spring Cloud Netflix docs
-
Prana - see Prana: A Sidecar for your Netflix PaaS based Applications and Services ← Not updated as it’s not used internally by Netflix
-
Keycloak - see Protecting Jaeger UI with a sidecar security proxy
This documentation is licensed under the Creative Commons License (Attribution-NoDerivatives 4.0 International).