Bael 5265: OpenFeign and Spring Exception handler (#12468)

* Implemented cassandra batch query

* Added netty version param

* Reformatted correctly

* Reformatted correctly

* Reformatted correctly

* Formatting fix resolved

* Formatting fix resolved

* Removed unused method

* Refactored method for better readability

* tab spaces corrected

* Added http headers in feign

* Updated code

* Updated code

* Removed unused code

* Removed unused logger code

* Implemented Interceptor and logging related code review

* Added AuthService Code

* Removed toString method

* Removed unnecessary declaration

* Removed new line

* Added feign headers log as well

* Moved to Authorisation package for better naming

* spaces removed

* @Override included

* [Saikat]| Adding the OpenFeign exception default and customizable exception handler and test cases

* [Saikat]| Test name updated

* [Saikat]| Test name updated

* [Saikat]| Removed unused import

* [Saikat]| Refactored code

* [Saikat]| Reformatted the test codes

* [Saikat]| Removed unnecessary code

* [Saikat]| Controller test fixed for data error

* [Saikat]| Code review fixes

* [Saikat]| Code review fixes

* [Saikat]| Removed unnecessary annotation

* [Saikat]| Added extends ResponseEntityExceptionHandler

* [Saikat]| Code review implemented

* [Saikat]| Indentation added

* Removed empty line before method start

Co-authored-by: saikat chakraborty <saikat.chakraborty@tesco.com>
This commit is contained in:
Saikat Chakraborty 2022-08-31 14:08:40 +05:30 committed by GitHub
parent e7c4294e3e
commit 8a79e04d39
18 changed files with 606 additions and 0 deletions

View File

@ -61,6 +61,12 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.33.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>

View File

@ -0,0 +1,17 @@
package com.baeldung.cloud.openfeign.customizederrorhandling.client;
import com.baeldung.cloud.openfeign.customizederrorhandling.config.FeignConfig;
import com.baeldung.cloud.openfeign.defaulterrorhandling.model.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(name = "product-client-2", url = "http://localhost:8081/product/", configuration = FeignConfig.class)
public interface ProductClient {
@RequestMapping(value = "{id}", method = RequestMethod.GET)
Product getProduct(@PathVariable(value = "id") String id);
}

View File

@ -0,0 +1,24 @@
package com.baeldung.cloud.openfeign.customizederrorhandling.config;
import com.baeldung.cloud.openfeign.exception.BadRequestException;
import com.baeldung.cloud.openfeign.customizederrorhandling.exception.ProductNotFoundException;
import com.baeldung.cloud.openfeign.customizederrorhandling.exception.ProductServiceNotAvailableException;
import feign.Response;
import feign.codec.ErrorDecoder;
public class CustomErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
switch (response.status()){
case 400:
return new BadRequestException();
case 404:
return new ProductNotFoundException("Product not found");
case 503:
return new ProductServiceNotAvailableException("Product Api is unavailable");
default:
return new Exception("Exception while getting product details");
}
}
}

View File

@ -0,0 +1,18 @@
package com.baeldung.cloud.openfeign.customizederrorhandling.config;
import feign.Logger;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
}

View File

@ -0,0 +1,27 @@
package com.baeldung.cloud.openfeign.customizederrorhandling.controller;
import com.baeldung.cloud.openfeign.customizederrorhandling.client.ProductClient;
import com.baeldung.cloud.openfeign.defaulterrorhandling.model.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController("product_controller2")
@RequestMapping(value = "myapp2")
public class ProductController {
private final ProductClient productClient;
@Autowired
public ProductController(ProductClient productClient) {
this.productClient = productClient;
}
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable String id) {
return productClient.getProduct(id);
}
}

View File

@ -0,0 +1,54 @@
package com.baeldung.cloud.openfeign.customizederrorhandling.exception;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.http.HttpStatus;
import java.util.Date;
public class ErrorResponse {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
private Date timestamp;
@JsonProperty(value = "code")
private int code;
@JsonProperty(value = "status")
private String status;
@JsonProperty(value = "message")
private String message;
@JsonProperty(value = "details")
private String details;
public ErrorResponse() {
}
public ErrorResponse(HttpStatus httpStatus, String message, String details) {
timestamp = new Date();
this.code = httpStatus.value();
this.status = httpStatus.name();
this.message = message;
this.details = details;
}
public Date getTimestamp() {
return timestamp;
}
public int getCode() {
return code;
}
public String getStatus() {
return status;
}
public String getMessage() {
return message;
}
public String getDetails() {
return details;
}
}

View File

