Type Here to Get Search Results !

Spring Cloud OpenFeign in Spring Boot | Synchronous Communication

0

FeignClient - Synchronous Communication

Feign is a Declarative HTTP client developed by Netflix. Declarative means "we tell What to do, not How to do".

FeignClientSpringBootSpring Cloud OpenFeignSynchronous Communication
FeignClient • Synchronous Communication

FeignClient
Synchronous Communication

Feign is a Declarative HTTP client developed by Netflix.

Feign
Table of Contents

1. FeignClient

Feign is a Declarative HTTP client developed by Netflix.

Declarative means "we tell What to do, not How to do".

In SpringBoot, Feign capability is available via Spring Cloud OpenFeign library.

Spring Cloud provides a set of tool and libraries, which helps to build distributed microservices. As it provides seamless integration with:

2. Pom.xml

Dependency

XML
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

In future, we might use more Spring Cloud libraries (for Load balancer, for Service Discovery etc.) and all those Spring Cloud libraries should have compatible version, therefore we use below dependency management, so that we don't have to mange it manually.

That’s why we are not specifying the version with above "spring-cloud-starter-openfeign" dependency, it will be taken care by our dependency management.

XML
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2023.0.1</version> <!-- Use latest compatible version-->
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

3. OrderService → ProductService

Service Flow
OrderService ---------------------> ProductService

ProductClient

Java
@FeignClient(
        name = "product-service",
        url = "${feign.client.product-service.url}"
)
public interface ProductClient {

    @GetMapping("/products/{id}")
    String getProductById(@PathVariable("id") String id);
}

ProductController

Java
@RestController
@RequestMapping("/products")
public class ProductController {

    @GetMapping("/{id}")
    public String getProduct(@PathVariable String id) {
        return "Product fetched with id: " + id;
    }
}

application.properties

Properties
server.port=8081

# Base URL for Product Service
feign.client.product-service.url=http://localhost:8082

OrderController

Java
@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    ProductClient productClient;

    @GetMapping("/{id}")
    public ResponseEntity<String> getOrder(@PathVariable String id) {

        String responseFromProductAPI =
                productClient.getProductById(id);
        System.out.println(
                "Response from Product api call is: " +
                responseFromProductAPI
        );

        return ResponseEntity.ok("order call successful");
    }
}

OrderserviceApplication

Java
@SpringBootApplication
@EnableFeignClients
public class OrderserviceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderserviceApplication.class, args);
    }
}

@EnableFeignClients enable Feign support and tells SpringBoot to scan for interfaces annotated with @FeignClient

Start the application and invoke the Order Endpoint.

Postman response for the Order endpoint from the supplied PDF
Postman output shown in the supplied PDF.
OrderService console output after calling ProductService from the supplied PDF
Console output shown in the supplied PDF.

4. How Declarative HTTP Calls Works?

Declarative HTTP call working screenshot from the supplied PDF
Declarative HTTP call implementation visual from the supplied PDF.

So, first important thing to understand is, how this Declarative HTTP Calls works?

MethodHandler also has method invoke(), which knows how to create HTTP request object from above info and make a HTTP call.

FeignClient invocation flow diagram from the supplied PDF
FeignClient internal invocation flow shown in the supplied PDF.

5. Annotation Usage Demo

An example below, with various annotation usage demo like @GetMapping, @PutMapping, @PathVariable, @RequetParam, @RequestHeader, @RequestBody etc..

Order Application — ProductClient

Java
@FeignClient(
        name = "product-service",
        url = "${feign.client.product-service.url}"
)
public interface ProductClient {

    @GetMapping("/products/{id}")
    String getProductById(@PathVariable("id") String id);

    @PutMapping(
            value = "/products/update/{id}",
            consumes = "application/json"
    )
    Product updateProduct(
        /*
            Ordering is not mandatory,
            Springboot will take care to pass
            values according to annotations.
        */
        @PathVariable("id") String id,
        @RequestParam("sendMail") boolean sendMail,
        @RequestHeader("X-ConceptCoding-ID") String uniqueID,
        @RequestBody Product updatedProductDetails
    );
}

Product Application — ProductController

ProductClient annotation usage screenshot from the supplied PDF
Annotation usage example shown in the supplied PDF.
Java
@RestController
@RequestMapping("/products")
public class ProductController {

    @GetMapping("/{id}")
    public ResponseEntity<String> getProduct(@PathVariable String id) {
        return ResponseEntity.ok("fetch the product details with id:" + id);
    }

    @PutMapping("/update/{id}")
    public ResponseEntity<Product> createProduct(
            @PathVariable String id,
            @RequestBody Product product,
            @RequestParam("sendMail") boolean sendMail,
            @RequestHeader("X-ConceptCoding-ID") String uniqueID
    ) {

        Product dbProductObject = getProductFromDB(id);
        dbProductObject.setName(product.getName());

        return ResponseEntity.ok(dbProductObject);
    }
}

6. Encoder and Decoder in FeignClient

  • Encoder: Converts a Java object into a request body (say JSON).
  • Decoder: Converts the HTTP response body (say JSON) into a Java object.

If we want our custom Encoder and Decoder implementation:

Custom Encoder and Decoder implementation screenshot from the supplied PDF
Custom Encoder and Decoder implementation visual from the supplied PDF.

ProductClient

Java
@FeignClient(
        name = "product-service",
        url = "${feign.client.product-service.url}",
        configuration = ProductClientConfig.class
)
public interface ProductClient {

    @PutMapping(
            value = "/products/update/{id}",
            consumes = "application/json"
    )
    Product updateProduct(
            @PathVariable("id") String id,
            @RequestParam("sendMail") boolean sendMail,
            @RequestHeader("X-ConceptCoding-ID") String uniqueID,
            @RequestBody Product updatedProductDetails
    );
}

