SpringCloud Alibaba microservice practice 36 - several problems of Feign call

Keywords: Java AI

This article is a charging column. When the traffic becomes less, it will be moved to the charging column. Watch and cherish it!

In the spring cloud architecture, the communication between microservices is based on Feign calls. In the actual use of Feign, we will probably face the following problems:

  • Is Feign client on the consumer side or an independent api layer?
  • How is the interface called by Feign wrapped?
  • How does Feign capture business exceptions at the business production end?

Let's discuss these issues together in this article. I hope it will be helpful to you after reading it.

First, let's take a look at how Feign's calling method is chosen?

How to select the calling method of Feign?

Generally speaking, Feign's calling methods are divided into two categories:

Declare Feign client in production side API

As mentioned above, the consumer service directly relies on the API package provided by the producer, and then directly calls the interface provided by the producer through @ Autowired annotation injection.

The advantages of this are: simple and convenient. The consumer can directly use the Feign interface provided by the producer.

The disadvantages of this are also obvious: the interface obtained by the consumer is the list of interfaces provided by the producer to all services. When a producer has many interfaces, it will be very chaotic; Moreover, the class is also on the production side. When calling, the consumer must scan the path of feign through @ SpringBootApplication(scanBasePackages = {"com.javadaily.feign"}) because the package path may be different from that of the producer. When the consumer needs to introduce many producer feign, it needs to scan many interface paths.

This calling method is described in detail in the previous two articles. Those interested can go directly through the following link:

SpringCloud Alibaba microservice practice III - service invocation

SpringCloud Alibaba microservice practice 20 - integration of Feign

Declare Feign client on the consumer side

A public API interface layer is also required. Both the production side and the consumer side need to introduce this jar package. At the same time, Feign client and fuse classes are written on the consumer side as needed.

The advantages of this are: the client can write its own interfaces on demand, and all interfaces are controlled by the consumer; There is no need to add additional scan annotation scanBasePackages on the startup class.

The disadvantages of this are: the consumer side code is redundant, and each consumer needs to write Feign client; The coupling between services is tight, and one interface and three interfaces need to be modified.

Summary

So the question comes: since there are two calling methods, which one is more reasonable?

What I suggest here is to give priority to the second method, and customize the Feign client by the client itself.

In terms of responsibilities, only the consumer can clearly know which service provider to call and which interfaces to call. If @ FeignClient is directly written on the API of the service provider, it will be difficult for the consumer to customize it on demand, and the fuse processing logic should also be customized by the consumer. Although it will lead to code redundancy, the responsibilities are clear, and the problem that the interface path cannot be scanned can be avoided.

Of course, this is just a personal suggestion. If you think what I said is wrong, you can follow your own ideas.

Next, let's take a look at whether the Feign interface should be encapsulated.

Should Feign interface be packaged?

Current situation analysis

In the front end and back end separation project, when the back end returns interface data to the front end, the return format is generally unified. At this time, our Controller will write as follows:

@RestController
@Log4j2
@Api(tags = "account Interface")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AccountController implements AccountApi {

    private final AccountService accountService;
  
  	...
		public ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode){   
        AccountDTO accountDTO = accountService.selectByCode(accountCode);
        return ResultData.success(accountDTO);
    }
    ...
}

The interface definition of feign needs to be consistent with the implementation class. Now, when order service obtains the order details, it also needs to return the user information. At this time, we call the getByCode() interface of account service through feign, and it will be written as follows:

/**
 * Get Order details
 * @param orderNo order number
 * @return ResultData<OrderDTO>
*/
@GetMapping("/order/{orderNo}")
public ResultData<OrderDTO> getById(@PathVariable("orderNo") String orderNo){
  OrderDTO orderDTO = orderService.selectByNo(orderNo);
  return ResultData.success(orderDTO);
}
public OrderDTO selectByNo(String orderNo) {
    OrderDTO orderDTO = new OrderDTO();

    //1. Query basic order information
    Order order = orderMapper.selectByNo(orderNo);
    BeanUtils.copyProperties(order,orderDTO);

    //2. Obtain user information
    ResultData<AccountDTO> accountResult = accountClient.getByCode("javadaily");
  	if(accountResult.isSuccess()){
    	orderDTO.setAccountDTO(accountResult.getData());
  	}

    return orderDTO;
}

Here, we need to get the ResultData wrapper class first, and then solve the returned result into a specific AccountDTO object through judgment. Obviously, this code has two problems:

  1. Each Controller interface needs to wrap the results manually with ResultData.success. Repeat Yourself!
  2. When Feign is called, it needs to be unpacked from the wrapper class into the required entity object, Repeat Yourself!

If there are many such interface calls, then

Optimize packaging

