diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ExceptionClassifier.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ExceptionClassifier.java index 9846ad2695..4b5626794a 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ExceptionClassifier.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ExceptionClassifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,6 +55,16 @@ private static ExtendedBinaryExceptionClassifier configureDefaultClassifier() { return new ExtendedBinaryExceptionClassifier(classified, true); } + /** + * By default, unmatched types classify as true. Call this method to make the default + * false, and remove types explicitly classified as false. This should be called before + * calling any of the classification modification methods. + * @since 2.8.4 + */ + public void defaultFalse() { + this.classifier = new ExtendedBinaryExceptionClassifier(new HashMap<>(), false); + } + /** * Return the exception classifier. * @return the classifier. @@ -97,20 +107,39 @@ public void setClassifications(Map, Boolean> classifi *
  • {@link NoSuchMethodException}
  • *
  • {@link ClassCastException}
  • * - * All others will be retried. + * All others will be retried, unless {@link #defaultFalse()} has been called. * @param exceptionTypes the exception types. - * @see #removeNotRetryableException(Class) + * @see #removeClassification(Class) * @see #setClassifications(Map, boolean) */ @SafeVarargs @SuppressWarnings("varargs") public final void addNotRetryableExceptions(Class... exceptionTypes) { + add(false, exceptionTypes); + } + + /** + * Add exception types that can be retried. Call this after {@link #defaultFalse()} to + * specify those exception types that should be classified as true. + * All others will be retried, unless {@link #defaultFalse()} has been called. + * @param exceptionTypes the exception types. + * @since 2.8.4 + * @see #removeClassification(Class) + * @see #setClassifications(Map, boolean) + */ + @SafeVarargs + @SuppressWarnings("varargs") + public final void addRetryableExceptions(Class... exceptionTypes) { + add(true, exceptionTypes); + } + + private void add(boolean classified, Class... exceptionTypes) { Assert.notNull(exceptionTypes, "'exceptionTypes' cannot be null"); Assert.noNullElements(exceptionTypes, "'exceptionTypes' cannot contain nulls"); for (Class exceptionType : exceptionTypes) { Assert.isTrue(Exception.class.isAssignableFrom(exceptionType), () -> "exceptionType " + exceptionType + " must be an Exception"); - this.classifier.getClassified().put(exceptionType, false); + this.classifier.getClassified().put(exceptionType, classified); } } @@ -125,13 +154,38 @@ public final void addNotRetryableExceptions(Class... except *
  • {@link NoSuchMethodException}
  • *
  • {@link ClassCastException}
  • * - * All others will be retried. + * All others will be retried, unless {@link #defaultFalse()} has been called. * @param exceptionType the exception type. * @return true if the removal was successful. + * @deprecated in favor of {@link #removeClassification(Class)} * @see #addNotRetryableExceptions(Class...) * @see #setClassifications(Map, boolean) + * @see #defaultFalse() */ + @Deprecated public boolean removeNotRetryableException(Class exceptionType) { + return this.removeClassification(exceptionType); + } + + /** + * Remove an exception type from the configured list. By default, the following + * exceptions will not be retried: + * + * All others will be retried, unless {@link #defaultFalse()} has been called. + * @param exceptionType the exception type. + * @return true if the removal was successful. + * @since 2.8.4 + * @see #addNotRetryableExceptions(Class...) + * @see #setClassifications(Map, boolean) + */ + public boolean removeClassification(Class exceptionType) { return this.classifier.getClassified().remove(exceptionType); } diff --git a/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/DeadLetterPublishingRecovererFactory.java b/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/DeadLetterPublishingRecovererFactory.java index 33b04f1bcf..fa3645558b 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/DeadLetterPublishingRecovererFactory.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/DeadLetterPublishingRecovererFactory.java @@ -143,7 +143,7 @@ protected DeadLetterPublishingRecoverer.HeaderNames getHeaderNames() { recoverer.setSkipSameTopicFatalExceptions(false); this.recovererCustomizer.accept(recoverer); this.fatalExceptions.forEach(recoverer::addNotRetryableExceptions); - this.nonFatalExceptions.forEach(recoverer::removeNotRetryableException); + this.nonFatalExceptions.forEach(recoverer::removeClassification); return recoverer; } diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/DeadLetterPublishingRecovererTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/DeadLetterPublishingRecovererTests.java index cfc4fda86f..056285ecd4 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/DeadLetterPublishingRecovererTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/listener/DeadLetterPublishingRecovererTests.java @@ -559,7 +559,7 @@ void noCircularRoutingIfFatal() { recoverer.addNotRetryableExceptions(IllegalStateException.class); recoverer.accept(record, new IllegalStateException()); verify(template, never()).send(any(ProducerRecord.class)); - recoverer.removeNotRetryableException(IllegalStateException.class); + recoverer.removeClassification(IllegalStateException.class); recoverer.setFailIfSendResultIsError(false); recoverer.accept(record, new IllegalStateException()); verify(template).send(any(ProducerRecord.class)); @@ -580,7 +580,7 @@ void doNotSkipCircularFatalIfSet() { recoverer.addNotRetryableExceptions(IllegalStateException.class); recoverer.accept(record, new IllegalStateException()); verify(template, times(2)).send(any(ProducerRecord.class)); - recoverer.removeNotRetryableException(IllegalStateException.class); + recoverer.removeClassification(IllegalStateException.class); recoverer.setFailIfSendResultIsError(false); recoverer.accept(record, new IllegalStateException()); verify(template, times(3)).send(any(ProducerRecord.class)); diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/ExceptionClassifierTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/ExceptionClassifierTests.java new file mode 100644 index 0000000000..0ef5688e50 --- /dev/null +++ b/spring-kafka/src/test/java/org/springframework/kafka/listener/ExceptionClassifierTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.kafka.listener; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * @author Gary Russell + * @since 2.8.4 + * + */ +public class ExceptionClassifierTests { + + @Test + void testDefault() { + ExceptionClassifier ec = new ExceptionClassifier() { + }; + assertThat(ec.getClassifier().classify(new Exception())).isTrue(); + assertThat(ec.getClassifier().classify(new ClassCastException())).isFalse(); + ec.removeClassification(ClassCastException.class); + assertThat(ec.getClassifier().classify(new ClassCastException())).isTrue(); + assertThat(ec.getClassifier().classify(new IllegalStateException())).isTrue(); + ec.addNotRetryableExceptions(IllegalStateException.class); + assertThat(ec.getClassifier().classify(new IllegalStateException())).isFalse(); + } + + @Test + void testDefaultFalse() { + ExceptionClassifier ec = new ExceptionClassifier() { + }; + assertThat(ec.getClassifier().classify(new Exception())).isTrue(); + ec.defaultFalse(); + assertThat(ec.getClassifier().classify(new Exception())).isFalse(); + assertThat(ec.getClassifier().classify(new IllegalStateException())).isFalse(); + ec.addRetryableExceptions(IllegalStateException.class); + assertThat(ec.getClassifier().classify(new IllegalStateException())).isTrue(); + ec.removeClassification(IllegalStateException.class); + assertThat(ec.getClassifier().classify(new IllegalStateException())).isFalse(); + } + +}