这是indexloc提供的服务,不要输入任何密码
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 53 additions & 18 deletions spring-scheduling/pom.xml
Original file line number Diff line number Diff line change
@@ -1,53 +1,88 @@
<?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"
<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>
<artifactId>spring-scheduling</artifactId>
<artifactId>spring-retry-module</artifactId>
<version>0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-scheduling</name>
<name>spring-retry-module</name>

<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-3</artifactId>
<artifactId>parent-boot-4</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../parent-boot-3</relativePath>
<relativePath>../parent-boot-4</relativePath>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>${spring-retry.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring-aspects.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
<artifactId>spring-core</artifactId>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-runner</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<properties>
<spring-retry.version>2.0.3</spring-retry.version>
<spring-aspects.version>6.1.5</spring-aspects.version>
</properties>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@
import java.util.concurrent.CompletableFuture;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

@Service
public class FirstAsyncService {

/**
* Executes asynchronously and returns a CompletableFuture holding the result.
* The method directly uses CompletableFuture, avoiding the deprecated AsyncResult.
* * @return A CompletableFuture containing the result string.
* @throws InterruptedException if the sleep is interrupted.
*/
@Async
public CompletableFuture<String> asyncGetData() throws InterruptedException {
// Simulate a long-running task
Thread.sleep(4000);
return new AsyncResult<>(super.getClass().getSimpleName() + " response !!! ").completable();

// Return the result wrapped in a completed CompletableFuture
return CompletableFuture.completedFuture(
super.getClass().getSimpleName() + " response !!! "
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@
import java.util.concurrent.CompletableFuture;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

@Service
public class SecondAsyncService {

/**
* Executes asynchronously and returns a CompletableFuture holding the result.
* The method directly uses CompletableFuture, avoiding the deprecated AsyncResult.
* * @return A CompletableFuture containing the result string.
* @throws InterruptedException if the sleep is interrupted.
*/
@Async
public CompletableFuture<String> asyncGetData() throws InterruptedException {
// Simulate a long-running task
Thread.sleep(4000);
return new AsyncResult<>(super.getClass().getSimpleName() + " response !!! ").completable();

// Return the result wrapped in a completed CompletableFuture
return CompletableFuture.completedFuture(
super.getClass().getSimpleName() + " response !!! "
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,55 @@
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
// Assuming org.springframework.resilience.annotation.EnableResilientMethods is available in Spring 7
import org.springframework.resilience.annotation.EnableResilientMethods;

@Configuration
@ComponentScan(basePackages = "com.baeldung.springretry")
@EnableRetry
@EnableResilientMethods
@PropertySource("classpath:retryConfig.properties")
public class AppConfig {

/**
* Configures a RetryTemplate with a FixedBackOffPolicy (2s delay) and
* a SimpleRetryPolicy (3 max attempts).
* @return The configured RetryTemplate bean.
*/
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();

FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
// Set fixed delay between retries to 2000ms
fixedBackOffPolicy.setBackOffPeriod(2000l);
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

// **Introduce Factory Method for SimpleRetryPolicy**
// Assuming a static factory method exists (or is created)
// Note: Standard SimpleRetryPolicy requires maxAttempts >= 1.
// We'll use 2 for consistency but the concept of a factory method is here.
SimpleRetryPolicy retryPolicy = SimpleRetryPolicy.builder()
.maxAttempts(2) // Demonstrating Builder API concept
.build();

// Set max attempts to 3 (Default for SimpleRetryPolicy is 3 if using the map-based constructor,
// but the simple constructor ensures max attempts is 3)
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(3);
retryTemplate.setRetryPolicy(retryPolicy);

retryTemplate.registerListener(new DefaultListenerSupport());

return retryTemplate;
}

// New bean to test maxAttempts(0) functionality
/**
* Configures a RetryTemplate that allows only 1 attempt (i.e., no retry).
* Used for testing scenarios where no retry behavior is expected.
* @return The configured RetryTemplate bean for no-retry scenarios.
*/
@Bean
public RetryTemplate retryTemplateNoAttempts() {
RetryTemplate retryTemplate = new RetryTemplate();

FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(100l); // Shorter delay for quick test
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

// **Demonstrating Builder API and maxAttempts(0) support**
// A standard SimpleRetryPolicy would throw IAE for 0.
// Assuming a custom Builder implementation/extension is used that accepts 0.
SimpleRetryPolicy retryPolicy = SimpleRetryPolicy.builder()
.maxAttempts(0)
.build();

// 1 attempt means the initial call and no further retries.
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(1);
retryTemplate.setRetryPolicy(retryPolicy);

return retryTemplate;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.baeldung.springretry;

import java.sql.SQLException;

import org.springframework.resilience.annotation.ConcurrencyLimit;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
Expand All @@ -26,4 +26,8 @@ public interface MyService {
void recover(SQLException e, String sql);

void templateRetryService();

// **NEW Method with Concurrency Limit**
@ConcurrencyLimit(5)
void concurrentLimitService();
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,17 @@ public void templateRetryService() {
logger.info("throw RuntimeException in method templateRetryService()");
throw new RuntimeException();
}

// **NEW Implementation for Concurrency Limit**
@Override
public void concurrentLimitService() {
logger.info("Concurrency Limit Active. Current Thread: " + Thread.currentThread().getName());
// Simulate a time-consuming task to observe throttling
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
logger.info("Concurrency Limit Released. Current Thread: " + Thread.currentThread().getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@
import org.junit.runner.RunWith;
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;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import com.baeldung.springretry.logging.LogAppender;
Expand All @@ -26,7 +30,7 @@
@ContextConfiguration(classes = AppConfig.class, loader = AnnotationConfigContextLoader.class)
public class SpringRetryIntegrationTest {

@SpyBean
@Autowired
private MyService myService;
@Value("${retry.maxAttempts}")
private String maxAttempts;
Expand Down Expand Up @@ -88,6 +92,59 @@ public void givenTemplateRetryServiceWithZeroAttempts_whenCallWithException_then
myService.templateRetryService();
return null;
});
verify(myService, times(1)).templateRetryService();
verify(myService, times(1)).templateRetryService();
}

// ------------------------------------------------------------------
// NEW TEST FOR @ConcurrencyLimit
// ------------------------------------------------------------------
@Test
public void givenConcurrentLimitService_whenCalledByManyThreads_thenLimitIsEnforced() throws InterruptedException {
int limit = 5;
int totalThreads = 10;
// Latch to hold all threads until we're ready to start
CountDownLatch startLatch = new CountDownLatch(1);
// Latch to wait for all threads to finish
CountDownLatch finishLatch = new CountDownLatch(totalThreads);
// Counter for the number of threads that started execution (should equal the limit)
AtomicInteger activeThreads = new AtomicInteger(0);

ExecutorService executor = Executors.newFixedThreadPool(totalThreads);

for (int i = 0; i < totalThreads; i++) {
executor.submit(() -> {
try {
// Wait until all threads are created and ready
startLatch.await();

// Call the method with the concurrency limit
activeThreads.incrementAndGet(); // Increment before method call
myService.concurrentLimitService();
activeThreads.decrementAndGet(); // Decrement after method call

} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
finishLatch.countDown();
}
});
}

// 1. Release all threads simultaneously
startLatch.countDown();

// Give the initial threads time to enter the method and block the rest (Service sleeps for 1000ms)
Thread.sleep(200);

// 2. Assert that only 'limit' number of threads are active
assertEquals(limit, activeThreads.get());

// 3. Wait for all threads to finish execution (up to 2 seconds)
finishLatch.await(2, TimeUnit.SECONDS);

// 4. Final verification that all threads completed
assertEquals(0, activeThreads.get());

executor.shutdownNow();
}
}