How to interpret and modify REST requests in Spring Boot in a centralized way?

Let’s say you have created a bunch of REST services in your Spring Boot app.

And you want to perform a common action on all the requests.

May be you want to log the incoming requests to a log file.

Instead of doing this action for every REST service you can do it in a centralized way.

Using RequestBodyAdvice.

This is different from HandlerInterceptor provided by Spring.

Strangely you cannot read a request using HandlerInterceptor , you can only interpret requests and perform some logic inside which is not dependent on the JSON request.

Let’s see how to use a RequestBodyAdvice.

I want to print out the inputs of all the hits to my rest services.

Below is the algorithm to implement it:

STEP 1: Create a REST Controller Advice class

@RestControllerAdvice
public class RequestAdvice extends RequestBodyAdviceAdapter {

}

Notice the annotation @RestControllerAdvice.

You can either implement the interface RequestBodyAdvice or extend the class RequestBodyAdviceAdapter.

RequestBodyAdvice interface has three methods but we are interested in only two of the methods in it:

  • supports
  • afterBodyRead

The method supports() can be used to configure which methods need to be interpreted by the RestControllerAdvice. It returns a boolean value , returning true means all the REST methods will be interpreted.

The method afterBodyRead() accepts the input as a parameter and so we can use this to read the request.

We don’t need the other two methods.

And so we can use the class RequestBodyAdviceAdapter which provides a default implementation for all the methods except supports() method.

By extending this class we can override these two methods alone.

STEP2: Override supports method

This method as already mentioned can be used to configure which methods are to be interpreted . You can return the value “true” if you want to interpret all REST requests.

@RestControllerAdvice
public class RequestAdvice extends RequestBodyAdviceAdapter {
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
}

In the above case I am returning true so that all the REST requests in this application will be interpreted.

We will see later how to configure this only for specific REST methods.

STEP3: Implement afterBodyRead method to read the request

Here is a sample implementation which prints out the request to the console. You can also print it to a log file.

  @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {

        Map<String,String> input = (Map)body;


        System.out.println(input);


        return body;
    }

This assumes that the input requests are of Map<String,String> type.

You can check for the instance type using instanceof operator and proceed accordingly if you use different request types.

Here is the entire class:

package com.example.requestadvice;

import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;

import java.lang.reflect.Type;
import java.util.Map;

@RestControllerAdvice
public class RequestAdvice extends RequestBodyAdviceAdapter {
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {

        Map<String,String> input = (Map)body;


        System.out.println(input);


        return body;
    }
}

That’s it!

All you need is this class to be placed in your application anywhere and it will start printing the input to the console.

Here is a sample REST controller class I created to test this:

package com.example.requestadvice;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class RestServices {


    @PostMapping("/test1")
    public String test1(@RequestBody Map<String,String> request){

        return "I am test1 method";
    }

    @PostMapping("/test2")
    public String test2(@RequestBody Map<String,String> request){

        return "I am test2 method";
    }

    @PostMapping("/test3")
    public String test3(@RequestBody Map<String,String> request){

        return "I am test3 method";
    }

}

There are three rest services .

Invoking any of these services will result in the input body being printed to the console.

For example , I hit one of the APIs using postman tool:

And I could see the request printed in the console:

2021-02-04 19:32:39.125  INFO 15976 --- [           main] c.e.r.RequestadviceApplication           : Started RequestadviceApplication in 2.371 seconds (JVM running for 2.849)
 2021-02-04 19:32:42.339  INFO 15976 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
 2021-02-04 19:32:42.339  INFO 15976 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
 2021-02-04 19:32:42.340  INFO 15976 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
 {message=hello}

Now let’s tinker with the above request advice.

What if you want to interpret only /test1 and /test2 methods and not /test3 REST method?

You can configure that in supports method like this:

   @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {

        String methodName = methodParameter.getMethod().getName();

        if(methodName.equals("test1")||methodName.equals("test2")) {
            return true;
        }else{

            return false;
        }
    }

Now if you hit /test3 the input will not be printed to the console.

Ideally in a production environment you many want to place the method names in application.yml file and fetch it from there so that it is easier for you to update the method names.

Now , if for some reason you want to modify the requests you can do so in the RestControllerAdvice like below:

  @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {

        Map<String,String> input = (Map)body;

        input.put("time", LocalDateTime.now().toString());
        System.out.println(input);


        return body;
    }

I have just added the current time to the input request.

This will be passed along to your handler classes.

That’s it!

Comments

Leave a Reply

Discover more from The Full Stack Developer

Subscribe now to keep reading and get access to the full archive.

Continue reading