Micronaut - HttpClient, Declarative Client and Filters

Micronaut - HttpClient, Declarative Client and Filters

Call Superheroes via Command Center

Kumar Pallav's photo
Kumar Pallav

Published on Sep 24, 2021

8 min read

Subscribe to my newsletter and never miss my upcoming articles

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

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.

2021-09-22.png

  • We have changed the name as command-center, Build Tool as maven, Java version as 16 and the base package as com.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 value SUPERHERO_URL which is a constant from CommandCenterConstants and has a value http://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 ``pathvariables and headers as required by the endpoint. Do note that the header is just to show that it can be added while invoking theURLs``` 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 adding User-Agent and a custom TRACKING-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.
 
Share this