How do Spring Cloud microservices design exception handling mechanisms?

Keywords: Java Spring JSON SDK network

Guide reading

 

Today I'm going to talk about how exception handling mechanism should be designed when calling between micro-services when using Spring Cloud to design micro-service architecture. We know that in the design of micro-service architecture, a micro-service will inevitably provide corresponding functional service interfaces both internally and externally. Externally provided service interfaces provide services to the public network through service gateways (such as apiGateway provided by Zuul), such as user login, registration and other service interfaces for App clients.

 

The internal-oriented service interface is the functional logic decentralization caused by the boundary delimitation problem of each micro-service system after the micro-service splitting, and it needs to provide internal invocation interface between micro-services, so as to realize a complete functional logic. It is the service-oriented upgrade splitting of local code interface invocation in previous monolithic applications. For example, in the group purchase system, from placing an order to completing a payment, the transaction system needs to call the payment system after invoking the order system to complete the order, thus completing the order process. At this time, because the transaction system, the order system and the payment system are three different micro-services, in order to complete this user order, App needs to invoke the transaction system to provide. After the external order interface is placed, the order system and payment system are invoked by the transaction system in the form of internal service invocation to complete the whole transaction process. As shown in the following figure:

 

 

It should be noted that in Spring Cloud-based micro-service architecture, all services are registered and found through service middleware such as consul or eureka, and then invoked. Only external-oriented service interfaces are exposed by gateway services, while internal-oriented service interfaces are shielded by service gateways to avoid direct exposure. To the public network. However, the invocation between internal micro-services can be done directly through consul or eureka, which does not conflict. The external client is through invoking the service gateway, and the service gateway is routed to the corresponding micro-service interface through consul, while the internal micro-services are directly invoked after consul or Eureka discovers the service.

 


Differences in exception handling

 

For external service interfaces, we usually respond in JSON mode. In addition to normal data packets, we usually redundant a field of response code and response information in message format, such as normal interface returned successfully:

{
    "code": "0",
    "msg": "success",
    "data": {
        "userId": "zhangsan",
        "balance": 5000
    }
}

 

If there are exceptions or errors, error codes and error messages will be returned accordingly, such as:

{
    "code": "-1",
    "msg": "Request parameter error",
    "data": null
}

 

When compiling an external-oriented service interface, all exception handling at the server side should be captured accordingly and mapped into corresponding error codes and error messages at the controller layer. Because the external-oriented is directly exposed to users, it needs to be displayed and prompted more friendly. Even if there are exceptions in the system, we should firmly export them to users friendly. Unable to output code level exception information, otherwise the user will be confused. For the client, it only needs to parse and process the message logically according to the agreed message format. Generally, the third-party open service interface we call in the development will also carry out similar design, and the error code and error information classification is very clear.

 

In the Spring Cloud-based micro-service system, the micro-service provider will provide the corresponding client SDK code, while the client SDK code is invoked by FeignClient, such as the invocation of micro-services to each other. In exception handling, we hope to be more straightforward, just like calling local interfaces. In Spring Cloud-based micro-service system, micro-service providers will provide corresponding client SDK code, while client SDK code is invoked by FeignClient, such as:

@FeignClient(value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class)
public interface OrderClient {
    //Order (internal)
    @RequestMapping(value = "/order/createOrder", method = RequestMethod.POST)
    OrderCostDetailVo orderCost(@RequestParam(value = "orderId") String orderId,
            @RequestParam(value = "userId") long userId,
            @RequestParam(value = "orderType") String orderType,
            @RequestParam(value = "orderCost") int orderCost,
            @RequestParam(value = "currency") String currency,
            @RequestParam(value = "tradeTime") String tradeTime)
}

 

When the service caller gets such SDK, he can ignore the specific invocation details and realize the internal interface of other micro-services like the local interface. Of course, this is the function provided by the FeignClient framework, which integrates frameworks like Ribbon and Hystrix to realize the load balancing and service fusing functions of client service invocation (annotations specify fusing). Processing code class after triggering), because the topic of this article is to discuss exception handling, it will not be expanded here for the time being.

 

The problem now is that although FeignClient provides service docking experience similar to local code invocation to service invokers, service invokers do not want errors to occur when invoking. Even if errors occur, how to handle errors is what service invokers want to know. On the other hand, when we design the internal interface, we don't want to make the message form as complex as the external interface, because in most scenarios, we want the caller of the service to get the data directly, so that we can directly use the encapsulation of the FeignClient client to convert it into the use of local objects.

 

