Micronaut - HttpClient, Declarative Client and Filters
Call Superheroes via Command Center
Introduction
In Microservices we will always have more than one service interacting with each other. One of the most popular ways for inter-service communication is REST API. While we are using POSTMAN to check our services as of now , in an actual service we require an agent to make REST calls for us. This is done by HttpClient in Micronaut.
Requirement
If you are following the series, you must be aware that we have created the API for Superheroes and added the reactive version of API as well. We also checked at TestContainers
to write efficient tests. In this section, we will look at calling the API from another service, let us call it command-center.
It will be the job of the command center to make calls to the services. It will help us to understand how API communication happens between two microservices. We will also add a Filter to add custom headers and track id as the header. In the end, we will update superhero API to check if the client who is making the request is valid using another HTTP Filter.
Git Repository
You can find the code of the article at
- command-center: github.com/CODINGSAINT/command-center/tree/..
- Superhero API : github.com/CODINGSAINT/super-heroes/tree/06..
Approach
Creating a new Micronaut project
command-center
Adding the POJO , controller & service layer
Call endpoints from service layer
Call superhero API
Add HTTP Filter to add Headers to all outgoing API calls
Add a Filter in superhero API to validate the incoming headers
Creating a new Micronaut project command-center
- In order to create the micronaut project we will vising https://micronaut.io/launch . We will launch a new project with the dependency of HTTP Client added to it.
We have changed the name as
command-center
, Build Tool asmaven
, Java version as 16 and the base package ascom.codingsaint
Generate the new project and import it into your IDE (IntelliJ, eclipse)
Adding the POJO , controller & service layer
Once our new project is imported into IDE we can add Superhero POJO (it's same as we did in superhero project) a Controller and a Service layer to our new project.
We will also create a class for keeping constant , let us name it CommandCenterConstants
public class CommandCenterConstants {
public static final String SUPERHERO_URL= "http://localhost:8080/";
public static final String USER_AGENT= "Coding Saint Command Center Client";
public static final String HEADER_ACCEPT= "application/json";
}
CommandCenterService.java
@Singleton
public class CommandCenterService {
private static final Logger logger = LoggerFactory.getLogger(CommandCenterService.class);
private final HttpClient client;
public CommandCenterService(@Client(SUPERHERO_URL) HttpClient client) {
this.client = client;
}
}
- Here you can see we have injected
HttpClient
to the Service layer. It will help us to do the Rest Calls to the Superhero API @Client
annotation is has a valueSUPERHERO_URL
which is a constant fromCommandCenterConstants
and has a valuehttp://localhost:8080/
.- The URL of the
Client
annotation will let the HTTP client to connect to it while making a call.
CommandCenterController.java
@Controller
@ExecuteOn(TaskExecutors.IO)
public class CommandCenterController {
private static final Logger logger = LoggerFactory.getLogger(CommandCenterController.class);
private final CommandCenterService service;
public CommandCenterController(CommandCenterService commandCenterService) {
this.service = commandCenterService;
}
- The Controller has
CommandCenterService
injected into it.
Call endpoints from the service layer
Let us call the endpoints of our Superhero through the service layer.
public Publisher<Superhero> create(Superhero superhero) {
logger.info("Adding a new saviour {} ", superhero);
var uri= UriBuilder.of("superhero").build();
HttpRequest req = HttpRequest.POST(uri, superhero)
.header("ACCEPT", "application/json");
var created = Mono.from( client.retrieve(req));
return created;
}
public Publisher<Superhero> update(Superhero superhero) {
logger.info("Updating the old saviour {} ", superhero);
var uri= UriBuilder.of("superhero").build();
HttpRequest req = HttpRequest.PUT(uri, superhero)
.header("ACCEPT", "application/json");
var updated = Mono.from( client.retrieve(req));
return updated;
}
public Publisher<Long> delete(Long id) {
logger.info("Purging the saviour {} ", id);
var uri= UriBuilder.of("superhero").path(String.valueOf(id)).build();
HttpRequest req = HttpRequest.DELETE(uri.toString())
.header("ACCEPT", "application/json");
return Mono.from( client.exchange(req));
}
- Here you can note that we can add ``path
variables and headers as required by the endpoint. Do note that the header is just to show that it can be added while invoking the
URLs``` via HTTP client. We can also configure common Headers like Content-Type and even Authorization headers via Http Filter which we will add later.
Call superhero API
Now we will not directly call Superhero API but via command center. We will add endpoints to command center Controller , which will talk to Superhero via REST API using injected service layer.
Let us add methods for Controller as below
@Get("superhero/{id}")
public Mono<Superhero> superheroesById(Long id) {
return service.superheroesById(id);
}
@Post("/superhero")
Mono<Superhero> create(@Valid Superhero superhero) {
logger.info("Call to add a new saviour {} ", superhero);
return Mono.from(service.create(superhero));
}
@Put("superhero")
Mono<Superhero> update(@Valid Superhero superhero) {
return Mono.from(service.update(superhero));
}
@Delete("superhero/{id}")
Mono<HttpResponse<?>> delete(@NotNull Long id) {
return Mono
.from(service.delete(id))
.map(deleted -> deleted > 0 ? HttpResponse.noContent() : HttpResponse.notFound());
}
If you restart your server it will work. But I would like to show you HTTP request and response filters as well to control the API misuse and let only valid clients call Superhero API. While we will only show it as demo , you can enhance it to use production-ready service adding Authorization headers, or any other heder of your need.
Add HTTP Filter to add Headers to all outgoing API calls
Let us add the HTTP Request Filter in the command-center config.
@Filter( "/**")
public class SuperheroHttpClientFilter implements HttpClientFilter {
private static final Logger logger= LoggerFactory.getLogger(SuperheroHttpClientFilter.class);
@Override
public Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
var header= request.getHeaders();
logger.debug("Adding HTTP Headers to client ");
header.add("User-Agent", "Coding Saint HTTP Client") ;
header.add("ACCEPT", "application/json");
String GUID= UUID.randomUUID().toString();
header.add("TRACKING_ID", GUID);
logger.info("Request with id {} , url {} , body {} to superheroes API ", GUID, request.getUri(), request.getBody());
return chain.proceed(request);
}
}
@Filter
itself is telling them it's valid for all outgoing HTTP request with**
doFilter
method is addingUser-Agent
and a customTRACKING-ID
to the header of all outgoing requests. ``` We are logging the tracking id and URL to record the outgoing HTTP requests.
We will start the command-center App at 8083. Let is update application.yml accordingly
micronaut:
application:
name: commandCenter
server:
port: 8083
Add a Filter in superhero API to validate the incoming headers
Since we are adding it for all of the outgoing requests from command-center to Superhero API, let us add another filter to check if it's well-received or not. In case User-Agent and TRACKING_ID are not present in the header, let us simulate it as a bad request
We will add the following class at Superhero API
@Filter("/**")
public class SuperheroServerFilter implements HttpServerFilter {
private static final Logger logger = LoggerFactory.getLogger(SuperheroServerFilter.class);
@Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
String userAgent = request.getHeaders().get("User-Agent");
String trackingId = request.getHeaders().get("TRACKING_ID");
logger.info("Received requests from {} and Tracking id {} ", userAgent, trackingId);
if (StringUtils.isEmpty(userAgent) || StringUtils.isEmpty(trackingId)) {
var map = new HashMap<String, String>();
map.put("id", "Please provide a valid tracking id and user agent ");
return Publishers.just(HttpResponse.ok(map)
.status(HttpStatus.FORBIDDEN)
.contentType(MediaType.APPLICATION_JSON_TYPE));
}
return chain.proceed(request);
}
- Her as dummy check we have rejected requests who don't have User-Agent and TRACKING-ID in the header and returning Forbidden status. We are also adding a meaningful message as a response (Please provide a valid tracking id and user agent ). Now we have done all of the hard work, it's showtime. Let us run both of the App and check that flow is working.
curl --location --request POST 'localhost:8080/superhero' \
--header 'User-Agent: Postman' \
--header 'TRACKING_ID: TEST_01' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Bug Killer",
"prefix": "Miss",
"suffix": "Bomber",
"power": "Bomb All the bugs in code"
}'
The response to the above request
{
"id": 1,
"name": "Bug Killer",
"prefix": "Miss",
"suffix": "Bomber",
"power": "Bomb All the bugs in code"
}
Do note that I have only shown POST endpoint . You can check all others similarly.
If you observe the logs , you can see that Filter logs are added in each request at the command-center app which is making an HTTP request and also is intercepted by API and validated.
Logs at command-center App
06:08:24.857 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 892ms. Server Running: http://localhost:8083
06:08:34.869 [io-executor-thread-2] INFO c.c.CommandCenterController - Call to add a new saviour Superhero[id=null, name=Bug Killer, prefix=Miss, suffix=Bomber, power=Bomb All the bugs in code]
06:08:34.891 [io-executor-thread-2] INFO c.c.c.SuperheroHttpClientFilter - Request with id ee8c9d5e-d5b9-49df-be6b-b63c4bd9e9fe , url http://localhost:8080/superhero , body Optional[Superhero[id=null, name=Bug Killer, prefix=Miss, suffix=Bomber, power=Bomb All the bugs in code]] to superheroes API
Logs at Superhero API
06:14:32.524 [default-nioEventLoopGroup-1-2] INFO c.c.configs.SuperheroServerFilter - Received requests from Coding Saint HTTP Client and Tracking id 599cc7f7-781d-476f-9d5b-5c4c433b1905
06:14:32.830 [io-executor-thread-2] INFO com.codingsaint.SuperHeroController - Saving new Superhero Superhero[id=null, name=Bug Killer, prefix=Miss, suffix=Bomber, power=Bomb All the bugs in code]
Declarative Client to call HTTP Requests at other Server
We have written a low-level client with CommandCenterService
, it gives us control before calling to Superhero API , but there are chances where you will not be interested in writing the full REST API calls.
We can use a declarative client which will be an interface. It will call the Superhero API as needed. Let us create SuperheroClient
in the service layer.
Below is the code for the Declarative REST client with Micronaut.
@Client(SUPERHERO_URL)
@Header(name = "Accept", value = HEADER_ACCEPT)
public interface SuperheroClient {
@Get("/rx/superheroes")
Flux<Superhero> superheroes();
@Get("/rx/superhero/{id}")
Mono<Superhero> superheroesById(Long id);
@Post("/rx/superhero")
Publisher<Superhero> create( @Body Superhero superhero);
@Put("/rx/superhero")
Publisher<Superhero> update( Superhero superhero);
@Delete("/rx/superhero/{id}")
Publisher<HttpResponse<Long>> delete(Long id);
We can update the Controller and use any of the two i.e. CommandCenterService
the low-level client or the SuperheroClient the declarative one.
Updated Controller to use Declarative Client
@Get("superhero/{id}")
public Mono<Superhero> superheroesById(Long id) {
/* return service.superheroesById(id);*/
return client.superheroesById(id);
}
@Post("/superhero")
Mono<Superhero> create(@Valid Superhero superhero) {
logger.info("Call to add a new saviour {} ", superhero);
/* return Mono.from(service.create(superhero));*/
return Mono.from(client.create(superhero));
}
@Put("superhero")
Mono<Superhero> update(@Valid Superhero superhero) {
/* return Mono.from(service.update(superhero));*/
return Mono.from(client.update(superhero));
}
@Delete("superhero/{id}")
Mono<HttpResponse<?>> delete(@NotNull Long id) {
/* return Mono
.from(service.delete(id))
.map(deleted -> deleted > 0 ? HttpResponse.noContent() : HttpResponse.notFound());*/
return Mono
.from(client.delete(id));
}
- Do note that the service layer is commented , you can use any of them.