@ -0,0 +1,40 @@
package com.baeldung.cloud.openfeign.customizederrorhandling.exception;
import com.baeldung.cloud.openfeign.exception.BadRequestException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@RestControllerAdvice
public class ProductExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({ProductServiceNotAvailableException.class})
public ResponseEntity<ErrorResponse> handleProductServiceNotAvailableException(ProductServiceNotAvailableException exception, WebRequest request) {
return new ResponseEntity<>(new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR,
exception.getMessage(),
request.getDescription(false)),
HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler({ProductNotFoundException.class})
public ResponseEntity<ErrorResponse> handleProductNotFoundException(ProductNotFoundException exception, WebRequest request) {
return new ResponseEntity<>(new ErrorResponse(
HttpStatus.NOT_FOUND,
exception.getMessage(),
request.getDescription(false)),
HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception exception, WebRequest request) {
return new ResponseEntity<>(new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR,
exception.getMessage(),
request.getDescription(false)),
HttpStatus.INTERNAL_SERVER_ERROR);
}
}

View File

@ -0,0 +1,8 @@
package com.baeldung.cloud.openfeign.customizederrorhandling.exception;
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,8 @@
package com.baeldung.cloud.openfeign.customizederrorhandling.exception;
public class ProductServiceNotAvailableException extends RuntimeException {
public ProductServiceNotAvailableException(String message) {
super(message);
}
}

View File

@ -0,0 +1,17 @@
package com.baeldung.cloud.openfeign.defaulterrorhandling.client;
import com.baeldung.cloud.openfeign.defaulterrorhandling.config.FeignConfig;
import com.baeldung.cloud.openfeign.defaulterrorhandling.model.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(name = "product-client", url = "http://localhost:8081/product/", configuration = FeignConfig.class)
public interface ProductClient {
@RequestMapping(value = "{id}", method = RequestMethod.GET)
Product getProduct(@PathVariable(value = "id") String id);
}

View File

@ -0,0 +1,12 @@
package com.baeldung.cloud.openfeign.defaulterrorhandling.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

View File

@ -0,0 +1,24 @@
package com.baeldung.cloud.openfeign.defaulterrorhandling.controller;
import com.baeldung.cloud.openfeign.defaulterrorhandling.client.ProductClient;
import com.baeldung.cloud.openfeign.defaulterrorhandling.model.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController("product_controller1")
@RequestMapping(value ="myapp1")
public class ProductController {
private final ProductClient productClient;
@Autowired
public ProductController(ProductClient productClient) {
this.productClient = productClient;
}
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable String id) {
return productClient.getProduct(id);
}
}

View File

@ -0,0 +1,24 @@
package com.baeldung.cloud.openfeign.defaulterrorhandling.model;
public class Product {
private String id;
private String productName;
private double price;
public String getId() {
return id;
}
public String getProductName() {
return productName;
}
public double getPrice() {
return price;
}
}

View File

@ -0,0 +1,57 @@
package com.baeldung.cloud.openfeign.customizederrorhandling.client;
import com.baeldung.cloud.openfeign.customizederrorhandling.exception.ProductNotFoundException;
import com.baeldung.cloud.openfeign.customizederrorhandling.exception.ProductServiceNotAvailableException;
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.junit.Assert.assertThrows;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProductClientUnitTest {
@Autowired
private ProductClient productClient;
private WireMockServer wireMockServer;
@Before
public void startWireMockServer() {
wireMockServer = new WireMockServer(8081);
configureFor("localhost", 8081);
wireMockServer.start();
}
@After
public void stopWireMockServer() {
wireMockServer.stop();
}
@Test
public void givenProductApiIsNotAvailable_whenGetProductCalled_thenThrowProductServiceNotAvailableException() {
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse().withStatus(503)));
assertThrows(ProductServiceNotAvailableException.class, () -> productClient.getProduct(productId));
}
@Test
public void givenProductNotFound_whenGetProductCalled_thenThrowBadRequestException() {
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse().withStatus(404)));
assertThrows(ProductNotFoundException.class, () -> productClient.getProduct(productId));
}
}

View File