@Data
@Builder
public class OrderCostDetailVo implements Serializable {

    private String orderId;
    private String userId;
    private int status;   //1: The state of arrears; 2: Successful deduction
    private int orderCost;
    private String currency;
    private int payCost;
    private int oweCost;

    public OrderCostDetailVo(String orderId, String userId, int status, int orderCost, String currency, int payCost,
            int oweCost) {
        this.orderId = orderId;
        this.userId = userId;
        this.status = status;
        this.orderCost = orderCost;
        this.currency = currency;
        this.payCost = payCost;
        this.oweCost = oweCost;
    }
}

 

For example, we are designing the return data as a normal VO/BO object instead of additional fields such as error codes or error messages to the external interface. Of course, it does not mean that the design method is not feasible, but it feels that it will make the internal normal logic calls more verbose and redundant, after all, for the internal micro-service calls. For example, either right or wrong, just Fallback logic.

 

Nevertheless, in spite of this, there will inevitably be anomalies in the service. If an error occurs during the internal service invocation, the caller should know the specific error message, but the prompt of the error message needs to be captured by the service caller integrated with FeignClient in an abnormal way, and does not affect the return object design under normal logic. That is to say, I do not want to add two redundant error message words to each object. Duan, because it doesn't look so elegant!

 

In this case, how should we design it?

 

Best Practice Design

 

First of all, whether internal or external micro services, we should design a global exception handling class on the server side to unify the return information of the system to the caller when throwing an exception. To implement such a mechanism, we can use Spring's annotation @Controller Advice to achieve global interception and unified processing of exceptions. Such as:

 

@Slf4j
@RestController
@ControllerAdvice
public class GlobalExceptionHandler {

    @Resource
    MessageSource messageSource;