Of course, we need to optimize such ugly code, and the optimization goal is also very clear: when we call Feign, we can directly obtain the entity object without additional disassembly. When the front end is called directly through the gateway, it returns a unified wrapper.

Here, we can implement it with the help of ResponseBodyAdvice. By enhancing the Controller return body, if it is recognized that it is a Feign call, we will directly return the object, otherwise we will add a unified packaging structure.

As for why the front and back ends need to unify the return format and how to implement it, in my old bird series articles How does SpringBoot unify the back-end return format? That's how old birds play! There is a detailed description, and those who are interested can move forward.

Now the question is: how to identify whether it is Feign's call or gateway's direct call?

There are two methods, one based on custom annotation and the other based on Feign interceptor.

Implementation based on custom annotation

Customize an annotation, such as Inner, and label Feign's interface with this annotation, so that it can be used for matching when using ResponseBodyAdvice.

However, this method has a disadvantage that the front end and feign cannot be shared. For example, an interface user/get/{id} can be called either through feign or directly through the gateway. Using this method, two interfaces with different paths need to be written.

Implementation of Interceptor Based on Feign

For feign calls, a special identifier is added to the feign interceptor. When converting an object, if it is found that it is a feign call, it will directly return the object.

Specific implementation process

Here we use the second method (the first method is also very simple, you can try it yourself)

  1. Add a specific request header t to the feign request in the feign interceptor_ REQUEST_ ID
/**
 * Set request header for Feign
 */
@Bean
public RequestInterceptor requestInterceptor(){
  return requestTemplate -> {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if(null != attributes){
      HttpServletRequest request = attributes.getRequest();
      Map<String, String> headers = getRequestHeaders(request);

      // Pass all request headers to prevent partial loss
      //You can also pass only authenticated header s here
      //requestTemplate.header("Authorization", request.getHeader("Authorization"));
      for (Map.Entry<String, String> entry : headers.entrySet()) {
        requestTemplate.header(entry.getKey(), entry.getValue());
      }

      // The unique identifier passed between microservices is case sensitive, so it is obtained through httpServletRequest
      if (request.getHeader(T_REQUEST_ID)==null) {
        String sid = String.valueOf(UUID.randomUUID());
        requestTemplate.header(T_REQUEST_ID, sid);
      }

    }
  };
}
  1. Customize BaseResponseAdvice and implement ResponseBodyAdvice
@RestControllerAdvice(basePackages = "com.javadaily")
@Slf4j
public class BaseResponseAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object object, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {

        //When Feign requests, the request header is set through the interceptor. If it is a Feign request, the entity object is returned directly
        boolean isFeign = serverHttpRequest.getHeaders().containsKey(OpenFeignConfig.T_REQUEST_ID);

        if(isFeign){
            return object;
        }

        if(object instanceof String){
            return objectMapper.writeValueAsString(ResultData.success(object));
        }

        if(object instanceof ResultData){
            return object;
        }

        return ResultData.success(object);
    }

}

If it is a Feign request, it will not be converted, otherwise it will be wrapped through ResultData.

  1. Modify the back-end interface return object
@ApiOperation("select Interface")
@GetMapping("/account/getByCode/{accountCode}")
@ResponseBody
public AccountDTO getByCode(@PathVariable(value = "accountCode") String accountCode){
  return accountService.selectByCode(accountCode);
}

There is no need to return package ResultData on the interface, and automatic enhancement is realized through ResponseBodyAdvice.

  1. Modify feign call logic
    @Override
    public OrderDTO selectByNo(String orderNo) {
        OrderDTO orderDTO = new OrderDTO();

        //1. Query basic order information
        Order order = orderMapper.selectByNo(orderNo);
        BeanUtils.copyProperties(order,orderDTO);
				//2. Obtain user information remotely
        AccountDTO accountResult = accountClient.getByCode(order.getAccountCode());
        orderDTO.setAccountDTO(accountResult);

        return orderDTO;
    }

After the above four steps, our optimization goal is achieved under normal circumstances. The entity object is returned directly through Feign call, and the unified wrapper is returned through gateway call. It looks perfect, but it's actually bad, which leads to the third problem, how does Feign handle exceptions?

Feign exception handling

Current situation analysis

The producer will verify the business rules for the provided interface methods, and throw a business exception BizException for the call requests that do not comply with the business rules. Under normal circumstances, there will be a global exception handler on the project. He will catch the business exception BizException, seal it into a unified wrapper and return it to the caller. Now let's simulate this business scenario:

  1. The producer threw a business exception
public AccountDTO selectByCode(String accountCode) {
  if("javadaily".equals(accountCode)){
    throw new BizException(accountCode + "user does not exist");
  }
  AccountDTO accountDTO = new AccountDTO();
  Account account = accountMapper.selectByCode(accountCode);
  BeanUtils.copyProperties(account,accountDTO);
  return accountDTO;
}