@ -0,0 +1,98 @@
package com.baeldung.cloud.openfeign.customizederrorhandling.controller;
import com.baeldung.cloud.openfeign.customizederrorhandling.client.ProductClient;
import com.baeldung.cloud.openfeign.customizederrorhandling.exception.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@WebMvcTest(ProductController.class)
@ImportAutoConfiguration({FeignAutoConfiguration.class})
public class ProductControllerUnitTest {
@Autowired
private ProductClient productClient;
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
private WireMockServer wireMockServer;
@Before
public void startWireMockServer() {
wireMockServer = new WireMockServer(8081);
configureFor("localhost", 8081);
wireMockServer.start();
}
@After
public void stopWireMockServer() {
wireMockServer.stop();
}
@Test
public void givenProductApiIsNotAvailable_whenGetProductCalled_ThenReturnInternalServerError() throws Exception {
String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
ErrorResponse expectedError = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR,
"Product Api is unavailable","uri=/myapp2/product/" + productId);
MvcResult result = mockMvc.perform(get("/myapp2/product/" + productId))
.andExpect(status().isInternalServerError()).andReturn();
ErrorResponse errorResponse = objectMapper.readValue(result.getResponse().getContentAsString(), ErrorResponse.class);
assertEquals(expectedError.getCode(), errorResponse.getCode());
assertEquals(expectedError.getMessage(), errorResponse.getMessage());
assertEquals(expectedError.getStatus(), errorResponse.getStatus());
assertEquals(expectedError.getDetails(), errorResponse.getDetails());
}
@Test
public void givenProductIsNotFound_whenGetProductCalled_ThenReturnInternalServerError() throws Exception {
String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.NOT_FOUND.value())));
ErrorResponse expectedError = new ErrorResponse(HttpStatus.NOT_FOUND,
"Product not found","uri=/myapp2/product/" + productId);
MvcResult result = mockMvc.perform(get("/myapp2/product/" + productId))
.andExpect(status().isNotFound()).andReturn();
ErrorResponse errorResponse = objectMapper.readValue(result.getResponse().getContentAsString(), ErrorResponse.class);
assertEquals(expectedError.getCode(), errorResponse.getCode());
assertEquals(expectedError.getMessage(), errorResponse.getMessage());
assertEquals(expectedError.getStatus(), errorResponse.getStatus());
assertEquals(expectedError.getDetails(), errorResponse.getDetails());
}
}

View File

@ -0,0 +1,84 @@
package com.baeldung.cloud.openfeign.defaulterrorhandling.client;
import com.baeldung.cloud.openfeign.defaulterrorhandling.model.Product;
import com.github.tomakehurst.wiremock.WireMockServer;
import feign.FeignException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.junit4.SpringRunner;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProductClientUnitTest {
@Autowired
private ProductClient productClient;
private WireMockServer wireMockServer;
@Before
public void startWireMockServer() {
wireMockServer = new WireMockServer(8081);
configureFor("localhost", 8081);
wireMockServer.start();
}
@After
public void stopWireMockServer() {
wireMockServer.stop();
}
@Test
public void givenProductIsAvailable_whenGetProductCalled_thenReturnMatchingProduct() {
String productId = "test";
String productResponse = "{ " +
" \"id\":\"test\",\n" +
" \"productName\":\"Watermelon\",\n" +
" \"price\":12\n" +
"}";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.OK.value())
.withHeader("Content-Type", "application/json")
.withBody(productResponse)));
Product product = productClient.getProduct(productId);
assertEquals(productId, product.getId());
assertEquals("Watermelon", product.getProductName());
assertEquals(12.00d, product.getPrice(), 0.00d);
}
@Test
public void givenProductApiIsNotAvailable_whenGetProductCalled_thenThrowFeignException() {
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
assertThrows(FeignException.class, () -> productClient.getProduct(productId));
}
@Test
public void givenProductIdNotFound_whenGetProductCalled_thenThrowFeignException() {
String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
.willReturn(aResponse()
.withStatus(HttpStatus.NOT_FOUND.value())));
assertThrows(FeignException.class, () -> productClient.getProduct(productId));
}
}

View File

@ -0,0 +1,72 @@
package com.baeldung.cloud.openfeign.defaulterrorhandling.controller;
import com.baeldung.cloud.openfeign.defaulterrorhandling.client.ProductClient;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@WebMvcTest(ProductController.class)
@ImportAutoConfiguration({FeignAutoConfiguration.class, TestControllerAdvice.class})
@EnableWebMvc
public class ProductControllerUnitTest {
@Autowired
private ProductClient productClient;
@Autowired
private MockMvc mockMvc;
private WireMockServer wireMockServer;
@Before
public void startWireMockServer() {
wireMockServer = new WireMockServer(8081);
configureFor("localhost", 8081);
wireMockServer.start();
}
@After
public void stopWireMockServer() {
wireMockServer.stop();
}
@Test
public void givenProductServiceIsnotAvailable_whenGetProductCalled_thenReturnInternalServerError() throws Exception {
String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))
.willReturn(aResponse().withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));
mockMvc.perform(get("/myapp1/product/" + productId))
.andExpect(status().is(HttpStatus.INTERNAL_SERVER_ERROR.value()));
}
@Test
public void givenProductIsNotFound_whenGetProductCalled_thenReturnBadeRequestError() throws Exception {
String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))
.willReturn(aResponse().withStatus(HttpStatus.NOT_FOUND.value())));
mockMvc.perform(get("/myapp1/product/" +productId))
.andExpect(status().is(HttpStatus.INTERNAL_SERVER_ERROR.value()));
}
}

View File

@ -0,0 +1,16 @@
package com.baeldung.cloud.openfeign.defaulterrorhandling.controller;
import feign.FeignException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class TestControllerAdvice {
@ExceptionHandler({FeignException.class})
public ResponseEntity<Object> handleFeignException(FeignException exception) {
return new ResponseEntity<>(exception.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}