1
头图

foreword

About two years ago, I wrote a Feign enhancement package . At that time, I was going to use SpringBoot + K8s to build an application. This library can be combined like SpringCloud SpringBoot a declarative interface to communicate between services.

However, due to the change of the technology stack (to Go) later, the project was shelved after only the basic requirements were realized.

Coincidentally, some internal projects recently planned to use SpringBoot + K8s for development, so I started to maintain it; now several versions have been iterated internally, which are relatively stable, and some practical functions have been added, which I would like to share with you here. .

https://github.com/crossoverJie/feign-plus

The first is to add some features :

  • A more unified API.
  • Unified request, response, exception logging.
  • Custom interceptor.
  • Metric support.
  • Exception delivery.

Example

Combining some of the features mentioned above to make some brief introductions, the unified API is mainly at the usage level:

In the previous version the interface was declared as follows:

 @FeignPlusClient(name = "github", url = "${github.url}")
public interface Github {
    @RequestLine("GET /repos/{owner}/{repo}/contributors")
    List<GitHubRes> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

The annotations such as @RequestLine are all provided by the feign package.

After this update, it is changed to the following way:

 @RequestMapping("/v1/demo")
@FeignPlusClient(name = "demo", url = "${feign.demo.url}", port = "${feign.demo.port}")
public interface DemoApi {
    @GetMapping("/id")
    String sayHello(@RequestParam(value = "id") Long id);

    @GetMapping("/id/{id}")
    String id(@PathVariable(value = "id") Long id);

    @PostMapping("/create")
    Order create(@RequestBody OrderCreateReq req);

    @GetMapping("/query")
    Order query(@SpringQueryMap OrderQueryDTO dto);
}

The familiar flavors are basically Spring the annotations that come with it, so that the learning cost is lower in use, and at the same time, it is consistent with the original interface writing in the project.

@SpringQueryMap(top.crossoverjie.feign.plus.contract.SpringQueryMap) is provided by feign-plus, which is actually copied from SpringCloud.

I wrote two demos here to simulate the call:

provider : As a service provider, it provides a series of interfaces for consumers to call, and provides an api module to the outside world.


demo : Depends on the provider-api module as a service consumer, and makes remote calls according to the interface declared in it.

Configuration file:

 server:
  port: 8181

feign:
  demo:
    url : http://127.0.0.1
    port: 8080

logging:
  level:
    top:
      crossoverjie: debug

management:
  endpoints:
    web:
      base-path: /actuator
      exposure:
        include: '*'
  metrics:
    distribution:
      percentiles:
        all: 0.5,0.75,0.95,0.99
    export:
      prometheus:
        enabled: true
        step: 1m
spring:
  application:
    name: demo

When we access the http://127.0.0.1:8181/hello/2 interface, we can see the call result from the console:

logging

As can be seen from the above figure feign-plus will use debug to record the request/response results. If you need to print out, you need to adjust the log level under the package to debug:

 logging:
  level:
    top:
      crossoverjie: debug

Due to the built-in interceptor, you can also inherit top.crossoverjie.feign.plus.log.DefaultLogInterceptor to implement your own log interception records, or other business logic.

 @Component
@Slf4j
public class CustomFeignInterceptor extends DefaultLogInterceptor {
    @Override
    public void request(String target, String url, String body) {
        super.request(target, url, body);
        log.info("request");
    }

    @Override
    public void exception(String target, String url, FeignException feignException) {
        super.exception(target, url, feignException);
    }

    @Override
    public void response(String target, String url, Object response) {
        super.response(target, url, response);
        log.info("response");
    }
}

monitor metrics

feign-plus will record the call time and exceptions between each interface by itself.

Visit http://127.0.0.1:8181/actuator/prometheus and you will see the relevant buried point information. Through the key of feign_call* , you can configure the relevant panel in Grafana by yourself, similar to the following figure:

exception delivery

rpc (remote call) To use it really like a local call, exception passing is essential.