When the user name is "javadaily", a business exception BizException is thrown directly.

  1. The global exception interceptor captures business exceptions
/**
 * Custom business exception handling
 * @param e the e
 * @return ResultData
 */
@ExceptionHandler(BaseException.class)
public ResultData<String> exception(BaseException e) {
  log.error("Business exception ex={}", e.getMessage(), e);
  return ResultData.fail(e.getErrorCode(),e.getMessage());
}

BaseException is captured. BizException belongs to the subclass of BaseException and will also be captured.

  1. The caller directly simulates the exception data call
public OrderDTO selectByNo(String orderNo) {
  OrderDTO orderDTO = new OrderDTO();
  //1. Query basic order information
  Order order = orderMapper.selectByNo(orderNo);
  BeanUtils.copyProperties(order,orderDTO);

  //2. Obtain user information remotely
  AccountDTO accountResult = accountClient.getByCode("javadaily");
  orderDTO.setAccountDTO(accountResult);
  return orderDTO;
}

When calling the getByCode() method, "javadaily" is passed to trigger the producer's business exception rule.

Feign could not catch exception

When we call the selectByNo() method, we will find that the caller cannot catch exceptions, and all accountdtos are set to null, as follows:

Set Feign's log level to FULL to view the returned results:

It can be seen from the log that Feign actually obtained the unified object ResultData converted by the global exception handler, and the response code is 200. The response is normal. The consumer accepts that the object is AccountDTO, and the attribute cannot be converted. All of them are treated as NULL values.

Obviously, this is not in line with our normal business logic. We should directly return the exceptions thrown by the producer. How to deal with it?

Very simply, we only need to set a non-200 response code for the business exception in the global exception interceptor, such as:

/**
 * Custom business exception handling.
 * @param e the e
 * @return ResultData
 */
@ExceptionHandler(BaseException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<String> exception(BaseException e) {
  log.error("Business exception ex={}", e.getMessage(), e);
  return ResultData.fail(e.getErrorCode(),e.getMessage());
}

In this way, consumers can normally catch business exceptions thrown by producers, as shown in the following figure:

Exceptions are additionally encapsulated

Although the exception can be obtained, Feign captures the exception and encapsulates it again on the basis of the business exception.

The reason is that feign's exception resolution is triggered when the feign call result is a response code other than 200. Feign's exception parser will wrap it as FeignException, that is, wrap it again on the basis of our business exception.

You can mark a breakpoint on feign.codec.ErrorDecoder#decode() method to observe the execution result, as follows:

Obviously, we don't need this wrapped exception. We should directly throw the captured producer's business exception to the front end. How can we solve it?

Very simply, we just need to rewrite Feign's exception parser, re implement the decode logic, return the normal BizException, and then the global exception interceptor will catch BizException again! (it feels like an infinite doll)

The code is as follows:

  1. Rewrite Feign exception parser
/**
 * Solve Feign's abnormal packaging and return results uniformly
 * @author Official account: JAVA RI Zhi Lu
 */
@Slf4j
public class OpenFeignErrorDecoder implements ErrorDecoder {
    /**
     * Feign Exception resolution
     * @param methodKey Method name
     * @param response Responder
     * @return BizException
     */
    @Override
    public Exception decode(String methodKey, Response response) {
        log.error("feign client error,response is {}:",response);
        try {
            //get data
            String errorContent = IOUtils.toString(response.body().asInputStream());
            String body = Util.toString(response.body().asReader(Charset.defaultCharset()));

            ResultData<?> resultData = JSON.parseObject(body,ResultData.class);
            if(!resultData.isSuccess()){
                return new BizException(resultData.getStatus(),resultData.getMessage());
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

        return new BizException("Feign client Call exception");
    }
}
  1. Inject a custom exception decoder into Feign configuration file
@ConditionalOnClass(Feign.class)
@Configuration
public class OpenFeignConfig {  
		/**
     * Custom exception decoder
     * @return OpenFeignErrorDecoder
     */
    @Bean
    public ErrorDecoder errorDecoder(){
        return new OpenFeignErrorDecoder();
    }
}
  1. Call again, as expected.

At this time, Feign's exception decoder is customized to directly throw the producer's business exception information to complete the goal.

summary

This paper makes a small summary of the problems Feign will encounter in the use process, and also puts forward some solutions that may not be mature. Of course, due to my limited level, the proposed solution is not necessarily the best. If you have a better solution, please leave a message and tell me. Thank you!

Posted by GamingWarrior on Tue, 16 Nov 2021 16:43:27 -0800