In the SpringCloud architecture, communication between microservices is based on Feign calls. In the actual use of Feign, we will most likely face the following problems:
- Is the Feign client on the consumer side or a separate API layer?
- How should Feign call interfaces be wrapped or not?
- How does Feign capture service exceptions at the production end?
This article we come together to discuss these questions, I hope to finish reading can help you.
So let’s start by looking at how to call Feign. Okay?
How to choose how to call Feign?
In general, Feign calls fall into two broad categories:
Declare the Feign client in the production API
As mentioned above, the consumer side service relies directly on the API package provided by the production side and can then invoke the producer-provided interface directly via injection via the @AutoWired annotation.
The advantage of this is that it is simple and the consumer can use the Feign interface provided by the producer directly.
The disadvantages of this are obvious: the consumer side gets the list of interfaces provided by the producer to all the services, which can be confusing if there are many producer interfaces. In addition, the fusing degradation class is also on the production side. The consumer side must scan the feign path with @SpringBootApplication(scanBasePackages = {“com.javadaily. Feign “}) because the package path may be different from that of the producer. When the consumer needs to import many producer Feign, it needs to scan many interface paths.
This method of calling is explained in detail in the previous two articles, and can be accessed via the link below:
SpringCloud Alibaba micro-service practice 3 – service invocation
SpringCloud Alibaba Micro service Combat 20 – Integrated Feign downgrade circuit breaker
Declare the Feign client on the consumer side
Still need a separate common API interface layer, the production side and the consumer side need to introduce this JAR package, at the same time on the consumer side write Feign client and fuse class on demand.
The advantage of this is that the client can write its own interface as needed, and the fuse degradation is controlled by the consumer. There is no need to add an additional scan annotation, scanBasePackages, to the startup class.
The downside of this is that the consumer side of the code is redundant, every consumer needs to write the Feign client; The coupling between services is relatively tight, and modification of one interface requires modification of three.
summary
So the question is: given that there are two ways to call it, which one makes more sense?
What I recommend here is to use the second approach in favor of customizing the Feign client by the client itself.
Responsibly, only the consumer can clearly know which service provider to call and which interfaces to call. If @FeignClient is written directly into the API of the service provider, it is difficult for the consumer to customize it on demand, and the circuit breaker processing logic should be customized by the consumer itself. This leads to code redundancy, but the responsibilities are clear and the problem of missing interface paths can be avoided.
Of course, this is only personal advice, if you think what I say is wrong, you can follow your own ideas.
Let’s take a look at the question of whether the Feign interface should be wrapped or not.
Should Feign interface be wrapped or not?
Analysis of the situation
In the front end separation project, when the back end returns interface data to the front end, it generally returns the same format. At this time, our Controller will write like this:
@RestController
@Log4j2
@API (tags = "Account interface ")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AccountController implements AccountApi {
private finalAccountService accountService; .public ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode){
AccountDTO accountDTO = accountService.selectByCode(accountCode);
returnResultData.success(accountDTO); }... }Copy the code
The Feign interface definition needs to be consistent with that of the implementation class. Now the order-service will return the user information when it fetches the details of the order.
/** * get Order details *@paramOrderNo 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);
}
Copy the code
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;
}
Copy the code
We need to get the ResultData wrapper class first, and then resolve the result into a concrete AccountDTO object. Obviously, there are two problems with this code:
- Each Controller interface needs to be used manually
ResultData.success
Packaging the results,Repeat Yourself! - Feign calls need to be unpacked from the wrapper class into the desired entity object, Repeat Yourself!
If there are many such interface calls, then…
Optimization of packaging
This ugly code certainly needs to be optimized, and the goal is clear: when we call it through Feign, we get the entity object directly, with no additional unassembly. The front end, when called directly through the gateway, returns a unified wrapper.
We can do this with the help of ResponseBodyAdvice, which enhances the return body of the Controller to return the object if it is identified as a call to Feign, otherwise we add a uniform wrapper structure.
As for why and how to achieve a uniform back end return format, in my old bird series of articles SpringBoot how to uniform back end return format? This is how old birds play! It goes into detail. If you’re interested, you can move on.
Now the question is: How do you tell if it’s a call from Feign or a direct call from the gateway?
There are two ways to do this, one based on custom annotations and the other based on Feign interceptors.
Implementation based on custom annotations
Define a custom annotation, such as Inner, to annotate Feign’s interface with this annotation so that it can be matched using ResponseBodyAdvice.
However, this method has a disadvantage, is the front-end and feign can not be common, such as a user/get/{ID} interface can be called through feign or directly through the gateway, using this method requires two different paths of the interface.
Based on Feign interceptor implementation
For Feign calls, special flags are placed on the Feign interceptor to return the object directly if it is found to be a Feign call when converting the object.
Concrete implementation process
Here we use the second method to implement (the first method is also very simple, you can try it yourself)
- Add a specific header to the FeIGN request in the FeIGN interceptor
T_REQUEST_ID
/** * Set the request header for Feign */
@Bean
public RequestInterceptor requestInterceptor(a){
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 headers here
//requestTemplate.header("Authorization", request.getHeader("Authorization"));
for (Map.Entry<String, String> entry : headers.entrySet()) {
requestTemplate.header(entry.getKey(), entry.getValue());
}
// Unique identifier passed between microservices, case sensitive so obtained via httpServletRequest
if (request.getHeader(T_REQUEST_ID)==null) { String sid = String.valueOf(UUID.randomUUID()); requestTemplate.header(T_REQUEST_ID, sid); }}}; }Copy the code
- 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
> aClass) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object object, MethodParameter methodParameter, MediaType mediaType, Class
> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
// The request header is set by the interceptor when Feign requests, and the entity object is returned directly if Feign requests
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;
}
returnResultData.success(object); }}Copy the code
If the request is made for Feign, the conversion is not done, otherwise it is wrapped with ResultData.
- Modify the returned object of the back-end interface
@ ApiOperation (" select interface ")
@GetMapping("/account/getByCode/{accountCode}")
@ResponseBody
public AccountDTO getByCode(@PathVariable(value = "accountCode") String accountCode){
return accountService.selectByCode(accountCode);
}
Copy the code
There is no need to return the wrapper body ResultData on the interface, which is automatically enhanced via ResponseBodyAdvice.
- 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;
}
Copy the code
Through the above four steps, our optimization goal is achieved under normal circumstances, which is to return the entity object directly through the Feign call and to return the unified wrapper through the gateway call. It looks perfect, but it sucks, which leads to the third question, how does Feign handle exceptions?
Feign Exception handling
Analysis of the situation
The producer will perform business rule verification for the provided interface methods, and will throw BizException for invocation requests that do not meet business rules. Normally, the project will have a global exception handler, which will capture BizException and return it to the caller as a unified wrapper. Now let’s simulate this business scenario:
- The producer throws 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;
}
Copy the code
When the user name is Javadaily, BizException is thrown.
- Global exception interceptor catches business exceptions
/** * Custom service exception handling *@param e the e
* @return ResultData
*/
@ExceptionHandler(BaseException.class)
public ResultData<String> exception(BaseException e) {
log.error("Abnormal service ex={}", e.getMessage(), e);
return ResultData.fail(e.getErrorCode(),e.getMessage());
}
Copy the code
Catch BaseException. BizException is a subclass of BaseException and will also be caught.
- 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;
}
Copy the code
“Javadaily” is passed when the getByCode() method is called, triggering the producer’s business exception rule.
Feign could not catch an exception
When we call selectByNo(), we see that no exception is caught and all accountdTos are set to NULL as follows:
Set Feign’s log level to FULL.
From the log, it can be seen that Feign has actually obtained the unified object ResultData converted by the global exception handler, and the response code is 200, indicating a normal response. The consumer accepts the object as AccountDTO, and the attribute cannot be converted and is treated as NULL.
Obviously, this is not in line with our normal business logic. We should return the exception thrown by the producer directly.
Simple, we just need to set the global exception interceptor business exception to a non-200 response code, such as:
/** * Custom service exception handling. *@param e the e
* @return ResultData
*/
@ExceptionHandler(BaseException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<String> exception(BaseException e) {
log.error("Abnormal service ex={}", e.getMessage(), e);
return ResultData.fail(e.getErrorCode(),e.getMessage());
}
Copy the code
The consumer can then normally catch the business exception thrown by the producer, as shown in the following figure:
Exceptions are extra encapsulated
Although an exception can be obtained, Feign captures the exception and encapsulates it on top of the business exception.
The reason is that feIGN’s exception resolution is triggered when it calls a response code that is not 200. Feign’s exception resolver wraps it into a FeignException, which is a wrapper around our business exception.
You can set a breakpoint on the feign.codec.errorDecoder #decode() method to see the result:
Obviously, we don’t need the wrapped exception. We should just throw the captured producer’s business exception directly to the front end. What about that?
We simply rewrite Feign’s exception parser, re-implement decode logic, return normal BizException, and the global exception interceptor catches BizException! (Feels like an infinite doll)
The code is as follows:
- Rewrite the Feign exception resolver
/** * Resolve Feign exception packaging, uniform return result *@authorPublic account: JAVA Daily Records */
@Slf4j
public class OpenFeignErrorDecoder implements ErrorDecoder {
/** * Feign exception resolution *@paramMethodKey Method name *@paramResponse Response body *@return BizException
*/
@Override
public Exception decode(String methodKey, Response response) {
log.error("feign client error,response is {}:",response);
try {
// Get dataString 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 newBizException(resultData.getStatus(),resultData.getMessage()); }}catch (IOException e) {
e.printStackTrace();
}
return new BizException("Feign Client call exception"); }}Copy the code
- Inject a custom exception decoder into the Feign configuration file
@ConditionalOnClass(Feign.class)
@Configuration
public class OpenFeignConfig {
/** * Custom exception decoder *@return OpenFeignErrorDecoder
*/
@Bean
public ErrorDecoder errorDecoder(a){
return newOpenFeignErrorDecoder(); }}Copy the code
- Called again, as expected.
At this point, by customising Feign’s exception decoder, the producer’s business exception information is directly thrown to achieve the goal.
conclusion
This article summarizes some of the problems Feign might encounter and suggests some of its own solutions that might not be mature enough. Of course, due to my limited level, the proposed solution may not be the best, if you have a better solution, please leave a message and tell me, thank you!
This is the thirty-sixth article in the SpringCloud hands-on series, and if you want to read other articles, you can check out the official Java Daily Guide