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:
- Customize a generic exception.
- The service provider needs to implement a global interceptor to unify external response data when an exception occurs.
- 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。