Skip to content

Commit 9ebd61c

Browse files
committed
ConcurrentModificationException when querying /v3/api-docs/{group} concurrently for different groups. Fixes #1641.
1 parent 2e91044 commit 9ebd61c

File tree

10 files changed

+84
-65
lines changed

10 files changed

+84
-65
lines changed

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

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

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

146146
/**
147147
* The Open api builder.
@@ -304,7 +304,7 @@ public static void addHiddenRestControllers(String... classes) {
304304
* @return the open api
305305
*/
306306
protected synchronized OpenAPI getOpenApi(Locale locale) {
307-
OpenAPI openApi;
307+
OpenAPI openAPI;
308308
final Locale finalLocale = locale == null ? Locale.getDefault() : locale;
309309
if (openAPIService.getCachedOpenAPI(finalLocale) == null || springDocConfigProperties.isCacheDisabled()) {
310310
Instant start = Instant.now();
@@ -317,33 +317,33 @@ protected synchronized OpenAPI getOpenApi(Locale locale) {
317317

318318
Map<String, Object> findControllerAdvice = openAPIService.getControllerAdviceMap();
319319
// calculate generic responses
320-
openApi = openAPIService.getCalculatedOpenAPI();
320+
openAPI = openAPIService.getCalculatedOpenAPI();
321321
if (OpenApiVersion.OPENAPI_3_1 == springDocConfigProperties.getApiDocs().getVersion())
322-
openApi.openapi(OpenApiVersion.OPENAPI_3_1.getVersion());
322+
openAPI.openapi(OpenApiVersion.OPENAPI_3_1.getVersion());
323323
if (springDocConfigProperties.isDefaultOverrideWithGenericResponse()) {
324324
if (!CollectionUtils.isEmpty(mappingsMap))
325325
findControllerAdvice.putAll(mappingsMap);
326-
responseBuilder.buildGenericResponse(openApi.getComponents(), findControllerAdvice, finalLocale);
326+
responseBuilder.buildGenericResponse(openAPI.getComponents(), findControllerAdvice, finalLocale);
327327
}
328-
getPaths(mappingsMap, finalLocale);
328+
getPaths(mappingsMap, finalLocale, openAPI);
329329

330330
Optional<CloudFunctionProvider> cloudFunctionProviderOptional = springDocProviders.getSpringCloudFunctionProvider();
331331
cloudFunctionProviderOptional.ifPresent(cloudFunctionProvider -> {
332-
List<RouterOperation> routerOperationList = cloudFunctionProvider.getRouterOperations(openApi);
332+
List<RouterOperation> routerOperationList = cloudFunctionProvider.getRouterOperations(openAPI);
333333
if (!CollectionUtils.isEmpty(routerOperationList))
334-
this.calculatePath(routerOperationList, locale);
334+
this.calculatePath(routerOperationList, locale, openAPI);
335335
}
336336
);
337337

338-
if (!CollectionUtils.isEmpty(openApi.getServers()))
338+
if (!CollectionUtils.isEmpty(openAPI.getServers()))
339339
openAPIService.setServersPresent(true);
340-
openAPIService.updateServers(openApi);
340+
openAPIService.updateServers(openAPI);
341341

342342
if (springDocConfigProperties.isRemoveBrokenReferenceDefinitions())
343-
this.removeBrokenReferenceDefinitions(openApi);
343+
this.removeBrokenReferenceDefinitions(openAPI);
344344

345345
// run the optional customisers
346-
List<Server> servers = openApi.getServers();
346+
List<Server> servers = openAPI.getServers();
347347
List<Server> serversCopy = null;
348348
try {
349349
serversCopy = springDocProviders.jsonMapper()
@@ -353,40 +353,43 @@ protected synchronized OpenAPI getOpenApi(Locale locale) {
353353
LOGGER.warn("Json Processing Exception occurred: {}", e.getMessage());
354354
}
355355

356-
openApiLocaleCustomizers.values().forEach(openApiLocaleCustomizer -> openApiLocaleCustomizer.customise(openApi, finalLocale));
357-
openApiCustomisers.ifPresent(apiCustomisers -> apiCustomisers.forEach(openApiCustomiser -> openApiCustomiser.customise(openApi)));
358-
if (!CollectionUtils.isEmpty(openApi.getServers()) && !openApi.getServers().equals(serversCopy))
356+
openApiLocaleCustomizers.values().forEach(openApiLocaleCustomizer -> openApiLocaleCustomizer.customise(openAPI, finalLocale));
357+
openApiCustomisers.ifPresent(apiCustomisers -> apiCustomisers.forEach(openApiCustomiser -> openApiCustomiser.customise(openAPI)));
358+
if (!CollectionUtils.isEmpty(openAPI.getServers()) && !openAPI.getServers().equals(serversCopy))
359359
openAPIService.setServersPresent(true);
360360

361-
openAPIService.setCachedOpenAPI(openApi, finalLocale);
361+
openAPIService.setCachedOpenAPI(openAPI, finalLocale);
362362
openAPIService.resetCalculatedOpenAPI();
363363

364364
LOGGER.info("Init duration for springdoc-openapi is: {} ms",
365365
Duration.between(start, Instant.now()).toMillis());
366366
}
367367
else {
368368
LOGGER.debug("Fetching openApi document from cache");
369-
openApi = openAPIService.updateServers(openAPIService.getCachedOpenAPI(finalLocale));
369+
openAPI = openAPIService.updateServers(openAPIService.getCachedOpenAPI(finalLocale));
370370
}
371-
return openApi;
371+
return openAPI;
372372
}
373373

374374
/**
375375
* Gets paths.
376376
*
377377
* @param findRestControllers the find rest controllers
378378
* @param locale the locale
379+
* @param openAPI the open api
379380
*/
380-
protected abstract void getPaths(Map<String, Object> findRestControllers, Locale locale);
381+
protected abstract void getPaths(Map<String, Object> findRestControllers, Locale locale, OpenAPI openAPI);
381382

382383
/**
383384
* Calculate path.
384385
*
385386
* @param handlerMethod the handler method
386387
* @param routerOperation the router operation
387388
* @param locale the locale
389+
* @param openAPI the open api
388390
*/
389-
protected void calculatePath(HandlerMethod handlerMethod, RouterOperation routerOperation, Locale locale) {
391+
protected void calculatePath(HandlerMethod handlerMethod,
392+
RouterOperation routerOperation, Locale locale, OpenAPI openAPI) {
390393
String operationPath = routerOperation.getPath();
391394
Set<RequestMethod> requestMethods = new HashSet<>(Arrays.asList(routerOperation.getMethods()));
392395
io.swagger.v3.oas.annotations.Operation apiOperation = routerOperation.getOperation();
@@ -395,7 +398,6 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router
395398
String[] headers = routerOperation.getHeaders();
396399
Map<String, String> queryParams = routerOperation.getQueryParams();
397400

398-
OpenAPI openAPI = openAPIService.getCalculatedOpenAPI();
399401
Components components = openAPI.getComponents();
400402
Paths paths = openAPI.getPaths();
401403

@@ -514,8 +516,9 @@ private void buildCallbacks(OpenAPI openAPI, MethodAttributes methodAttributes,
514516
*
515517
* @param routerOperationList the router operation list
516518
* @param locale the locale
519+
* @param openAPI the open api
517520
*/
518-
protected void calculatePath(List<RouterOperation> routerOperationList, Locale locale) {
521+
protected void calculatePath(List<RouterOperation> routerOperationList, Locale locale, OpenAPI openAPI) {
519522
ApplicationContext applicationContext = openAPIService.getContext();
520523
if (!CollectionUtils.isEmpty(routerOperationList)) {
521524
Collections.sort(routerOperationList);
@@ -545,7 +548,7 @@ protected void calculatePath(List<RouterOperation> routerOperationList, Locale l
545548
LOGGER.error(e.getMessage());
546549
}
547550
if (handlerMethod != null && isFilterCondition(handlerMethod, routerOperation.getPath(), routerOperation.getProduces(), routerOperation.getConsumes(), routerOperation.getHeaders()))
548-
calculatePath(handlerMethod, routerOperation, locale);
551+
calculatePath(handlerMethod, routerOperation, locale, openAPI);
549552
}
550553
}
551554
else if (routerOperation.getOperation() != null && StringUtils.isNotBlank(routerOperation.getOperation().operationId()) && isFilterCondition(routerOperation.getPath(), routerOperation.getProduces(), routerOperation.getConsumes(), routerOperation.getHeaders())) {
@@ -616,10 +619,11 @@ protected void calculatePath(RouterOperation routerOperation, Locale locale) {
616619
* @param produces the produces
617620
* @param headers the headers
618621
* @param locale the locale
622+
* @param openAPI the open api
619623
*/
620624
protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
621-
Set<RequestMethod> requestMethods, String[] consumes, String[] produces, String[] headers, Locale locale) {
622-
this.calculatePath(handlerMethod, new RouterOperation(operationPath, requestMethods.toArray(new RequestMethod[requestMethods.size()]), consumes, produces, headers), locale);
625+
Set<RequestMethod> requestMethods, String[] consumes, String[] produces, String[] headers, Locale locale, OpenAPI openAPI) {
626+
this.calculatePath(handlerMethod, new RouterOperation(operationPath, requestMethods.toArray(new RequestMethod[requestMethods.size()]), consumes, produces, headers), locale, openAPI);
623627
}
624628

625629
/**
@@ -628,13 +632,15 @@ protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
628632
* @param beanName the bean name
629633
* @param routerFunctionVisitor the router function visitor
630634
* @param locale the locale
635+
* @param openAPI the open api
631636
*/
632-
protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVisitor routerFunctionVisitor, Locale locale) {
637+
protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVisitor routerFunctionVisitor,
638+
Locale locale, OpenAPI openAPI) {
633639
boolean withRouterOperation = routerFunctionVisitor.getRouterFunctionDatas().stream()
634640
.anyMatch(routerFunctionData -> routerFunctionData.getAttributes().containsKey(OPERATION_ATTRIBUTE));
635641
if (withRouterOperation) {
636642
List<RouterOperation> operationList = routerFunctionVisitor.getRouterFunctionDatas().stream().map(RouterOperation::new).collect(Collectors.toList());
637-
calculatePath(operationList, locale);
643+
calculatePath(operationList, locale, openAPI);
638644
}
639645
else {
640646
List<org.springdoc.core.annotations.RouterOperation> routerOperationList = new ArrayList<>();
@@ -648,11 +654,11 @@ protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVis
648654
else
649655
routerOperationList.addAll(Arrays.asList(routerOperations.value()));
650656
if (routerOperationList.size() == 1)
651-
calculatePath(routerOperationList.stream().map(routerOperation -> new RouterOperation(routerOperation, routerFunctionVisitor.getRouterFunctionDatas().get(0))).collect(Collectors.toList()), locale);
657+
calculatePath(routerOperationList.stream().map(routerOperation -> new RouterOperation(routerOperation, routerFunctionVisitor.getRouterFunctionDatas().get(0))).collect(Collectors.toList()), locale, openAPI);
652658
else {
653659
List<RouterOperation> operationList = routerOperationList.stream().map(RouterOperation::new).collect(Collectors.toList());
654660
mergeRouters(routerFunctionVisitor.getRouterFunctionDatas(), operationList);
655-
calculatePath(operationList, locale);
661+
calculatePath(operationList, locale, openAPI);
656662
}
657663
}
658664
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.ArrayList;
3030
import java.util.Arrays;
3131
import java.util.Collection;
32+
import java.util.Collections;
3233
import java.util.HashMap;
3334
import java.util.LinkedHashMap;
3435
import java.util.List;
@@ -98,7 +99,7 @@ public abstract class AbstractRequestService {
9899
/**
99100
* The constant PARAM_TYPES_TO_IGNORE.
100101
*/
101-
private static final List<Class<?>> PARAM_TYPES_TO_IGNORE = new ArrayList<>();
102+
private static final List<Class<?>> PARAM_TYPES_TO_IGNORE = Collections.synchronizedList(new ArrayList<>());
102103

103104
/**
104105
* The constant ANNOTATIONS_FOR_REQUIRED.

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.lang.reflect.WildcardType;
2929
import java.util.ArrayList;
3030
import java.util.Arrays;
31+
import java.util.Collections;
3132
import java.util.HashMap;
3233
import java.util.List;
3334
import java.util.Locale;
@@ -74,7 +75,7 @@ public class GenericParameterService {
7475
/**
7576
* The constant FILE_TYPES.
7677
*/
77-
private static final List<Class<?>> FILE_TYPES = new ArrayList<>();
78+
private static final List<Class<?>> FILE_TYPES = Collections.synchronizedList(new ArrayList<>());
7879

7980
/**
8081
* The Optional delegating method parameter customizer.

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,9 @@ public void buildGenericResponse(Components components, Map<String, Object> find
239239
apiResponses.forEach(controllerAdviceInfoApiResponseMap::put);
240240
}
241241
}
242-
controllerAdviceInfos.add(controllerAdviceInfo);
242+
synchronized (this){
243+
controllerAdviceInfos.add(controllerAdviceInfo);
244+
}
243245
}
244246
}
245247

@@ -631,7 +633,7 @@ else if (returnType instanceof ParameterizedType) {
631633
* @param beanType the bean type
632634
* @return the generic map response
633635
*/
634-
private Map<String, ApiResponse> getGenericMapResponse(Class<?> beanType) {
636+
private synchronized Map<String, ApiResponse> getGenericMapResponse(Class<?> beanType) {
635637
return controllerAdviceInfos.stream()
636638
.filter(controllerAdviceInfo -> new ControllerAdviceBean(controllerAdviceInfo.getControllerAdvice()).isApplicableToBeanType(beanType))
637639
.map(ControllerAdviceInfo::getApiResponseMap)

springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocAnnotationsUtils.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.lang.reflect.Type;
2828
import java.util.ArrayList;
2929
import java.util.Arrays;
30+
import java.util.Collections;
3031
import java.util.LinkedHashMap;
3132
import java.util.List;
3233
import java.util.Map;
@@ -73,7 +74,7 @@ public class SpringDocAnnotationsUtils extends AnnotationsUtils {
7374
/**
7475
* The constant ANNOTATIOSN_TO_IGNORE.
7576
*/
76-
private static final List<Class> ANNOTATIONS_TO_IGNORE = new ArrayList<>();
77+
private static final List<Class> ANNOTATIONS_TO_IGNORE = Collections.synchronizedList(new ArrayList<>());
7778

7879
static {
7980
ANNOTATIONS_TO_IGNORE.add(Hidden.class);

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import java.util.ArrayList;
2626
import java.util.Arrays;
27+
import java.util.Collections;
2728
import java.util.List;
2829
import java.util.concurrent.Callable;
2930
import java.util.concurrent.CompletionStage;
@@ -40,17 +41,17 @@ public class ConverterUtils {
4041
/**
4142
* The constant RESULT_WRAPPERS_TO_IGNORE.
4243
*/
43-
private static final List<Class<?>> RESULT_WRAPPERS_TO_IGNORE = new ArrayList<>();
44+
private static final List<Class<?>> RESULT_WRAPPERS_TO_IGNORE = Collections.synchronizedList(new ArrayList<>());
4445

4546
/**
4647
* The constant RESPONSE_TYPES_TO_IGNORE.
4748
*/
48-
private static final List<Class<?>> RESPONSE_TYPES_TO_IGNORE = new ArrayList<>();
49+
private static final List<Class<?>> RESPONSE_TYPES_TO_IGNORE = Collections.synchronizedList(new ArrayList<>());
4950

5051
/**
5152
* The constant FLUX_WRAPPERS_TO_IGNORE.
5253
*/
53-
private static final List<Class<?>> FLUX_WRAPPERS_TO_IGNORE = new ArrayList<>();
54+
private static final List<Class<?>> FLUX_WRAPPERS_TO_IGNORE = Collections.synchronizedList(new ArrayList<>());
5455

5556
static {
5657
RESULT_WRAPPERS_TO_IGNORE.add(Callable.class);

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.lang.annotation.Annotation;
2626
import java.lang.reflect.Method;
2727
import java.util.ArrayList;
28+
import java.util.Collections;
2829
import java.util.Iterator;
2930
import java.util.List;
3031
import java.util.stream.Stream;
@@ -45,7 +46,7 @@ public class SchemaPropertyDeprecatingConverter implements ModelConverter {
4546
/**
4647
* The constant DEPRECATED_ANNOTATIONS.
4748
*/
48-
private static final List<Class<? extends Annotation>> DEPRECATED_ANNOTATIONS = new ArrayList<>();
49+
private static final List<Class<? extends Annotation>> DEPRECATED_ANNOTATIONS = Collections.synchronizedList(new ArrayList<>());
4950

5051
static {
5152
DEPRECATED_ANNOTATIONS.add(Deprecated.class);

springdoc-openapi-common/src/test/java/org/springdoc/api/AbstractOpenApiResourceTest.java

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
/*
22
*
33
* *
4-
* * * Copyright 2019-2021 the original author or authors.
54
* * *
6-
* * * Licensed under the Apache License, Version 2.0 (the "License");
7-
* * * you may not use this file except in compliance with the License.
8-
* * * You may obtain a copy of the License at
5+
* * * * Copyright 2019-2022 the original author or authors.
6+
* * * *
7+
* * * * Licensed under the Apache License, Version 2.0 (the "License");
8+
* * * * you may not use this file except in compliance with the License.
9+
* * * * You may obtain a copy of the License at
10+
* * * *
11+
* * * * https://www.apache.org/licenses/LICENSE-2.0
12+
* * * *
13+
* * * * Unless required by applicable law or agreed to in writing, software
14+
* * * * distributed under the License is distributed on an "AS IS" BASIS,
15+
* * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* * * * See the License for the specific language governing permissions and
17+
* * * * limitations under the License.
918
* * *
10-
* * * https://www.apache.org/licenses/LICENSE-2.0
11-
* * *
12-
* * * Unless required by applicable law or agreed to in writing, software
13-
* * * distributed under the License is distributed on an "AS IS" BASIS,
14-
* * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15-
* * * See the License for the specific language governing permissions and
16-
* * * limitations under the License.
1719
* *
1820
*
1921
*/
@@ -293,7 +295,7 @@ private static class EmptyPathsOpenApiResource extends AbstractOpenApiResource {
293295
}
294296

295297
@Override
296-
public void getPaths(Map<String, Object> findRestControllers, Locale locale) {
298+
public void getPaths(Map<String, Object> findRestControllers, Locale locale, OpenAPI openAPI) {
297299
}
298300
}
299301
}

0 commit comments

Comments
 (0)