Hello, I am crooked.
A few days ago, when I reviewed the code, I found that there was a piece of logic in the project that was very poorly written, and it looked extremely ugly at first glance.
I don't know why such code exists in the project, so I glanced at the submission record and asked the corresponding colleagues to ask why such code was written.
Then...
That chunk of code was submitted by me in 2019.
I thought about it carefully. At that time, it seemed that because I was not familiar with the project, and there was a similar function in other projects, I directly came up with CV Dafa, and I didn't take a close look at the logic inside.
Well, it turned out to be historical reasons, understandable, understandable.
The code is mainly a lot of retry logic, all kinds of hard coding, and all kinds of hot-eyed patches.
In particular, the logic for retrying is everywhere. So I decided to optimize a wave with a retry component.
Today, I will take you to roll up the Spring-retry component.
ugly code
Let's briefly talk about what ugly code looks like.
To give you a scenario, assuming that you are responsible for payment services, you need to connect to an external channel and call their order query interface.
They told you: Due to network problems, if the interaction between us times out and you do not receive any response from me, then according to the agreement, you can retry this interface three times. After three times, there is still no response, then there should be If there is a problem, you can handle it according to the exception process.
Assuming you don't know the Spring-retry component, you will probably write code like this:
The logic is very simple, it is to create a for loop, and then initiate a retry when an exception occurs, and check the number of retries.
Then make an interface to call it:
After initiating the call, the output of the log is like this, which is very clear at a glance:
Call it once normally, retry three times, and call it 4 times in total. Throws an exception on the fifth call.
It fully meets the requirements, and the self-test is completed. You can directly submit the code and give it to the test students.
It's perfect, but have you ever thought that such code is actually very inelegant.
You think, if there are several similar "retry after timeout" requirements.
Then your for loop is not moving around. Something like this, ugly as hell:
To be honest, I've written ugly code like this before.
But I am a person with code cleanliness now, and such code is definitely intolerable.
Retry should be a general method like a tool class, which can be extracted and separated from the business code. When developing, we only need to pay attention to the Baba suitable for the business code.
So how to get out?
You said it's a coincidence, I'm sharing this with you today, and it's very good to extract the retry function:
https://github.com/spring-projects/spring-retry
After using spring-retry, our code above becomes like this:
Just adding a @Retryable annotation, it's ridiculously simple.
At first glance, it is very elegant!
So, I decided to take everyone to take a look at this annotation. See how others have abstracted the "retry" function into a component, which is more interesting than writing business code.
I will not teach you how to use spring-retry in this article. Its functions are very rich, and there are already many articles about how to use it. What I want to write is how I know it through the source code after I will use it.
How to change it from a thing that can only be used to the sentence on the resume: I have read the relevant source code.
But if you don't know how to use it at all, what if you haven't heard of this component?
It doesn't matter, the first step for me to understand a technical point must be to build a very simple Demo first.
Those who haven't run the Demo will be treated as ignorant.
Take the Demo first
I didn't know anything about this annotation at first.
So, in this case, let’s not talk nonsense, and it’s the kingly way to build a demo and run it first.
But you remember that building a demo is also a skill: just go to the official website or github to find it, there are the most authoritative and concise demos.
For example, the Quick Start on spring-retry's github is very concise and easy to understand.
It provides examples of annotated development and programmatic development, respectively.
We mainly look at its annotation development case here:
There are three annotations involved:
- @EnableRetry: added to the startup class to support the retry function.
- @Retryable: When added to a method, it will empower the method and make it useful for retrying.
- @Recover: If the retry is still unsuccessful, the method modified by this annotation will be executed.
After reading the Quick Start on git, I quickly built a demo.
If you don't know how to use this component before, I strongly recommend that you build one too, it's very simple.
The first is to introduce maven dependencies:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.1</version>
</dependency>
Since this component depends on what AOP gives you, you also need to introduce this dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.6.1</version>
</dependency>
Then there is the code, and that's enough:
Finally, run the project and call a stroke, it is indeed effective, and the method modified by @Recover is executed:
But there is only one line in the log, and there is no retry operation. It is a bit too simple, right?
I used to think it didn't matter, I couldn't wait to rush into the source code and go through it, looking left and right.
How do I go to the source code to do it?
Just look directly at the place where the annotation is called, like this:
There are not many places to call, and it is indeed easy to locate the following key class:
org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor
Then put a breakpoint at the corresponding position, start running the program, and debug:
But I will not be so anxious now. As an old programmer, I have matured a lot now. I will not rush to roll the source code first, but will dig a little more from the log first.
My first reaction to this problem now is to adjust the log level to debug:
logging.level.root=debug
After changing the log level and restarting and calling again, you can see a lot of valuable logs:
Based on the logs, this place can be found directly:
org.springframework.retry.support.RetryTemplate#doExecute
Putting a breakpoint here for debugging is the most appropriate place.
This is also a debugging trick. past, I often ignored the output in the log, and felt that it was hard to see. In fact, after carefully analyzing the log, you will find that there are many valuable things in it, which is much more effective than diving into the source code.
If you don't believe it, you can try to look at the debug log related to the Spring transaction. I think it is a very good case, and the printed one is called a clear one.
From the log, you can promote the debug process under different isolation levels, and maintain a clear link, without the feeling of clutter.
Okay, let's not go too far.
Let's take a look at this log again, doesn't this output sound familiar to you?
Doesn't this look like a picture we just saw before?
I saw a smile on the corner of my mouth here: Sample, I blindly guess that you must have written a for loop in your source code. If an exception is thrown in the loop, then check whether the retry condition is met, and if so, continue to retry. If it is not satisfied, the logic of @Recover is executed.
If I guess wrong, I just eat the computer screen.
OK, flag is standing here first, then let's go to the source code.
Wait, stop for a while.
If we have found the location of the first breakpoint in Debug, then there is a very critical operation before we actually enter the source code debugging. That is what I have repeatedly emphasized before, we must go to the source code with more specific questions. .
And the flag I set up before is actually my problem: I first give a guess, and then find out whether it is implemented in this way, and how it is implemented in the code.
So to sort out my question again:
- 1. Find where its for loop is.
- 2. How does it decide that it should try again?
- 3. How does it execute the @Recover logic?
Now it's time to start driving.
Flip the source code
There are no secrets under the source code.
First, let's take a look at the Debug entry we found earlier:
org.springframework.retry.support.RetryTemplate#doExecute
It can be seen intuitively from the log that this method must contain the for loop I am looking for.
but...
Unfortunately, not a for loop, but a while loop. It's not a big problem, the meaning is almost the same:
Hit a breakpoint, and then run the project. When I reach the breakpoint, I am most concerned about the following call stack:
It is framed in two parts, one is the content in the spring-aop package, and the other is spring-retry.
Then we see the first method related to spring-retry:
Congratulations, if we found the location of the first breakpoint through the log, then through the call stack of the first breakpoint, we found the initial entry of the entire retry, and another breakpoint should be hit below At the entrance of this method:
org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor#invoke
To be honest, the simplest combination of observing logs and call stacks is used, and debugging most of the source code will not feel particularly chaotic.
After finding the entrance, we will look at the source code from the interface.
When this invoke method comes in, it first tries to get from the cache whether the method has been successfully parsed before, and if there is no cache, it parses whether the currently called method has the @Retryable annotation.
If it is modified by @Retryable, the returned delegate object will not be null. So it will go to the code logic of the retry package.
Then there is a small detail in invoke, if the recoverer object is not empty, execute with callback. If empty, no recoverCallback object method is executed.
I made a wild guess when I saw these lines of code: the @Recover annotation is not required.
So I excitedly annotated this method and ran the project again, and found that it was true, a little different:
After I didn't read other articles or the official introduction, I discovered one of his usages only through a simple example, this is an unexpected gain, and it is also a little fun to read the source code.
In fact, the source code is not so scary.
But when I saw this, another problem followed:
This recoverer object seems to be the channelNotResp method I wrote, but when is it resolved?
Press the no table, we will talk about it later, the most important thing is to find a place to try again.
A few steps down the current method, and you'll soon get to the while loop I mentioned earlier:
Mainly focus on this canRetry method:
org.springframework.retry.RetryPolicy#canRetry
After clicking in, I found that it is an interface with multiple implementations:
Briefly explain what some of them mean:
- AlwaysRetryPolicy: Allows infinite retry until successful. Improper logic in this method will lead to an infinite loop
- NeverRetryPolicy: Only one call to RetryCallback is allowed, retries are not allowed
- SimpleRetryPolicy: a fixed number of retry policy, the default maximum number of retries is 3, the policy used by RetryTemplate by default
- TimeoutRetryPolicy: timeout retry policy, the default timeout is 1 second, and retry is allowed within the specified timeout period
- ExceptionClassifierRetryPolicy: Set the retry policy for different exceptions, similar to the combined retry policy, the difference is that only the retry of different exceptions is distinguished here
- CircuitBreakerRetryPolicy: A retry policy with circuit breaker function. Three parameters, openTimeout, resetTimeout and delegate, need to be set.
- CompositeRetryPolicy: Combined retry policy, there are two combinations, optimistic combined retry policy means that as long as one policy is allowed, you can retry, pessimistic combined retry policy means that as long as one policy is not allowed, you cannot retry, but Regardless of the combination, each strategy in the combination will execute
So here comes the problem again. When we debug the source code, there are so many implementations. How do I know which method to enter?
Remember, interface methods can also have breakpoints. You don't know which implementation will be used, but idea knows:
Here is the SimpleRetryPolicy strategy used, that is, this strategy is the default retry strategy of Spring-retry.
t == null || retryForException(t)) && context.getRetryCount() < this.maxAttempts
The logic of this strategy is also very simple:
- 1. If there is an exception, execute the retryForException method to determine whether the exception can be retried.
- 2. Determine whether the current number of retries exceeds the maximum number of times.
Here we find the place to control the retry logic.
The second point above is easy to understand. The first point shows that this annotation, like the transaction annotation @Transaction, can handle specified exceptions. You can take a look at the options it supports:
Note that I marked a sentence in include, which means that this value is empty by default. And when exclude is also empty, the default is all exceptions.
So although there is nothing in the demo, throwing a TimeoutException will also trigger the retry logic.
It's another knowledge point discovered by flipping through the source code. This thing is like exploring an easter egg, and it's comfortable.
After reading the logic of judging whether the retry call can be made, let's take a look at the place where the business method is actually executed:
org.springframework.retry.RetryCallback#doWithRetry
At a glance, you can see that there is a dynamic proxy mechanism that you should be very familiar with. The invocation here is our callChannel method:
We know from the code that the exception thrown by the callChannel method will be caught in the doWithRetry method and then thrown out directly:
This is actually very easy to understand, because an exception needs to be thrown to trigger the next retry.
But this also exposes a drawback of Spring-retry, that is, related services must be triggered by throwing exceptions.
It sounds like there is nothing wrong with it, but if you think about it, suppose the channel side said that if I return an ErrorCode of 500 to you, then you can also try again.
There should be many such business scenarios.
What if you were to use Spring-retry?
Do you have to write code like this:
if(errorCode==500){
throw new Exception("手动抛出异常");
}
It means that the retry logic is triggered by throwing an exception, which is not a particularly elegant design.
In fact, it is not difficult to extend this framework to determine whether to retry according to a property in the returned object.
You think, it could have been returned here. Just provide a configuration entry, let's tell it which object should retry when which field has a certain value.
Of course, the boss must have his own ideas, I am just some immature opinions here. In fact, another retry framework, Guava-Retry, supports retrying based on the return value.
If it is not the focus of this article, it will not be expanded.
Then move on to the part of the while loop that catches the exception.
The logic inside is not complicated, but you can pay attention to the part framed below:
Here it is judged again whether it can be retried again, what is it for?
is to execute this line of code:
backOffPolicy.backOff(backOffContext);
What does it do?
I don't know, debug takes a look, and finally comes to this place:
org.springframework.retry.backoff.ThreadWaitSleeper#sleep
The operation of sleeping for 1000ms is performed here.
I understand it right away, this thing leaves a handle for you here, you can set the handle for the retry interval. Then by default, the function of retrying after 1000ms is enabled for you.
Then I found this thing inside the @Retryable annotation:
I don't understand how this thing is configured at first glance, but the comments on it tell me to look at the Backoff thing.
It looks like this:
This thing seems to be easier to understand, let alone other parameters, at least I saw that the default value of value is 1000.
I suspected it was the specified retry interval controlled by this parameter, so I tried:
Sure enough, it's you kid, and let me dig another easter egg.
In @Backoff, in addition to the value parameter, there are many other parameters, and their meanings are as follows:
- delay: wait time between retries (in milliseconds)
- maxDelay: Maximum wait time between retries (in milliseconds)
- multiplier: specifies the multiple of the delay
- delayExpression: expression for waiting time between retries
- maxDelayExpression: maximum wait time expression between retries
- multiplierExpression: specifies a multiple expression for the delay
- random: randomly specify the delay time
I won't show you one by one, if you are interested in playing it yourself.
Because of the rich retry time configuration strategy, different implementations are also written according to different strategies:
Through Debug I know that the default implementation is FixedBackOffPolicy.
Other implementations will not be studied in detail. I mainly focus on the main link, first get through the whole process, and then look at these branches when I play by myself.
In the Demo scenario, after waiting for one second to initiate a retry, the while loop will go through again, and the main link of the retry will be sorted out in this way.
In fact, I folded the code, you can see that it is just a try-catch code block inside the while loop:
This is the same skeleton as the ugly code we wrote before, except that Spring-retry expands and hides this part of the code, and only provides you with an annotation.
When you only get this annotation, you will exclaim when you use it as a black box: This thing is really awesome.
But now when you flip through the source code, you will say: This is it? However, I think it can be written.
The first two of the questions raised here are clear:
Question 1: Find where is the for loop for it.
There is no for loop, but there is a while loop with a try-catch in it.
Question 2: How does it decide that it should try again?
The logic for judging to trigger the retry mechanism is very simple, that is, it is triggered by throwing an exception.
But whether or not to perform retry is a key point that needs to be carefully analyzed.
Spring-retry has a lot of retry policies, the default is SimpleRetryPolicy, and the number of retries is 3.
But it should be noted that this "3 times" is the total number of calls for three times. Instead of calling it three times after the first call fails, that's a total of 4 times. Regarding the question of how many times it is called, it is still necessary to score clearly.
And it is not necessarily that an exception will be retried, because Spring-retry supports processing or not processing specified exceptions.
Configurable, which is the basic capability a component should have.
Still one last question left: how does it get to the @Recover logic?
Then go to the source code.
Recover logic
The first thing to note is that the @Recover annotation is not a must have. We have also analyzed it before, so I won't repeat it.
But this function is really good to use, and most of the exceptions should have corresponding bottom-up measures.
This thing is to perform the bottom line action.
Its source code is also very easy to find, right after the retry logic:
Debug a few steps down and you will come to this place:
org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler#recover
Another reflection call, the method here is already the channelNotResp method.
So the question is: how does Spring-retry know that my retry method is channelNotResp?
Looking closely at the method object in the screenshot above, it is not difficult to see that it is generated by the first line of code in the method:
Method method = findClosestMatch(args, cause.getClass());
This method is called Find the closest method in terms of name and return value. But I don't know exactly what it means.
Follow up to see what it's doing:
There are two key pieces of information in this, one is called recoverMethodName, when the value is empty and not empty, two different branches are taken.
Another parameter is methods, which is a HashMap:
Inside this Map is our bottom-line method channelNotResp:
And this Map needs to be traversed no matter which branch it takes.
When was the channelNotResp in this Map put in?
It's very simple, just look at the place where the put method of this Map is called and you're done:
For these two put places, the source code is located in the following method:
org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler#init
As can be seen from the screenshot, here is a method to find whether the class is modified by the @Recover annotation.
I put a breakpoint on line 172, debug it and see the specific information, you will know what is going on here.
After you initiate the call, the program will stop at the breakpoint. As for how to get here, I said earlier, look at the call stack, and I won't repeat it.
Regarding this doWith method, we take a step up the call stack and know that we are parsing all the methods in our RetryService class:
When parsing to the channelNotResp method, it will recognize that the method is marked with the @Recover annotation.
However, from the source code point of view, for further analysis, the if condition must be satisfied. In addition to Recover, the if condition also needs to satisfy this thing:
method.getReturnType().isAssignableFrom(failingMethod.getReturnType())
The isAssignableFrom method is to determine whether it is the parent class of a class.
That is, the method and failingMethod are as follows:
This is checking whether the return value of the method annotated with @Retryable matches the method annotated with @Recover. Only if the return value matches, it means that this is a pair and should be parsed.
For example, I changed the source code to this:
When it parses to the channelNotRespStr method, it will find that although it is modified by the @Recover annotation, the return value is not consistent, so it is known that it is not the bottom-line method of the target method callChannel.
It's just the usual routines in the source code.
Add a callChannelSrt method, and in the above source code, Spring-retry can help you parse out who is a pair with whom:
Then take a look at if the conditions are met and matched, what is going on in the if?
This is the input parameter of the acquisition method, but if you look closely, it is only to get the first parameter, and this parameter must meet a condition:
Throwable.class.isAssignableFrom(parameterTypes[0])
Must be a subclass of Throwable, which means it must be an exception. Use the type field to undertake, and then it will be stored below.
The first time I watched it, I definitely didn't understand what it was doing. It didn't matter. I read it a few times and understood it. I'll share it with you. Here is for the method that appeared at the beginning of this section:
The type is obtained here, and if the type is null, the default is Throwable.class.
If there is a value, it is judged whether the type here is the same class or parent class of the cause thrown by the current program.
To emphasize again, from the name and return value of this method, we know that we are looking for the most similar method. I said earlier that I didn't quite understand what it meant, just to give you a lot of methods. What is this Map? come.
In fact, my heart is like a mirror, and I have long wanted to tear off its veil.
Come, follow my train of thought and you can see what kind of wine is sold in the gourd right away.
You think, findClosestMatch, this Closest is the highest level of Close, which means the closest.
Since there is the closest, there must be several things put together, and only one of them is the most suitable.
In the source code, this requirement is "cause", which is the currently thrown exception.
And "several things" refers to the type attribute in the things installed in the methods.
Still a little dizzy, right, don’t panic, as soon as the following picture comes out, you won’t be dizzy right away:
Take this code and use the "Closest" thing.
First, the cause is the TimeoutException thrown.
The methods map contains three methods modified by the @Recover annotation.
Why are there three?
Good question, it means that what I wrote in front of me is very bad, so you don't understand it well. It's okay, I'll show you the code for the part that puts things in methods:
These three methods all satisfy the condition of being annotated with @Recover, and also satisfy the condition that the return value is consistent with the return value of the target method callChannel. Then you have to put them in methods, so there are three.
It also explains why the bottom line method is loaded with a Map?
At first I thought it was a "do it all" strategy, because always treat the user like that, you don't know what magic code it will write.
For example, in my example above, in fact, this method must take effect in the end:
@Recover
public void channelNotResp(TimeoutException timeoutException) throws Exception {
log.info("3.没有获取到渠道的返回信息,发送预警!");
}
Because it's Closest.
I'll take a screenshot for you to show that I'm not talking nonsense:
However, when proofreading, I found that this place was wrong. It was not the user's fault, but it was really possible that a @Retryable modified method would appear, and there were different bottom-up methods for different exceptions.
For example the following:
When num=1, the timeout strategy is triggered, and the log is as follows:
http://localhost:8080/callChannel?num=1
When num>1, the null pointer strategy is triggered, and the log is as follows:
Wonderful, really amazing.
Seeing this, I think that the Spring-retry component is an entry point. With a basic grasp, I have a good grasp of the main process, and I can use "master" on my resume.
In the follow-up, you only need to touch the big branches and details, and you can change the "mastery" to "familiar".
a little flawed
Finally, add one more flaw.
Let's take a look at the method it handles @Recover. Here, it just handles the return value of the method:
When I saw the first glance here, I felt that something was wrong, and there was less judgment on one situation, that is: generics.
For example, I do this stuff:
It stands to reason that the bottom line strategy I hope is the channelNotRespInt method.
But after execution, you will find that there is a certain chance to select the channelNotRespStr method:
This thing is wrong, I obviously want the channelNotRespInt method to get the bottom of it, why didn't I choose the right one?
Because the generic information is gone, old iron:
Suppose we want to support generics?
From the description on github, the author has begun to focus on the research of this method:
Generics will be supported since version 1.3.2.
But currently the highest version in the maven repository is still 1.3.1:
Want to see the code?
Just pull down the source code and take a look.
Look directly at the submission record of this class:
org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler
It can be seen that the judgment conditions have changed, and the processing of generics has been increased.
I'm just pointing the way here. If you are interested in researching, pull down the source code and take a look. I will not write about how to achieve it. It is too long and no one will read it. Let’s leave a hole here.
Mainly because when I wrote this, my girlfriend urged me to play table tennis. She belongs to the type of people who are very addicted to vegetables. She was given to the church yesterday, but today she actually threatened to beat me 11-0 to see if I didn't take a good slap on her and kill her without leaving her.
This article has been included in my personal blog, which is full of high-quality originals. Welcome everyone to take a look:
https://www.whywhy.vip/
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。