Detailed explanation of using Feign in actual projects
Introduction
In our daily study, we simply know how to call the feign interface or perform service downgrade; however, when using feign in enterprise-level projects, we will face the following problems:
- How should the Feign client be provided?
- Should the interface called by Feign be packaged?
- How does Feign capture business exceptions on the business production side?
How should a Feign client be provided?
How to provide the feign interface to the outside world?
analyze:
-
The consumer side needs to reference these feign interfaces. If the feign interface is written directly in the consumer project, then if another one also needs the feign interface, will it have to be written again? Naturally, we will consider making the feign interface independent. Whoever needs the feign interface can add the corresponding dependencies.
-
The feign interface contains entity objects. Generally, these entities are in the provider. When transforming through the feign interface, we need to extract the entity classes used in the controller. It can be processed in the following two ways
. Method 1 extracts the entity class and puts it in an independent module. The provider and feign interfaces depend on the entity class module respectively.
Method 2 puts the entity class in the module of the feign interface and the provider depends on this feign module.
Project There are many cases that are handled according to method one, which will not cause dependence on code that does not need to be used;
2. Should the interface called by Feign be packaged?
2.1.Problem description
In a flat front-end and back-end separation project, when the back-end returns interface data to the front-end, the return format will generally be unified; our Controller will basically look like this:
@GetMapping("getTest")
public Result<TestVO> getTest() {
TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
return Result.success(testVO);
Feign's interface definition needs to be consistent with the implementation class;
So when we use the feign interface of this method, the situation is like this.
@GetMapping("getContent")
public Result<String> getContent() {
String content=null;
Result<TestVO> test = commentRestApi.getTest();
if (test.isSuccess()) {
TestVO data = test.getData();
content = data.getContent();
}else {
throw new RuntimeException(test.getMessage());
}
return Result.success(content);
}
Here we need to first obtain the Result packaging class, and then parse the returned result into a specific TestVO object through judgment. Obviously, this code has two problems:
- Each Controller interface needs to manually use Result.success to package the results
- When Feign is called, it needs to be unpacked from the packaging class into the required entity object.
There are many, many interfaces in the project. Isn’t it too useless to do this kind of operation constantly? ! ! It undoubtedly adds unnecessary development burden.
2.2.Problem solving
The optimization goal is also very clear:
- When we call it through Feign, we get the entity object directly without additional unassembly.
- When the front end calls directly through the gateway, a unified packaging body is returned.
Here we can use ResponseBodyAdvice to achieve this. By enhancing the Controller return body, if a call from Feign is recognized, the object will be returned directly. Otherwise, a unified packaging structure will be added to us. ( SpringBoot uniformly encapsulates the results returned by the controller layer )
New question: How to identify whether it is a call from Feign or a direct call from the gateway?
Implemented based on custom annotations and implemented based on Feign interceptor.
-
Implemented based on custom annotations
Customize an annotation, such as @ResponseNotIntercept, and mark Feign's interface with this annotation, so that you can use this annotation to match when using ResponseBodyAdvice matching.
However, this method has a drawback, that is, the front end and feign cannot be shared. For example, an interface user/get/{id} can be called either through feign or directly through the gateway. To use this method, you need to write 2 interfaces with different paths. -
Implemented based on Feign interceptor
For Feign calls, add a special identifier to the Feign interceptor. When converting the object, if it is found to be a Feign call, the object will be returned directly.
The specific implementation steps of the second method:
- Add a specific request header T_REQUEST_ID to the feign request in the feign interceptor
/**
* @ClassName: OpenFeignConfig Feign拦截器
* @Description: 对于Feign的调用,在请求头中加上特殊标识
* @Author: wang xiao le
* @Date: 2023/08/25 23:13
**/
@ConditionalOnClass(Feign.class)
@Configuration
public class OpenFeignConfig implements RequestInterceptor {
/**
* Feign请求唯一标识
*/
public static final String T_REQUEST_ID = "T_REQUEST_ID";
/**
* get请求标头
*
* @param request 请求
* @return {@link Map }<{@link String }, {@link String }>
* @Author wxl
* @Date 2023-08-27
**/
private Map<String, String> getRequestHeaders(HttpServletRequest request) {
Map<String, String> map = new HashMap<>(16);
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = (String) headerNames.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
return map;
}
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (null != attributes) {
HttpServletRequest request = attributes.getRequest();
Map<String, String> headers = getRequestHeaders(request);
// 传递所有请求头,防止部分丢失
for (Map.Entry<String, String> entry : headers.entrySet()) {
requestTemplate.header(entry.getKey(), entry.getValue());
}
// 微服务之间传递的唯一标识,区分大小写所以通过httpServletRequest获取
if (request.getHeader(T_REQUEST_ID) == null) {
String sid = String.valueOf(UUID.randomUUID());
requestTemplate.header(T_REQUEST_ID, sid);
}
}
}
}
- Customize CommonResponseResult and implement ResponseBodyAdvice
/**
* 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成
*
* @RestControllerAdvice(basePackages = "com.wxl52d41")
* @ClassName: CommonResponseResult
* @Description: controller返回结果统一封装
* @Author wxl
* @Date 2023-08-27
* @Version 1.0.0
**/
@RestControllerAdvice
public class CommonResponseResult implements ResponseBodyAdvice<Object> {
/**
* 支持注解@ResponseNotIntercept,使某些方法无需使用Result封装
*
* @param returnType 返回类型
* @param converterType 选择的转换器类型
* @return true 时会执行beforeBodyWrite方法,false时直接返回给前端
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if (returnType.getDeclaringClass().isAnnotationPresent(ResponseNotIntercept.class)) {
//若在类中加了@ResponseNotIntercept 则该类中的方法不用做统一的拦截
return false;
}
if (returnType.getMethod().isAnnotationPresent(ResponseNotIntercept.class)) {
//若方法上加了@ResponseNotIntercept 则该方法不用做统一的拦截
return false;
}
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (request.getHeaders().containsKey(OpenFeignConfig.T_REQUEST_ID)) {
//Feign请求时通过拦截器设置请求头,如果是Feign请求则直接返回实体对象
return body;
}
if (body instanceof Result) {
// 提供一定的灵活度,如果body已经被包装了,就不进行包装
return body;
}
if (body instanceof String) {
//解决返回值为字符串时,不能正常包装
return JSON.toJSONString(Result.success(body));
}
return Result.success(body);
}
}
- Modify the provider backend interface to return the object and the feign interface
. If it is a Feign request, no conversion will be performed, otherwise it will be packaged by Result.
/**
* 对象返回值测试,是否能正常封装返回体
*/
@GetMapping("getOne")
public TestVO getOne() {
TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
return testVO;
}
- Modify the feign calling logic in the consumer module.
There is no need to return the encapsulated ResultData on the interface, and automatic enhancement is achieved through ResponseBodyAdvice.
@GetMapping("getOne")
public TestVO getOne() {
TestVO one = commentRestApi.getOne();
return one;
}
- The test
is called on the consumer side. It is found that the methods returned by calling the feign interface in the console are not uniformly encapsulated.
Call the provider layer method directly through postman. The discovery methods are unified and encapsulated.
Under normal circumstances, our optimization goal is achieved. The entity object is directly returned through the Feign call, and the unified package is returned through the gateway call. It looks perfect, but is actually terrible, which leads to the third question, how does Feign handle exceptions?
How does SanFeign capture business anomalies on the business production side?
3.1.Analysis
The producer will perform business rule verification on the interface methods provided. Business exceptions will be thrown for calling requests that do not comply with business rules. Under normal circumstances, there will be a global exception handler on the project, which will capture Business exception BusinessException, and encapsulates it into a unified package body and returns it to the caller. Now let us simulate this business scenario:
- The producer throws a business exception
and the name in the simulated business is empty.
/**
* 对象返回值测试,是否能正常封装返回体
*/
@GetMapping("getOne")
public TestVO getOne() {
TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
if (true) {
throw new BusinessException(ResultEnum.VALIDATE_FAILED.getCode(), "名称为空");
}
return testVO;
}
- Global exception interceptor captures business exceptions
/**
* 捕获 自定 异常
*/
@ExceptionHandler({
BusinessException.class}
public Result<?> handleBusinessException(BusinessException ex) {
log.error(ex.getMessage(), ex);
return Result.failed(ex.getCode(),ex.getMessage());
}
- The consumer side calls the abnormal feign interface
@Resource
CommentRestApi commentRestApi;
@GetMapping("getOne")
public TestVO getOne() {
TestVO one = commentRestApi.getOne();
System.out.println("one = " + one);
return one;
}
3.2.Feign cannot catch the exception
- Observation results:
Call the getOne() method in the consumer and find that there is no exception in the returned information. The object fields in the data are all set to null, as follows: View the
provider-side log and indeed throw a custom exception:
Set Feign's log level to FULL to view Return results:
@Bean
Logger.Level feginLoggerLevel(){
return Logger.Level.FULL;
}
From the log, we can see that Feign actually obtained the unified object Result converted by the global exception handler, and the response code is 200, which is a normal response. When the consumer accepts the object as TestVO, the attributes cannot be converted and are all treated as NULL values.
Obviously, this does not conform to our normal business logic. We should directly return the exception thrown by the producer. So how to deal with it?
It's very simple. We only need to set a non-200 response code for the business exception in the global exception interceptor , such as:
/**
* 捕获 自定 异常
*/
@ExceptionHandler({
BusinessException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> handleBusinessException(BusinessException ex) {
log.error(ex.getMessage(), ex);
return Result.failed(ex.getCode(),ex.getMessage());
}
In this way, the consumer can normally catch the business exception thrown by the producer, as shown in the following figure:
3.3. Exceptions are additionally encapsulated
Although the exception can be obtained, Feign encapsulates it again based on the business exception after catching the exception.
BecauseWhen the feign call result is a response code other than 200, Feign's exception parsing is triggered. Feign's exception parser will wrap it into FeignException, that is, wrap it again based on our business exception.
You can put a breakpoint on the feign.codec.ErrorDecoder#decode() method to observe the execution results, as follows:
Obviously, we do not need this wrapped exception, we should directly capture the producer's business The exception is thrown directly to the front end, so how to solve this?
3.4.Solution
It's very simple. We only need to rewrite Feign's exception parser, reimplement the decode logic, and return a normal BusinessException. Then the global exception interceptor will capture the BusinessException! ( It feels a bit like infinite nesting dolls)
code show as below:
- Rewrite Feign exception parser
/**
* @ClassName: OpenFeignErrorDecoder
* @Description: 解决Feign的异常包装,统一返回结果
* @Author wxl
* @Date 2023-08-26
* @Version 1.0.0
**/
@Configuration
public class OpenFeignErrorDecoder implements ErrorDecoder {
/**
* Feign异常解析
*
* @param methodKey 方法名
* @param response 响应体
* @return {@link Exception }
* @Author wxl
* @Date 2023-08-26
**/
@SneakyThrows
@Override
public Exception decode(String methodKey, Response response) {
//获取数据
String body = Util.toString(response.body().asReader(Charset.defaultCharset()));
Result<?> result = JSON.parseObject(body, Result.class);
if (!result.isSuccess()) {
return new BusinessException(result.getStatus(), result.getMessage());
}
return new BusinessException(500, "Feign client 调用异常");
}
}
- The exception information thrown by calling the provider layer again
can be captured by the consumer layer and processed into a custom exception through the custom exception parser, and is no longer wrapped by the default feign exception; the custom exception thrown is uniformly returned for encapsulation processing.