    @ExceptionHandler({org.springframework.web.bind.MissingServletRequestParameterException.class})
    @ResponseBody
    public APIResponse processRequestParameterException(HttpServletRequest request,
            HttpServletResponse response,
            MissingServletRequestParameterException e) {

        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setContentType("application/json;charset=UTF-8");
        APIResponse result = new APIResponse();
        result.setCode(ApiResultStatus.BAD_REQUEST.getApiResultStatus());
        result.setMessage(
                messageSource.getMessage(ApiResultStatus.BAD_REQUEST.getMessageResourceName(),
                        null, LocaleContextHolder.getLocale()) + e.getParameterName());
        return result;
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public APIResponse processDefaultException(HttpServletResponse response,
            Exception e) {
        //log.error("Server exception", e);
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        APIResponse result = new APIResponse();
        result.setCode(ApiResultStatus.INTERNAL_SERVER_ERROR.getApiResultStatus());
        result.setMessage(messageSource.getMessage(ApiResultStatus.INTERNAL_SERVER_ERROR.getMessageResourceName(), null,
                LocaleContextHolder.getLocale()));
        return result;
    }

    @ExceptionHandler(ApiException.class)
    @ResponseBody
    public APIResponse processApiException(HttpServletResponse response,
            ApiException e) {
        APIResponse result = new APIResponse();
        response.setStatus(e.getApiResultStatus().getHttpStatus());
        response.setContentType("application/json;charset=UTF-8");
        result.setCode(e.getApiResultStatus().getApiResultStatus());
        String message = messageSource.getMessage(e.getApiResultStatus().getMessageResourceName(),
                null, LocaleContextHolder.getLocale());
        result.setMessage(message);
        //log.error("Knowned exception", e.getMessage(), e);
        return result;
    }

    /**
     * Unified Processing Method for Internal Microservice Exceptions
     */
    @ExceptionHandler(InternalApiException.class)
    @ResponseBody
    public APIResponse processMicroServiceException(HttpServletResponse response,
            InternalApiException e) {
        response.setStatus(HttpStatus.OK.value());
        response.setContentType("application/json;charset=UTF-8");
        APIResponse result = new APIResponse();
        result.setCode(e.getCode());
        result.setMessage(e.getMessage());
        return result;
    }
}

 

For example, in the global exception, we deal with the internal and external unified exception globally, so as long as the service interface throws such exception, it will be intercepted by the global processing class and handle the error return information uniformly.

 

In theory, we can capture and handle all exceptions thrown by the business layer of the service interface in this global exception handling class and unify the response, but that will make the global exception handling class very bulky. Therefore, considering from the best practice, we usually design a unified exception object for the internal and external interfaces, such as the external unified interface exception I. They are called ApiException, while the internal unified interface exception is called Internal ApiException. In this way, we need to convert all business exceptions to ApiException in the external-oriented service interface controller layer, and all business exceptions to International ApiException in the internal-oriented service controller layer. Such as:

@RequestMapping(value = "/creatOrder", method = RequestMethod.POST)
public OrderCostDetailVo orderCost(
         @RequestParam(value = "orderId") String orderId,
         @RequestParam(value = "userId") long userId,
         @RequestParam(value = "orderType") String orderType,
         @RequestParam(value = "orderCost") int orderCost,
         @RequestParam(value = "currency") String currency,
         @RequestParam(value = "tradeTime") String tradeTime)throws InternalApiException {
         OrderCostVo costVo = OrderCostVo.builder().orderId(orderId).userId(userId).busiId(busiId).orderType(orderType)
                .duration(duration).bikeType(bikeType).bikeNo(bikeNo).cityId(cityId).orderCost(orderCost)
                .currency(currency).strategyId(strategyId).tradeTime(tradeTime).countryName(countryName)
                .build();
        OrderCostDetailVo orderCostDetailVo;
        try {
            orderCostDetailVo = orderCostServiceImpl.orderCost(costVo);
            return orderCostDetailVo;
        } catch (VerifyDataException e) {
            log.error(e.toString());
            throw new InternalApiException(e.getCode(), e.getMessage());
        } catch (RepeatDeductException e) {
            log.error(e.toString());
            throw new InternalApiException(e.getCode(), e.getMessage());
        } 
}

 

For example, in the controller layer of the internal service interface above, all business exception types are unified into the internal service unified exception object, Internal ApiException. In this way, the global exception handling class can deal with the exception in a unified response.

 

There's not much to say about the handling of external service callers. For internal service callers, in order to achieve more elegant and convenient exception handling, we also need to throw uniform internal service exception objects in FeignClient-based SDK code, such as:

@FeignClient(value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class)
public interface OrderClient {
    //Order (internal)
    @RequestMapping(value = "/order/createOrder", method = RequestMethod.POST)
    OrderCostDetailVo orderCost(@RequestParam(value = "orderId") String orderId,
            @RequestParam(value = "userId") long userId,
            @RequestParam(value = "orderType") String orderType,
            @RequestParam(value = "orderCost") int orderCost,
            @RequestParam(value = "currency") String currency,
            @RequestParam(value = "tradeTime") String tradeTime)throws InternalApiException};

In this way, when the caller makes a call, the caller will be forced to catch the exception. Normally, the caller does not need to pay attention to the exception, so it can process the returned object data as a local call. In exceptional cases, the exception information is captured, which is usually designed as a json data with error codes and error information in the server-side global processing class. To avoid additional parsing code written by the client, FeignClient provides us with an exception decoding mechanism. Such as:

 

@Slf4j
@Configuration
public class FeignClientErrorDecoder implements feign.codec.ErrorDecoder {

    private static final Gson gson = new Gson();

    @Override
    public Exception decode(String methodKey, Response response) {
        if (response.status() != HttpStatus.OK.value()) {
            if (response.status() == HttpStatus.SERVICE_UNAVAILABLE.value()) {
                String errorContent;
                try {
                    errorContent = Util.toString(response.body().asReader());
                    InternalApiException internalApiException = gson.fromJson(errorContent, InternalApiException.class);
                    return internalApiException;
                } catch (IOException e) {
                    log.error("handle error exception");
                    return new InternalApiException(500, "unknown error");
                }
            }
        }
        return new InternalApiException(500, "unknown error");
    }
}

 

We only need to add such a FeignClient decoder to the service caller to complete the conversion of error messages in the decoder. In this way, we can directly capture the exception objects when we invoke the micro-service through FeignClient, so that we can process the exception objects returned by the remote service as locally.

 

Above all, I share a little about exception handling mechanism after using Spring Cloud for micro-service splitting. Because I recently found that the company project is confused in the process of using Spring Cloud for micro-service splitting, so I write an article to discuss with you. If there is a better way, you are welcome to leave a message for me.

Posted by Twysted on Thu, 09 May 2019 13:20:38 -0700