This guide is about consuming (calling) services from other applications (micro-services). For providing services see the Service-Layer Guide. Services can be consumed in the client or the server. As the client is typically not written in Java you should consult the according guide for your client technology. In case you want to call a service within your Java code this guide is the right place to get help.
Various solutions already exist for calling services such as RestTemplate
from spring or the JAX-RS client API. Further each and every service framework offers its own API as well. These solutions might be suitable for very small and simple projects (with one or two such invocations). However, with the trend of microservices the invocation of a service becomes a very common use-case that occurs all over the place. Have a look at the following features to get an idea why you want to use the solution offered here.
You need to add (at least one of) these dependencies to your application:
<!-- Starter for consuming REST services -->
<dependency>
<groupId>com.devonfw.java.starters</groupId>
<artifactId>devon4j-starter-cxf-client-rest</artifactId>
</dependency>
<!-- Starter for consuming SOAP services -->
<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 implement them multiple times redundantly. 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);
}
Then all you need to do is this:
@Named
public class UcMyUseCaseImpl extends MyUseCaseBase implements UcMyUseCase {
@Inject
private ServiceClientFactory serviceClientFactory;
...
private MyResult callMyServiceMethod(MyArgs myArgs) {
MyService myService = this.serviceClientFactory.create(MyService.class);
MyResult myResult = myService.myMethod(myArgs); // synchronous call of service over the wire
return myResult;
}
As you can see the synchronous invocation of a service is very simple. Still it is 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 marshaling the arguments (e.g. converting myArgs
to JSON) and unmarshalling the result (e.g. converting the received JSON to myResult
).
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 fallback to the fully qualified name of the service interface.
Configuration on Level 3 has to be provided as 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 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());
Note
|
TODO add configuration properties for external/mock service from Sneha |
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 (see above).
So you can simply add this to your application.properties
:
service.client.app.foo.url=https://foo.company.com:8443/services/rest
service.client.app.bar.url=http://bar.company.com:8080/services/rest
service.client.default.url=https://api.company.com/services/rest
Assuming your service interface would have 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 used for testing with spring-boot random port mechanism (technically spring can not 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}
As you can use any implementation of ServiceDiscoverer
, you can also easily use eureka (or anything else) instead to discover your services.
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 (mainly for testing). -
ServiceHeaderCustomizerOAuth
for OAuth (passes a security token from security context such as a JWT via OAuth). -
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 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.
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.
An important aspect is also asynchronous (and reactive) support. So far we only propose this as an enhancement for a future release with an API like this:
public interface ServiceClientAsyncFactory extends ServiceClientFactory {
AsyncClient<S> createAsync(Class<S> serviceInterface);
}
public interface AsyncClient<S> {
S getClient();
<R> Mono<R> call(R result);
<T> Flux<T> call(Collection<? extends T> result);
<R> void call(R result, Consumer<R> callback);
<R> CompletableFuture<R> callFuture(R result);
}
This API would allow typesafe usage like this:
@Named
public class UcMyUseCaseImpl extends MyUseCaseBase implements UcMyUseCase {
@Inject private ServiceClientFactoryAsync clientFactory;
@Override @RolesAllowed(...)
public void doSomething(Bar bar) {
AsyncClient<MyExternalServiceApi> client = this.clientFactory.createAsync(MyExternalServiceApi.class);
Mono<Some> result = client.call(client.getService().doSomething(convert(bar)));
// client.call(client.getService().doSomething(convert(bar)), x -> processSync(x));
return process(result);
}
}
How can this work? The ServiceClientAsyncFactory implementation would create its own dynamic proxy for the given service interface. That proxy would only track the last call that was invoked internally and always return a dummy result (null
for Object types, false
for boolean, 0
for primitive numbers). The actual implementation of the call
methods can access the internal invocation that has been recorded from the last service call. It will then trigger the actual service call internally according to the desired style (using a Consumer
callback, Mono
, Flux
, Future
…).
Resilience adds a lot of complexity and that 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 are calling 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 and 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.).
Various implementations of such sidecar proxy apps are available as free open source software. TODO: Decide for the best available solution and suggest here as default.
-
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