foreword

I wrote an article about two years ago Picked up a Feign enhancement packwas prepared to use SpringBoot + K8s To build the application, this library can be similar to SpringCloud combine like that SpringBoot Use declarative interfaces for communication 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 have recently planned to use SpringBoot + K8s Development, so I started to maintain it; now it has been iterated for several versions, which is 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);
}

one of them @RequestLine and other annotations are provided using 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);
}

Familiar taste, basically Spring With its own annotations, the learning cost is lower in use, and it is consistent with the original interface writing method 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 as a service consumer provider-api A module that 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 visit http://127.0.0.1:8181/hello/2 You can see the call result from the console when you interface:

logging

As can be seen from the above figure feign-plus The request/response results will be recorded with debug. If you need to print them 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 it yourself top.crossoverjie.feign.plus.log.DefaultLogInterceptor To implement your own log interception record, 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 It will record the call time and exceptions between each interface by itself.

access http://127.0.0.1:8181/actuator/prometheus You will see the relevant buried point information, through feign_call* The key can be found in Grafana Configure the relevant panel, similar to the following image:

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 provider throws a custom exception in consumer through the try/catch The exception is caught.

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 am provider custom one DemoException:

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

Then define a HttpStatus The class is used to unify external responses.

@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 in provider Define global exception handling in:

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

At this point, there will be another leading topic: should 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 an exception.

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: