Describes the correct way to open Feign in a project
If you read the last Feign remote call, you might be wondering: Ah Jian, didn’t you say that the last Feign remote call covered 99% of the common methods? Why is there a correct way to open it today?
Ah jian: it is 99% of the common way, ah Jian absolutely did not deceive everyone, just 1% of this issue as the finishing touch, heh heh
Let’s start with a set of examples
-
Commodity service Interface
@RestController @RequestMapping("/goods") public class GoodsController { @GetMapping("/get-goods") public Goods getGoods(a) throws InterruptedException { TimeUnit.SECONDS.sleep(10); System.out.println("xxxxxxx"); return new Goods().setName(The word "apple") .setPrice(1.1) .setNumber(2); } @PostMapping("save") public void save(@RequestBody Goods goods){ System.out.println(goods); }}Copy the code
-
Commodity service FEIGN interface
@FeignClient(name = "my-goods", path = "/goods", contextId = "goods") public interface GoodsApi { @GetMapping("/get-goods") Goods getGoods(a); @PostMapping(value = "/save") void save(Goods goods); } Copy the code
-
Order Service Interface
@RestController @RequestMapping("/order") public class OrderController { @Resource private GoodsApi goodsApi; @GetMapping("/get-goods") public Goods getGoods(a){ return goodsApi.getGoods(); } @PostMapping("/save-goods") public String saveGoods(a){ goodsApi.save(new Goods().setName("banana").setNumber(1).setPrice(1.1)); return "ok"; }}Copy the code
Yes, this is the query and save interface from the previous installment, where the order service invokes the goods service
Normally, this case runs without any problems, but the project runs with all sorts of problems. Let’s take it one by one.
timeout
Last time, we learned that when the service provider responds with a timeout (something goes wrong with the network, or the service does not respond), the service caller can configure the timeout to block the request in time to avoid thread blocking. As follows:
feign:
client:
config:
default:
The default connection timeout is 10 seconds
connectTimeout: 1000
The default timeout unit is 60 seconds
readTimeout: 5000
Copy the code
Now, we simulate a timeout by sleeping for 10s in the commodity service interface
Then, when you make the call, you will see that the data returned by the interface was in JSON format. Now, due to the timeout, the page looks like this:
A page was returned!
This is definitely not the case, we need to return an expected error when a timeout occurs, such as an exception that the service call failed
The writers at Feign also thought of this and gave us a Fallback mechanism that works like this:
-
Open the hystrix
feign: hystrix: enabled: true Copy the code
-
Write GoodsApiFallback
@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(a) { return new Goods(); } @Override public void save(Goods goods) {}}; }}Copy the code
-
Add the property fallbackFactory to FeignClient
@FeignClient(name = "my-goods", path = "/goods", contextId = "goods", fallbackFactory = GoodsApiFallback.class) public interface GoodsApi {}Copy the code
The response logic in fallback is enabled when the request times out again, and we wrote the logic to return a new Goods(), so the request logic will get an empty Goods object when the request times out, like this:
It seems that the problem of unfriendly messages being returned due to timeout is resolved. However, when we return an empty object in fallback, there is a logical confusion: is the item not in the service or the service timed out? I don’t know…
Use return objects with exception information
To solve this logical confusion, we came up with the idea of using a return object with exception information, which has the following structure:
{
"code": 0."message": ""."data": {}}Copy the code
We define code zero to return correctly
Based on this, we can modify the above logic:
- Product service returns normally with code:0
- When timeout occurs, code: -1
The adjusted code is as follows:
-
Goods and services
@GetMapping("/get-goods") public BaseResult<Goods> getGoods(a) throws InterruptedException { System.out.println("xxxxxxx"); return BaseResult.success(new Goods().setName(The word "apple") .setPrice(1.1) .setNumber(2)); } Copy the code
-
Commodity service FEIGN interface
@GetMapping("/get-goods") BaseResult<Goods> getGoods(a); Copy the code
-
Feign Interface Fallback
return new GoodsApi() { @Override public BaseResult<Goods> getGoods(a) { BaseResult<Goods> result = new BaseResult<>(); result.setCode(-1); result.setMessage("Goods and services response time out"); returnresult; }}Copy the code
-
Order service
@GetMapping("/get-goods") public Goods getGoods(a){ BaseResult<Goods> result = goodsApi.getGoods(); if(result.getCode() ! =0) {throw new RuntimeException(Error calling commodity service: + result.getMessage()); } return result.getData(); } Copy the code
Now, we have solved the problem that the service response time is not friendly, but also solved the problem of logic confusion, and we are done?
Unified exception verification and unpacking
The above solution is really ok, the general project technique is here, just use it…
You’ll notice a really nasty problem, and the way we used it was like this:
Goods goods = goodsApi.getGoods();
Copy the code
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();
Copy the code
And this code is everywhere, because many Feign interfaces have exactly the same validation logic:
BaseResult<xxx> result = xxxApi.getXxx();
if(result.getCode() ! =0) {throw new RuntimeException(Error calling XXX service: + result.getMessage());
}
Xxx xxx = result.getData();
Copy the code
——————— split line ———————
Would I, Kam, as a code clean freak, allow this to happen? That’s impossible!
What works and what is safe can’t be both, as an adult: I want both!
Now let’s change it to be used in the original way and get a friendly return message.
In the last installment, we mentioned that Feign has a codec process, and decoding involves parsing the information returned by the server into what the client needs.
If the BaseResult code is 0, data will be returned directly. If the BaseResult code is 0, data will be returned directly.
The code:
-
Write custom decoders
@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 instanceofBaseResult) { BaseResult<? > result = (BaseResult<? >) object;if (result.isFailure()) { log.error("Error calling Feign interface, interface :{}, exception :{}", response.request().url(), result.getMessage()); throw new BusinessException(result.getCode(), result.getMessage()); } returnresult.getData(); }}}return super.decode(response, type); }}Copy the code
The default decoder in Feign is ResponseEntityDecoder, so we just need to inherit it and make some changes to it.
-
Inject the decoder into Spring
@Configuration public class DecodeConfiguration { @Bean public Decoder feignDecoder(ObjectFactory<HttpMessageConverters> messageConverters) { return new OptionalDecoder( new BaseResultDecode(newSpringDecoder(messageConverters))); }}Copy the code
This code is a direct copy of the source code, the source code is like this:
new OptionalDecoder( new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)))
I just replaced ResponseEntityDecoder with my own BaseResultDecode
Now let’s switch the code back to the original way
-
Goods and services
@GetMapping("/get-goods") public BaseResult<Goods> getGoods(a) throws InterruptedException { System.out.println("xxxxxxx"); return BaseResult.success(new Goods().setName(The word "apple") .setPrice(1.1) .setNumber(2)); } Copy the code
I still need to put back the BaseResult
-
Commodity service FEIGN interface
@GetMapping("/get-goods") Goods getGoods(a); Copy the code
-
Feign Interface Fallback
return new GoodsApi() { @Override public Goods getGoods(a) { throw new RuntimeException("An exception occurred when calling the commodity service"); }}Copy the code
-
Order service
@GetMapping("/get-goods") public Goods getGoods(a){ return goodsApi.getGoods(); } Copy the code
Printing curl Logs
Curl curl curl curl curl curl curl curl curl curl curl curl curl curl
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); }}Copy the code
Again, the default Slf4jLogger is inherited directly
-
Custom log factories
public class CurlFeignLoggerFactory extends DefaultFeignLoggerFactory { public CurlFeignLoggerFactory(Logger logger) { super(logger); } @Override public Logger create(Class type) { return newCurlLogger(type); }}Copy the code
-
Into the Spring
@Bean public FeignLoggerFactory curlFeignLoggerFactory(a){ return new CurlFeignLoggerFactory(null); } Copy the code
The effect is 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 '
Copy the code
summary
In this section, I show you how Feign can be used in a real project: using return objects with exception information
And why: The service caller needs to be able to get clear response information
The downside of this use is that it is always necessary to determine whether the information returned by the service is correct
Solution: define a decoder
Do you want to curl curl?
Finally, AH Jian would like to say a few words to you. I don’t know if you have any feelings after seeing ah Jian’s custom decoder and custom logger. Before, you may always think that it is difficult and powerful to expand some frameworks, but in fact it is not that difficult. A lot of times we just need to make a little extension based on the logic in the framework, which is basically discover it, inherit it, modify it.
See you next time
Want to know more exciting content, welcome to pay attention to the public number: programmer AH Jian, AH Jian in the public number welcome your arrival ~
Personal blog space: zijiancode. Cn/archives/fe…