Describes the correct way to open Feign in your project

Those of you who watched the last issue of Feign Remote Call might ask: Ah Kam, didn’t you say that the last issue covered 99% of the common methods of Feign? How come there is still a correct way to open it today?

A jian: is 99% of the common way, a jian absolutely no deceive everyone, just this period of 1% as the finishing touch pen, 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); return new Goods().setName(" apple ").setPrice(1.1).setNumber(2); } @PostMapping("save") public void save(@RequestBody Goods goods){ System.out.println(goods); }}
  • Feign interface for goods and services

    @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 (.) elegantly-named setName (" banana "). SetNumber (1). The setPrice (1.1)); return "ok"; }}

Yes, this is the query and save interface from the previous installment, where the order service invokes the goods service

Under normal circumstances, this case is running without any problems, but the actual operation of the project will encounter a variety of problems. Let’s do it one by one.

timeout

Last time, we learned that when a service provider responds to a timeout (a network problem, or the service does not respond), the service caller can configure the timeout to choke off the request in time to avoid thread blocking. Such as the following configuration

Feign: client: config: default: # ConnectTimeout: 1000 # Request Timeout: 60sec ReadTimeout: 5000

Now, let’s do 10s of sleep in the goods service interface to simulate the timeout situation

Then, when you make the call, you’ll see that the data returned from the interface was in JSON format, and now the page looks like this because Feign has thrown an exception due to a timeout:

I returned a page!

This will not work. We need to return an expected error when a timeout occurs, such as an exception for service invocation failure

The Feign writers also thought of this and provided us with a Fallback mechanism to use as follows:

  1. Open the hystrix

    feign:
      hystrix:
        enabled: true
  2. Write GoodsApiFallback

    @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 to FeignClient

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

When the request timeouts again, the response logic in Fallback is enabled, and we wrote the logic to return a new Goods(), so we get an empty Goods object in the request logic when the request timeouts, like this:

It looks like the problem with unfriendly messages returned by timeout has been solved. However, when we return an empty object in Fallback, there is a logical confusion: does the item not exist in the service or does the service timeout? I don’t know…

Use the return object with the exception message

To solve this logical confusion, we thought of using a return object that could have an exception message, which would have the following structure:

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

We define code to be 0 to return correctly

Based on this, we can modify the above logic:

  • Code :0 when goods and services return normally
  • Code: -1 when the timeout status occurs

The adjusted code is 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)); }
  • Feign interface for goods and services

    @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 service 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 in calling a commodity service: "+ Result.GetMessage ()); } return result.getData(); }

Now, we have solved both the problem of the service response timeout being unfriendly and the problem of the logic confusion, and we are done?

Unify exception checking and unpack

The above solution really can, the general project technique is here, just use up…

You’ll notice a nasty problem, but the way we use it is like this:

Goods goods = goodsApi.getGoods();

Now it looks like this:

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

And this code is everywhere, because there are many Feign interfaces, and the validation logic is the same for each of them:

BaseResult<xxx> result = xxxApi.getXxx(); if(result.getCode() ! = 0){Throw new RuntimeException(" Error calling XXX service: "+ Result.GetMessage ()); } Xxx xxx = result.getData();

The ——————— line is divided

Do I, Kam, as a code cleaner, allow this to happen? That’s impossible!

What good way and safe way can not have both, as an adult: I want both!

Now we’re going to make it the way it was used, but we’re going to get a friendly return message.

In the last issue, we mentioned that Feign has a codec process, and decoding this process involves parsing the information returned by the server into what the client wants.

So the idea is: define a custom decoder, decode the information returned by the server, determine the BaseResult code value, code is 0 directly return data, code does not throw an exception.

The code:

  • Write a 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(" Failure to call Feign interface, interface :{}, exception :{} ", 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 just need to inherit it and make some changes on the original basis.

  • Inject the 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 a direct copy of the source code. The source code looks like this:

`new OptionalDecoder(

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

I’m just replacing my ResponseEntityDecoder with my own BaseSoltDecode

Now let’s switch 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)); }

Again, I need to put it back in the BaseResult

  • Feign interface for goods and services

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

    Return new GoodsApi() {@Override public Goods getGoods() {throw new RuntimeException(" Exception happened when a service was called "); }}
  • Order service

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

Print curl log

This section has nothing to do with the previous section, except that you can copy a curl that you want to use for debugging purposes.

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); }}

Again, it inherits directly from 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);
        }
    }
  • Into the Spring

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

The effect is 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 section, I’ve shown you how to use Feign in a real project: return objects with exception information

And the reason for this use: the need to give the service caller explicit response information

The downside of this approach is that it is always necessary to determine whether the information returned by the service is correct

Solution: Customize a decoder

You can also print a URL to curl a log.

Finally, Kam would like to say something to you. I don’t know if you have any feelings after watching Kam’s custom decoder and custom logger. In the past, you may always think how difficult it is to extend some frameworks, but it is not that difficult. A lot of times we just need to make small extensions based on the logic in the framework. In a nutshell, discover it, inherit it, modify it.

See you next time

Want to know more exciting content, welcome to pay attention to the public number: programmer A Jian, A Jian in the public number to welcome your arrival ~

Personal blog space: https://zijiancode.cn/archive…