MyCustomProductClientEncoder

Java
public class MyCustomProductClientEncoder implements Encoder {

    @Override
    public void encode(
            Object object,
            Type bodyType,
            RequestTemplate template
    ) throws EncodeException {

        try {
            String jsonString =
                    new ObjectMapper().writeValueAsString(object);
            template.body(jsonString);
        } catch (Exception e) {
            throw new EncodeException("Unable to encode object");
        }
    }
}

MyCustomProductClientDecoder

Java
public class MyCustomProductClientDecoder implements Decoder {

    @Override
    public Object decode(Response response, Type type)
            throws IOException, DecodeException, FeignException {

        InputStream responseBody =
                response.body().asInputStream();

        return new ObjectMapper().readValue(
                responseBody,
                new TypeReference<Object>() {
                    @Override
                    public Type getType() {
                        return type;
                    }
                }
        );
    }
}

ProductClientConfig

Java
@Configuration
public class ProductClientConfig {

    @Bean
    public Encoder myCustomEncoder() {
        return new MyCustomProductClientEncoder();
    }

    @Bean
    public Decoder myCustomDecoder() {
        return new MyCustomProductClientDecoder();
    }
}

7. ErrorDecoder in FeignClient

It is used to handle non 2xx status codes like 4xx and 5xx.

If we want our custom ErrorDecoder implementation:

Custom ErrorDecoder implementation screenshot from the supplied PDF
Custom ErrorDecoder implementation visual from the supplied PDF.

ProductClient

Java
@FeignClient(
        name = "product-service",
        url = "${feign.client.product-service.url}",
        configuration = ProductClientConfig.class
)
public interface ProductClient {

    @PutMapping(
            value = "/products/update/{id}",
            consumes = "application/json"
    )
    Product updateProduct(
            @PathVariable("id") String id,
            @RequestParam("sendMail") boolean sendMail,
            @RequestHeader("X-ConceptCoding-ID") String uniqueID,
            @RequestBody Product updatedProductDetails
    );
}

MyCustomProductClientErrorDecoder

Java
public class MyCustomProductClientErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultErrorDecoder = new Default();

    @Override
    public Exception decode(String methodKey, Response response) {

        HttpStatus statusCode = HttpStatus.valueOf(response.status());

        if (statusCode.is4xxClientError()) {
            return new MyCustomBadRequestException("Client Error");
        } else if (statusCode.is5xxServerError()) {
            return new MyCustomServerException("Server Error");
        } else {
            return defaultErrorDecoder.decode(methodKey, response);
        }
    }
}

ProductClientConfig

Java
@Configuration
public class ProductClientConfig {

    @Bean
    public ErrorDecoder myCustomErrorDecoder() {
        return new MyCustomProductClientErrorDecoder();
    }
}

8. Retryer in FeignClient

Retryer implementation settings screenshot from the supplied PDF
Retryer settings shown in the supplied PDF.
Retryer execution flow screenshot from the supplied PDF
Retryer execution flow shown in the supplied PDF.
Retryer default settings diagram from the supplied PDF
Retryer default settings shown in the supplied PDF.

Retry only happens when there is either:

  • Connection time out
  • Network related exception like (IOException)

After all retry finished, then ErrorDecoder is invoked.

For 4xx and 5xx, retry do not happens, its handled by ErrorDecoder directly.

Retry Attempts
Try 1 : (immediate attempt) Try 2 : wait 100ms Try 3 : wait 200ms Try 4 : wait 400ms Try 5 : wait 800ms (but max capped at 1 second)

After all retry attempt finished, ErrorDecoder is invoked.

If, we don’t want to retry at all, we can use Retryer.NEVER_RETRY (this already present in Retryer class)

Java
@Configuration
public class ProductClientConfig {

    @Bean
    public Retryer myCustomRetryer() {
        return Retryer.NEVER_RETRY;
    }
}

If, we want custom implementation

UserCase-1: I only want to control Attempt, wait time and max period, rest I want to reuse the "Retryer.Default" logic.

Java
public class MyCustomRetryer extends Retryer.Default {

    // control attempts & wait time, rest uses Default implementation
    public MyCustomRetryer() {
        super(200, 1000, 4); // period, maxPeriod, maxAttempts
    }
}
Java
@Configuration
public class ProductClientConfig {

    @Bean
    public Retryer myCustomRetryer() {
        return new MyCustomRetryer();
    }
}

UserCase-2: I want full control, then I have to implement Retryer itself and provide the custom implementation for the "continueOrPropogate()" method.

Java
public class MyCustomRetryer implements Retryer {

    private int attempt = 1;
    private final int maxAttempts = 5;

    @Override
    public void continueOrPropagate(RetryableException e) {
        if (attempt >= maxAttempts) {
            throw e;
        }
        attempt++;
        try {
            Thread.sleep(100);
        } catch (InterruptedException ie) {
            // ignore
        }
    }

    @Override
    public Retryer clone() {
        return new MyCustomRetryer();
    }
}

9. FeignClient Name and Timeout Configuration

Last but not the least:

During start, we discussed that, this name is just a arbitrary value. And its just we are giving the name to our FeignClient.

But where its exactly used?

Yes, this name comes handy, when we have to provide any configuration in application.properties.

If we want to set request and connection timeout only for product-service FeignClient

application.properties

Properties
# timeout applicable only to product-service FeignClient
feign.client.config.product-service.connectTimeout=3000
feign.client.config.product-service.readTimeout=5000

If we want to set request and connection timeout for all FeignClient

application.properties

Properties
# timeout applicable to all FeignClients
feign.client.config.default.connectTimeout=3000
feign.client.config.default.readTimeout=5000

Post a Comment

0 Comments