Skip to content

Please make the customizers support @Order. #3320

Description

@zhujianxintian

Currently, the internal org.springdoc.core.customizers.SpringDocCustomizers is as follows:

public class SpringDocCustomizers implements ApplicationContextAware, InitializingBean {

	/**
	 * The Open api customisers.
	 */
	private final Optional<Set<OpenApiCustomizer>> openApiCustomizers;

	/**
	 * The Operation customizers.
	 */
	private final Optional<Set<OperationCustomizer>> operationCustomizers;
	
	// ... other code ...
	
}

And this is where it is used:

public abstract class AbstractOpenApiResource extends SpecFilter {

	// ... other code ...
	
	protected OpenAPI getOpenApi(String serverBaseUrl, Locale locale) {
		this.reentrantLock.lock();
		try {
			final OpenAPI openAPI;
			final Locale finalLocale = selectLocale(locale);
			if (openAPIService.getCachedOpenAPI(finalLocale) == null || springDocConfigProperties.isCacheDisabled()) {

				// ... code ...

                // here
				openAPIService.getContext().getBeansOfType(OpenApiLocaleCustomizer.class).values().forEach(openApiLocaleCustomizer -> openApiLocaleCustomizer.customise(openAPI, finalLocale));
				springDocCustomizers.getOpenApiCustomizers().ifPresent(apiCustomizers -> apiCustomizers.forEach(openApiCustomizer -> openApiCustomizer.customise(openAPI)));

				// ... code ...
			}
			else {
				LOGGER.debug("Fetching openApi document from cache");
				openAPI = openAPIService.getCachedOpenAPI(finalLocale);
				openAPIService.updateServers(serverBaseUrl, openAPI);
			}
			return openAPI;
		}
		finally {
			SpringDocAnnotationsUtils.clearCache(operationParser.getJavadocProvider());
			this.reentrantLock.unlock();
		}
	}

	// ... other code ...

}

This approach does not support the @Order annotation.

Describe the solution you'd like

I would like to use ObjectProvider<T> instead of Optional<Set<T>>.

import org.springframework.beans.factory.ObjectProvider;

public class SpringDocCustomizers implements ApplicationContextAware, InitializingBean {

    /**
     * The Open api customisers.
     */
    private final ObjectProvider<OpenApiCustomizer> openApiCustomizers;

    /**
     * The Operation customizers.
     */
    private final ObjectProvider<OperationCustomizer> operationCustomizers;

    // ... other code ...

}
public abstract class AbstractOpenApiResource extends SpecFilter {

	// ... other code ...
	
	protected OpenAPI getOpenApi(String serverBaseUrl, Locale locale) {
		this.reentrantLock.lock();
		try {
			final OpenAPI openAPI;
			final Locale finalLocale = selectLocale(locale);
			if (openAPIService.getCachedOpenAPI(finalLocale) == null || springDocConfigProperties.isCacheDisabled()) {

				// ... code ...

                // here
                openAPIService.getContext().getBeanProvider(OpenApiLocaleCustomizer.class).orderedStream().forEach(customizer -> customizer.customise(openAPI, finalLocale));
                springDocCustomizers.getOpenApiCustomizers().orderedStream().forEach(customizer -> customizer.customise(openAPI));

				// ... code ...
			}
			else {
				LOGGER.debug("Fetching openApi document from cache");
				openAPI = openAPIService.getCachedOpenAPI(finalLocale);
				openAPIService.updateServers(serverBaseUrl, openAPI);
			}
			return openAPI;
		}
		finally {
			SpringDocAnnotationsUtils.clearCache(operationParser.getJavadocProvider());
			this.reentrantLock.unlock();
		}
	}

	// ... other code ...

}

This way, Spring's built-in .orderedStream() can be used to handle the ordering of @Order.

Otherwise, the execution order cannot be guaranteed, because I want to execute the following code after SpringDocSecurityConfiguration.SpringSecurityLoginEndpointConfiguration#springSecurityLoginEndpointCustomizer:

Add example values to the default login endpoint.

@Bean
// Set the lowest priority, execute last, to ensure it runs after SpringDocSecurityConfiguration.SpringSecurityLoginEndpointConfiguration#springSecurityLoginEndpointCustomizer
// Otherwise the login endpoint cannot be found
@Order
OpenApiCustomizer springSecurityLoginEndpointSchemaCustomizer(ApplicationContext applicationContext) {
    FilterChainProxy filterChainProxy = applicationContext.getBean(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME, FilterChainProxy.class);
    return openAPI -> {
        for (SecurityFilterChain filterChain : filterChainProxy.getFilterChains()) {
            Optional<UsernamePasswordAuthenticationFilter> optionalFilter =
                    filterChain.getFilters().stream()
                            .filter(UsernamePasswordAuthenticationFilter.class::isInstance)
                            .map(UsernamePasswordAuthenticationFilter.class::cast)
                            .findAny();
            if (optionalFilter.isPresent()) {
                UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = optionalFilter.get();

                try {
                    RequestMatcher requestMatcher = (RequestMatcher) FieldUtils.readField(
                            usernamePasswordAuthenticationFilter,
                            "requiresAuthenticationRequestMatcher",
                            true
                    );

                    if (requestMatcher instanceof PathPatternRequestMatcher pathPatternRequestMatcher) {
                        String loginPath = getPath(pathPatternRequestMatcher);

                        Map.Entry<String, PathItem> loginPathEntry = openAPI.getPaths().entrySet().stream().filter(entry -> entry.getKey().equals(loginPath)).findAny().orElse(null);

                        if (loginPathEntry == null) {
                            return;
                        }

                        PathItem value = loginPathEntry.getValue();

                        value.readOperations().forEach(operation -> {
                            // Add example values to the login operation
                        });
                    }
                }
                catch (IllegalAccessException | ClassCastException e) {
                    // Exception escaped
                    log.trace(e.getMessage());
                }
            }
        }
    };
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions