Feign practical skills

Posted by gerbs987 on Tue, 18 Jan 2022 00:56:23 +0100

Introduce the correct opening method of Feign in the project

Read the last issue Feign remote call My little partner may ask: ah Jian, didn't you say that the last issue talked about the 99% common ways of Feign? Why is there a correct opening method today?

Ajin: it's 99% of the common way. Ajin definitely didn't deceive everyone. It's just that 1% of this issue is the finishing touch of the dragon. Hey hey

Let's start with a set of cases

  • Commodity service interface

    @RestController
    @RequestMapping("/goods")
    public class GoodsController {
      	@GetMapping("/get-goods")
        public Goods getGoods() throws InterruptedException {
            TimeUnit.SECONDS.sleep(10);
            System.out.println("xxxxxxx");
            return new Goods().setName("Apple")
                    .setPrice(1.1)
                    .setNumber(2);
        }
      
      	@PostMapping("save")
        public void save(@RequestBody Goods goods){
            System.out.println(goods);
        }
    }
    
  • Commodity service feign interface

    @FeignClient(name = "my-goods", path = "/goods", contextId = "goods")
    public interface GoodsApi {
    
        @GetMapping("/get-goods")
        Goods getGoods();
    
        @PostMapping(value = "/save")
        void save(Goods goods);
    }
    
  • Order service interface

    @RestController
    @RequestMapping("/order")
    public class OrderController {
    
        @Resource
        private GoodsApi goodsApi;
    
        @GetMapping("/get-goods")
        public Goods getGoods(){
            return goodsApi.getGoods();
        }
    
        @PostMapping("/save-goods")
        public String saveGoods(){
            goodsApi.save(new Goods().setName("banana").setNumber(1).setPrice(1.1));
            return "ok";
        }
    
    }
    

Yes, this is the query and save interface of the previous issue. The order service calls the commodity service

Under normal circumstances, there is no problem in the operation of this case, but various problems will be encountered in the actual operation of the project. Let's come one by one.

overtime

Last time, we learned that when the service provider responds to a timeout (there is a problem with the network, or the service does not respond), the service caller can configure the timeout to cut off the request in time to avoid thread blocking. As follows:

feign:
  client:
    config:
      default:
        # The connection timeout unit is milliseconds. The default is 10 seconds
        connectTimeout: 1000
        # The request timeout is in milliseconds. The default is 60 seconds
        readTimeout: 5000

Now, we sleep for 10s in the commodity service interface to simulate the timeout

Then, initiate the call, and you will find that the data returned by the original interface is in json format. Now Feign throws an exception due to timeout, and the page looks like this:

Actually returned a page!

This is definitely not possible. We need to return an expected error when a timeout occurs, such as the exception of service call failure

Feign's author also thought of this and provided us with a Fallback mechanism. The usage is as follows:

  1. Open hystrix

    feign:
      hystrix:
        enabled: true
    
  2. Write goodsapi fallback

    @Slf4j
    @Component
    public class GoodsApiFallback implements FallbackFactory<GoodsApi> {
        @Override
        public GoodsApi create(Throwable throwable) {
            log.error(throwable.getMessage(), throwable);
            return new GoodsApi() {
    
                @Override
                public Goods getGoods() {
                    return new Goods();
                }
                
                @Override
                public void save(Goods goods) {
    
                }
    
            };
        }
    }
    
  3. Add the property fallbackFactory in FeignClient

    @FeignClient(name = "my-goods", path = "/goods", contextId = "goods", fallbackFactory = GoodsApiFallback.class)
    public interface GoodsApi {
    }
    

When the timeout is requested again, the response logic in the fallback will be enabled, and the logic we write returns a new Goods(), so an empty Goods object will be obtained in the timeout request logic, like this:

It seems that the problem of unfriendly information returned due to timeout has indeed been solved. However, when we return an empty object in fallback, logic confusion will be caused: is there no commodity in the commodity service or the service timeout? I don't know

Use the return object with exception information

In order to solve this logical confusion, we thought of using a return object with exception information. Its structure is as follows:

