background

In the development of microservices architecture project, the API interface uniformly uses the response structure.

The HTTP status code is returned as 200. The status code is obtained according to the structure code.

{
  "code": 0,
  "message": "success",
  "data": {
    "name": "kent"
  }
}
Copy the code

The service invokes the process when the user requests it.

The problem

In microservices architecture, under normal circumstances, the returned data structure is returned as the response structure, but when an exception occurs in the service invocation, no code is returned.

Call product-service (order-service) to product-service (order-service);
{
  "timestamp": "2020-08-11 13:25:03", 
  "status": 500, 
  "error": "Internal Server Error",
  "exception": "tech.xproject.common.core.exception.BusinessException",
  "message": "not enough stock", 
  "trace": "tech.xproject.common.core.exception.BusinessException: not enough stock"
}
Copy the code

To solve

Customize FeignErrorDecoder, TerrorAttributes and BusinessException to handle exceptions.

1. Customize FeignErrorDecoder

Code location: service-API

package tech.xproject.order.config; import com.alibaba.fastjson.JSON; import feign.FeignException; import feign.Response; import feign.RetryableException; import feign.codec.ErrorDecoder; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import tech.xproject.common.core.entity.ExceptionInfo; import tech.xproject.common.core.enums.ResultCodeEnum; import tech.xproject.common.core.exception.BusinessException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Optional; /** * @author kent */ @Slf4j @Configuration public class FeignErrorDecoder extends ErrorDecoder.Default { @Override public Exception decode(String methodKey, Response response) { Exception exception = super.decode(methodKey, response); If (exception instanceof RetryableException) {return exception; } try {// if FeignException is used, If (Exception Instanceof FeignException && ((FeignException) exception).responseBody().isPresent())  { ByteBuffer responseBody = ((FeignException) exception).responseBody().get(); String bodyText = StandardCharsets.UTF_8.newDecoder().decode(responseBody.asReadOnlyBuffer()).toString(); ExceptionInfo ExceptionInfo = json.parseObject (bodyText, ExceptionInfo.class); // If code in Excepiton is not empty, use this code, Otherwise the ERROR code using the default Integer code = Optional. OfNullable (exceptionInfo. GetCode ()). OrElse (ResultCodeEnum. ERROR. GetCode ()); // If message in Excepiton is not empty, the message is used, Otherwise, the default error message String message = is used Optional.ofNullable(exceptionInfo.getMessage()).orElse(ResultCodeEnum.ERROR.getMessage()); return new BusinessException(code, message); } } catch (Exception ex) { log.error(ex.getMessage(), ex); } return exception; }}Copy the code

2. Use FeignErrorDecoder in FeignClient

Code location: service-API

@FeignClient(name = ServiceNameConstant.ORDER_SERVICE, configuration = {FeignErrorDecoder.class})
Copy the code

Complete code example

package tech.xproject.order.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import tech.xproject.common.core.constant.ServiceNameConstant; import tech.xproject.order.config.FeignErrorDecoder; import tech.xproject.order.pojo.dto.CreateOrderReqDTO; import tech.xproject.order.pojo.entity.Order; /** * @author kent */ @FeignClient(name = ServiceNameConstant.ORDER_SERVICE, Configuration = {feignErrorDecoder.class}) public interface RemoteOrderService {/** * @param createOrderReqDTO  createOrderReqDTO * @return Order */ @PostMapping("/order/create") Order create(@RequestBody CreateOrderReqDTO createOrderReqDTO); }Copy the code

Customization terrorAttributes

Code location: service

Terrorattributes that are not customised do not return with a code and need to add customised parameters to the returned object

package tech.xproject.product.handler; import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import org.springframework.web.context.request.WebRequest; import tech.xproject.common.core.exception.BusinessException; import java.util.Map; /** * @author kent */ @Component @Primary public class CustomErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); Throwable error = this.getError(webRequest); if (error instanceof BusinessException) { errorAttributes.put("code", ((BusinessException) error).getCode()); } return errorAttributes; }}Copy the code

4. Unified handling of global exceptions

Code location: Web

