diff --git a/resilience4j-annotations/src/main/java/io/github/resilience4j/bulkhead/annotation/Bulkhead.java b/resilience4j-annotations/src/main/java/io/github/resilience4j/bulkhead/annotation/Bulkhead.java index 205537a4fd..385321c527 100644 --- a/resilience4j-annotations/src/main/java/io/github/resilience4j/bulkhead/annotation/Bulkhead.java +++ b/resilience4j-annotations/src/main/java/io/github/resilience4j/bulkhead/annotation/Bulkhead.java @@ -12,4 +12,11 @@ * @return the name of the bulkhead */ String name(); + + /** + * recovery method name. + * + * @return recovery method name. + */ + String recovery() default ""; } diff --git a/resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/CircuitBreaker.java b/resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/CircuitBreaker.java index 629bf813d2..767bea750b 100644 --- a/resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/CircuitBreaker.java +++ b/resilience4j-annotations/src/main/java/io/github/resilience4j/circuitbreaker/annotation/CircuitBreaker.java @@ -39,4 +39,11 @@ * @return the name of the circuit breaker */ String name(); + + /** + * recovery method name. + * + * @return recovery method name. + */ + String recovery() default ""; } diff --git a/resilience4j-annotations/src/main/java/io/github/resilience4j/ratelimiter/annotation/RateLimiter.java b/resilience4j-annotations/src/main/java/io/github/resilience4j/ratelimiter/annotation/RateLimiter.java index 8dd0e3b3b5..2ef51386b9 100644 --- a/resilience4j-annotations/src/main/java/io/github/resilience4j/ratelimiter/annotation/RateLimiter.java +++ b/resilience4j-annotations/src/main/java/io/github/resilience4j/ratelimiter/annotation/RateLimiter.java @@ -38,4 +38,11 @@ * @return the name of the limiter */ String name(); + + /** + * recovery method name. + * + * @return recovery method name. + */ + String recovery() default ""; } diff --git a/resilience4j-annotations/src/main/java/io/github/resilience4j/retry/annotation/AsyncRetry.java b/resilience4j-annotations/src/main/java/io/github/resilience4j/retry/annotation/AsyncRetry.java new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resilience4j-annotations/src/main/java/io/github/resilience4j/retry/annotation/Retry.java b/resilience4j-annotations/src/main/java/io/github/resilience4j/retry/annotation/Retry.java index 32d55c32c3..50dbdb4b6a 100644 --- a/resilience4j-annotations/src/main/java/io/github/resilience4j/retry/annotation/Retry.java +++ b/resilience4j-annotations/src/main/java/io/github/resilience4j/retry/annotation/Retry.java @@ -38,4 +38,11 @@ * @return the name of the sync retry. */ String name(); + + /** + * recovery method name. + * + * @return recovery method name. + */ + String recovery() default ""; } diff --git a/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/bulkhead/autoconfigure/AbstractBulkheadConfigurationOnMissingBean.java b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/bulkhead/autoconfigure/AbstractBulkheadConfigurationOnMissingBean.java index 05f8f6749b..7a91f7afbc 100644 --- a/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/bulkhead/autoconfigure/AbstractBulkheadConfigurationOnMissingBean.java +++ b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/bulkhead/autoconfigure/AbstractBulkheadConfigurationOnMissingBean.java @@ -19,6 +19,8 @@ import io.github.resilience4j.bulkhead.configure.*; import io.github.resilience4j.bulkhead.event.BulkheadEvent; import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.recovery.RecoveryDecorators; +import io.github.resilience4j.recovery.autoconfigure.RecoveryConfigurationOnMissingBean; import io.github.resilience4j.utils.ReactorOnClasspathCondition; import io.github.resilience4j.utils.RxJava2OnClasspathCondition; import org.springframework.beans.factory.annotation.Autowired; @@ -26,6 +28,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import java.util.List; @@ -34,6 +37,7 @@ * Configuration} for resilience4j-bulkhead. */ @Configuration +@Import(RecoveryConfigurationOnMissingBean.class) public abstract class AbstractBulkheadConfigurationOnMissingBean { protected final BulkheadConfiguration bulkheadConfiguration; @@ -52,8 +56,9 @@ public BulkheadRegistry bulkheadRegistry(BulkheadConfigurationProperties bulkhea @Bean @ConditionalOnMissingBean public BulkheadAspect bulkheadAspect(BulkheadConfigurationProperties bulkheadConfigurationProperties, - BulkheadRegistry bulkheadRegistry, @Autowired(required = false) List bulkHeadAspectExtList) { - return bulkheadConfiguration.bulkheadAspect(bulkheadConfigurationProperties, bulkheadRegistry, bulkHeadAspectExtList); + BulkheadRegistry bulkheadRegistry, @Autowired(required = false) List bulkHeadAspectExtList, + RecoveryDecorators recoveryDecorators) { + return bulkheadConfiguration.bulkheadAspect(bulkheadConfigurationProperties, bulkheadRegistry, bulkHeadAspectExtList, recoveryDecorators); } @Bean diff --git a/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/AbstractCircuitBreakerConfigurationOnMissingBean.java b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/AbstractCircuitBreakerConfigurationOnMissingBean.java index f59a50968a..0d128da5d8 100644 --- a/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/AbstractCircuitBreakerConfigurationOnMissingBean.java +++ b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/AbstractCircuitBreakerConfigurationOnMissingBean.java @@ -20,6 +20,8 @@ import io.github.resilience4j.circuitbreaker.configure.*; import io.github.resilience4j.circuitbreaker.event.CircuitBreakerEvent; import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.recovery.RecoveryDecorators; +import io.github.resilience4j.recovery.autoconfigure.RecoveryConfigurationOnMissingBean; import io.github.resilience4j.utils.ReactorOnClasspathCondition; import io.github.resilience4j.utils.RxJava2OnClasspathCondition; import org.springframework.beans.factory.annotation.Autowired; @@ -27,10 +29,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import java.util.List; @Configuration +@Import(RecoveryConfigurationOnMissingBean.class) public abstract class AbstractCircuitBreakerConfigurationOnMissingBean { protected final CircuitBreakerConfiguration circuitBreakerConfiguration; @@ -64,8 +68,9 @@ public CircuitBreakerRegistry circuitBreakerRegistry(EventConsumerRegistry circuitBreakerAspectExtList) { - return circuitBreakerConfiguration.circuitBreakerAspect(circuitBreakerRegistry, circuitBreakerAspectExtList); + @Autowired(required = false) List circuitBreakerAspectExtList, + RecoveryDecorators recoveryDecorators) { + return circuitBreakerConfiguration.circuitBreakerAspect(circuitBreakerRegistry, circuitBreakerAspectExtList, recoveryDecorators); } @Bean diff --git a/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/AbstractRateLimiterConfigurationOnMissingBean.java b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/AbstractRateLimiterConfigurationOnMissingBean.java index 183a28aa41..c3be2918e7 100644 --- a/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/AbstractRateLimiterConfigurationOnMissingBean.java +++ b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/AbstractRateLimiterConfigurationOnMissingBean.java @@ -19,6 +19,8 @@ import io.github.resilience4j.ratelimiter.RateLimiterRegistry; import io.github.resilience4j.ratelimiter.configure.*; import io.github.resilience4j.ratelimiter.event.RateLimiterEvent; +import io.github.resilience4j.recovery.RecoveryDecorators; +import io.github.resilience4j.recovery.autoconfigure.RecoveryConfigurationOnMissingBean; import io.github.resilience4j.utils.ReactorOnClasspathCondition; import io.github.resilience4j.utils.RxJava2OnClasspathCondition; import org.springframework.beans.factory.annotation.Autowired; @@ -26,10 +28,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import java.util.List; @Configuration +@Import(RecoveryConfigurationOnMissingBean.class) public abstract class AbstractRateLimiterConfigurationOnMissingBean { protected final RateLimiterConfiguration rateLimiterConfiguration; @@ -46,8 +50,8 @@ public RateLimiterRegistry rateLimiterRegistry(RateLimiterConfigurationPropertie @Bean @ConditionalOnMissingBean - public RateLimiterAspect rateLimiterAspect(RateLimiterConfigurationProperties rateLimiterProperties, RateLimiterRegistry rateLimiterRegistry, @Autowired(required = false) List rateLimiterAspectExtList) { - return rateLimiterConfiguration.rateLimiterAspect(rateLimiterProperties, rateLimiterRegistry, rateLimiterAspectExtList); + public RateLimiterAspect rateLimiterAspect(RateLimiterConfigurationProperties rateLimiterProperties, RateLimiterRegistry rateLimiterRegistry, @Autowired(required = false) List rateLimiterAspectExtList, RecoveryDecorators recoveryDecorators) { + return rateLimiterConfiguration.rateLimiterAspect(rateLimiterProperties, rateLimiterRegistry, rateLimiterAspectExtList, recoveryDecorators); } @Bean diff --git a/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/recovery/autoconfigure/RecoveryConfigurationOnMissingBean.java b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/recovery/autoconfigure/RecoveryConfigurationOnMissingBean.java new file mode 100644 index 0000000000..80f0988536 --- /dev/null +++ b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/recovery/autoconfigure/RecoveryConfigurationOnMissingBean.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019 Kyuhyen Hwang + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.recovery.autoconfigure; + +import io.github.resilience4j.recovery.RecoveryDecorator; +import io.github.resilience4j.recovery.RecoveryDecorators; +import io.github.resilience4j.recovery.configure.RecoveryConfiguration; +import io.github.resilience4j.utils.ReactorOnClasspathCondition; +import io.github.resilience4j.utils.RxJava2OnClasspathCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * {@link Configuration} for {@link RecoveryDecorators}. + */ +@Configuration +public class RecoveryConfigurationOnMissingBean { + private final RecoveryConfiguration recoveryConfiguration; + + public RecoveryConfigurationOnMissingBean() { + this.recoveryConfiguration = new RecoveryConfiguration(); + } + + @Bean + @ConditionalOnMissingBean + public RecoveryDecorators recoveryDecorators(List recoveryDecorator) { + return recoveryConfiguration.recoveryDecorators(recoveryDecorator); + } + + @Bean + @Conditional(value = {RxJava2OnClasspathCondition.class}) + @ConditionalOnMissingBean + public RecoveryDecorator rxJava2RecoveryDecorator() { + return recoveryConfiguration.rxJava2RecoveryDecorator(); + } + + @Bean + @Conditional(value = {ReactorOnClasspathCondition.class}) + @ConditionalOnMissingBean + public RecoveryDecorator reactorRecoveryDecorator() { + return recoveryConfiguration.reactorRecoveryDecorator(); + } + + @Bean + @ConditionalOnMissingBean + public RecoveryDecorator completionStageRecoveryDecorator() { + return recoveryConfiguration.completionStageRecoveryDecorator(); + } +} diff --git a/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/retry/autoconfigure/AbstractRetryConfigurationOnMissingBean.java b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/retry/autoconfigure/AbstractRetryConfigurationOnMissingBean.java index eba4b597cb..f6f6e5aff7 100644 --- a/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/retry/autoconfigure/AbstractRetryConfigurationOnMissingBean.java +++ b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/retry/autoconfigure/AbstractRetryConfigurationOnMissingBean.java @@ -16,6 +16,8 @@ package io.github.resilience4j.retry.autoconfigure; import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.recovery.RecoveryDecorators; +import io.github.resilience4j.recovery.autoconfigure.RecoveryConfigurationOnMissingBean; import io.github.resilience4j.retry.RetryRegistry; import io.github.resilience4j.retry.configure.*; import io.github.resilience4j.retry.event.RetryEvent; @@ -26,6 +28,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import java.util.List; @@ -34,6 +37,7 @@ * Configuration} for resilience4j-retry. */ @Configuration +@Import(RecoveryConfigurationOnMissingBean.class) public abstract class AbstractRetryConfigurationOnMissingBean { protected final RetryConfiguration retryConfiguration; @@ -61,8 +65,9 @@ public RetryRegistry retryRegistry(RetryConfigurationProperties retryConfigurati @Bean @ConditionalOnMissingBean public RetryAspect retryAspect(RetryConfigurationProperties retryConfigurationProperties, - RetryRegistry retryRegistry, @Autowired(required = false) List retryAspectExtList) { - return retryConfiguration.retryAspect(retryConfigurationProperties, retryRegistry, retryAspectExtList); + RetryRegistry retryRegistry, @Autowired(required = false) List retryAspectExtList, + RecoveryDecorators recoveryDecorators) { + return retryConfiguration.retryAspect(retryConfigurationProperties, retryRegistry, retryAspectExtList, recoveryDecorators); } @Bean diff --git a/resilience4j-spring-boot-common/src/test/java/io/github/resilience4j/SpringBootCommonTest.java b/resilience4j-spring-boot-common/src/test/java/io/github/resilience4j/SpringBootCommonTest.java index 8070f13b30..a9738dafdb 100644 --- a/resilience4j-spring-boot-common/src/test/java/io/github/resilience4j/SpringBootCommonTest.java +++ b/resilience4j-spring-boot-common/src/test/java/io/github/resilience4j/SpringBootCommonTest.java @@ -23,6 +23,8 @@ import io.github.resilience4j.consumer.DefaultEventConsumerRegistry; import io.github.resilience4j.ratelimiter.RateLimiterRegistry; import io.github.resilience4j.ratelimiter.configure.RateLimiterConfigurationProperties; +import io.github.resilience4j.recovery.CompletionStageRecoveryDecorator; +import io.github.resilience4j.recovery.RecoveryDecorators; import io.github.resilience4j.retry.RetryRegistry; import io.github.resilience4j.retry.configure.RetryConfigurationProperties; import io.github.resilience4j.bulkhead.autoconfigure.AbstractBulkheadConfigurationOnMissingBean; @@ -31,6 +33,7 @@ import io.github.resilience4j.retry.autoconfigure.AbstractRetryConfigurationOnMissingBean; import org.junit.Test; +import java.util.Arrays; import java.util.Collections; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -46,7 +49,7 @@ public void testBulkHeadCommonConfig() { assertThat(bulkheadConfigurationOnMissingBean.bulkheadRegistry(new BulkheadConfigurationProperties(), new DefaultEventConsumerRegistry<>())).isNotNull(); assertThat(bulkheadConfigurationOnMissingBean.reactorBulkHeadAspectExt()).isNotNull(); assertThat(bulkheadConfigurationOnMissingBean.rxJava2BulkHeadAspectExt()).isNotNull(); - assertThat(bulkheadConfigurationOnMissingBean.bulkheadAspect(new BulkheadConfigurationProperties(), BulkheadRegistry.ofDefaults(), Collections.emptyList())); + assertThat(bulkheadConfigurationOnMissingBean.bulkheadAspect(new BulkheadConfigurationProperties(), BulkheadRegistry.ofDefaults(), Collections.emptyList(), new RecoveryDecorators(Arrays.asList(new CompletionStageRecoveryDecorator())))); } @Test @@ -55,7 +58,7 @@ public void testCircuitBreakerCommonConfig() { assertThat(circuitBreakerConfig.reactorCircuitBreakerAspect()).isNotNull(); assertThat(circuitBreakerConfig.rxJava2CircuitBreakerAspect()).isNotNull(); assertThat(circuitBreakerConfig.circuitBreakerRegistry(new DefaultEventConsumerRegistry<>())).isNotNull(); - assertThat(circuitBreakerConfig.circuitBreakerAspect(CircuitBreakerRegistry.ofDefaults(), Collections.emptyList())); + assertThat(circuitBreakerConfig.circuitBreakerAspect(CircuitBreakerRegistry.ofDefaults(), Collections.emptyList(), new RecoveryDecorators(Arrays.asList(new CompletionStageRecoveryDecorator())))); } @Test @@ -64,7 +67,7 @@ public void testRetryCommonConfig() { assertThat(retryConfigurationOnMissingBean.reactorRetryAspectExt()).isNotNull(); assertThat(retryConfigurationOnMissingBean.rxJava2RetryAspectExt()).isNotNull(); assertThat(retryConfigurationOnMissingBean.retryRegistry(new RetryConfigurationProperties(), new DefaultEventConsumerRegistry<>())).isNotNull(); - assertThat(retryConfigurationOnMissingBean.retryAspect(new RetryConfigurationProperties(), RetryRegistry.ofDefaults(), Collections.emptyList())); + assertThat(retryConfigurationOnMissingBean.retryAspect(new RetryConfigurationProperties(), RetryRegistry.ofDefaults(), Collections.emptyList(), new RecoveryDecorators(Arrays.asList(new CompletionStageRecoveryDecorator())))); } @Test @@ -73,7 +76,7 @@ public void testRateLimiterCommonConfig() { assertThat(rateLimiterConfigurationOnMissingBean.reactorRateLimiterAspectExt()).isNotNull(); assertThat(rateLimiterConfigurationOnMissingBean.rxJava2RateLimterAspectExt()).isNotNull(); assertThat(rateLimiterConfigurationOnMissingBean.rateLimiterRegistry(new RateLimiterConfigurationProperties(), new DefaultEventConsumerRegistry<>())).isNotNull(); - assertThat(rateLimiterConfigurationOnMissingBean.rateLimiterAspect(new RateLimiterConfigurationProperties(), RateLimiterRegistry.ofDefaults(), Collections.emptyList())); + assertThat(rateLimiterConfigurationOnMissingBean.rateLimiterAspect(new RateLimiterConfigurationProperties(), RateLimiterRegistry.ofDefaults(), Collections.emptyList(), new RecoveryDecorators(Arrays.asList(new CompletionStageRecoveryDecorator())))); } diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadConfigurationOnMissingBeanTest.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadConfigurationOnMissingBeanTest.java index fa36d9c8ed..f0e96a2287 100644 --- a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadConfigurationOnMissingBeanTest.java +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadConfigurationOnMissingBeanTest.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.util.List; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -104,8 +105,9 @@ public BulkheadRegistry bulkheadRegistry() { @Bean public BulkheadAspect bulkheadAspect(BulkheadRegistry bulkheadRegistry, - @Autowired(required = false) List bulkheadAspectExts) { - bulkheadAspect = new BulkheadAspect(new BulkheadProperties(), bulkheadRegistry, bulkheadAspectExts); + @Autowired(required = false) List bulkheadAspectExts, + RecoveryDecorators recoveryDecorators) { + bulkheadAspect = new BulkheadAspect(new BulkheadProperties(), bulkheadRegistry, bulkheadAspectExts, recoveryDecorators); return bulkheadAspect; } diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerConfigurationOnMissingBeanTest.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerConfigurationOnMissingBeanTest.java index c90aaededc..4bca8e150c 100644 --- a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerConfigurationOnMissingBeanTest.java +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerConfigurationOnMissingBeanTest.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.util.List; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -104,8 +105,9 @@ public CircuitBreakerRegistry circuitBreakerRegistry() { @Bean public CircuitBreakerAspect circuitBreakerAspect(CircuitBreakerRegistry circuitBreakerRegistry, - @Autowired(required = false) List circuitBreakerAspectExtList) { - circuitBreakerAspect = new CircuitBreakerAspect(new CircuitBreakerProperties(), circuitBreakerRegistry, circuitBreakerAspectExtList); + @Autowired(required = false) List circuitBreakerAspectExtList, + RecoveryDecorators recoveryDecorators) { + circuitBreakerAspect = new CircuitBreakerAspect(new CircuitBreakerProperties(), circuitBreakerRegistry, circuitBreakerAspectExtList, recoveryDecorators); return circuitBreakerAspect; } diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterConfigurationOnMissingBeanTest.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterConfigurationOnMissingBeanTest.java index 74fbc359a2..a28c64653b 100644 --- a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterConfigurationOnMissingBeanTest.java +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterConfigurationOnMissingBeanTest.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.util.List; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -105,8 +106,8 @@ public RateLimiterRegistry rateLimiterRegistry() { } @Bean - public RateLimiterAspect rateLimiterAspect(RateLimiterRegistry rateLimiterRegistry, @Autowired(required = false) List rateLimiterAspectExtList) { - rateLimiterAspect = new RateLimiterAspect(rateLimiterRegistry, new RateLimiterConfigurationProperties(), rateLimiterAspectExtList); + public RateLimiterAspect rateLimiterAspect(RateLimiterRegistry rateLimiterRegistry, @Autowired(required = false) List rateLimiterAspectExtList, RecoveryDecorators recoveryDecorators) { + rateLimiterAspect = new RateLimiterAspect(rateLimiterRegistry, new RateLimiterConfigurationProperties(), rateLimiterAspectExtList, recoveryDecorators); return rateLimiterAspect; } diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/retry/autoconfigure/RetryConfigurationOnMissingBeanTest.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/retry/autoconfigure/RetryConfigurationOnMissingBeanTest.java index f4e89688cd..19f1e2a1b8 100644 --- a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/retry/autoconfigure/RetryConfigurationOnMissingBeanTest.java +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/retry/autoconfigure/RetryConfigurationOnMissingBeanTest.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.util.List; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -104,8 +105,9 @@ public RetryRegistry retryRegistry() { @Bean public RetryAspect retryAspect(RetryRegistry retryRegistry, - @Autowired(required = false) List retryAspectExts) { - this.retryAspect = new RetryAspect(new RetryProperties(), retryRegistry, retryAspectExts); + @Autowired(required = false) List retryAspectExts, + RecoveryDecorators recoveryDecorators) { + this.retryAspect = new RetryAspect(new RetryProperties(), retryRegistry, retryAspectExts, recoveryDecorators); return retryAspect; } diff --git a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadAutoConfiguration.java b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadAutoConfiguration.java index b57011e0f4..8ad1f6b96d 100644 --- a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadAutoConfiguration.java +++ b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadAutoConfiguration.java @@ -15,6 +15,7 @@ */ package io.github.resilience4j.bulkhead.autoconfigure; +import io.github.resilience4j.recovery.autoconfigure.RecoveryConfigurationOnMissingBean; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -38,7 +39,7 @@ @Configuration @ConditionalOnClass(Bulkhead.class) @EnableConfigurationProperties(BulkheadProperties.class) -@Import(BulkheadConfigurationOnMissingBean.class) +@Import({BulkheadConfigurationOnMissingBean.class, RecoveryConfigurationOnMissingBean.class}) @AutoConfigureBefore(EndpointAutoConfiguration.class) public class BulkheadAutoConfiguration { @Bean diff --git a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAutoConfiguration.java b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAutoConfiguration.java index 6f835f6e9f..f3d398a931 100644 --- a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAutoConfiguration.java +++ b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAutoConfiguration.java @@ -15,6 +15,7 @@ */ package io.github.resilience4j.circuitbreaker.autoconfigure; +import io.github.resilience4j.recovery.autoconfigure.RecoveryConfigurationOnMissingBean; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -39,7 +40,7 @@ @Configuration @ConditionalOnClass(CircuitBreaker.class) @EnableConfigurationProperties(CircuitBreakerProperties.class) -@Import(CircuitBreakerConfigurationOnMissingBean.class) +@Import({CircuitBreakerConfigurationOnMissingBean.class, RecoveryConfigurationOnMissingBean.class}) @AutoConfigureBefore(EndpointAutoConfiguration.class) public class CircuitBreakerAutoConfiguration { diff --git a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterAutoConfiguration.java b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterAutoConfiguration.java index 0d5056acc7..9cc561f6d5 100644 --- a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterAutoConfiguration.java +++ b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterAutoConfiguration.java @@ -17,6 +17,7 @@ import javax.annotation.PostConstruct; +import io.github.resilience4j.recovery.autoconfigure.RecoveryConfigurationOnMissingBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; @@ -42,7 +43,7 @@ @Configuration @ConditionalOnClass(RateLimiter.class) @EnableConfigurationProperties(RateLimiterProperties.class) -@Import(RateLimiterConfigurationOnMissingBean.class) +@Import({RateLimiterConfigurationOnMissingBean.class, RecoveryConfigurationOnMissingBean.class}) @AutoConfigureBefore(EndpointAutoConfiguration.class) public class RateLimiterAutoConfiguration { private final RateLimiterProperties rateLimiterProperties; diff --git a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/retry/autoconfigure/RetryAutoConfiguration.java b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/retry/autoconfigure/RetryAutoConfiguration.java index d544e36bad..da19f1f280 100644 --- a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/retry/autoconfigure/RetryAutoConfiguration.java +++ b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/retry/autoconfigure/RetryAutoConfiguration.java @@ -15,6 +15,7 @@ */ package io.github.resilience4j.retry.autoconfigure; +import io.github.resilience4j.recovery.autoconfigure.RecoveryConfigurationOnMissingBean; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -37,7 +38,7 @@ @Configuration @ConditionalOnClass(Retry.class) @EnableConfigurationProperties(RetryProperties.class) -@Import(RetryConfigurationOnMissingBean.class) +@Import({RetryConfigurationOnMissingBean.class, RecoveryConfigurationOnMissingBean.class}) public class RetryAutoConfiguration { @Bean @ConditionalOnEnabledEndpoint diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadConfigurationOnMissingBeanTest.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadConfigurationOnMissingBeanTest.java index 41b7fdafca..8f8fc1e8ee 100644 --- a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadConfigurationOnMissingBeanTest.java +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/bulkhead/autoconfigure/BulkheadConfigurationOnMissingBeanTest.java @@ -19,6 +19,7 @@ import java.util.List; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -89,8 +90,9 @@ public BulkheadRegistry bulkheadRegistry() { @Bean public BulkheadAspect bulkheadAspect(BulkheadRegistry bulkheadRegistry, - @Autowired(required = false) List bulkheadAspectExts) { - bulkheadAspect = new BulkheadAspect(new BulkheadProperties(), bulkheadRegistry, bulkheadAspectExts); + @Autowired(required = false) List bulkheadAspectExts, + RecoveryDecorators recoveryDecorators) { + bulkheadAspect = new BulkheadAspect(new BulkheadProperties(), bulkheadRegistry, bulkheadAspectExts, recoveryDecorators); return bulkheadAspect; } diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerConfigurationOnMissingBeanTest.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerConfigurationOnMissingBeanTest.java index 913c96074d..7c7e0309c3 100644 --- a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerConfigurationOnMissingBeanTest.java +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerConfigurationOnMissingBeanTest.java @@ -4,6 +4,7 @@ import java.util.List; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -74,8 +75,9 @@ public CircuitBreakerRegistry circuitBreakerRegistry() { @Bean public CircuitBreakerAspect circuitBreakerAspect(CircuitBreakerRegistry circuitBreakerRegistry, - @Autowired(required = false) List circuitBreakerAspectExtList) { - circuitBreakerAspect = new CircuitBreakerAspect(new CircuitBreakerProperties(), circuitBreakerRegistry, circuitBreakerAspectExtList); + @Autowired(required = false) List circuitBreakerAspectExtList, + RecoveryDecorators recoveryDecorators) { + circuitBreakerAspect = new CircuitBreakerAspect(new CircuitBreakerProperties(), circuitBreakerRegistry, circuitBreakerAspectExtList, recoveryDecorators); return circuitBreakerAspect; } diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterConfigurationOnMissingBeanTest.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterConfigurationOnMissingBeanTest.java index f0c2667229..ddb2b3e833 100644 --- a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterConfigurationOnMissingBeanTest.java +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterConfigurationOnMissingBeanTest.java @@ -4,6 +4,7 @@ import java.util.List; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -75,8 +76,8 @@ public RateLimiterRegistry rateLimiterRegistry() { } @Bean - public RateLimiterAspect rateLimiterAspect(RateLimiterRegistry rateLimiterRegistry, @Autowired(required = false) List rateLimiterAspectExtList) { - rateLimiterAspect = new RateLimiterAspect(rateLimiterRegistry, new RateLimiterConfigurationProperties(), rateLimiterAspectExtList); + public RateLimiterAspect rateLimiterAspect(RateLimiterRegistry rateLimiterRegistry, @Autowired(required = false) List rateLimiterAspectExtList, RecoveryDecorators recoveryDecorators) { + rateLimiterAspect = new RateLimiterAspect(rateLimiterRegistry, new RateLimiterConfigurationProperties(), rateLimiterAspectExtList, recoveryDecorators); return rateLimiterAspect; } diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/retry/autoconfigure/RetryConfigurationOnMissingBeanTest.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/retry/autoconfigure/RetryConfigurationOnMissingBeanTest.java index 418752c7f7..0057eaa7af 100644 --- a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/retry/autoconfigure/RetryConfigurationOnMissingBeanTest.java +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/retry/autoconfigure/RetryConfigurationOnMissingBeanTest.java @@ -19,6 +19,7 @@ import java.util.List; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -89,8 +90,9 @@ public RetryRegistry retryRegistry() { @Bean public RetryAspect retryAspect(RetryRegistry retryRegistry, - @Autowired(required = false) List retryAspectExts) { - this.retryAspect = new RetryAspect(new RetryProperties(), retryRegistry, retryAspectExts); + @Autowired(required = false) List retryAspectExts, + RecoveryDecorators recoveryDecorators) { + this.retryAspect = new RetryAspect(new RetryProperties(), retryRegistry, retryAspectExts, recoveryDecorators); return retryAspect; } diff --git a/resilience4j-spring/build.gradle b/resilience4j-spring/build.gradle index a021a87fe5..bcc76267a4 100644 --- a/resilience4j-spring/build.gradle +++ b/resilience4j-spring/build.gradle @@ -26,5 +26,8 @@ dependencies { testCompile project(':resilience4j-rxjava2') testCompile(libraries.spring_context) testCompile(libraries.spring_test) + testCompile( libraries.spring_boot_web ) + testCompile( libraries.spring_boot_aop ) + testCompile( libraries.spring_boot_test ) } ext.moduleName = 'io.github.resilience4j.spring' \ No newline at end of file diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspect.java b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspect.java index 5b88b9be7c..24a6e404cb 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspect.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspect.java @@ -20,6 +20,8 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; +import io.github.resilience4j.recovery.RecoveryDecorators; +import io.github.resilience4j.recovery.RecoveryMethod; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -29,6 +31,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; +import org.springframework.util.StringUtils; import io.github.resilience4j.bulkhead.BulkheadRegistry; import io.github.resilience4j.bulkhead.annotation.Bulkhead; @@ -48,11 +51,13 @@ public class BulkheadAspect implements Ordered { private final BulkheadConfigurationProperties bulkheadConfigurationProperties; private final BulkheadRegistry bulkheadRegistry; private final @Nullable List bulkheadAspectExts; + private final RecoveryDecorators recoveryDecorators; - public BulkheadAspect(BulkheadConfigurationProperties backendMonitorPropertiesRegistry, BulkheadRegistry bulkheadRegistry, @Autowired(required = false) List bulkheadAspectExts) { + public BulkheadAspect(BulkheadConfigurationProperties backendMonitorPropertiesRegistry, BulkheadRegistry bulkheadRegistry, @Autowired(required = false) List bulkheadAspectExts, RecoveryDecorators recoveryDecorators) { this.bulkheadConfigurationProperties = backendMonitorPropertiesRegistry; this.bulkheadRegistry = bulkheadRegistry; this.bulkheadAspectExts = bulkheadAspectExts; + this.recoveryDecorators = recoveryDecorators; } @Pointcut(value = "@within(Bulkhead) || @annotation(Bulkhead)", argNames = "Bulkhead") @@ -72,6 +77,16 @@ public Object bulkheadAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, @Nul String backend = backendMonitored.name(); io.github.resilience4j.bulkhead.Bulkhead bulkhead = getOrCreateBulkhead(methodName, backend); Class returnType = method.getReturnType(); + + if (StringUtils.isEmpty(backendMonitored.recovery())) { + return proceed(proceedingJoinPoint, methodName, bulkhead, returnType); + } + + RecoveryMethod recoveryMethod = new RecoveryMethod(backendMonitored.recovery(), method, proceedingJoinPoint.getArgs(), proceedingJoinPoint.getTarget()); + return recoveryDecorators.decorate(recoveryMethod, () -> proceed(proceedingJoinPoint, methodName, bulkhead, returnType)).apply(); + } + + private Object proceed(ProceedingJoinPoint proceedingJoinPoint, String methodName, io.github.resilience4j.bulkhead.Bulkhead bulkhead, Class returnType) throws Throwable { if (bulkheadAspectExts != null && !bulkheadAspectExts.isEmpty()) { for (BulkheadAspectExt bulkHeadAspectExt : bulkheadAspectExts) { if (bulkHeadAspectExt.canHandleReturnType(returnType)) { diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadConfiguration.java b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadConfiguration.java index 1ac2c9ea64..4c18c60dea 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadConfiguration.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadConfiguration.java @@ -20,6 +20,7 @@ import java.util.Optional; import java.util.stream.Collectors; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -89,8 +90,9 @@ private void registerEventConsumer(EventConsumerRegistry eventCon @Bean public BulkheadAspect bulkheadAspect(BulkheadConfigurationProperties bulkheadConfigurationProperties, - BulkheadRegistry bulkheadRegistry, @Autowired(required = false) List bulkHeadAspectExtList) { - return new BulkheadAspect(bulkheadConfigurationProperties, bulkheadRegistry, bulkHeadAspectExtList); + BulkheadRegistry bulkheadRegistry, @Autowired(required = false) List bulkHeadAspectExtList, + RecoveryDecorators recoveryDecorators) { + return new BulkheadAspect(bulkheadConfigurationProperties, bulkheadRegistry, bulkHeadAspectExtList, recoveryDecorators); } @Bean diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/ReactorBulkheadAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/ReactorBulkheadAspectExt.java index 89dab6b10c..1085098862 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/ReactorBulkheadAspectExt.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/ReactorBulkheadAspectExt.java @@ -37,7 +37,6 @@ public class ReactorBulkheadAspectExt implements BulkheadAspectExt { * @param returnType the AOP method return type class * @return boolean if the method has Reactor return type */ - @SuppressWarnings("unchecked") @Override public boolean canHandleReturnType(Class returnType) { return (Flux.class.isAssignableFrom(returnType)) || (Mono.class.isAssignableFrom(returnType)); @@ -53,7 +52,6 @@ public boolean canHandleReturnType(Class returnType) { * @return the result object * @throws Throwable exception in case of faulty flow */ - @SuppressWarnings("unchecked") @Override public Object handle(ProceedingJoinPoint proceedingJoinPoint, Bulkhead bulkhead, String methodName) throws Throwable { Object returnValue = proceedingJoinPoint.proceed(); diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/RxJava2BulkheadAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/RxJava2BulkheadAspectExt.java index 96fb54cc4e..babd5e1ad9 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/RxJava2BulkheadAspectExt.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/RxJava2BulkheadAspectExt.java @@ -61,7 +61,6 @@ public boolean canHandleReturnType(Class returnType) { * @return the result object * @throws Throwable exception in case of faulty flow */ - @SuppressWarnings("unchecked") @Override public Object handle(ProceedingJoinPoint proceedingJoinPoint, Bulkhead bulkhead, String methodName) throws Throwable { BulkheadOperator bulkheadOperator = BulkheadOperator.of(bulkhead); diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspect.java b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspect.java index 5a05d23043..ab36b83d89 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspect.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspect.java @@ -18,7 +18,10 @@ import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import io.github.resilience4j.core.lang.Nullable; +import io.github.resilience4j.recovery.RecoveryDecorators; import io.github.resilience4j.utils.AnnotationExtractor; +import io.github.resilience4j.recovery.RecoveryDecorators; +import io.github.resilience4j.recovery.RecoveryMethod; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -28,6 +31,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; +import org.springframework.util.StringUtils; import java.lang.reflect.Method; import java.util.List; @@ -47,11 +51,13 @@ public class CircuitBreakerAspect implements Ordered { private final CircuitBreakerConfigurationProperties circuitBreakerProperties; private final CircuitBreakerRegistry circuitBreakerRegistry; private final @Nullable List circuitBreakerAspectExtList; + private final RecoveryDecorators recoveryDecorators; - public CircuitBreakerAspect(CircuitBreakerConfigurationProperties circuitBreakerProperties, CircuitBreakerRegistry circuitBreakerRegistry, @Autowired(required = false) List circuitBreakerAspectExtList) { + public CircuitBreakerAspect(CircuitBreakerConfigurationProperties circuitBreakerProperties, CircuitBreakerRegistry circuitBreakerRegistry, @Autowired(required = false) List circuitBreakerAspectExtList, RecoveryDecorators recoveryDecorators) { this.circuitBreakerProperties = circuitBreakerProperties; this.circuitBreakerRegistry = circuitBreakerRegistry; this.circuitBreakerAspectExtList = circuitBreakerAspectExtList; + this.recoveryDecorators = recoveryDecorators; } @Pointcut(value = "@within(circuitBreaker) || @annotation(circuitBreaker)", argNames = "circuitBreaker") @@ -71,6 +77,16 @@ public Object circuitBreakerAroundAdvice(ProceedingJoinPoint proceedingJoinPoint String backend = backendMonitored.name(); io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker = getOrCreateCircuitBreaker(methodName, backend); Class returnType = method.getReturnType(); + + if (StringUtils.isEmpty(backendMonitored.recovery())) { + return proceed(proceedingJoinPoint, methodName, circuitBreaker, returnType); + } + + RecoveryMethod recoveryMethod = new RecoveryMethod(backendMonitored.recovery(), method, proceedingJoinPoint.getArgs(), proceedingJoinPoint.getTarget()); + return recoveryDecorators.decorate(recoveryMethod, () -> proceed(proceedingJoinPoint, methodName, circuitBreaker, returnType)).apply(); + } + + private Object proceed(ProceedingJoinPoint proceedingJoinPoint, String methodName, io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker, Class returnType) throws Throwable { if (circuitBreakerAspectExtList != null && !circuitBreakerAspectExtList.isEmpty()) { for (CircuitBreakerAspectExt circuitBreakerAspectExt : circuitBreakerAspectExtList) { if (circuitBreakerAspectExt.canHandleReturnType(returnType)) { diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerConfiguration.java b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerConfiguration.java index b64f0dbee6..cd981200d1 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerConfiguration.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerConfiguration.java @@ -21,6 +21,7 @@ import io.github.resilience4j.circuitbreaker.event.CircuitBreakerEvent; import io.github.resilience4j.consumer.DefaultEventConsumerRegistry; import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.recovery.RecoveryDecorators; import io.github.resilience4j.utils.ReactorOnClasspathCondition; import io.github.resilience4j.utils.RxJava2OnClasspathCondition; import org.springframework.beans.factory.annotation.Autowired; @@ -55,8 +56,9 @@ public CircuitBreakerRegistry circuitBreakerRegistry(EventConsumerRegistry circuitBreakerAspectExtList) { - return new CircuitBreakerAspect(circuitBreakerProperties, circuitBreakerRegistry, circuitBreakerAspectExtList); + @Autowired(required = false) List circuitBreakerAspectExtList, + RecoveryDecorators recoveryDecorators) { + return new CircuitBreakerAspect(circuitBreakerProperties, circuitBreakerRegistry, circuitBreakerAspectExtList, recoveryDecorators); } diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/ReactorCircuitBreakerAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/ReactorCircuitBreakerAspectExt.java index b2cde933c5..83c742469f 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/ReactorCircuitBreakerAspectExt.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/ReactorCircuitBreakerAspectExt.java @@ -36,7 +36,6 @@ public class ReactorCircuitBreakerAspectExt implements CircuitBreakerAspectExt { * @param returnType the AOP method return type class * @return boolean if the method has Reactor return type */ - @SuppressWarnings("unchecked") @Override public boolean canHandleReturnType(Class returnType) { return (Flux.class.isAssignableFrom(returnType)) || (Mono.class.isAssignableFrom(returnType)); diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/RxJava2CircuitBreakerAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/RxJava2CircuitBreakerAspectExt.java index 760b2f7d6a..f496d5dba5 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/RxJava2CircuitBreakerAspectExt.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/RxJava2CircuitBreakerAspectExt.java @@ -15,25 +15,16 @@ */ package io.github.resilience4j.circuitbreaker.configure; -import static io.github.resilience4j.utils.AspectUtil.newHashSet; - -import java.util.Set; - +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.operator.CircuitBreakerOperator; +import io.reactivex.*; import org.aspectj.lang.ProceedingJoinPoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.github.resilience4j.circuitbreaker.CircuitBreaker; -import io.github.resilience4j.circuitbreaker.operator.CircuitBreakerOperator; -import io.reactivex.Completable; -import io.reactivex.CompletableSource; -import io.reactivex.Flowable; -import io.reactivex.Maybe; -import io.reactivex.MaybeSource; -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.Single; -import io.reactivex.SingleSource; +import java.util.Set; + +import static io.github.resilience4j.utils.AspectUtil.newHashSet; /** * the Rx circuit breaker logic support for the spring AOP @@ -66,6 +57,12 @@ public boolean canHandleReturnType(Class returnType) { public Object handle(ProceedingJoinPoint proceedingJoinPoint, CircuitBreaker circuitBreaker, String methodName) throws Throwable { CircuitBreakerOperator circuitBreakerOperator = CircuitBreakerOperator.of(circuitBreaker); Object returnValue = proceedingJoinPoint.proceed(); + + return executeRxJava2Aspect(circuitBreakerOperator, returnValue, methodName); + } + + @SuppressWarnings("unchecked") + private Object executeRxJava2Aspect(CircuitBreakerOperator circuitBreakerOperator, Object returnValue, String methodName) { if (returnValue instanceof ObservableSource) { Observable observable = (Observable) returnValue; return observable.lift(circuitBreakerOperator); diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RateLimiterAspect.java b/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RateLimiterAspect.java index c171a9f14f..2e660961e0 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RateLimiterAspect.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RateLimiterAspect.java @@ -20,6 +20,8 @@ import io.github.resilience4j.ratelimiter.RateLimiterRegistry; import io.github.resilience4j.ratelimiter.annotation.RateLimiter; import io.github.resilience4j.utils.AnnotationExtractor; +import io.github.resilience4j.recovery.RecoveryDecorators; +import io.github.resilience4j.recovery.RecoveryMethod; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -29,6 +31,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; +import org.springframework.util.StringUtils; import java.lang.reflect.Method; import java.util.List; @@ -48,11 +51,13 @@ public class RateLimiterAspect implements Ordered { private final RateLimiterRegistry rateLimiterRegistry; private final RateLimiterConfigurationProperties properties; private final @Nullable List rateLimiterAspectExtList; + private final RecoveryDecorators recoveryDecorators; - public RateLimiterAspect(RateLimiterRegistry rateLimiterRegistry, RateLimiterConfigurationProperties properties, @Autowired(required = false) List rateLimiterAspectExtList) { + public RateLimiterAspect(RateLimiterRegistry rateLimiterRegistry, RateLimiterConfigurationProperties properties, @Autowired(required = false) List rateLimiterAspectExtList, RecoveryDecorators recoveryDecorators) { this.rateLimiterRegistry = rateLimiterRegistry; this.properties = properties; this.rateLimiterAspectExtList = rateLimiterAspectExtList; + this.recoveryDecorators = recoveryDecorators; } /** @@ -75,7 +80,16 @@ public Object rateLimiterAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, @ } String name = targetService.name(); Class returnType = method.getReturnType(); - io.github.resilience4j.ratelimiter.RateLimiter rateLimiter = getOrCreateRateLimiter(methodName, name); + io.github.resilience4j.ratelimiter.RateLimiter rateLimiter = getOrCreateRateLimiter(methodName, name); + if (StringUtils.isEmpty(targetService.recovery())) { + return proceed(proceedingJoinPoint, methodName, returnType, rateLimiter); + } + + RecoveryMethod recoveryMethod = new RecoveryMethod(targetService.recovery(), method, proceedingJoinPoint.getArgs(), proceedingJoinPoint.getTarget()); + return recoveryDecorators.decorate(recoveryMethod, () -> proceed(proceedingJoinPoint, methodName, returnType, rateLimiter)).apply(); + } + + private Object proceed(ProceedingJoinPoint proceedingJoinPoint, String methodName, Class returnType, io.github.resilience4j.ratelimiter.RateLimiter rateLimiter) throws Throwable { if (rateLimiterAspectExtList != null && !rateLimiterAspectExtList.isEmpty()) { for (RateLimiterAspectExt rateLimiterAspectExt : rateLimiterAspectExtList) { if (rateLimiterAspectExt.canHandleReturnType(returnType)) { diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RateLimiterConfiguration.java b/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RateLimiterConfiguration.java index 9bd1bfc549..00096ec9c1 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RateLimiterConfiguration.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RateLimiterConfiguration.java @@ -20,6 +20,7 @@ import java.util.Map; import java.util.stream.Collectors; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -85,8 +86,8 @@ private void registerEventConsumer(EventConsumerRegistry event } @Bean - public RateLimiterAspect rateLimiterAspect(RateLimiterConfigurationProperties rateLimiterProperties, RateLimiterRegistry rateLimiterRegistry, @Autowired(required = false) List rateLimiterAspectExtList) { - return new RateLimiterAspect(rateLimiterRegistry, rateLimiterProperties, rateLimiterAspectExtList); + public RateLimiterAspect rateLimiterAspect(RateLimiterConfigurationProperties rateLimiterProperties, RateLimiterRegistry rateLimiterRegistry, @Autowired(required = false) List rateLimiterAspectExtList, RecoveryDecorators recoveryDecorators) { + return new RateLimiterAspect(rateLimiterRegistry, rateLimiterProperties, rateLimiterAspectExtList, recoveryDecorators); } @Bean diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/ReactorRateLimiterAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/ReactorRateLimiterAspectExt.java index 2ad5268a30..5e429fc05e 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/ReactorRateLimiterAspectExt.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/ReactorRateLimiterAspectExt.java @@ -37,7 +37,6 @@ public class ReactorRateLimiterAspectExt implements RateLimiterAspectExt { * @param returnType the AOP method return type class * @return boolean if the method has Reactor return type */ - @SuppressWarnings("unchecked") @Override public boolean canHandleReturnType(Class returnType) { return (Flux.class.isAssignableFrom(returnType)) || (Mono.class.isAssignableFrom(returnType)); @@ -53,7 +52,6 @@ public boolean canHandleReturnType(Class returnType) { * @return the result object * @throws Throwable exception in case of faulty flow */ - @SuppressWarnings("unchecked") @Override public Object handle(ProceedingJoinPoint proceedingJoinPoint, RateLimiter rateLimiter, String methodName) throws Throwable { Object returnValue = proceedingJoinPoint.proceed(); diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RxJava2RateLimiterAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RxJava2RateLimiterAspectExt.java index 3be0b62816..52e280f6d0 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RxJava2RateLimiterAspectExt.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/ratelimiter/configure/RxJava2RateLimiterAspectExt.java @@ -61,7 +61,6 @@ public boolean canHandleReturnType(Class returnType) { * @return the result object * @throws Throwable exception in case of faulty flow */ - @SuppressWarnings("unchecked") @Override public Object handle(ProceedingJoinPoint proceedingJoinPoint, RateLimiter rateLimiter, String methodName) throws Throwable { RateLimiterOperator rateLimiterOperator = RateLimiterOperator.of(rateLimiter); diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/CompletionStageRecoveryDecorator.java b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/CompletionStageRecoveryDecorator.java new file mode 100644 index 0000000000..3ac199972f --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/CompletionStageRecoveryDecorator.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 Kyuhyen Hwang + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.recovery; + +import io.vavr.CheckedFunction0; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +/** + * recovery decorator for {@link CompletionStage} + */ +public class CompletionStageRecoveryDecorator implements RecoveryDecorator { + + @Override + public boolean supports(Class target) { + return CompletionStage.class.isAssignableFrom(target); + } + + @SuppressWarnings("unchecked") + @Override + public CheckedFunction0 decorate(RecoveryMethod recoveryMethod, CheckedFunction0 supplier) { + return supplier.andThen(request -> { + CompletionStage completionStage = (CompletionStage) request; + + CompletableFuture promise = new CompletableFuture(); + + completionStage.whenComplete((result, throwable) -> { + if (throwable != null) { + try { + ((CompletionStage) recoveryMethod.recover((Throwable) throwable)) + .whenComplete((recoveryResult, recoveryThrowable) -> { + if (recoveryThrowable != null) { + promise.completeExceptionally((Throwable) recoveryThrowable); + } else { + promise.complete(recoveryResult); + } + }); + } catch (Throwable recoveryThrowable) { + promise.completeExceptionally(recoveryThrowable); + } + } else { + promise.complete(result); + } + }); + + return promise; + }); + } +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/DefaultRecoveryDecorator.java b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/DefaultRecoveryDecorator.java new file mode 100644 index 0000000000..0c5c1ee5bf --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/DefaultRecoveryDecorator.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 Kyuhyen Hwang + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.recovery; + +import io.vavr.CheckedFunction0; + +/** + * default recovery decorator. it catches throwable and invoke the recovery method. + */ +public class DefaultRecoveryDecorator implements RecoveryDecorator { + + @Override + public boolean supports(Class target) { + return true; + } + + @Override + public CheckedFunction0 decorate(RecoveryMethod recoveryMethod, CheckedFunction0 supplier) { + return () -> { + try { + return supplier.apply(); + } catch (Throwable throwable) { + return recoveryMethod.recover(throwable); + } + }; + } +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/ReactorRecoveryDecorator.java b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/ReactorRecoveryDecorator.java new file mode 100644 index 0000000000..34bf3aef08 --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/ReactorRecoveryDecorator.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Kyuhyen Hwang + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.recovery; + +import io.vavr.CheckedFunction0; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Set; +import java.util.function.Function; + +import static io.github.resilience4j.utils.AspectUtil.newHashSet; + +/** + * recovery decorator for {@link Flux} and {@link Mono} + */ +public class ReactorRecoveryDecorator implements RecoveryDecorator { + private static final Set> REACTORS_SUPPORTED_TYPES = newHashSet(Mono.class, Flux.class); + + @Override + public boolean supports(Class target) { + return REACTORS_SUPPORTED_TYPES.stream().anyMatch(it -> it.isAssignableFrom(target)); + } + + @SuppressWarnings("unchecked") + @Override + public CheckedFunction0 decorate(RecoveryMethod recoveryMethod, CheckedFunction0 supplier) { + return supplier.andThen(returnValue -> { + if (Flux.class.isAssignableFrom(returnValue.getClass())) { + Flux fluxReturnValue = (Flux) returnValue; + return fluxReturnValue.onErrorResume(reactorOnErrorResume(recoveryMethod, Flux::error)); + } else if (Mono.class.isAssignableFrom(returnValue.getClass())) { + Mono monoReturnValue = (Mono) returnValue; + return monoReturnValue.onErrorResume(reactorOnErrorResume(recoveryMethod, Mono::error)); + } else { + return returnValue; + } + }); + } + + @SuppressWarnings("unchecked") + private Function> reactorOnErrorResume(RecoveryMethod recoveryMethod, Function> errorFunction) { + return (throwable) -> { + try { + return (Publisher) recoveryMethod.recover(throwable); + } catch (Throwable recoverThrowable) { + return errorFunction.apply(recoverThrowable); + } + }; + } +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RecoveryDecorator.java b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RecoveryDecorator.java new file mode 100644 index 0000000000..56fd2cf00a --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RecoveryDecorator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 Kyuhyen Hwang + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.recovery; + +import io.vavr.CheckedFunction0; + +/** + * interface of RecoveryDecorator + */ +public interface RecoveryDecorator { + boolean supports(Class target); + + /** + * @param recoveryMethod recovery method. + * @param supplier target function should be decorated. + * @return decorated function + */ + CheckedFunction0 decorate(RecoveryMethod recoveryMethod, CheckedFunction0 supplier); +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RecoveryDecorators.java b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RecoveryDecorators.java new file mode 100644 index 0000000000..46e11fe08d --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RecoveryDecorators.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 Kyuhyen Hwang + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.recovery; + +import io.vavr.CheckedFunction0; + +import java.util.List; + +/** + * {@link RecoveryDecorator} resolver + */ +public class RecoveryDecorators { + private final List recoveryDecorator; + private final RecoveryDecorator defaultRecoveryDecorator = new DefaultRecoveryDecorator(); + + public RecoveryDecorators(List recoveryDecorator) { + this.recoveryDecorator = recoveryDecorator; + } + + /** + * find a {@link RecoveryDecorator} by return type of the {@link RecoveryMethod} and decorate supplier + * + * @param recoveryMethod recovery method that handles supplier's exception + * @param supplier original function + * @return a function which is decorated by a {@link RecoveryMethod} + */ + public CheckedFunction0 decorate(RecoveryMethod recoveryMethod, CheckedFunction0 supplier) { + return get(recoveryMethod.getReturnType()) + .decorate(recoveryMethod, supplier); + } + + private RecoveryDecorator get(Class returnType) { + return recoveryDecorator.stream().filter(it -> it.supports(returnType)) + .findFirst() + .orElse(defaultRecoveryDecorator); + } +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RecoveryMethod.java b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RecoveryMethod.java new file mode 100644 index 0000000000..8dd1d80f20 --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RecoveryMethod.java @@ -0,0 +1,201 @@ +/* + * Copyright 2019 Kyuhyen Hwang + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.recovery; + +import io.github.resilience4j.core.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Reflection utility for invoking recovery method. Recovery method should have same return type and parameter types of original method but the last additional parameter. + * The last additional parameter should be a subclass of {@link Throwable}. When {@link RecoveryMethod#recover(Throwable)} is invoked, {@link Throwable} will be passed to that last parameter. + * If there are multiple recovery method, one of the methods that has most closest superclass parameter of thrown object will be invoked. + *
+ * For example, there are two recovery methods
+ * {@code
+ * String recovery(String parameter, RuntimeException exception)
+ * String recovery(String parameter, IllegalArgumentException exception)
+ * }
+ * and if try to recover from {@link NumberFormatException}, {@code String recovery(String parameter, IllegalArgumentException exception)} will be invoked.
+ * 
+ */ +public class RecoveryMethod { + private static final Map, Method>> RECOVERY_METHODS_CACHE = new ConcurrentReferenceHashMap<>(); + private final Map, Method> recoveryMethods; + private final Object[] args; + private final Object target; + private final Class returnType; + + /** + * create a recovery method. + * + * @param recoveryMethodName recovery method name + * @param originalMethod will be used for checking return type and parameter types of the recovery method + * @param args arguments those were passed to the original method. They will be passed to the recovery method. + * @param target target object the recovery method will be invoked + * @throws NoSuchMethodException will be thrown, if recovery method is not found + */ + public RecoveryMethod(String recoveryMethodName, Method originalMethod, Object[] args, Object target) throws NoSuchMethodException { + Class[] params = originalMethod.getParameterTypes(); + Class originalReturnType = originalMethod.getReturnType(); + + Map, Method> methods = extractMethods(recoveryMethodName, params, originalReturnType, target.getClass()); + + if (methods.isEmpty()) { + throw new NoSuchMethodException(String.format("%s %s.%s(%s,%s)", originalReturnType, target.getClass(), recoveryMethodName, StringUtils.arrayToDelimitedString(params, ","), Throwable.class)); + } + + this.recoveryMethods = methods; + this.args = args; + this.target = target; + this.returnType = originalReturnType; + } + + /** + * try to recover from {@link Throwable} + * + * @param thrown {@link Throwable} that should be recover + * @return recovered value + * @throws Throwable if throwable is unrecoverable, throwable will be thrown + */ + @Nullable + public Object recover(Throwable thrown) throws Throwable { + if (recoveryMethods.size() == 1) { + Map.Entry, Method> entry = recoveryMethods.entrySet().iterator().next(); + if (entry.getKey().isAssignableFrom(thrown.getClass())) { + return invoke(entry.getValue(), thrown); + } else { + throw thrown; + } + } + + Method recovery = null; + + for (Class thrownClass = thrown.getClass(); recovery == null && thrownClass != Object.class; thrownClass = thrownClass.getSuperclass()) { + recovery = recoveryMethods.get(thrownClass); + } + + if (recovery == null) { + throw thrown; + } + + return invoke(recovery, thrown); + } + + /** + * get return type of recovery method + * + * @return return type of recovery method + */ + public Class getReturnType() { + return returnType; + } + + private Object invoke(Method recovery, Throwable throwable) throws IllegalAccessException, InvocationTargetException { + boolean accessible = recovery.isAccessible(); + try { + if (!accessible) { + ReflectionUtils.makeAccessible(recovery); + } + + if (args.length != 0) { + Object[] newArgs = Arrays.copyOf(args, args.length + 1); + newArgs[args.length] = throwable; + + return recovery.invoke(target, newArgs); + + } else { + return recovery.invoke(target, throwable); + } + } finally { + if (!accessible) { + recovery.setAccessible(false); + } + } + } + + private static Map, Method> extractMethods(String recoveryMethodName, Class[] params, Class originalReturnType, Class targetClass) { + MethodMeta methodMeta = new MethodMeta(recoveryMethodName, params, originalReturnType, targetClass); + Map, Method> cachedMethods = RECOVERY_METHODS_CACHE.get(methodMeta); + + if (cachedMethods != null) { + return cachedMethods; + } + + Map, Method> methods = new HashMap<>(); + + ReflectionUtils.doWithMethods(targetClass, method -> { + Class[] recoveryParams = method.getParameterTypes(); + methods.put(recoveryParams[recoveryParams.length - 1], method); + }, method -> { + if (!method.getName().equals(recoveryMethodName) || method.getParameterCount() != params.length + 1) { + return false; + } + if (!originalReturnType.isAssignableFrom(method.getReturnType())) { + return false; + } + + Class[] targetParams = method.getParameterTypes(); + for (int i = 0; i < params.length; i++) { + if (params[i] != targetParams[i]) { + return false; + } + } + + return Throwable.class.isAssignableFrom(targetParams[params.length]); + }); + + RECOVERY_METHODS_CACHE.putIfAbsent(methodMeta, methods); + return methods; + } + + private static class MethodMeta { + final String recoveryMethodName; + final Class[] params; + final Class returnType; + final Class targetClass; + + MethodMeta(String recoveryMethodName, Class[] params, Class returnType, Class targetClass) { + this.recoveryMethodName = recoveryMethodName; + this.params = params; + this.returnType = returnType; + this.targetClass = targetClass; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MethodMeta that = (MethodMeta) o; + return targetClass.equals(that.targetClass) && + recoveryMethodName.equals(that.recoveryMethodName) && + returnType.equals(that.returnType) && + Arrays.equals(params, that.params); + } + + @Override + public int hashCode() { + return targetClass.getName().hashCode() ^ recoveryMethodName.hashCode(); + } + } +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RxJava2RecoveryDecorator.java b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RxJava2RecoveryDecorator.java new file mode 100644 index 0000000000..bc073ac086 --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/RxJava2RecoveryDecorator.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019 Kyuhyen Hwang + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.recovery; + +import io.reactivex.*; +import io.vavr.CheckedFunction0; + +import java.util.Set; +import java.util.function.Function; + +import static io.github.resilience4j.utils.AspectUtil.newHashSet; + +/** + * recovery decorator for {@link ObservableSource}, {@link SingleSource}, {@link CompletableSource}, {@link MaybeSource} and {@link Flowable}. + */ +public class RxJava2RecoveryDecorator implements RecoveryDecorator { + private static final Set> RX_SUPPORTED_TYPES = newHashSet(ObservableSource.class, SingleSource.class, CompletableSource.class, MaybeSource.class, Flowable.class); + + @Override + public boolean supports(Class target) { + return RX_SUPPORTED_TYPES.stream().anyMatch(it -> it.isAssignableFrom(target)); + } + + @Override + public CheckedFunction0 decorate(RecoveryMethod recoveryMethod, CheckedFunction0 supplier) { + return supplier.andThen(request -> { + if (request instanceof ObservableSource) { + Observable observable = (Observable) request; + return observable.onErrorResumeNext(rxJava2OnErrorResumeNext(recoveryMethod, Observable::error)); + } else if (request instanceof SingleSource) { + Single single = (Single) request; + return single.onErrorResumeNext(rxJava2OnErrorResumeNext(recoveryMethod, Single::error)); + } else if (request instanceof CompletableSource) { + Completable completable = (Completable) request; + return completable.onErrorResumeNext(rxJava2OnErrorResumeNext(recoveryMethod, Completable::error)); + } else if (request instanceof MaybeSource) { + Maybe maybe = (Maybe) request; + return maybe.onErrorResumeNext(rxJava2OnErrorResumeNext(recoveryMethod, Maybe::error)); + } else if (request instanceof Flowable) { + Flowable flowable = (Flowable) request; + return flowable.onErrorResumeNext(rxJava2OnErrorResumeNext(recoveryMethod, Flowable::error)); + } else { + return request; + } + }); + } + + @SuppressWarnings("unchecked") + private io.reactivex.functions.Function rxJava2OnErrorResumeNext(RecoveryMethod recoveryMethod, Function errorFunction) { + return (throwable) -> { + try { + return (T) recoveryMethod.recover(throwable); + } catch (Throwable recoverThrowable) { + return (T) errorFunction.apply(recoverThrowable); + } + }; + } +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/configure/RecoveryConfiguration.java b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/configure/RecoveryConfiguration.java new file mode 100644 index 0000000000..9aecd0e23a --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/configure/RecoveryConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019 Kyuhyen Hwang + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.recovery.configure; + +import io.github.resilience4j.recovery.*; +import io.github.resilience4j.utils.ReactorOnClasspathCondition; +import io.github.resilience4j.utils.RxJava2OnClasspathCondition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * {@link Configuration} for {@link RecoveryDecorators}. + */ +@Configuration +public class RecoveryConfiguration { + + @Bean + @Conditional(value = {RxJava2OnClasspathCondition.class}) + public RecoveryDecorator rxJava2RecoveryDecorator() { + return new RxJava2RecoveryDecorator(); + } + + @Bean + @Conditional(value = {ReactorOnClasspathCondition.class}) + public RecoveryDecorator reactorRecoveryDecorator() { + return new ReactorRecoveryDecorator(); + } + + @Bean + public RecoveryDecorator completionStageRecoveryDecorator() { + return new CompletionStageRecoveryDecorator(); + } + + @Bean + public RecoveryDecorators recoveryDecorators(List recoveryDecorator) { + return new RecoveryDecorators(recoveryDecorator); + } +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/package-info.java b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/package-info.java new file mode 100644 index 0000000000..d2ca92226f --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/recovery/package-info.java @@ -0,0 +1,24 @@ +/* + * + * Copyright 2019: Kyuhyen Hwang + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ +@NonNullApi +@NonNullFields +package io.github.resilience4j.recovery; + +import io.github.resilience4j.core.lang.NonNullApi; +import io.github.resilience4j.core.lang.NonNullFields; \ No newline at end of file diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/AsyncRetryAspect.java b/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/AsyncRetryAspect.java new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RetryAspect.java b/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RetryAspect.java index 137ce567e6..4d35030feb 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RetryAspect.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RetryAspect.java @@ -23,6 +23,8 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import io.github.resilience4j.recovery.RecoveryDecorators; +import io.github.resilience4j.recovery.RecoveryMethod; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -32,6 +34,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; +import org.springframework.util.StringUtils; import io.github.resilience4j.core.lang.Nullable; import io.github.resilience4j.retry.RetryRegistry; @@ -51,16 +54,18 @@ public class RetryAspect implements Ordered { private final RetryConfigurationProperties retryConfigurationProperties; private final RetryRegistry retryRegistry; private final @Nullable List retryAspectExtList; + private final RecoveryDecorators recoveryDecorators; /** * @param retryConfigurationProperties spring retry config properties * @param retryRegistry retry definition registry * @param retryAspectExtList */ - public RetryAspect(RetryConfigurationProperties retryConfigurationProperties, RetryRegistry retryRegistry, @Autowired(required = false) List retryAspectExtList) { + public RetryAspect(RetryConfigurationProperties retryConfigurationProperties, RetryRegistry retryRegistry, @Autowired(required = false) List retryAspectExtList, RecoveryDecorators recoveryDecorators) { this.retryConfigurationProperties = retryConfigurationProperties; this.retryRegistry = retryRegistry; this.retryAspectExtList = retryAspectExtList; + this.recoveryDecorators = recoveryDecorators; cleanup(); } @@ -82,6 +87,15 @@ public Object retryAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, @Nullab String backend = backendMonitored.name(); io.github.resilience4j.retry.Retry retry = getOrCreateRetry(methodName, backend); Class returnType = method.getReturnType(); + if (StringUtils.isEmpty(backendMonitored.recovery())) { + return proceed(proceedingJoinPoint, methodName, retry, returnType); + } + + RecoveryMethod recoveryMethod = new RecoveryMethod(backendMonitored.recovery(), method, proceedingJoinPoint.getArgs(), proceedingJoinPoint.getTarget()); + return recoveryDecorators.decorate(recoveryMethod, () -> proceed(proceedingJoinPoint, methodName, retry, returnType)).apply(); + } + + private Object proceed(ProceedingJoinPoint proceedingJoinPoint, String methodName, io.github.resilience4j.retry.Retry retry, Class returnType) throws Throwable { if (CompletionStage.class.isAssignableFrom(returnType)) { return handleJoinPointCompletableFuture(proceedingJoinPoint, retry); } diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RetryConfiguration.java b/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RetryConfiguration.java index 5cc1039fc7..b3f59b0d6d 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RetryConfiguration.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RetryConfiguration.java @@ -20,6 +20,7 @@ import java.util.Optional; import java.util.stream.Collectors; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -95,8 +96,9 @@ private void registerEventConsumer(EventConsumerRegistry eventConsum */ @Bean public RetryAspect retryAspect(RetryConfigurationProperties retryConfigurationProperties, - RetryRegistry retryRegistry, @Autowired(required = false) List retryAspectExtList) { - return new RetryAspect(retryConfigurationProperties, retryRegistry, retryAspectExtList); + RetryRegistry retryRegistry, @Autowired(required = false) List retryAspectExtList, + RecoveryDecorators recoveryDecorators) { + return new RetryAspect(retryConfigurationProperties, retryRegistry, retryAspectExtList, recoveryDecorators); } @Bean diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RxJava2RetryAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RxJava2RetryAspectExt.java index 8b0219fb86..d5e10f33a7 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RxJava2RetryAspectExt.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/retry/configure/RxJava2RetryAspectExt.java @@ -61,7 +61,6 @@ public boolean canHandleReturnType(Class returnType) { * @return the result object * @throws Throwable exception in case of faulty flow */ - @SuppressWarnings("unchecked") @Override public Object handle(ProceedingJoinPoint proceedingJoinPoint, Retry retry, String methodName) throws Throwable { RetryTransformer retryTransformer = RetryTransformer.of(retry); diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/BulkheadDummyService.java b/resilience4j-spring/src/test/java/io/github/resilience4j/BulkheadDummyService.java new file mode 100644 index 0000000000..3390c52bfa --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/BulkheadDummyService.java @@ -0,0 +1,66 @@ +package io.github.resilience4j; + +import io.github.resilience4j.bulkhead.annotation.Bulkhead; +import io.reactivex.*; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.concurrent.CompletionStage; + +@Component +public class BulkheadDummyService implements TestDummyService { + @Override + @Bulkhead(name = BACKEND, recovery = "recovery") + public String sync() { + return syncError(); + } + + @Override + @Bulkhead(name = BACKEND, recovery = "completionStageRecovery") + public CompletionStage async() { + return asyncError(); + } + + @Override + @Bulkhead(name = BACKEND, recovery = "fluxRecovery") + public Flux flux() { + return fluxError(); + } + + @Override + @Bulkhead(name = BACKEND, recovery = "monoRecovery") + public Mono mono(String parameter) { + return monoError(parameter); + } + + @Override + @Bulkhead(name = BACKEND, recovery = "observableRecovery") + public Observable observable() { + return observableError(); + } + + @Override + @Bulkhead(name = BACKEND, recovery = "singleRecovery") + public Single single() { + return singleError(); + } + + @Override + @Bulkhead(name = BACKEND, recovery = "completableRecovery") + public Completable completable() { + return completableError(); + } + + @Override + @Bulkhead(name = BACKEND, recovery = "maybeRecovery") + public Maybe maybe() { + return maybeError(); + } + + @Override + @Bulkhead(name = BACKEND, recovery = "flowableRecovery") + public Flowable flowable() { + return flowableError(); + } +} diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/CircuitBreakerDummyService.java b/resilience4j-spring/src/test/java/io/github/resilience4j/CircuitBreakerDummyService.java new file mode 100644 index 0000000000..158d79238f --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/CircuitBreakerDummyService.java @@ -0,0 +1,66 @@ +package io.github.resilience4j; + +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.reactivex.*; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.concurrent.CompletionStage; + +@Component +public class CircuitBreakerDummyService implements TestDummyService { + @Override + @CircuitBreaker(name = BACKEND, recovery = "recovery") + public String sync() { + return syncError(); + } + + @Override + @CircuitBreaker(name = BACKEND, recovery = "completionStageRecovery") + public CompletionStage async() { + return asyncError(); + } + + @Override + @CircuitBreaker(name = BACKEND, recovery = "fluxRecovery") + public Flux flux() { + return fluxError(); + } + + @Override + @CircuitBreaker(name = BACKEND, recovery = "monoRecovery") + public Mono mono(String parameter) { + return monoError(parameter); + } + + @Override + @CircuitBreaker(name = BACKEND, recovery = "observableRecovery") + public Observable observable() { + return observableError(); + } + + @Override + @CircuitBreaker(name = BACKEND, recovery = "singleRecovery") + public Single single() { + return singleError(); + } + + @Override + @CircuitBreaker(name = BACKEND, recovery = "completableRecovery") + public Completable completable() { + return completableError(); + } + + @Override + @CircuitBreaker(name = BACKEND, recovery = "maybeRecovery") + public Maybe maybe() { + return maybeError(); + } + + @Override + @CircuitBreaker(name = BACKEND, recovery = "flowableRecovery") + public Flowable flowable() { + return flowableError(); + } +} diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/RateLimiterDummyService.java b/resilience4j-spring/src/test/java/io/github/resilience4j/RateLimiterDummyService.java new file mode 100644 index 0000000000..880500ed42 --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/RateLimiterDummyService.java @@ -0,0 +1,66 @@ +package io.github.resilience4j; + +import io.github.resilience4j.ratelimiter.annotation.RateLimiter; +import io.reactivex.*; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.concurrent.CompletionStage; + +@Component +public class RateLimiterDummyService implements TestDummyService { + @Override + @RateLimiter(name = BACKEND, recovery = "recovery") + public String sync() { + return syncError(); + } + + @Override + @RateLimiter(name = BACKEND, recovery = "completionStageRecovery") + public CompletionStage async() { + return asyncError(); + } + + @Override + @RateLimiter(name = BACKEND, recovery = "fluxRecovery") + public Flux flux() { + return fluxError(); + } + + @Override + @RateLimiter(name = BACKEND, recovery = "monoRecovery") + public Mono mono(String parameter) { + return monoError(parameter); + } + + @Override + @RateLimiter(name = BACKEND, recovery = "observableRecovery") + public Observable observable() { + return observableError(); + } + + @Override + @RateLimiter(name = BACKEND, recovery = "singleRecovery") + public Single single() { + return singleError(); + } + + @Override + @RateLimiter(name = BACKEND, recovery = "completableRecovery") + public Completable completable() { + return completableError(); + } + + @Override + @RateLimiter(name = BACKEND, recovery = "maybeRecovery") + public Maybe maybe() { + return maybeError(); + } + + @Override + @RateLimiter(name = BACKEND, recovery = "flowableRecovery") + public Flowable flowable() { + return flowableError(); + } +} diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/RetryDummyService.java b/resilience4j-spring/src/test/java/io/github/resilience4j/RetryDummyService.java new file mode 100644 index 0000000000..1bcdbe642a --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/RetryDummyService.java @@ -0,0 +1,66 @@ +package io.github.resilience4j; + +import io.github.resilience4j.retry.annotation.Retry; +import io.reactivex.*; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.concurrent.CompletionStage; + +@Component +public class RetryDummyService implements TestDummyService { + @Override + @Retry(name = BACKEND, recovery = "recovery") + public String sync() { + return syncError(); + } + + @Override + @Retry(name = BACKEND, recovery = "completionStageRecovery") + public CompletionStage async() { + return asyncError(); + } + + @Override + @Retry(name = BACKEND, recovery = "fluxRecovery") + public Flux flux() { + return fluxError(); + } + + @Override + @Retry(name = BACKEND, recovery = "monoRecovery") + public Mono mono(String parameter) { + return monoError(parameter); + } + + @Override + @Retry(name = BACKEND, recovery = "observableRecovery") + public Observable observable() { + return observableError(); + } + + @Override + @Retry(name = BACKEND, recovery = "singleRecovery") + public Single single() { + return singleError(); + } + + @Override + @Retry(name = BACKEND, recovery = "completableRecovery") + public Completable completable() { + return completableError(); + } + + @Override + @Retry(name = BACKEND, recovery = "maybeRecovery") + public Maybe maybe() { + return maybeError(); + } + + @Override + @Retry(name = BACKEND, recovery = "flowableRecovery") + public Flowable flowable() { + return flowableError(); + } +} diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/TestApplication.java b/resilience4j-spring/src/test/java/io/github/resilience4j/TestApplication.java new file mode 100644 index 0000000000..c71291e8ee --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/TestApplication.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 lespinsideg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j; + +import io.github.resilience4j.bulkhead.BulkheadRegistry; +import io.github.resilience4j.bulkhead.configure.BulkheadConfigurationProperties; +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import io.github.resilience4j.circuitbreaker.configure.CircuitBreakerConfigurationProperties; +import io.github.resilience4j.ratelimiter.RateLimiterRegistry; +import io.github.resilience4j.ratelimiter.configure.RateLimiterConfigurationProperties; +import io.github.resilience4j.retry.RetryRegistry; +import io.github.resilience4j.retry.configure.RetryConfigurationProperties; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@SpringBootApplication +@Configuration +public class TestApplication { + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + + @Bean + public BulkheadRegistry bulkheadRegistry() { + return BulkheadRegistry.ofDefaults(); + } + + @Bean + public BulkheadConfigurationProperties bulkheadConfigurationProperties() { + return new BulkheadConfigurationProperties(); + } + + @Bean + public CircuitBreakerRegistry circuitBreakerRegistry() { + return CircuitBreakerRegistry.ofDefaults(); + } + + @Bean + public CircuitBreakerConfigurationProperties circuitBreakerConfigurationProperties() { + return new CircuitBreakerConfigurationProperties(); + } + + @Bean + public RateLimiterRegistry rateLimiterRegistry() { + return RateLimiterRegistry.ofDefaults(); + } + + @Bean + public RateLimiterConfigurationProperties rateLimiterConfigurationProperties() { + return new RateLimiterConfigurationProperties(); + } + + @Bean + public RetryRegistry retryRegistry() { + return RetryRegistry.ofDefaults(); + } + + @Bean + public RetryConfigurationProperties retryConfigurationProperties() { + return new RetryConfigurationProperties(); + } +} diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/TestDummyService.java b/resilience4j-spring/src/test/java/io/github/resilience4j/TestDummyService.java new file mode 100644 index 0000000000..31fa638837 --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/TestDummyService.java @@ -0,0 +1,112 @@ +/* + * Copyright 2019 lespinsideg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j; + +import io.reactivex.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +public interface TestDummyService { + String BACKEND = "backendA"; + + String sync(); + CompletionStage async(); + Flux flux(); + Mono mono(String parameter); + Observable observable(); + Single single(); + Completable completable(); + Maybe maybe(); + Flowable flowable(); + + default String syncError() { + throw new RuntimeException("Test"); + } + + default CompletionStage asyncError() { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new RuntimeException("Test")); + + return future; + } + + default Flux fluxError() { + return Flux.error(new RuntimeException("Test")); + } + + default Mono monoError(String parameter) { + return Mono.error(new RuntimeException("Test")); + } + + default Observable observableError() { + return Observable.error(new RuntimeException("Test")); + } + + default Single singleError() { + return Single.error(new RuntimeException("Test")); + } + + default Completable completableError() { + return Completable.error(new RuntimeException("Test")); + } + + default Maybe maybeError() { + return Maybe.error(new RuntimeException("Test")); + } + + default Flowable flowableError() { + return Flowable.error(new RuntimeException("Test")); + } + + default String recovery(RuntimeException throwable) { + return "recovered"; + } + + default CompletionStage completionStageRecovery(Throwable throwable) { + return CompletableFuture.supplyAsync(() -> "recovered"); + } + + default Flux fluxRecovery(Throwable throwable) { + return Flux.just("recovered"); + } + + default Mono monoRecovery(String parameter, Throwable throwable) { + return Mono.just(parameter); + } + + default Observable observableRecovery(Throwable throwable) { + return Observable.just("recovered"); + } + + default Single singleRecovery(Throwable throwable) { + return Single.just("recovered"); + } + + default Completable completableRecovery(Throwable throwable) { + return Completable.complete(); + } + + default Maybe maybeRecovery(Throwable throwable) { + return Maybe.just("recovered"); + } + + default Flowable flowableRecovery(Throwable throwable) { + return Flowable.just("recovered"); + } +} diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/bulkhead/configure/BulkHeadConfigurationSpringTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/bulkhead/configure/BulkHeadConfigurationSpringTest.java index bc2fa0a806..e2b6e900f6 100644 --- a/resilience4j-spring/src/test/java/io/github/resilience4j/bulkhead/configure/BulkHeadConfigurationSpringTest.java +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/bulkhead/configure/BulkHeadConfigurationSpringTest.java @@ -20,6 +20,7 @@ import java.util.List; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -54,7 +55,7 @@ public void testAllCircuitBreakerConfigurationBeansOverridden() { } @Configuration - @ComponentScan("io.github.resilience4j.bulkhead") + @ComponentScan({"io.github.resilience4j.bulkhead","io.github.resilience4j.recovery"}) public static class ConfigWithOverrides { private BulkheadRegistry bulkheadRegistry; @@ -73,8 +74,9 @@ public BulkheadRegistry bulkheadRegistry() { @Bean public BulkheadAspect bulkheadAspect(BulkheadRegistry bulkheadRegistry, - @Autowired(required = false) List bulkheadAspectExts) { - bulkheadAspect = new BulkheadAspect(bulkheadConfigurationProperties(), bulkheadRegistry, bulkheadAspectExts); + @Autowired(required = false) List bulkheadAspectExts, + RecoveryDecorators recoveryDecorators) { + bulkheadAspect = new BulkheadAspect(bulkheadConfigurationProperties(), bulkheadRegistry, bulkheadAspectExts, recoveryDecorators); return bulkheadAspect; } diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/bulkhead/configure/BulkheadRecoveryTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/bulkhead/configure/BulkheadRecoveryTest.java new file mode 100644 index 0000000000..f9d7ea08a0 --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/bulkhead/configure/BulkheadRecoveryTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 lespinsideg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.bulkhead.configure; + +import io.github.resilience4j.TestApplication; +import io.github.resilience4j.TestDummyService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = TestApplication.class) +public class BulkheadRecoveryTest { + @Autowired + @Qualifier("bulkheadDummyService") + TestDummyService testDummyService; + + @Test + public void testRecovery() { + assertThat(testDummyService.sync()).isEqualTo("recovered"); + } + + @Test + public void testAsyncRecovery() throws Exception { + assertThat(testDummyService.async().toCompletableFuture().get(5, TimeUnit.SECONDS)).isEqualTo("recovered"); + } + + @Test + public void testMonoRecovery() { + assertThat(testDummyService.mono("test").block()).isEqualTo("test"); + } + + @Test + public void testFluxRecovery() { + assertThat(testDummyService.flux().blockFirst()).isEqualTo("recovered"); + } + + @Test + public void testObservableRecovery() { + assertThat(testDummyService.observable().blockingFirst()).isEqualTo("recovered"); + } + + @Test + public void testSingleRecovery() { + assertThat(testDummyService.single().blockingGet()).isEqualTo("recovered"); + } + + @Test + public void testCompletableRecovery() { + assertThat(testDummyService.completable().blockingGet()).isNull(); + } + + @Test + public void testMaybeRecovery() { + assertThat(testDummyService.maybe().blockingGet()).isEqualTo("recovered"); + } + + @Test + public void testFlowableRecovery() { + assertThat(testDummyService.flowable().blockingFirst()).isEqualTo("recovered"); + } +} \ No newline at end of file diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerRecoveryTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerRecoveryTest.java new file mode 100644 index 0000000000..8a5e8f2fda --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerRecoveryTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 lespinsideg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.circuitbreaker.configure; + +import io.github.resilience4j.TestApplication; +import io.github.resilience4j.TestDummyService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = TestApplication.class) +public class CircuitBreakerRecoveryTest { + @Autowired + @Qualifier("circuitBreakerDummyService") + TestDummyService testDummyService; + + @Test + public void testRecovery() { + assertThat(testDummyService.sync()).isEqualTo("recovered"); + } + + @Test + public void testAsyncRecovery() throws Exception { + assertThat(testDummyService.async().toCompletableFuture().get(5, TimeUnit.SECONDS)).isEqualTo("recovered"); + } + + @Test + public void testMonoRecovery() { + assertThat(testDummyService.mono("test").block()).isEqualTo("test"); + } + + @Test + public void testFluxRecovery() { + assertThat(testDummyService.flux().blockFirst()).isEqualTo("recovered"); + } + + @Test + public void testObservableRecovery() { + assertThat(testDummyService.observable().blockingFirst()).isEqualTo("recovered"); + } + + @Test + public void testSingleRecovery() { + assertThat(testDummyService.single().blockingGet()).isEqualTo("recovered"); + } + + @Test + public void testCompletableRecovery() { + assertThat(testDummyService.completable().blockingGet()).isNull(); + } + + @Test + public void testMaybeRecovery() { + assertThat(testDummyService.maybe().blockingGet()).isEqualTo("recovered"); + } + + @Test + public void testFlowableRecovery() { + assertThat(testDummyService.flowable().blockingFirst()).isEqualTo("recovered"); + } +} \ No newline at end of file diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/ratelimiter/configure/RateLimiterConfigurationSpringTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/ratelimiter/configure/RateLimiterConfigurationSpringTest.java index 002dc9686a..7a59d93628 100644 --- a/resilience4j-spring/src/test/java/io/github/resilience4j/ratelimiter/configure/RateLimiterConfigurationSpringTest.java +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/ratelimiter/configure/RateLimiterConfigurationSpringTest.java @@ -20,6 +20,7 @@ import java.util.List; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -54,7 +55,7 @@ public void testAllCircuitBreakerConfigurationBeansOverridden() { } @Configuration - @ComponentScan("io.github.resilience4j.ratelimiter") + @ComponentScan({"io.github.resilience4j.ratelimiter","io.github.resilience4j.recovery"}) public static class ConfigWithOverrides { private RateLimiterRegistry rateLimiterRegistry; @@ -73,8 +74,9 @@ public RateLimiterRegistry rateLimiterRegistry() { @Bean public RateLimiterAspect rateLimiterAspect(RateLimiterRegistry rateLimiterRegistry, - @Autowired(required = false) List rateLimiterAspectExts) { - rateLimiterAspect = new RateLimiterAspect(rateLimiterRegistry, rateLimiterConfigurationProperties(), rateLimiterAspectExts); + @Autowired(required = false) List rateLimiterAspectExts, + RecoveryDecorators recoveryDecorators) { + rateLimiterAspect = new RateLimiterAspect(rateLimiterRegistry, rateLimiterConfigurationProperties(), rateLimiterAspectExts, recoveryDecorators); return rateLimiterAspect; } diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/ratelimiter/configure/RateLimiterRecoveryTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/ratelimiter/configure/RateLimiterRecoveryTest.java new file mode 100644 index 0000000000..c6c2311f04 --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/ratelimiter/configure/RateLimiterRecoveryTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 lespinsideg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.ratelimiter.configure; + +import io.github.resilience4j.TestApplication; +import io.github.resilience4j.TestDummyService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = TestApplication.class) +public class RateLimiterRecoveryTest { + @Autowired + @Qualifier("rateLimiterDummyService") + TestDummyService testDummyService; + + @Test + public void testRecovery() { + assertThat(testDummyService.sync()).isEqualTo("recovered"); + } + + @Test + public void testAsyncRecovery() throws Exception { + assertThat(testDummyService.async().toCompletableFuture().get(5, TimeUnit.SECONDS)).isEqualTo("recovered"); + } + + @Test + public void testMonoRecovery() { + assertThat(testDummyService.mono("test").block()).isEqualTo("test"); + } + + @Test + public void testFluxRecovery() { + assertThat(testDummyService.flux().blockFirst()).isEqualTo("recovered"); + } + + @Test + public void testObservableRecovery() { + assertThat(testDummyService.observable().blockingFirst()).isEqualTo("recovered"); + } + + @Test + public void testSingleRecovery() { + assertThat(testDummyService.single().blockingGet()).isEqualTo("recovered"); + } + + @Test + public void testCompletableRecovery() { + assertThat(testDummyService.completable().blockingGet()).isNull(); + } + + @Test + public void testMaybeRecovery() { + assertThat(testDummyService.maybe().blockingGet()).isEqualTo("recovered"); + } + + @Test + public void testFlowableRecovery() { + assertThat(testDummyService.flowable().blockingFirst()).isEqualTo("recovered"); + } +} \ No newline at end of file diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/recovery/RecoveryMethodTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/recovery/RecoveryMethodTest.java new file mode 100644 index 0000000000..e2cd426ddc --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/recovery/RecoveryMethodTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2019 Kyuhyen Hwang + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.recovery; + +import org.junit.Test; + +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Java6Assertions.assertThatThrownBy; + +@SuppressWarnings({"WeakerAccess", "unused"}) +public class RecoveryMethodTest { + @Test + public void recoverRuntimeExceptionTest() throws Throwable { + RecoveryMethodTest target = new RecoveryMethodTest(); + Method testMethod = target.getClass().getMethod("testMethod", String.class); + RecoveryMethod recoveryMethod = new RecoveryMethod("recovery", testMethod, new Object[]{"test"}, target); + + assertThat(recoveryMethod.recover(new RuntimeException("err"))).isEqualTo("recovered-RuntimeException"); + } + + @Test + public void recoverClosestSuperclassExceptionTest() throws Throwable { + RecoveryMethodTest target = new RecoveryMethodTest(); + Method testMethod = target.getClass().getMethod("testMethod", String.class); + RecoveryMethod recoveryMethod = new RecoveryMethod("recovery", testMethod, new Object[]{"test"}, target); + + assertThat(recoveryMethod.recover(new NumberFormatException("err"))).isEqualTo("recovered-IllegalArgumentException"); + } + + @Test + public void shouldThrowUnrecoverableThrowable() throws Throwable { + RecoveryMethodTest target = new RecoveryMethodTest(); + Method testMethod = target.getClass().getMethod("testMethod", String.class); + RecoveryMethod recoveryMethod = new RecoveryMethod("recovery", testMethod, new Object[]{"test"}, target); + + Throwable unrecoverableThrown = new Throwable("err"); + assertThatThrownBy(() -> recoveryMethod.recover(unrecoverableThrown)).isEqualTo(unrecoverableThrown); + } + + @Test + public void shouldCallPrivateRecoveryMethod() throws Throwable { + RecoveryMethodTest target = new RecoveryMethodTest(); + Method testMethod = target.getClass().getMethod("testMethod", String.class); + RecoveryMethod recoveryMethod = new RecoveryMethod("privateRecovery", testMethod, new Object[]{"test"}, target); + + assertThat(recoveryMethod.recover(new RuntimeException("err"))).isEqualTo("recovered-privateMethod"); + } + + @Test + public void mismatchReturnType_shouldThrowNoSuchMethodException() throws Throwable { + RecoveryMethodTest target = new RecoveryMethodTest(); + Method testMethod = target.getClass().getMethod("testMethod", String.class); + + assertThatThrownBy(() -> new RecoveryMethod("returnMismatchRecovery", testMethod, new Object[]{"test"}, target)) + .isInstanceOf(NoSuchMethodException.class) + .hasMessage("class java.lang.String class io.github.resilience4j.recovery.RecoveryMethodTest.returnMismatchRecovery(class java.lang.String,class java.lang.Throwable)"); + } + + @Test + public void notFoundRecoveryMethod_shouldThrowsNoSuchMethodException() throws Throwable { + RecoveryMethodTest target = new RecoveryMethodTest(); + Method testMethod = target.getClass().getMethod("testMethod", String.class); + + assertThatThrownBy(() -> new RecoveryMethod("noMethod", testMethod, new Object[]{"test"}, target)) + .isInstanceOf(NoSuchMethodException.class) + .hasMessage("class java.lang.String class io.github.resilience4j.recovery.RecoveryMethodTest.noMethod(class java.lang.String,class java.lang.Throwable)"); + } + + public String testMethod(String parameter) { + return null; + } + + public String recovery(String parameter, RuntimeException exception) { + return "recovered-RuntimeException"; + } + + public String recovery(String parameter, IllegalArgumentException exception) { + return "recovered-IllegalArgumentException"; + } + + public Object returnMismatchRecovery(String parameter, RuntimeException exception) { + return "recovered"; + } + + private String privateRecovery(String parameter, RuntimeException exception) { + return "recovered-privateMethod"; + } +} \ No newline at end of file diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/retry/configure/RetryConfigurationSpringTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/retry/configure/RetryConfigurationSpringTest.java index 38451b7d1e..0cf3e015d0 100644 --- a/resilience4j-spring/src/test/java/io/github/resilience4j/retry/configure/RetryConfigurationSpringTest.java +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/retry/configure/RetryConfigurationSpringTest.java @@ -20,6 +20,7 @@ import java.util.List; +import io.github.resilience4j.recovery.RecoveryDecorators; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -54,7 +55,7 @@ public void testAllCircuitBreakerConfigurationBeansOverridden() { } @Configuration - @ComponentScan("io.github.resilience4j.retry") + @ComponentScan({"io.github.resilience4j.retry","io.github.resilience4j.recovery"}) public static class ConfigWithOverrides { private RetryRegistry retryRegistry; @@ -73,8 +74,9 @@ public RetryRegistry retryRegistry() { @Bean public RetryAspect retryAspect(RetryRegistry retryRegistry, - @Autowired(required = false) List retryAspectExts) { - retryAspect = new RetryAspect(retryConfigurationProperties(), retryRegistry, retryAspectExts); + @Autowired(required = false) List retryAspectExts, + RecoveryDecorators recoveryDecorators) { + retryAspect = new RetryAspect(retryConfigurationProperties(), retryRegistry, retryAspectExts, recoveryDecorators); return retryAspect; } diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/retry/configure/RetryRecoveryTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/retry/configure/RetryRecoveryTest.java new file mode 100644 index 0000000000..48c12f06f1 --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/retry/configure/RetryRecoveryTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 lespinsideg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.retry.configure; + +import io.github.resilience4j.TestApplication; +import io.github.resilience4j.TestDummyService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = TestApplication.class) +public class RetryRecoveryTest { + @Autowired + @Qualifier("retryDummyService") + TestDummyService testDummyService; + + @Test + public void testRecovery() { + assertThat(testDummyService.sync()).isEqualTo("recovered"); + } + + @Test + public void testAsyncRecovery() throws Exception { + assertThat(testDummyService.async().toCompletableFuture().get(5, TimeUnit.SECONDS)).isEqualTo("recovered"); + } + + @Test + public void testMonoRecovery() { + assertThat(testDummyService.mono("test").block()).isEqualTo("test"); + } + + @Test + public void testFluxRecovery() { + assertThat(testDummyService.flux().blockFirst()).isEqualTo("recovered"); + } + + @Test + public void testObservableRecovery() { + assertThat(testDummyService.observable().blockingFirst()).isEqualTo("recovered"); + } + + @Test + public void testSingleRecovery() { + assertThat(testDummyService.single().blockingGet()).isEqualTo("recovered"); + } + + @Test + public void testCompletableRecovery() { + assertThat(testDummyService.completable().blockingGet()).isNull(); + } + + @Test + public void testMaybeRecovery() { + assertThat(testDummyService.maybe().blockingGet()).isEqualTo("recovered"); + } + + @Test + public void testFlowableRecovery() { + assertThat(testDummyService.flowable().blockingFirst()).isEqualTo("recovered"); + } +} \ No newline at end of file