Describes the correct way to open Feign in a project

If you read the last Feign remote call, you might be wondering: Ah Jian, didn’t you say that the last Feign remote call covered 99% of the common methods? Why is there a correct way to open it today?

Ah jian: it is 99% of the common way, ah Jian absolutely did not deceive everyone, just 1% of this issue as the finishing touch, heh heh

Let’s start with a set of examples

  • Commodity service Interface

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

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

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

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

Normally, this case runs without any problems, but the project runs with all sorts of problems. Let’s take it one by one.

timeout

Last time, we learned that when the service provider responds with a timeout (something goes wrong with the network, or the service does not respond), the service caller can configure the timeout to block the request in time to avoid thread blocking. As follows:

feign:
  client:
    config:
      default:
        The default connection timeout is 10 seconds
        connectTimeout: 1000
        The default timeout unit is 60 seconds
        readTimeout: 5000
Copy the code

Now, we simulate a timeout by sleeping for 10s in the commodity service interface

Then, when you make the call, you will see that the data returned by the interface was in JSON format. Now, due to the timeout, the page looks like this:

A page was returned!

This is definitely not the case, we need to return an expected error when a timeout occurs, such as an exception that the service call failed

The writers at Feign also thought of this and gave us a Fallback mechanism that works like this:

  1. Open the hystrix

    feign:
      hystrix:
        enabled: true
    Copy the code
  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(a) {
                    return new Goods();
                }
                
                @Override
                public void save(Goods goods) {}}; }}Copy the code
  3. Add the property fallbackFactory to FeignClient

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

The response logic in fallback is enabled when the request times out again, and we wrote the logic to return a new Goods(), so the request logic will get an empty Goods object when the request times out, like this:

It seems that the problem of unfriendly messages being returned due to timeout is resolved. However, when we return an empty object in fallback, there is a logical confusion: is the item not in the service or the service timed out? I don’t know…

Use return objects with exception information

To solve this logical confusion, we came up with the idea of using a return object with exception information, which has the following structure:

{
  "code": 0."message": ""."data": {}}Copy the code

We define code zero to return correctly

Based on this, we can modify the above logic:

  • Product service returns normally with code:0
  • When timeout occurs, code: -1

The adjusted code is as follows:

  • Goods and services

    @GetMapping("/get-goods")
    public BaseResult<Goods> getGoods(a) throws InterruptedException {
      System.out.println("xxxxxxx");
      return BaseResult.success(new Goods().setName(The word "apple")
                                .setPrice(1.1)
                                .setNumber(2));
    }
    Copy the code
  • Commodity service FEIGN interface

    @GetMapping("/get-goods")
    BaseResult<Goods> getGoods(a);
    Copy the code
  • Feign Interface Fallback

    return new GoodsApi() {
    
      @Override
      public BaseResult<Goods> getGoods(a) {
        BaseResult<Goods> result = new BaseResult<>();
        result.setCode(-1);
        result.setMessage("Goods and services response time out");
        returnresult; }}Copy the code
  • Order service

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

Now, we have solved the problem that the service response time is not friendly, but also solved the problem of logic confusion, and we are done?

Unified exception verification and unpacking

The above solution is really ok, the general project technique is here, just use it…

You’ll notice a really nasty problem, and the way we used it was like this:

Goods goods = goodsApi.getGoods();
Copy the code

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();
Copy the code

And this code is everywhere, because many Feign interfaces have exactly the same validation logic:

BaseResult<xxx> result = xxxApi.getXxx();
if(result.getCode() ! =0) {throw new RuntimeException(Error calling XXX service: + result.getMessage());
}
Xxx xxx = result.getData();
Copy the code

——————— split line ———————

Would I, Kam, as a code clean freak, allow this to happen? That’s impossible!

What works and what is safe can’t be both, as an adult: I want both!

Now let’s change it to be used in the original way and get a friendly return message.

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

If the BaseResult code is 0, data will be returned directly. If the BaseResult code is 0, data will be returned directly.

The code:

  • Write custom decoders

    @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 instanceofBaseResult) { BaseResult<? > result = (BaseResult<? >) object;if (result.isFailure()) {
                            log.error("Error calling Feign interface, interface :{}, exception :{}", response.request().url(), result.getMessage());
                            throw new BusinessException(result.getCode(), result.getMessage());
                        }
                        returnresult.getData(); }}}return super.decode(response, type); }}Copy the code

    The default decoder in Feign is ResponseEntityDecoder, so we just need to inherit it and make some changes to it.

  • Inject the decoder into Spring

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

    This code is a direct copy of the source code, the source code is like this:

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

    I just replaced ResponseEntityDecoder with my own BaseResultDecode

Now let’s switch the code back to the original way

  • Goods and services

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

    I still need to put back the BaseResult

  • Commodity service FEIGN interface

    @GetMapping("/get-goods")
    Goods getGoods(a);
    Copy the code
  • Feign Interface Fallback

    return new GoodsApi() {
    
      @Override
      public Goods getGoods(a) {
        throw new RuntimeException("An exception occurred when calling the commodity service"); }}Copy the code
  • Order service

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

Printing curl Logs

Curl curl curl curl curl curl curl curl curl curl curl curl curl curl

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); }}Copy the code

    Again, the default Slf4jLogger is inherited directly

  • Custom log factories

    public class CurlFeignLoggerFactory extends DefaultFeignLoggerFactory {
    
        public CurlFeignLoggerFactory(Logger logger) {
            super(logger);
        }
    
        @Override
        public Logger create(Class
              type) {
            return newCurlLogger(type); }}Copy the code
  • Into the Spring

    @Bean
    public FeignLoggerFactory curlFeignLoggerFactory(a){
      return new CurlFeignLoggerFactory(null);
    }
    Copy the code

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 ' 
Copy the code

summary

In this section, I show you how Feign can be used in a real project: using return objects with exception information

And why: The service caller needs to be able to get clear response information

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

Solution: define a decoder

Do you want to curl curl?

Finally, AH Jian would like to say a few words to you. I don’t know if you have any feelings after seeing ah Jian’s custom decoder and custom logger. Before, you may always think that it is difficult and powerful to expand some frameworks, but in fact it is not that difficult. A lot of times we just need to make a little extension based on the logic in the framework, which is basically 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 AH Jian, AH Jian in the public number welcome your arrival ~

Personal blog space: zijiancode. Cn/archives/fe…