 // provider
    public Order query(OrderQueryDTO dto) {
        log.info("dto = {}", dto);
        if (dto.getId().equals("1")) {
            throw new DemoException("provider test exception");
        }
        return new Order(dto.getId());
    }

// consumer
        try {
            demoApi.query(new OrderQueryDTO(id, "zhangsan"));
        } catch (DemoException e) {
            log.error("feignCall:{}, sourceApp:[{}], sourceStackTrace:{}", e.getMessage(), e.getAppName(), e.getDebugStackTrace(), e);
        }

For example, a custom exception is thrown in provider consumer , which can be caught by try/catch in ---bc8f98b183b820c9505a7a67241315ae---.

To implement this functionality in feign-plus requires several steps:

  1. Customize a generic exception.
  2. The service provider needs to implement a global interceptor to unify external response data when an exception occurs.
  3. The service consumer needs to customize an exception decoder bean.

Here I have customized a ---3b79158868a6fe441fcffb5b56a838c3 provider in DemoException :

Usually this class should be defined in the company's internal general package, here for the convenience of demonstration.

Then a class HttpStatus is defined for unified external response.

 @Data
@AllArgsConstructor
@NoArgsConstructor
public class HttpStatus {
    private String appName;
    private int code;
    private String message;
    private String debugStackTrace;
}
This should also be placed in the generic package.

Then define global exception handling in provider :

When an exception occurs, it will return a http_code=500 data:

At this point, there will be another leading topic: Does the HTTP interface return all return 200 and then judge by code, or refer to http_code for return?

I won't discuss too much here. For details, please refer to Uncle Mouse's article:
"A shuttle: REST API uses POST"

feign-plus The default http_code !=200 will be considered abnormal.

The http_status here also refers to Google's api design:

For details, please refer to this link:
https://cloud.google.com/apis/design/errors#propagating_errors

Then define an exception parser:

 @Configuration
public class FeignExceptionConfig {
    @Bean
    public FeignErrorDecoder feignExceptionDecoder() {
        return (methodName, response, e) -> {
            HttpStatus status = JSONUtil.toBean(response, HttpStatus.class);
            return new DemoException(status.getAppName(), status.getCode(), status.getMessage(), status.getDebugStackTrace());
        };
    }
}
Usually this code is also placed in the base package.

In this way, when the service provider throws an exception, the consumer can successfully get the exception:

Implementation principle

The implementation principle is actually relatively simple. If you understand the rpc principle, you should know that the exception returned by the service provider cannot be received by the caller, and it does not matter whether it is implemented by a language.

After all, the stacks between the two processes are completely different, not on the same server, or even in the same region.

So provider After an exception is thrown, the consumer can only get a series of messages, we can only parse the exception information according to this message, and then recreate an internal custom exception ( For example, here DemoException ), which is what our custom exception parser does.

The following figure is the general flow of this exception delivery:

code message mode

Since feign-plus uses the http_code != 200 method to throw exceptions by default, the response data in the http_code=200, code message method will not pass the exception, and the task will still be a normal call.

However, it is possible to pass exceptions based on this mode, but it cannot be unified. For example, some teams are used to code !=0 to indicate exceptions, and even the fields are not code; or some exception information is placed in the message or msg fields middle.

Every team and individual have different habits, so there is no way to abstract a standard, so there is no relevant adaptation.

This also confirms the benefits of using international standards.

Due to space limitations, friends who have related needs can also communicate in the comment area, and the implementation will be a little more complicated than now 🤏🏻.

Summarize

Project source code:
https://github.com/crossoverJie/feign-plus

Based on the background of cloud native in 2022, of course, it is recommended that you use gRPC for inter-service communication, so that there is no need to maintain a library similar to this.

However, when some third-party interfaces are called and the other party does not provide SDK, this library also has a certain role. Although the use of native feign can also achieve the same purpose, using this library can make the development experience consistent with Spring , and built-in log, metric and other functions to avoid repeated development.

Your likes and shares are the greatest support for me


crossoverJie
5.4k 声望4k 粉丝