-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
365 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
### Resources used in this project: | ||
- [Guide to Spring Retry](https://www.baeldung.com/spring-retry) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<parent> | ||
<groupId>org.savea</groupId> | ||
<artifactId>spring-boot-utils</artifactId> | ||
<version>1.0-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>springretry</artifactId> | ||
|
||
<properties> | ||
<maven.compiler.source>17</maven.compiler.source> | ||
<maven.compiler.target>17</maven.compiler.target> | ||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.springframework.retry</groupId> | ||
<artifactId>spring-retry</artifactId> | ||
<version>2.0.3</version> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.springframework</groupId> | ||
<artifactId>spring-aspects</artifactId> | ||
<version>6.0.11</version> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-web</artifactId> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.springframework</groupId> | ||
<artifactId>spring-test</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-test</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>junit</groupId> | ||
<artifactId>junit</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.mockito</groupId> | ||
<artifactId>mockito-core</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
</project> |
12 changes: 12 additions & 0 deletions
12
springretry/src/main/java/org/savea/springretry/SpringRetryApplication.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package org.savea.springretry; | ||
|
||
import org.springframework.boot.SpringApplication; | ||
import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
|
||
@SpringBootApplication | ||
public class SpringRetryApplication { | ||
|
||
public static void main(String[] args) { | ||
SpringApplication.run(SpringRetryApplication.class, args); | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
springretry/src/main/java/org/savea/springretry/configs/AppConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package org.savea.springretry.configs; | ||
|
||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.ComponentScan; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.context.annotation.PropertySource; | ||
import org.springframework.retry.annotation.EnableRetry; | ||
import org.springframework.retry.backoff.FixedBackOffPolicy; | ||
import org.springframework.retry.policy.SimpleRetryPolicy; | ||
import org.springframework.retry.support.RetryTemplate; | ||
|
||
@Configuration | ||
@EnableRetry | ||
@PropertySource("classpath:retryConfig.properties") | ||
@ComponentScan(basePackages = "org.savea.springretry") | ||
public class AppConfig { | ||
|
||
/** | ||
* The RetryPolicy determines when an operation should be retried. | ||
*<p/> | ||
* A SimpleRetryPolicy is used to retry a fixed number of times. | ||
* On the other hand, the BackOffPolicy is used to control backoff between retry attempts. | ||
*<p/> | ||
* Finally, a FixedBackOffPolicy pauses for a fixed period of time before continuing. | ||
* @return RetryTemplate | ||
*/ | ||
@Bean | ||
public RetryTemplate retryTemplate() { | ||
RetryTemplate retryTemplate = new RetryTemplate(); | ||
|
||
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy(); | ||
fixedBackOffPolicy.setBackOffPeriod(2000l); | ||
retryTemplate.setBackOffPolicy(fixedBackOffPolicy); | ||
|
||
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); | ||
retryPolicy.setMaxAttempts(2); | ||
retryTemplate.setRetryPolicy(retryPolicy); | ||
|
||
// Registering our listener (DefaultListenerSupport) to our RetryTemplate bean | ||
retryTemplate.registerListener(new DefaultListenerSupport()); | ||
return retryTemplate; | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
springretry/src/main/java/org/savea/springretry/configs/DefaultListenerSupport.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package org.savea.springretry.configs; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.retry.RetryCallback; | ||
import org.springframework.retry.RetryContext; | ||
import org.springframework.retry.listener.MethodInvocationRetryListenerSupport; | ||
|
||
/** | ||
* Listeners provide additional callbacks upon retries. | ||
* And we can use these for various cross-cutting concerns across different retries. | ||
* <p/> | ||
* The callbacks are provided in a RetryListener interface. | ||
* <p/> | ||
* The open and close callbacks come before and after the entire retry, | ||
* while onError applies to the individual RetryCallback calls. | ||
*/ | ||
@Slf4j | ||
public class DefaultListenerSupport extends MethodInvocationRetryListenerSupport { | ||
|
||
@Override | ||
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { | ||
log.info("onClose"); | ||
super.close(context, callback, throwable); | ||
} | ||
|
||
@Override | ||
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { | ||
log.info("onError"); | ||
super.onError(context, callback, throwable); | ||
} | ||
|
||
@Override | ||
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) { | ||
log.info("onOpen"); | ||
return super.open(context, callback); | ||
} | ||
|
||
} |
65 changes: 65 additions & 0 deletions
65
springretry/src/main/java/org/savea/springretry/services/MyService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package org.savea.springretry.services; | ||
|
||
import java.sql.SQLException; | ||
|
||
import org.springframework.retry.annotation.Backoff; | ||
import org.springframework.retry.annotation.Recover; | ||
import org.springframework.retry.annotation.Retryable; | ||
|
||
|
||
public interface MyService { | ||
|
||
/** | ||
* @Retryable Without Recovery | ||
* <p/> | ||
* Since we have not specified any exceptions here, retry will be attempted for all the exceptions. | ||
* Also, once the max attempts are reached and there is still an exception, ExhaustedRetryException will be thrown. | ||
*<p/> | ||
* Per @Retryable's default behavior, the retry may happen up to three times, with a delay of one second between retries. | ||
*/ | ||
@Retryable | ||
void retryService(); | ||
|
||
/** | ||
* @Retryable and @Recover | ||
* <p/> | ||
* Here, the retry is attempted when an SQLException is thrown. | ||
* The @Recover annotation defines a separate recovery method when a @Retryable method fails with a specified exception. | ||
* <p/> | ||
* Consequently, if the retryServiceWithRecovery method keeps throwing an SqlException after three attempts, | ||
* the recover() method will be called. | ||
* <p/> | ||
* The recovery handler should have the first parameter of type Throwable (optional) and the same return type. | ||
* The following arguments are populated from the argument list of the failed method in the same order. | ||
* @param sql, the SQL query | ||
* @throws SQLException if the SQL query is empty | ||
*/ | ||
@Retryable(retryFor = SQLException.class) | ||
void retryServiceWithRecovery(String sql) throws SQLException; | ||
|
||
/** | ||
* Customizing @Retryable’s Behavior | ||
* <p/> | ||
* There will be up to two attempts and a delay of 100 milliseconds. | ||
* @param sql, the SQL query | ||
* @throws SQLException if the SQL query is empty | ||
*/ | ||
@Retryable(retryFor = SQLException.class , maxAttempts = 2, backoff = @Backoff(delay = 100)) | ||
void retryServiceWithCustomization(String sql) throws SQLException; | ||
|
||
/** | ||
* We can also use properties in the @Retryable annotation. | ||
* <p/> | ||
* Please note that we are now using maxAttemptsExpression and delayExpression instead of maxAttempts and delay. | ||
* @param sql, the SQL query | ||
* @throws SQLException if the SQL query is empty | ||
*/ | ||
@Retryable(retryFor = SQLException.class, maxAttemptsExpression = "${retry.maxAttempts}", | ||
backoff = @Backoff(delayExpression = "${retry.maxDelay}")) | ||
void retryServiceWithExternalConfiguration(String sql) throws SQLException; | ||
|
||
@Recover | ||
void recover(SQLException e, String sql); | ||
|
||
void templateRetryService(); | ||
} |
54 changes: 54 additions & 0 deletions
54
springretry/src/main/java/org/savea/springretry/services/impl/MyServiceImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package org.savea.springretry.services.impl; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.savea.springretry.services.MyService; | ||
import org.springframework.stereotype.Service; | ||
|
||
import java.sql.SQLException; | ||
|
||
@Service | ||
@Slf4j | ||
public class MyServiceImpl implements MyService { | ||
|
||
@Override | ||
public void retryService() { | ||
log.info("throw RuntimeException in method retryService()"); | ||
throw new RuntimeException(); | ||
} | ||
|
||
@Override | ||
public void retryServiceWithRecovery(String sql) throws SQLException { | ||
if (StringUtils.isEmpty(sql)) { | ||
log.info("throw SQLException in method retryServiceWithRecovery()"); | ||
throw new SQLException(); | ||
} | ||
} | ||
|
||
@Override | ||
public void retryServiceWithCustomization(String sql) throws SQLException { | ||
if (StringUtils.isEmpty(sql)) { | ||
log.info("throw SQLException in method retryServiceWithCustomization()"); | ||
throw new SQLException(); | ||
} | ||
} | ||
|
||
@Override | ||
public void retryServiceWithExternalConfiguration(String sql) throws SQLException { | ||
if (StringUtils.isEmpty(sql)) { | ||
log.info("throw SQLException in method retryServiceWithExternalConfiguration()"); | ||
throw new SQLException(); | ||
} | ||
} | ||
|
||
@Override | ||
public void recover(SQLException e, String sql) { | ||
log.info("In recover method"); | ||
} | ||
|
||
@Override | ||
public void templateRetryService() { | ||
log.info("throw RuntimeException in method templateRetryService()"); | ||
throw new RuntimeException(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
retry.maxAttempts=2 | ||
retry.maxDelay=100 |
82 changes: 82 additions & 0 deletions
82
springretry/src/test/java/com/savea/SpringRetryIntegrationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package com.savea; | ||
|
||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.Mockito.times; | ||
import static org.mockito.Mockito.verify; | ||
|
||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
import org.savea.springretry.configs.AppConfig; | ||
import org.savea.springretry.services.MyService; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.boot.test.mock.mockito.SpyBean; | ||
import org.springframework.retry.support.RetryTemplate; | ||
import org.springframework.test.context.ContextConfiguration; | ||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | ||
import org.springframework.test.context.support.AnnotationConfigContextLoader; | ||
|
||
import java.sql.SQLException; | ||
|
||
@RunWith(SpringJUnit4ClassRunner.class) | ||
@ContextConfiguration(classes = AppConfig.class, loader = AnnotationConfigContextLoader.class) | ||
public class SpringRetryIntegrationTest { | ||
|
||
@SpyBean | ||
private MyService myService; | ||
@Value("${retry.maxAttempts}") | ||
private String maxAttempts; | ||
|
||
@Autowired | ||
private RetryTemplate retryTemplate; | ||
|
||
/** | ||
* Expect to see the method retried three times in intervals of 1 second. | ||
* You should see three error logs in the console relating to the {@link RuntimeException}. | ||
*/ | ||
@Test(expected = RuntimeException.class) | ||
public void givenRetryService_whenCallWithException_thenRetry() { | ||
myService.retryService(); | ||
} | ||
|
||
@Test | ||
public void givenRetryServiceWithRecovery_whenCallWithException_thenRetryRecover() throws SQLException { | ||
myService.retryServiceWithRecovery(null); | ||
verify(myService, times(3)).retryServiceWithRecovery(any()); | ||
} | ||
|
||
@Test | ||
public void givenRetryServiceWithCustomization_whenCallWithException_thenRetryRecover() throws SQLException { | ||
myService.retryServiceWithCustomization(null); | ||
|
||
/* | ||
* This test method is used to verify the behavior of the `retryServiceWithCustomization` method in the `MyService` class. | ||
* | ||
* The `verify` method from Mockito is used to check that the `retryServiceWithCustomization` method of the `myService` object | ||
* was called a certain number of times, as specified by the `maxAttempts` value. | ||
* | ||
* The `times` method from Mockito is used with `Integer.parseInt(maxAttempts)`, which converts the `maxAttempts` string to an integer. | ||
* | ||
* The `any()` method from Mockito is used as an argument to the `retryServiceWithCustomization` method. It is a flexible argument matcher | ||
* that accepts any argument of the given type. | ||
*/ | ||
verify(myService, times(Integer.parseInt(maxAttempts))).retryServiceWithCustomization(any()); | ||
} | ||
|
||
@Test | ||
public void givenRetryServiceWithExternalConfiguration_whenCallWithException_thenRetryRecover() throws SQLException { | ||
myService.retryServiceWithExternalConfiguration(null); | ||
verify(myService, times(Integer.parseInt(maxAttempts))).retryServiceWithExternalConfiguration(any()); | ||
} | ||
|
||
/** | ||
* Testing the use of the retry template. | ||
*/ | ||
@Test(expected = RuntimeException.class) | ||
public void givenTemplateRetryService_whenCallWithException_thenRetry() { | ||
retryTemplate.execute(arg0 -> { | ||
myService.templateRetryService(); | ||
return null; | ||
}); | ||
} | ||
} |