{
  "code": 0,
  "message": "",
  "data": {}
}

We define that a code of 0 indicates a correct return

Based on this, we can modify the above logic:

  • When goods and services return normally, code:0
  • When timeout occurs, code: -1

The adjusted codes are as follows:

  • Goods and services

    @GetMapping("/get-goods")
    public BaseResult<Goods> getGoods() throws InterruptedException {
      System.out.println("xxxxxxx");
      return BaseResult.success(new Goods().setName("Apple")
                                .setPrice(1.1)
                                .setNumber(2));
    }
    
  • Commodity service feign interface

    @GetMapping("/get-goods")
    BaseResult<Goods> getGoods();
    
  • Commodity service feign interface Fallback

    return new GoodsApi() {
    
      @Override
      public BaseResult<Goods> getGoods() {
        BaseResult<Goods> result = new BaseResult<>();
        result.setCode(-1);
        result.setMessage("Goods and services response timeout");
        return result;
      }
    }
    
  • Order service

    @GetMapping("/get-goods")
    public Goods getGoods(){
      BaseResult<Goods> result = goodsApi.getGoods();
      if(result.getCode() != 0){
        throw new RuntimeException("Error calling commodity service:" + result.getMessage());
      }
      return result.getData();
    }
    

Now, we have solved the problem that the service response timeout returns unfriendly information, and also solved the problem of logical confusion. Is it done?

Unified exception verification and unpacking

The above solutions are really OK. That's all for the general project methods. It's just for use...

You will find a disgusting problem. Originally, we used it like this:

Goods goods = goodsApi.getGoods();

Now it's like this:

BaseResult<Goods> result = goodsApi.getGoods();
if(result.getCode() != 0){
  throw new RuntimeException("Error calling commodity service:" + result.getMessage());
}
Goods goods = result.getData();

And this code is as like as two peas, because many Feign interfaces, each Feign interface's checkout logic is exactly the same.

BaseResult<xxx> result = xxxApi.getXxx();
if(result.getCode() != 0){
  throw new RuntimeException("call xxx Service error:" + result.getMessage());
}
Xxx xxx = result.getData();

——————- split line -——————

I, ajin, as a code cleaner, will I allow this to happen? impossible!

What easy-to-use way and safe way can't have both. As an adult: I want both!

Now let's turn it into the original way of use and get friendly return information.

In the last issue, we mentioned that Feign has an encoding and decoding process, and decoding this action will involve parsing the information returned by the server into the content required by the client.

So the idea is: customize a decoder, decode the information returned by the server, judge the code value of BaseResult, return the data directly when the code is 0, and throw an exception when the code is not 0.

Upper Code:

  • Write custom decoder

    @Slf4j
    public class BaseResultDecode extends ResponseEntityDecoder {
    
        public BaseResultDecode(Decoder decoder) {
            super(decoder);
        }
    
        @Override
        public Object decode(Response response, Type type) throws IOException, FeignException {
            if (type instanceof ParameterizedType) {
                if (((ParameterizedType) type).getRawType() != BaseResult.class) {
                    type = new ParameterizedTypeImpl(new Type[]{type}, null, BaseResult.class);
                    Object object = super.decode(response, type);
                    if (object instanceof BaseResult) {
                        BaseResult<?> result = (BaseResult<?>) object;
                        if (result.isFailure()) {
                            log.error("call Feign Interface exception, interface:{}, abnormal: {}", response.request().url(), result.getMessage());
                            throw new BusinessException(result.getCode(), result.getMessage());
                        }
                        return result.getData();
                    }
                }
            }
    
            return super.decode(response, type);
        }
    }
    

    The default decoder in Feign is ResponseEntityDecoder, so we only need to inherit it and make some modifications on the original basis.

  • Inject decoder into Spring

    @Configuration
    public class DecodeConfiguration {
    
        @Bean
        public Decoder feignDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {
            return new OptionalDecoder(
                    new BaseResultDecode(new SpringDecoder(messageConverters)));
        }
    
    }
    

    This code is directly copied from the source code. The source code is as follows:

    new OptionalDecoder( new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)))

    I just replaced the ResponseEntityDecoder with my BaseResultDecode

