From b4b3ed818ac7029fcb2474d6fd2e52629cdfb996 Mon Sep 17 00:00:00 2001 From: awinishere Date: Mon, 2 Nov 2026 01:46:32 +0700 Subject: [PATCH] fix: make the deprecated annotations registry thread safe --- .../SchemaPropertyDeprecatingConverter.java | 7 +- ...chemaPropertyDeprecatingConverterTest.java | 143 ++++++++++++++++++ 2 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 springdoc-openapi-starter-common/src/test/java/org/springdoc/core/converters/SchemaPropertyDeprecatingConverterTest.java diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/SchemaPropertyDeprecatingConverter.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/SchemaPropertyDeprecatingConverter.java index 38b7896ab0..02f5e8c6be 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/SchemaPropertyDeprecatingConverter.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/SchemaPropertyDeprecatingConverter.java @@ -28,10 +28,9 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; -import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.stream.Stream; import io.swagger.v3.core.converter.AnnotatedType; @@ -51,7 +50,7 @@ public class SchemaPropertyDeprecatingConverter implements ModelConverter { /** * The constant DEPRECATED_ANNOTATIONS. */ - private static final List> DEPRECATED_ANNOTATIONS = Collections.synchronizedList(new ArrayList<>()); + private static final Set> DEPRECATED_ANNOTATIONS = new CopyOnWriteArraySet<>(); static { DEPRECATED_ANNOTATIONS.add(Deprecated.class); diff --git a/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/converters/SchemaPropertyDeprecatingConverterTest.java b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/converters/SchemaPropertyDeprecatingConverterTest.java new file mode 100644 index 0000000000..2b974ef840 --- /dev/null +++ b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/converters/SchemaPropertyDeprecatingConverterTest.java @@ -0,0 +1,143 @@ +/* + * + * * + * * * + * * * * + * * * * * Copyright 2019-2026 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.springdoc.core.converters; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springdoc.core.utils.SpringDocUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests that the registry of deprecated annotation types can be read while another thread + * registers a type, and that registering an already known type does not grow it. The registry + * was a synchronized list streamed without holding its lock, and it gained a duplicate entry on + * every application context start. + * + * @author awinishere + */ +class SchemaPropertyDeprecatingConverterTest { + + private static final List> TEST_TYPES = List.of( + DeprecatedA.class, DeprecatedB.class, DeprecatedC.class, DeprecatedD.class); + + private final Collection> registry = registry(); + + @AfterEach + void removeTestTypes() { + registry.removeAll(TEST_TYPES); + } + + @Test + void registeringSameTypeDoesNotGrowRegistry() { + int sizeBefore = registry.size(); + + for (int i = 0; i < 100; i++) { + SpringDocUtils.getConfig().addDeprecatedType(Deprecated.class); + } + + assertEquals(sizeBefore, registry.size()); + } + + @Test + void isDeprecatedIsSafeWhileAnotherThreadRegistersTypes() throws Exception { + Method method = TestEndpoint.class.getMethod("endpoint"); + AtomicReference thrown = new AtomicReference<>(); + Thread reader = new Thread(() -> { + try { + while (!Thread.currentThread().isInterrupted()) { + SchemaPropertyDeprecatingConverter.isDeprecated(method); + } + } + catch (Throwable t) { + thrown.set(t); + } + }); + + reader.start(); + try { + for (int i = 0; i < 20_000; i++) { + SpringDocUtils.getConfig().addDeprecatedType(TEST_TYPES.get(i % TEST_TYPES.size())); + } + } + finally { + reader.interrupt(); + reader.join(5000); + } + + assertNull(thrown.get()); + } + + @Test + void deprecatedMethodIsStillDetected() throws Exception { + assertTrue(SchemaPropertyDeprecatingConverter.isDeprecated(TestEndpoint.class.getMethod("endpoint"))); + } + + @SuppressWarnings("unchecked") + private static Collection> registry() { + try { + Field field = SchemaPropertyDeprecatingConverter.class.getDeclaredField("DEPRECATED_ANNOTATIONS"); + field.setAccessible(true); + return (Collection>) field.get(null); + } + catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + + static class TestEndpoint { + + @Deprecated + public void endpoint() { + } + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DeprecatedA { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DeprecatedB { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DeprecatedC { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DeprecatedD { + } +}