Introduce the correct opening method of Feign in the project
Read the last issue Feign remote call My little partner may ask: ah Jian, didn't you say that the last issue talked about the 99% common ways of Feign? Why is there a correct opening method today?
Ajin: it's 99% of the common way. Ajin definitely didn't deceive everyone. It's just that 1% of this issue is the finishing touch of the dragon. Hey hey
Let's start with a set of cases
-
Commodity service interface
@RestController @RequestMapping("/goods") public class GoodsController { @GetMapping("/get-goods") public Goods getGoods() throws InterruptedException { TimeUnit.SECONDS.sleep(10); System.out.println("xxxxxxx"); return new Goods().setName("Apple") .setPrice(1.1) .setNumber(2); } @PostMapping("save") public void save(@RequestBody Goods goods){ System.out.println(goods); } }
-
Commodity service feign interface
@FeignClient(name = "my-goods", path = "/goods", contextId = "goods") public interface GoodsApi { @GetMapping("/get-goods") Goods getGoods(); @PostMapping(value = "/save") void save(Goods goods); }
-
Order service interface
@RestController @RequestMapping("/order") public class OrderController { @Resource private GoodsApi goodsApi; @GetMapping("/get-goods") public Goods getGoods(){ return goodsApi.getGoods(); } @PostMapping("/save-goods") public String saveGoods(){ goodsApi.save(new Goods().setName("banana").setNumber(1).setPrice(1.1)); return "ok"; } }
Yes, this is the query and save interface of the previous issue. The order service calls the commodity service
Under normal circumstances, there is no problem in the operation of this case, but various problems will be encountered in the actual operation of the project. Let's come one by one.
overtime
Last time, we learned that when the service provider responds to a timeout (there is a problem with the network, or the service does not respond), the service caller can configure the timeout to cut off the request in time to avoid thread blocking. As follows:
feign: client: config: default: # The connection timeout unit is milliseconds. The default is 10 seconds connectTimeout: 1000 # The request timeout is in milliseconds. The default is 60 seconds readTimeout: 5000
Now, we sleep for 10s in the commodity service interface to simulate the timeout
Then, initiate the call, and you will find that the data returned by the original interface is in json format. Now Feign throws an exception due to timeout, and the page looks like this:
Actually returned a page!
This is definitely not possible. We need to return an expected error when a timeout occurs, such as the exception of service call failure
Feign's author also thought of this and provided us with a Fallback mechanism. The usage is as follows:
-
Open hystrix
feign: hystrix: enabled: true
-
Write goodsapi fallback
@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() { return new Goods(); } @Override public void save(Goods goods) { } }; } }
-
Add the property fallbackFactory in FeignClient
@FeignClient(name = "my-goods", path = "/goods", contextId = "goods", fallbackFactory = GoodsApiFallback.class) public interface GoodsApi { }
When the timeout is requested again, the response logic in the fallback will be enabled, and the logic we write returns a new Goods(), so an empty Goods object will be obtained in the timeout request logic, like this:
It seems that the problem of unfriendly information returned due to timeout has indeed been solved. However, when we return an empty object in fallback, logic confusion will be caused: is there no commodity in the commodity service or the service timeout? I don't know
Use the return object with exception information
In order to solve this logical confusion, we thought of using a return object with exception information. Its structure is as follows:
{ "code": 0, "message": "", "data": {} }
We define that a code of 0 indicates a correct return
Based on this, we can modify the above logic:
- When goods and services return normally, code:0
- When timeout occurs, code: -1
The adjusted codes are as follows:
-
Goods and services
@GetMapping("/get-goods") public BaseResult<Goods> getGoods() throws InterruptedException { System.out.println("xxxxxxx"); return BaseResult.success(new Goods().setName("Apple") .setPrice(1.1) .setNumber(2)); }
-
Commodity service feign interface
@GetMapping("/get-goods") BaseResult<Goods> getGoods();
-
Commodity service feign interface Fallback
return new GoodsApi() { @Override public BaseResult<Goods> getGoods() { BaseResult<Goods> result = new BaseResult<>(); result.setCode(-1); result.setMessage("Goods and services response timeout"); return result; } }
-
Order service
@GetMapping("/get-goods") public Goods getGoods(){ BaseResult<Goods> result = goodsApi.getGoods(); if(result.getCode() != 0){ throw new RuntimeException("Error calling commodity service:" + result.getMessage()); } return result.getData(); }
Now, we have solved the problem that the service response timeout returns unfriendly information, and also solved the problem of logical confusion. Is it done?
Unified exception verification and unpacking
The above solutions are really OK. That's all for the general project methods. It's just for use...
You will find a disgusting problem. Originally, we used it like this:
Goods goods = goodsApi.getGoods();
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();
And this code is as like as two peas, because many Feign interfaces, each Feign interface's checkout logic is exactly the same.
BaseResult<xxx> result = xxxApi.getXxx(); if(result.getCode() != 0){ throw new RuntimeException("call xxx Service error:" + result.getMessage()); } Xxx xxx = result.getData();
——————- split line -——————
I, ajin, as a code cleaner, will I allow this to happen? impossible!
What easy-to-use way and safe way can't have both. As an adult: I want both!
Now let's turn it into the original way of use and get friendly return information.
In the last issue, we mentioned that Feign has an encoding and decoding process, and decoding this action will involve parsing the information returned by the server into the content required by the client.
So the idea is: customize a decoder, decode the information returned by the server, judge the code value of BaseResult, return the data directly when the code is 0, and throw an exception when the code is not 0.
Upper Code:
-
Write custom decoder
@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 instanceof BaseResult) { BaseResult<?> result = (BaseResult<?>) object; if (result.isFailure()) { log.error("call Feign Interface exception, interface:{}, abnormal: {}", response.request().url(), result.getMessage()); throw new BusinessException(result.getCode(), result.getMessage()); } return result.getData(); } } } return super.decode(response, type); } }
The default decoder in Feign is ResponseEntityDecoder, so we only need to inherit it and make some modifications on the original basis.
-
Inject decoder into Spring
@Configuration public class DecodeConfiguration { @Bean public Decoder feignDecoder(ObjectFactory<HttpMessageConverters> messageConverters) { return new OptionalDecoder( new BaseResultDecode(new SpringDecoder(messageConverters))); } }
This code is directly copied from the source code. The source code is as follows:
new OptionalDecoder( new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)))
I just replaced the ResponseEntityDecoder with my BaseResultDecode
Now let's change the code back to the original way
-
Goods and services
@GetMapping("/get-goods") public BaseResult<Goods> getGoods() throws InterruptedException { System.out.println("xxxxxxx"); return BaseResult.success(new Goods().setName("Apple") .setPrice(1.1) .setNumber(2)); }
You still need to put the BaseResult back here
-
Commodity service feign interface
@GetMapping("/get-goods") Goods getGoods();
-
Commodity service feign interface Fallback
return new GoodsApi() { @Override public Goods getGoods() { throw new RuntimeException("Exception occurred while calling commodity service"); } }
-
Order service
@GetMapping("/get-goods") public Goods getGoods(){ return goodsApi.getGoods(); }
Print curl log
This chapter has nothing to do with the previous one, but it can copy a curl when imitating the front-end request, which is very convenient for debugging.
The 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); } }
Similarly, directly inherit the default Slf4jLogger
-
Custom log factory
public class CurlFeignLoggerFactory extends DefaultFeignLoggerFactory { public CurlFeignLoggerFactory(Logger logger) { super(logger); } @Override public Logger create(Class<?> type) { return new CurlLogger(type); } }
-
Inject into Spring
@Bean public FeignLoggerFactory curlFeignLoggerFactory(){ return new CurlFeignLoggerFactory(null); }
The effects are 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 '
Summary
In this chapter, I introduce the use of Feign in actual projects: using return objects with exception information
And the reason for this use: it is necessary to enable the service caller to get clear response information
The disadvantage of this use: it is always necessary to judge whether the information returned by the service is correct
Solution: customize a decoder
Finally, we provide a way to print curl logs.
Finally, ah Jian would like to say a few words to you. I don't know if you have any feelings after reading ah Jian's custom decoder and custom logger. You may have always felt how difficult and powerful it is to expand some frameworks. In fact, it's not so difficult, Many times, we only need to make some small extensions based on the logic in the framework. To sum up, we need to find it, inherit it and modify it.
Well, I'll see you next time~
For more information, welcome to the official account: programmer, Jian, ah Jian, welcome to you in the official account.
Personal blog space: https://zijiancode.cn/archives/feign2