Detailed explanation of using Feign in actual projects

Introduction

In our daily study, we simply know how to call the feign interface or perform service downgrade; however, when using feign in enterprise-level projects, we will face the following problems:

  1. How should the Feign client be provided?
  2. Should the interface called by Feign be packaged?
  3. How does Feign capture business exceptions on the business production side?

How should a Feign client be provided?

How to provide the feign interface to the outside world?
analyze:

  1. The consumer side needs to reference these feign interfaces. If the feign interface is written directly in the consumer project, then if another one also needs the feign interface, will it have to be written again? Naturally, we will consider making the feign interface independent. Whoever needs the feign interface can add the corresponding dependencies.

  2. The feign interface contains entity objects. Generally, these entities are in the provider. When transforming through the feign interface, we need to extract the entity classes used in the controller. It can be processed in the following two ways
    . Method 1 extracts the entity class and puts it in an independent module. The provider and feign interfaces depend on the entity class module respectively.
    Method 2 puts the entity class in the module of the feign interface and the provider depends on this feign module.
    Project There are many cases that are handled according to method one, which will not cause dependence on code that does not need to be used;

    Insert image description here

2. Should the interface called by Feign be packaged?

2.1.Problem description

In a flat front-end and back-end separation project, when the back-end returns interface data to the front-end, the return format will generally be unified; our Controller will basically look like this:

    @GetMapping("getTest")
    public Result<TestVO> getTest() {
    
    
        TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
        return Result.success(testVO);
    

Feign's interface definition needs to be consistent with the implementation class;

Insert image description here
So when we use the feign interface of this method, the situation is like this.

    @GetMapping("getContent")
    public Result<String> getContent() {
    
    
        String content=null;
        Result<TestVO> test = commentRestApi.getTest();
        if (test.isSuccess()) {
    
    
            TestVO data = test.getData();
             content = data.getContent();
        }else {
    
    
            throw new  RuntimeException(test.getMessage());
        }
        
        return Result.success(content);
    }

Here we need to first obtain the Result packaging class, and then parse the returned result into a specific TestVO object through judgment. Obviously, this code has two problems:

  • Each Controller interface needs to manually use Result.success to package the results
  • When Feign is called, it needs to be unpacked from the packaging class into the required entity object.

There are many, many interfaces in the project. Isn’t it too useless to do this kind of operation constantly? ! ! It undoubtedly adds unnecessary development burden.

2.2.Problem solving

The optimization goal is also very clear:​

  • When we call it through Feign, we get the entity object directly without additional unassembly.
  • When the front end calls directly through the gateway, a unified packaging body is returned.

Here we can use ResponseBodyAdvice to achieve this. By enhancing the Controller return body, if a call from Feign is recognized, the object will be returned directly. Otherwise, a unified packaging structure will be added to us. ( SpringBoot uniformly encapsulates the results returned by the controller layer )

New question: How to identify whether it is a call from Feign or a direct call from the gateway?

Implemented based on custom annotations and implemented based on Feign interceptor.

  • Implemented based on custom annotations

    Customize an annotation, such as @ResponseNotIntercept, and mark Feign's interface with this annotation, so that you can use this annotation to match when using ResponseBodyAdvice matching.
    Insert image description here
    However, this method has a drawback, that is, 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. To use this method, you need to write 2 interfaces with different paths.

  • ​Implemented based on Feign interceptor​

    For Feign calls, add a special identifier to the Feign interceptor. When converting the object, if it is found to be a Feign call, the object will be returned directly.

Insert image description here
Insert image description here

The specific implementation steps of the second method:

  1. Add a specific request header T_REQUEST_ID to the feign request in the feign interceptor

/**
 * @ClassName: OpenFeignConfig Feign拦截器
 * @Description: 对于Feign的调用,在请求头中加上特殊标识
 * @Author: wang xiao le
 * @Date: 2023/08/25 23:13
 **/
@ConditionalOnClass(Feign.class)
@Configuration
public class OpenFeignConfig implements RequestInterceptor {
    
    

    /**
     * Feign请求唯一标识
     */
    public static final String T_REQUEST_ID = "T_REQUEST_ID";


    /**
     * get请求标头
     *
     * @param request 请求
     * @return {@link Map }<{@link String }, {@link String }>
     * @Author wxl
     * @Date 2023-08-27
     **/
    private Map<String, String> getRequestHeaders(HttpServletRequest request) {
    
    
        Map<String, String> map = new HashMap<>(16);
        Enumeration headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
    
    
            String key = (String) headerNames.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        return map;
    }

    @Override
    public void apply(RequestTemplate requestTemplate) {
    
    
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (null != attributes) {
    
    
            HttpServletRequest request = attributes.getRequest();
            Map<String, String> headers = getRequestHeaders(request);

            // 传递所有请求头,防止部分丢失
            for (Map.Entry<String, String> entry : headers.entrySet()) {
    
    
                requestTemplate.header(entry.getKey(), entry.getValue());
            }

            // 微服务之间传递的唯一标识,区分大小写所以通过httpServletRequest获取
            if (request.getHeader(T_REQUEST_ID) == null) {
    
    
                String sid = String.valueOf(UUID.randomUUID());
                requestTemplate.header(T_REQUEST_ID, sid);
            }

        }
    }


}

  1. Customize CommonResponseResult and implement ResponseBodyAdvice​​
/**
 * 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成
 *
 * @RestControllerAdvice(basePackages = "com.wxl52d41")
 * @ClassName: CommonResponseResult
 * @Description: controller返回结果统一封装
 * @Author wxl
 * @Date 2023-08-27
 * @Version 1.0.0
 **/
@RestControllerAdvice
public class CommonResponseResult implements ResponseBodyAdvice<Object> {
    
    
    /**
     * 支持注解@ResponseNotIntercept,使某些方法无需使用Result封装
     *
     * @param returnType    返回类型
     * @param converterType 选择的转换器类型
     * @return true 时会执行beforeBodyWrite方法,false时直接返回给前端
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
    
    
        if (returnType.getDeclaringClass().isAnnotationPresent(ResponseNotIntercept.class)) {
    
    
            //若在类中加了@ResponseNotIntercept 则该类中的方法不用做统一的拦截
            return false;
        }
        if (returnType.getMethod().isAnnotationPresent(ResponseNotIntercept.class)) {
    
    
            //若方法上加了@ResponseNotIntercept 则该方法不用做统一的拦截
            return false;
        }
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
    
    

        if (request.getHeaders().containsKey(OpenFeignConfig.T_REQUEST_ID)) {
    
    
            //Feign请求时通过拦截器设置请求头,如果是Feign请求则直接返回实体对象
            return body;
        }
        if (body instanceof Result) {
    
    
            // 提供一定的灵活度,如果body已经被包装了,就不进行包装
            return body;
        }
        if (body instanceof String) {
    
    
            //解决返回值为字符串时,不能正常包装
            return JSON.toJSONString(Result.success(body));
        }
        return Result.success(body);
    }
}

  1. Modify the provider backend interface to return the object and the feign interface
    . If it is a Feign request, no conversion will be performed, otherwise it will be packaged by Result.
    /**
     * 对象返回值测试,是否能正常封装返回体
     */
    @GetMapping("getOne")
    public TestVO getOne() {
    
    
        TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
        return testVO;
    }

Insert image description here

  1. Modify the feign calling logic in the consumer module.
    There is no need to return the encapsulated ResultData on the interface, and automatic enhancement is achieved through ResponseBodyAdvice.
   @GetMapping("getOne")
    public TestVO getOne() {
    
    
        TestVO one = commentRestApi.getOne();
        return one;
    }

  1. The test
    is called on the consumer side. It is found that the methods returned by calling the feign interface in the console are not uniformly encapsulated.
    Insert image description here

Call the provider layer method directly through postman. The discovery methods are unified and encapsulated.

Insert image description here

Under normal circumstances, our optimization goal is achieved. The entity object is directly returned through the Feign call, and the unified package is returned through the gateway call. It looks perfect, but is actually terrible, which leads to the third question, how does Feign handle exceptions?

How does SanFeign capture business anomalies on the business production side?

3.1.Analysis

The producer will perform business rule verification on the interface methods provided. Business exceptions will be thrown for calling requests that do not comply with business rules. Under normal circumstances, there will be a global exception handler on the project, which will capture Business exception BusinessException, and encapsulates it into a unified package body and returns it to the caller. Now let us simulate this business scenario:

  1. The producer throws a business exception
    and the name in the simulated business is empty.
    /**
     * 对象返回值测试,是否能正常封装返回体
     */
    @GetMapping("getOne")
    public TestVO getOne() {
    
    
        TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
        if (true) {
    
    
            throw new BusinessException(ResultEnum.VALIDATE_FAILED.getCode(), "名称为空");
        }
        return testVO;
    }

  1. Global exception interceptor captures business exceptions
   /**
     * 捕获 自定 异常
     */
    @ExceptionHandler({
    
    BusinessException.class}
    public Result<?> handleBusinessException(BusinessException ex) {
    
    
        log.error(ex.getMessage(), ex);
        return Result.failed(ex.getCode(),ex.getMessage());
    }
  1. The consumer side calls the abnormal feign interface
    @Resource
    CommentRestApi commentRestApi;


    @GetMapping("getOne")
    public TestVO getOne() {
    
    
        TestVO one = commentRestApi.getOne();
        System.out.println("one = " + one);
        return one;
    }

3.2.Feign cannot catch the exception

  1. Observation results:
    Call the getOne() method in the consumer and find that there is no exception in the returned information. The object fields in the data are all set to null, as follows: View the
    Insert image description here
    provider-side log and indeed throw a custom exception:
    Insert image description here
    Set Feign's log level to FULL to view Return results:
    @Bean
    Logger.Level feginLoggerLevel(){
    
    
        return Logger.Level.FULL;
    }

Insert image description here
From the log, we can see that Feign actually obtained the unified object Result converted by the global exception handler, and the response code is 200, which is a normal response. When the consumer accepts the object as TestVO, the attributes cannot be converted and are all treated as NULL values.

Obviously, this does not conform to our normal business logic. We should directly return the exception thrown by the producer. So how to deal with it?

It's very simple. We only need to set a non-200 response code for the business exception in the global exception interceptor , such as:
Insert image description here

    /**
     * 捕获 自定 异常
     */
    @ExceptionHandler({
    
    BusinessException.class})
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> handleBusinessException(BusinessException ex) {
    
    
        log.error(ex.getMessage(), ex);
        return Result.failed(ex.getCode(),ex.getMessage());
    }

In this way, the consumer can normally catch the business exception thrown by the producer, as shown in the following figure:Insert image description here

3.3. Exceptions are additionally encapsulated

Although the exception can be obtained, Feign encapsulates it again based on the business exception after catching the exception.

BecauseWhen the feign call result is a response code other than 200, Feign's exception parsing is triggered. Feign's exception parser will wrap it into FeignException, that is, wrap it again based on our business exception.

You can put a breakpoint on the feign.codec.ErrorDecoder#decode() method to observe the execution results, as follows:
Insert image description here
Obviously, we do not need this wrapped exception, we should directly capture the producer's business The exception is thrown directly to the front end, so how to solve this?

3.4.Solution

It's very simple. We only need to rewrite Feign's exception parser, reimplement the decode logic, and return a normal BusinessException. Then the global exception interceptor will capture the BusinessException! ( It feels a bit like infinite nesting dolls)

code show as below:

  1. Rewrite Feign exception parser
/**
 * @ClassName: OpenFeignErrorDecoder
 * @Description: 解决Feign的异常包装,统一返回结果
 * @Author wxl
 * @Date 2023-08-26
 * @Version 1.0.0
 **/
@Configuration
public class OpenFeignErrorDecoder implements ErrorDecoder {
    
    
    /**
     * Feign异常解析
     *
     * @param methodKey 方法名
     * @param response  响应体
     * @return {@link Exception }
     * @Author wxl
     * @Date 2023-08-26
     **/
    @SneakyThrows
    @Override
    public Exception decode(String methodKey, Response response) {
    
    
        //获取数据
        String body = Util.toString(response.body().asReader(Charset.defaultCharset()));
        Result<?> result = JSON.parseObject(body, Result.class);
        if (!result.isSuccess()) {
    
    
            return new BusinessException(result.getStatus(), result.getMessage());
        }
        return new BusinessException(500, "Feign client 调用异常");
    }
    
}

  1. The exception information thrown by calling the provider layer again
    can be captured by the consumer layer and processed into a custom exception through the custom exception parser, and is no longer wrapped by the default feign exception; the custom exception thrown is uniformly returned for encapsulation processing.Insert image description hereInsert image description here
    Insert image description here

Case source code

Case source code conveyor belt

Guess you like

Origin blog.csdn.net/weixin_43811057/article/details/132522402
Recommended