Skip to content

Commit 425eff2

Browse files
committed
Changes report: ConcurrentModificationException when querying /v3/api-docs/{group} concurrently for different groups. #1641
1 parent 4d7d4d0 commit 425eff2

File tree

12 files changed

+89
-98
lines changed

12 files changed

+89
-98
lines changed

springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java

+37-36
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,12 @@ public abstract class AbstractOpenApiResource extends SpecFilter {
138138
/**
139139
* The constant ADDITIONAL_REST_CONTROLLERS.
140140
*/
141-
private static final List<Class<?>> ADDITIONAL_REST_CONTROLLERS = new ArrayList<>();
141+
private static final List<Class<?>> ADDITIONAL_REST_CONTROLLERS = Collections.synchronizedList(new ArrayList<>());
142142

143143
/**
144144
* The constant HIDDEN_REST_CONTROLLERS.
145145
*/
146-
private static final List<Class<?>> HIDDEN_REST_CONTROLLERS = new ArrayList<>();
146+
private static final List<Class<?>> HIDDEN_REST_CONTROLLERS = Collections.synchronizedList(new ArrayList<>());
147147

148148
/**
149149
* The Open api builder.
@@ -306,11 +306,11 @@ public static void addHiddenRestControllers(String... classes) {
306306
* @return the open api
307307
*/
308308
protected synchronized OpenAPI getOpenApi(Locale locale) {
309-
OpenAPI openApi;
309+
OpenAPI openAPI;
310310
final Locale finalLocale = locale == null ? Locale.getDefault() : locale;
311311
if (openAPIService.getCachedOpenAPI(finalLocale) == null || springDocConfigProperties.isCacheDisabled()) {
312312
Instant start = Instant.now();
313-
openAPIService.build(finalLocale);
313+
openAPI = openAPIService.build(finalLocale);
314314
Map<String, Object> mappingsMap = openAPIService.getMappingsMap().entrySet().stream()
315315
.filter(controller -> (AnnotationUtils.findAnnotation(controller.getValue().getClass(),
316316
Hidden.class) == null))
@@ -319,33 +319,32 @@ protected synchronized OpenAPI getOpenApi(Locale locale) {
319319

320320
Map<String, Object> findControllerAdvice = openAPIService.getControllerAdviceMap();
321321
// calculate generic responses
322-
openApi = openAPIService.getCalculatedOpenAPI();
323322
if (OpenApiVersion.OPENAPI_3_1 == springDocConfigProperties.getApiDocs().getVersion())
324-
openApi.openapi(OpenApiVersion.OPENAPI_3_1.getVersion());
323+
openAPI.openapi(OpenApiVersion.OPENAPI_3_1.getVersion());
325324
if (springDocConfigProperties.isDefaultOverrideWithGenericResponse()) {
326325
if (!CollectionUtils.isEmpty(mappingsMap))
327326
findControllerAdvice.putAll(mappingsMap);
328-
responseBuilder.buildGenericResponse(openApi.getComponents(), findControllerAdvice, finalLocale);
327+
responseBuilder.buildGenericResponse(openAPI.getComponents(), findControllerAdvice, finalLocale);
329328
}
330-
getPaths(mappingsMap, finalLocale);
329+
getPaths(mappingsMap, finalLocale, openAPI);
331330

332331
Optional<CloudFunctionProvider> cloudFunctionProviderOptional = springDocProviders.getSpringCloudFunctionProvider();
333332
cloudFunctionProviderOptional.ifPresent(cloudFunctionProvider -> {
334-
List<RouterOperation> routerOperationList = cloudFunctionProvider.getRouterOperations(openApi);
333+
List<RouterOperation> routerOperationList = cloudFunctionProvider.getRouterOperations(openAPI);
335334
if (!CollectionUtils.isEmpty(routerOperationList))
336-
this.calculatePath(routerOperationList, locale);
335+
this.calculatePath(routerOperationList, locale, openAPI);
337336
}
338337
);
339338

340-
if (!CollectionUtils.isEmpty(openApi.getServers()))
339+
if (!CollectionUtils.isEmpty(openAPI.getServers()))
341340
openAPIService.setServersPresent(true);
342-
openAPIService.updateServers(openApi);
341+
openAPIService.updateServers(openAPI);
343342

344343
if (springDocConfigProperties.isRemoveBrokenReferenceDefinitions())
345-
this.removeBrokenReferenceDefinitions(openApi);
344+
this.removeBrokenReferenceDefinitions(openAPI);
346345

347346
// run the optional customisers
348-
List<Server> servers = openApi.getServers();
347+
List<Server> servers = openAPI.getServers();
349348
List<Server> serversCopy = null;
350349
try {
351350
serversCopy = springDocProviders.jsonMapper()
@@ -355,40 +354,41 @@ protected synchronized OpenAPI getOpenApi(Locale locale) {
355354
LOGGER.warn("Json Processing Exception occurred: {}", e.getMessage());
356355
}
357356

358-
openApiLocaleCustomizers.values().forEach(openApiLocaleCustomizer -> openApiLocaleCustomizer.customise(openApi, finalLocale));
359-
openApiCustomizers.ifPresent(apiCustomisers -> apiCustomisers.forEach(openApiCustomizer -> openApiCustomizer.customise(openApi)));
360-
if (!CollectionUtils.isEmpty(openApi.getServers()) && !openApi.getServers().equals(serversCopy))
357+
openApiLocaleCustomizers.values().forEach(openApiLocaleCustomizer -> openApiLocaleCustomizer.customise(openAPI, finalLocale));
358+
openApiCustomizers.ifPresent(apiCustomisers -> apiCustomisers.forEach(openApiCustomizer -> openApiCustomizer.customise(openAPI)));
359+
if (!CollectionUtils.isEmpty(openAPI.getServers()) && !openAPI.getServers().equals(serversCopy))
361360
openAPIService.setServersPresent(true);
362361

363-
openAPIService.setCachedOpenAPI(openApi, finalLocale);
364-
openAPIService.resetCalculatedOpenAPI();
362+
openAPIService.setCachedOpenAPI(openAPI, finalLocale);
365363

366364
LOGGER.info("Init duration for springdoc-openapi is: {} ms",
367365
Duration.between(start, Instant.now()).toMillis());
368366
}
369367
else {
370368
LOGGER.debug("Fetching openApi document from cache");
371-
openApi = openAPIService.updateServers(openAPIService.getCachedOpenAPI(finalLocale));
369+
openAPI = openAPIService.updateServers(openAPIService.getCachedOpenAPI(finalLocale));
372370
}
373-
return openApi;
371+
return openAPI;
374372
}
375373

376374
/**
377375
* Gets paths.
378376
*
379377
* @param findRestControllers the find rest controllers
380378
* @param locale the locale
379+
* @param openAPI the open api
381380
*/
382-
protected abstract void getPaths(Map<String, Object> findRestControllers, Locale locale);
381+
protected abstract void getPaths(Map<String, Object> findRestControllers, Locale locale, OpenAPI openAPI);
383382

384383
/**
385384
* Calculate path.
386385
*
387386
* @param handlerMethod the handler method
388387
* @param routerOperation the router operation
389388
* @param locale the locale
389+
* @param openAPI the open api
390390
*/
391-
protected void calculatePath(HandlerMethod handlerMethod, RouterOperation routerOperation, Locale locale) {
391+
protected void calculatePath(HandlerMethod handlerMethod, RouterOperation routerOperation, Locale locale, OpenAPI openAPI) {
392392
String operationPath = routerOperation.getPath();
393393
Set<RequestMethod> requestMethods = new HashSet<>(Arrays.asList(routerOperation.getMethods()));
394394
io.swagger.v3.oas.annotations.Operation apiOperation = routerOperation.getOperation();
@@ -397,7 +397,6 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router
397397
String[] headers = routerOperation.getHeaders();
398398
Map<String, String> queryParams = routerOperation.getQueryParams();
399399

400-
OpenAPI openAPI = openAPIService.getCalculatedOpenAPI();
401400
Components components = openAPI.getComponents();
402401
Paths paths = openAPI.getPaths();
403402

@@ -516,8 +515,9 @@ private void buildCallbacks(OpenAPI openAPI, MethodAttributes methodAttributes,
516515
*
517516
* @param routerOperationList the router operation list
518517
* @param locale the locale
518+
* @param openAPI the open api
519519
*/
520-
protected void calculatePath(List<RouterOperation> routerOperationList, Locale locale) {
520+
protected void calculatePath(List<RouterOperation> routerOperationList, Locale locale, OpenAPI openAPI) {
521521
ApplicationContext applicationContext = openAPIService.getContext();
522522
if (!CollectionUtils.isEmpty(routerOperationList)) {
523523
Collections.sort(routerOperationList);
@@ -547,14 +547,14 @@ protected void calculatePath(List<RouterOperation> routerOperationList, Locale l
547547
LOGGER.error(e.getMessage());
548548
}
549549
if (handlerMethod != null && isFilterCondition(handlerMethod, routerOperation.getPath(), routerOperation.getProduces(), routerOperation.getConsumes(), routerOperation.getHeaders()))
550-
calculatePath(handlerMethod, routerOperation, locale);
550+
calculatePath(handlerMethod, routerOperation, locale, openAPI);
551551
}
552552
}
553553
else if (routerOperation.getOperation() != null && StringUtils.isNotBlank(routerOperation.getOperation().operationId()) && isFilterCondition(routerOperation.getPath(), routerOperation.getProduces(), routerOperation.getConsumes(), routerOperation.getHeaders())) {
554-
calculatePath(routerOperation, locale);
554+
calculatePath(routerOperation, locale, openAPI);
555555
}
556556
else if (routerOperation.getOperationModel() != null && StringUtils.isNotBlank(routerOperation.getOperationModel().getOperationId()) && isFilterCondition(routerOperation.getPath(), routerOperation.getProduces(), routerOperation.getConsumes(), routerOperation.getHeaders())) {
557-
calculatePath(routerOperation, locale);
557+
calculatePath(routerOperation, locale, openAPI);
558558
}
559559
}
560560
}
@@ -566,15 +566,14 @@ else if (routerOperation.getOperationModel() != null && StringUtils.isNotBlank(r
566566
* @param routerOperation the router operation
567567
* @param locale the locale
568568
*/
569-
protected void calculatePath(RouterOperation routerOperation, Locale locale) {
569+
protected void calculatePath(RouterOperation routerOperation, Locale locale, OpenAPI openAPI ) {
570570
String operationPath = routerOperation.getPath();
571571
io.swagger.v3.oas.annotations.Operation apiOperation = routerOperation.getOperation();
572572
String[] methodConsumes = routerOperation.getConsumes();
573573
String[] methodProduces = routerOperation.getProduces();
574574
String[] headers = routerOperation.getHeaders();
575575
Map<String, String> queryParams = routerOperation.getQueryParams();
576576

577-
OpenAPI openAPI = openAPIService.getCalculatedOpenAPI();
578577
Paths paths = openAPI.getPaths();
579578
Map<HttpMethod, Operation> operationMap = null;
580579
if (paths.containsKey(operationPath)) {
@@ -620,8 +619,8 @@ protected void calculatePath(RouterOperation routerOperation, Locale locale) {
620619
* @param locale the locale
621620
*/
622621
protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
623-
Set<RequestMethod> requestMethods,String[] consumes, String[] produces, String[] headers, Locale locale) {
624-
this.calculatePath(handlerMethod, new RouterOperation(operationPath, requestMethods.toArray(new RequestMethod[requestMethods.size()]), consumes, produces, headers), locale);
622+
Set<RequestMethod> requestMethods,String[] consumes, String[] produces, String[] headers, Locale locale, OpenAPI openAPI) {
623+
this.calculatePath(handlerMethod, new RouterOperation(operationPath, requestMethods.toArray(new RequestMethod[requestMethods.size()]), consumes, produces, headers), locale, openAPI);
625624
}
626625

627626
/**
@@ -630,13 +629,15 @@ protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
630629
* @param beanName the bean name
631630
* @param routerFunctionVisitor the router function visitor
632631
* @param locale the locale
632+
* @param openAPI the open api
633633
*/
634-
protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVisitor routerFunctionVisitor, Locale locale) {
634+
protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVisitor routerFunctionVisitor,
635+
Locale locale, OpenAPI openAPI ) {
635636
boolean withRouterOperation = routerFunctionVisitor.getRouterFunctionDatas().stream()
636637
.anyMatch(routerFunctionData -> routerFunctionData.getAttributes().containsKey(OPERATION_ATTRIBUTE));
637638
if (withRouterOperation) {
638639
List<RouterOperation> operationList = routerFunctionVisitor.getRouterFunctionDatas().stream().map(RouterOperation::new).collect(Collectors.toList());
639-
calculatePath(operationList, locale);
640+
calculatePath(operationList, locale, openAPI);
640641
}
641642
else {
642643
List<org.springdoc.core.annotations.RouterOperation> routerOperationList = new ArrayList<>();
@@ -650,11 +651,11 @@ protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVis
650651
else
651652
routerOperationList.addAll(Arrays.asList(routerOperations.value()));
652653
if (routerOperationList.size() == 1)
653-
calculatePath(routerOperationList.stream().map(routerOperation -> new RouterOperation(routerOperation, routerFunctionVisitor.getRouterFunctionDatas().get(0))).collect(Collectors.toList()), locale);
654+
calculatePath(routerOperationList.stream().map(routerOperation -> new RouterOperation(routerOperation, routerFunctionVisitor.getRouterFunctionDatas().get(0))).collect(Collectors.toList()), locale, openAPI);
654655
else {
655656
List<RouterOperation> operationList = routerOperationList.stream().map(RouterOperation::new).collect(Collectors.toList());
656657
mergeRouters(routerFunctionVisitor.getRouterFunctionDatas(), operationList);
657-
calculatePath(operationList, locale);
658+
calculatePath(operationList, locale, openAPI);
658659
}
659660
}
660661
}

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/ConverterUtils.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import java.util.ArrayList;
2828
import java.util.Arrays;
29+
import java.util.Collections;
2930
import java.util.List;
3031
import java.util.concurrent.Callable;
3132
import java.util.concurrent.CompletionStage;
@@ -42,17 +43,17 @@ public class ConverterUtils {
4243
/**
4344
* The constant RESULT_WRAPPERS_TO_IGNORE.
4445
*/
45-
private static final List<Class<?>> RESULT_WRAPPERS_TO_IGNORE = new ArrayList<>();
46+
private static final List<Class<?>> RESULT_WRAPPERS_TO_IGNORE = Collections.synchronizedList(new ArrayList<>());
4647

4748
/**
4849
* The constant RESPONSE_TYPES_TO_IGNORE.
4950
*/
50-
private static final List<Class<?>> RESPONSE_TYPES_TO_IGNORE = new ArrayList<>();
51+
private static final List<Class<?>> RESPONSE_TYPES_TO_IGNORE = Collections.synchronizedList(new ArrayList<>());
5152

5253
/**
5354
* The constant FLUX_WRAPPERS_TO_IGNORE.
5455
*/
55-
private static final List<Class<?>> FLUX_WRAPPERS_TO_IGNORE = new ArrayList<>();
56+
private static final List<Class<?>> FLUX_WRAPPERS_TO_IGNORE = Collections.synchronizedList(new ArrayList<>());
5657

5758
static {
5859
RESULT_WRAPPERS_TO_IGNORE.add(Callable.class);

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/SchemaPropertyDeprecatingConverter.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.lang.annotation.Annotation;
2828
import java.lang.reflect.Method;
2929
import java.util.ArrayList;
30+
import java.util.Collections;
3031
import java.util.Iterator;
3132
import java.util.List;
3233
import java.util.stream.Stream;
@@ -47,7 +48,7 @@ public class SchemaPropertyDeprecatingConverter implements ModelConverter {
4748
/**
4849
* The constant DEPRECATED_ANNOTATIONS.
4950
*/
50-
private static final List<Class<? extends Annotation>> DEPRECATED_ANNOTATIONS = new ArrayList<>();
51+
private static final List<Class<? extends Annotation>> DEPRECATED_ANNOTATIONS = Collections.synchronizedList(new ArrayList<>());
5152

5253
static {
5354
DEPRECATED_ANNOTATIONS.add(Deprecated.class);

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/GroupedOpenApi.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ public GroupedOpenApi build() {
476476
public GroupedOpenApi addAllOpenApiCustomizer(Collection<? extends OpenApiCustomizer> openApiCustomizerCollection) {
477477
List<OpenApiCustomizer> result = new ArrayList<>();
478478
result.addAll(openApiCustomizerCollection);
479-
result.addAll(openApiCustomizerCollection);
479+
result.addAll(openApiCustomizers);
480480
openApiCustomizers = result;
481481
return this;
482482
}

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.ArrayList;
3232
import java.util.Arrays;
3333
import java.util.Collection;
34+
import java.util.Collections;
3435
import java.util.HashMap;
3536
import java.util.LinkedHashMap;
3637
import java.util.List;
@@ -104,7 +105,7 @@ public abstract class AbstractRequestService {
104105
/**
105106
* The constant PARAM_TYPES_TO_IGNORE.
106107
*/
107-
private static final List<Class<?>> PARAM_TYPES_TO_IGNORE = new ArrayList<>();
108+
private static final List<Class<?>> PARAM_TYPES_TO_IGNORE = Collections.synchronizedList(new ArrayList<>());
108109

109110
/**
110111
* The constant ANNOTATIONS_FOR_REQUIRED.

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericParameterService.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.lang.reflect.WildcardType;
3131
import java.util.ArrayList;
3232
import java.util.Arrays;
33+
import java.util.Collections;
3334
import java.util.HashMap;
3435
import java.util.List;
3536
import java.util.Locale;
@@ -83,7 +84,7 @@ public class GenericParameterService {
8384
/**
8485
* The constant FILE_TYPES.
8586
*/
86-
private static final List<Class<?>> FILE_TYPES = new ArrayList<>();
87+
private static final List<Class<?>> FILE_TYPES = Collections.synchronizedList(new ArrayList<>());
8788

8889
/**
8990
* The Optional delegating method parameter customizer.

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericResponseService.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,9 @@ public void buildGenericResponse(Components components, Map<String, Object> find
248248
apiResponses.forEach(controllerAdviceInfoApiResponseMap::put);
249249
}
250250
}
251-
controllerAdviceInfos.add(controllerAdviceInfo);
251+
synchronized (this) {
252+
controllerAdviceInfos.add(controllerAdviceInfo);
253+
}
252254
}
253255
}
254256

@@ -572,7 +574,8 @@ else if (CollectionUtils.isEmpty(apiResponse.getContent()))
572574
exceptions.add(parameter.getType());
573575
}
574576
}
575-
} else {
577+
}
578+
else {
576579
exceptions.addAll(asList(exceptionHandler.value()));
577580
}
578581
apiResponse.addExtension(EXTENSION_EXCEPTION_CLASSES, exceptions);
@@ -640,9 +643,9 @@ else if (returnType instanceof ParameterizedType) {
640643
* @param beanType the bean type
641644
* @return the generic map response
642645
*/
643-
private Map<String, ApiResponse> getGenericMapResponse(Class<?> beanType) {
646+
private synchronized Map<String, ApiResponse> getGenericMapResponse(Class<?> beanType) {
644647
return controllerAdviceInfos.stream()
645-
.filter(controllerAdviceInfo -> new ControllerAdviceBean(controllerAdviceInfo.getControllerAdvice()).isApplicableToBeanType(beanType))
648+
.filter(controllerAdviceInfo -> new ControllerAdviceBean(controllerAdviceInfo.getControllerAdvice()).isApplicableToBeanType(beanType))
646649
.map(ControllerAdviceInfo::getApiResponseMap)
647650
.collect(LinkedHashMap::new, Map::putAll, Map::putAll);
648651
}

0 commit comments

Comments
 (0)