package tech.xproject.web.manager.handler; import feign.FeignException; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import tech.xproject.common.core.entity.R; import tech.xproject.common.core.enums.ResultCodeEnum; import tech.xproject.common.core.exception.BusinessException; import java.util.List; /** * @author kent */ @Slf4j @RestControllerAdvice public class WebGlobalExceptionHandler { @ExceptionHandler({FeignException.class}) @ResponseBody public R<? > feignExceptionHandler(FeignException exception) { log.error(exception.getMessage(), exception); return R.error(exception.getMessage()); } @ExceptionHandler({RuntimeException.class}) @ResponseBody public R<? > runtimeExceptionHandler(RuntimeException exception) { log.error(exception.getMessage(), exception); return R.error(exception.getMessage()); } @ExceptionHandler({Exception.class}) @ResponseBody public R<? > exceptionHandler(Exception exception) { log.error(exception.getMessage(), exception); return R.error(exception.getMessage()); } @ExceptionHandler({BusinessException.class}) public R<? > businessExceptionHandler(BusinessException exception) { log.error(exception.getMessage(), exception); return R.error(exception.getCode(), exception.getMessage()); } @ExceptionHandler({BindException.class}) @ResponseBody public R<? > bindExceptionHandler(BindException exception) { List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors(); String errorMessage = fieldErrors.get(0).getDefaultMessage(); log.error(errorMessage, exception); return R.error(ResultCodeEnum.ERROR_PARAMETER.getCode(), errorMessage); } @ExceptionHandler({MethodArgumentNotValidException.class}) public R<? > validateExceptionHandler(MethodArgumentNotValidException exception) { List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors(); String errorMessage = fieldErrors.get(0).getDefaultMessage(); log.error(errorMessage, exception); return R.error(ResultCodeEnum.ERROR_PARAMETER.getCode(), errorMessage); }}Copy the code

5, custom ErrorHandlerConfiguration [gateway]

Code location: gateway

@Bean @Order(Ordered.HIGHEST_PRECEDENCE) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(errorAttributes, this.resourceProperties, this.serverProperties.getError(), this.applicationContext); exceptionHandler.setViewResolvers(this.viewResolvers); exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders()); return exceptionHandler; }Copy the code

Complete code example

package tech.xproject.gateway.config; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.result.view.ViewResolver; import tech.xproject.gateway.handler.JsonExceptionHandler; import java.util.Collections; import java.util.List; /** * @author kent */ @Configuration @EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class}) public class ErrorHandlerConfiguration { private final ServerProperties serverProperties; private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public ErrorHandlerConfiguration(ServerProperties serverProperties, ResourceProperties resourceProperties, ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) { this.serverProperties = serverProperties; this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) { JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(errorAttributes, this.resourceProperties, this.serverProperties.getError(), this.applicationContext); exceptionHandler.setViewResolvers(this.viewResolvers); exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders()); return exceptionHandler; }}Copy the code

6. [gateway] Customize JsonExceptionHandler

Code location: gateway

Returns code, message, data using a custom structure

/** * get error attributes */ @Override protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { Throwable error = super.getError(request); String exMessage = error ! = null ? error.getMessage() : ResultCodeEnum.ERROR.getMessage(); Format (" Request error [%s %s], exception: %s", request.methodName(), request.uri(), exMessage); Map<String, Object> map = new HashMap<>(3); map.put("code", ResultCodeEnum.ERROR.getCode()); map.put("message", message); map.put("data", null); return map; }Copy the code

Override the getHttpStatus method to return the HTTP status code 200

/**
 * response http code 200
 * the error code need use the code in response content
 *
 * @param errorAttributes
 */
@Override
protected int getHttpStatus(Map<String, Object> errorAttributes) {
    return HttpStatus.OK.value();
}
Copy the code

Complete code example

package tech.xproject.gateway.handler; import org.springframework.boot.autoconfigure.web.ErrorProperties; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.web.reactive.function.server.*; import tech.xproject.common.core.enums.ResultCodeEnum; import java.util.HashMap; import java.util.Map; /** * @author kent */ public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler { public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) { super(errorAttributes, resourceProperties, errorProperties, applicationContext); } /** * get error attributes */ @Override protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { Throwable error = super.getError(request); String exMessage = error ! = null ? error.getMessage() : ResultCodeEnum.ERROR.getMessage(); Format (" Request error [%s %s], exception: %s", request.methodName(), request.uri(), exMessage); Map<String, Object> map = new HashMap<>(3); map.put("code", ResultCodeEnum.ERROR.getCode()); map.put("message", message); map.put("data", null); return map; } /** * render with json * * @param errorAttributes */ @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); } /** * response http code 200 * the error code need use the code in response content * * @param errorAttributes */ @Override protected int getHttpStatus(Map<String, Object> errorAttributes) { return HttpStatus.OK.value(); }}Copy the code

summary

Looking up all kinds of articles on the Internet, I never found a solution, only various scattered solutions, and finally through my own code, breakpoint debugging, and combined with the relevant articles finally solved.

Use articles and code solutions to record them for reference to those who need them.

Full code Demo

Github.com/new9xgh/fei…

reference

Resolve FeignException to return a status code thrown by the underlying service

Feign Returns a unified error object for custom ErrorDecoder errors

Global exception handling for the Spring Cloud Gateway