Now let's change the code back to the original way

  • Goods and services

    @GetMapping("/get-goods")
    public BaseResult<Goods> getGoods() throws InterruptedException {
      System.out.println("xxxxxxx");
      return BaseResult.success(new Goods().setName("Apple")
                                .setPrice(1.1)
                                .setNumber(2));
    }
    

    You still need to put the BaseResult back here

  • Commodity service feign interface

    @GetMapping("/get-goods")
    Goods getGoods();
    
  • Commodity service feign interface Fallback

    return new GoodsApi() {
    
      @Override
      public Goods getGoods() {
        throw new RuntimeException("Exception occurred while calling commodity service");
      }
    }
    
  • Order service

    @GetMapping("/get-goods")
    public Goods getGoods(){
      return goodsApi.getGoods();
    }
    

Print curl log

This chapter has nothing to do with the previous one, but it can copy a curl when imitating the front-end request, which is very convenient for debugging.

The same logic: customize a log printer

The code is as follows:

  • Custom logger

    public class CurlLogger extends Slf4jLogger {
    
        private final Logger logger;
    
        public CurlLogger(Class<?> clazz) {
            super(clazz);
            this.logger = LoggerFactory.getLogger(clazz);
        }
    
        @Override
        protected void logRequest(String configKey, Level logLevel, Request request) {
            if (logger.isDebugEnabled()) {
                logger.debug(toCurl(request.requestTemplate()));
            }
            super.logRequest(configKey, logLevel, request);
        }
    
        public String toCurl(feign.RequestTemplate template) {
            String headers = Arrays.stream(template.headers().entrySet().toArray())
                    .map(header -> header.toString().replace('=', ':')
                            .replace('[', ' ')
                            .replace(']', ' '))
                    .map(h -> String.format(" --header '%s' %n", h))
                    .collect(Collectors.joining());
            String httpMethod = template.method().toUpperCase(Locale.ROOT);
            String url = template.url();
            if(template.body() != null){
                String body = new String(template.body(), StandardCharsets.UTF_8);
                return String.format("curl --location --request %s '%s' %n%s %n--data-raw '%s'", httpMethod, url, headers, body);
            }
            return String.format("curl --location --request %s '%s' %n%s", httpMethod, url, headers);
        }
    }
    

    Similarly, directly inherit the default Slf4jLogger

  • Custom log factory

    public class CurlFeignLoggerFactory extends DefaultFeignLoggerFactory {
    
        public CurlFeignLoggerFactory(Logger logger) {
            super(logger);
        }
    
        @Override
        public Logger create(Class<?> type) {
            return new CurlLogger(type);
        }
    }
    
  • Inject into Spring

    @Bean
    public FeignLoggerFactory curlFeignLoggerFactory(){
      return new CurlFeignLoggerFactory(null);
    }
    

The effects are as follows:

curl --location --request POST 'http://my-goods/goods/save' 
 --header 'Content-Encoding: gzip, deflate ' 
 --header 'Content-Length: 40 ' 
 --header 'Content-Type: application/json ' 
 --header 'token: 123456 ' 

Summary

In this chapter, I introduce the use of Feign in actual projects: using return objects with exception information

And the reason for this use: it is necessary to enable the service caller to get clear response information

The disadvantage of this use: it is always necessary to judge whether the information returned by the service is correct

Solution: customize a decoder

Finally, we provide a way to print curl logs.

Finally, ah Jian would like to say a few words to you. I don't know if you have any feelings after reading ah Jian's custom decoder and custom logger. You may have always felt how difficult and powerful it is to expand some frameworks. In fact, it's not so difficult, Many times, we only need to make some small extensions based on the logic in the framework. To sum up, we need to find it, inherit it and modify it.

Well, I'll see you next time~

For more information, welcome to the official account: programmer, Jian, ah Jian, welcome to you in the official account.

Personal blog space: https://zijiancode.cn/archives/feign2

Topics: Microservices