Spring WebFlux provides flexible content negotiation to determine the best media type for responses based on client preferences. It also includes comprehensive API versioning support with multiple resolution strategies (header, query parameter, path, media type parameter) and deprecation handling.
The RequestedContentTypeResolver interface determines the requested content types from a request.
@FunctionalInterface
public interface RequestedContentTypeResolver {
// Resolve requested media types from the request
List<MediaType> resolveMediaTypes(ServerWebExchange exchange);
// Default content type when none specified
default List<MediaType> getDefaultMediaTypes() {
return Collections.emptyList();
}
}The RequestedContentTypeResolverBuilder builds a resolver with multiple resolution strategies.
public class RequestedContentTypeResolverBuilder {
public RequestedContentTypeResolverBuilder() { ... }
// Enable header-based resolution (Accept header)
public RequestedContentTypeResolverBuilder headerResolver() { ... }
// Enable parameter-based resolution with default parameter name "format"
public RequestedContentTypeResolverBuilder parameterResolver() { ... }
// Enable parameter-based resolution with custom parameter name
public RequestedContentTypeResolverBuilder parameterResolver(String parameterName) { ... }
// Add media type mapping for parameter values
public RequestedContentTypeResolverBuilder mediaType(String key, MediaType mediaType) { ... }
// Add multiple media type mappings
public RequestedContentTypeResolverBuilder mediaTypes(Map<String, MediaType> mediaTypes) { ... }
// Set fixed content type (ignores client preferences)
public RequestedContentTypeResolverBuilder fixedResolver(MediaType... mediaTypes) { ... }
// Set default content types
public RequestedContentTypeResolverBuilder defaultContentTypes(MediaType... defaultMediaTypes) { ... }
// Build the resolver
public RequestedContentTypeResolver build() { ... }
}Usage:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.reactive.config.WebFluxConfigurer;
@Configuration
public class ContentNegotiationConfig implements WebFluxConfigurer {
@Override
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
builder
.headerResolver()
.parameterResolver("format")
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML)
.defaultContentTypes(MediaType.APPLICATION_JSON);
}
}
// Example requests:
// GET /users - Uses Accept header
// GET /users?format=json - Returns JSON
// GET /users?format=xml - Returns XMLThe HeaderContentTypeResolver resolves content types from the Accept header.
public class HeaderContentTypeResolver implements RequestedContentTypeResolver {
public HeaderContentTypeResolver() { ... }
@Override
public List<MediaType> resolveMediaTypes(ServerWebExchange exchange) { ... }
}Usage:
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
RequestedContentTypeResolver resolver = new HeaderContentTypeResolver();
// Resolves from Accept header:
// Accept: application/json -> [application/json]
// Accept: text/html, application/xml -> [text/html, application/xml]
// Accept: */* -> []The FixedContentTypeResolver returns a fixed list of media types.
public class FixedContentTypeResolver implements RequestedContentTypeResolver {
public FixedContentTypeResolver(List<MediaType> mediaTypes) { ... }
@Override
public List<MediaType> resolveMediaTypes(ServerWebExchange exchange) { ... }
@Override
public List<MediaType> getDefaultMediaTypes() { ... }
}Usage:
import org.springframework.web.reactive.accept.FixedContentTypeResolver;
import org.springframework.http.MediaType;
// Always returns JSON regardless of client preferences
RequestedContentTypeResolver resolver = new FixedContentTypeResolver(
List.of(MediaType.APPLICATION_JSON)
);The ParameterContentTypeResolver resolves content types from a query parameter.
public class ParameterContentTypeResolver implements RequestedContentTypeResolver {
public static final String DEFAULT_PARAMETER_NAME = "format";
public ParameterContentTypeResolver(Map<String, MediaType> mediaTypes) { ... }
// Set parameter name
public void setParameterName(String parameterName) { ... }
// Get parameter name
public String getParameterName() { ... }
@Override
public List<MediaType> resolveMediaTypes(ServerWebExchange exchange) { ... }
}Usage:
import org.springframework.web.reactive.accept.ParameterContentTypeResolver;
import org.springframework.http.MediaType;
import java.util.Map;
Map<String, MediaType> mediaTypes = Map.of(
"json", MediaType.APPLICATION_JSON,
"xml", MediaType.APPLICATION_XML,
"html", MediaType.TEXT_HTML
);
ParameterContentTypeResolver resolver = new ParameterContentTypeResolver(mediaTypes);
resolver.setParameterName("format");
// Example requests:
// GET /users?format=json -> [application/json]
// GET /users?format=xml -> [application/xml]
// GET /users?format=html -> [text/html]The ApiVersionResolver interface extracts API version from requests.
@FunctionalInterface
public interface ApiVersionResolver {
// Resolve API version from the request
String resolveVersion(ServerWebExchange exchange);
}The HeaderApiVersionResolver extracts version from a request header.
public class HeaderApiVersionResolver implements ApiVersionResolver {
public static final String DEFAULT_HEADER_NAME = "API-Version";
public HeaderApiVersionResolver() { ... }
public HeaderApiVersionResolver(String headerName) { ... }
// Set header name
public void setHeaderName(String headerName) { ... }
// Get header name
public String getHeaderName() { ... }
@Override
public String resolveVersion(ServerWebExchange exchange) { ... }
}Usage:
import org.springframework.web.reactive.accept.HeaderApiVersionResolver;
// Default header "API-Version"
ApiVersionResolver resolver = new HeaderApiVersionResolver();
// Custom header
ApiVersionResolver customResolver = new HeaderApiVersionResolver("X-API-Version");
// Example requests:
// GET /users
// API-Version: 1.0
// -> resolves to "1.0"The QueryApiVersionResolver extracts version from a query parameter.
public class QueryApiVersionResolver implements ApiVersionResolver {
public static final String DEFAULT_PARAMETER_NAME = "version";
public QueryApiVersionResolver() { ... }
public QueryApiVersionResolver(String parameterName) { ... }
// Set parameter name
public void setParameterName(String parameterName) { ... }
// Get parameter name
public String getParameterName() { ... }
@Override
public String resolveVersion(ServerWebExchange exchange) { ... }
}Usage:
import org.springframework.web.reactive.accept.QueryApiVersionResolver;
// Default parameter "version"
ApiVersionResolver resolver = new QueryApiVersionResolver();
// Custom parameter
ApiVersionResolver customResolver = new QueryApiVersionResolver("api-version");
// Example requests:
// GET /users?version=1.0 -> resolves to "1.0"
// GET /users?api-version=2.0 -> resolves to "2.0"The PathApiVersionResolver extracts version from the request path.
public class PathApiVersionResolver implements ApiVersionResolver {
public PathApiVersionResolver(String pattern) { ... }
// Get pattern
public String getPattern() { ... }
@Override
public String resolveVersion(ServerWebExchange exchange) { ... }
}Usage:
import org.springframework.web.reactive.accept.PathApiVersionResolver;
// Extract version from path segment
ApiVersionResolver resolver = new PathApiVersionResolver("/v{version}/**");
// Example requests:
// GET /v1/users -> resolves to "1"
// GET /v1.0/users -> resolves to "1.0"
// GET /v2/products -> resolves to "2"
// Different pattern
ApiVersionResolver resolver2 = new PathApiVersionResolver("/api/{version}/**");
// GET /api/1.0/users -> resolves to "1.0"The MediaTypeParamApiVersionResolver extracts version from a media type parameter.
public class MediaTypeParamApiVersionResolver implements ApiVersionResolver {
public static final String DEFAULT_PARAMETER_NAME = "v";
public MediaTypeParamApiVersionResolver() { ... }
public MediaTypeParamApiVersionResolver(String parameterName) { ... }
// Set parameter name
public void setParameterName(String parameterName) { ... }
// Get parameter name
public String getParameterName() { ... }
@Override
public String resolveVersion(ServerWebExchange exchange) { ... }
}Usage:
import org.springframework.web.reactive.accept.MediaTypeParamApiVersionResolver;
// Default parameter "v"
ApiVersionResolver resolver = new MediaTypeParamApiVersionResolver();
// Custom parameter
ApiVersionResolver customResolver = new MediaTypeParamApiVersionResolver("version");
// Example requests:
// GET /users
// Accept: application/json;v=1.0
// -> resolves to "1.0"
// GET /users
// Accept: application/vnd.myapi+json;version=2.0
// -> resolves to "2.0"The ApiVersionStrategy interface defines how versions are parsed and validated.
public interface ApiVersionStrategy {
// Check if version string is valid
boolean isValid(String version);
// Check if version is deprecated
boolean isDeprecated(String version);
// Compare two versions
int compare(String version1, String version2);
// Get latest version
String getLatestVersion();
}The DefaultApiVersionStrategy supports fixed versions (e.g., "1.0") and baseline versions (e.g., "1.0+").
public class DefaultApiVersionStrategy implements ApiVersionStrategy {
public DefaultApiVersionStrategy() { ... }
// Add supported version
public DefaultApiVersionStrategy addVersion(String version) { ... }
// Add supported versions
public DefaultApiVersionStrategy addVersions(String... versions) { ... }
// Add deprecated version
public DefaultApiVersionStrategy addDeprecatedVersion(String version) { ... }
// Add deprecated versions
public DefaultApiVersionStrategy addDeprecatedVersions(String... versions) { ... }
// Set latest version
public DefaultApiVersionStrategy setLatestVersion(String latestVersion) { ... }
@Override
public boolean isValid(String version) { ... }
@Override
public boolean isDeprecated(String version) { ... }
@Override
public int compare(String version1, String version2) { ... }
@Override
public String getLatestVersion() { ... }
}Usage:
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;
DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy()
.addVersions("1.0", "1.1", "2.0", "2.1")
.addDeprecatedVersions("1.0", "1.1")
.setLatestVersion("2.1");
// Check validity
strategy.isValid("2.0"); // true
strategy.isValid("3.0"); // false
// Check deprecation
strategy.isDeprecated("1.0"); // true
strategy.isDeprecated("2.0"); // false
// Compare versions
strategy.compare("1.0", "2.0"); // negative
strategy.compare("2.1", "2.0"); // positive
strategy.compare("2.0", "2.0"); // 0
// Baseline versions (match current and future versions)
strategy.addVersion("2.0+");
strategy.isValid("2.0"); // true
strategy.isValid("2.1"); // true
strategy.isValid("2.5"); // true
strategy.isValid("3.0"); // true
strategy.isValid("1.9"); // falseThe ApiVersionDeprecationHandler interface handles deprecated API versions.
public interface ApiVersionDeprecationHandler {
// Handle deprecated version
void handleDeprecatedVersion(String version,
ServerWebExchange exchange);
}The StandardApiVersionDeprecationHandler adds deprecation warning headers to responses.
public class StandardApiVersionDeprecationHandler implements ApiVersionDeprecationHandler {
public static final String DEFAULT_DEPRECATION_HEADER = "Deprecation";
public static final String DEFAULT_SUNSET_HEADER = "Sunset";
public static final String DEFAULT_LINK_HEADER = "Link";
public StandardApiVersionDeprecationHandler() { ... }
// Set deprecation date for version
public void setDeprecationDate(String version, Instant deprecationDate) { ... }
// Set sunset date for version
public void setSunsetDate(String version, Instant sunsetDate) { ... }
// Set documentation link for version
public void setDocumentationLink(String version, String link) { ... }
@Override
public void handleDeprecatedVersion(String version, ServerWebExchange exchange) { ... }
}Usage:
import org.springframework.web.reactive.accept.StandardApiVersionDeprecationHandler;
import java.time.Instant;
StandardApiVersionDeprecationHandler handler = new StandardApiVersionDeprecationHandler();
// Configure deprecation for version 1.0
handler.setDeprecationDate("1.0", Instant.parse("2024-01-01T00:00:00Z"));
handler.setSunsetDate("1.0", Instant.parse("2024-12-31T23:59:59Z"));
handler.setDocumentationLink("1.0", "https://api.example.com/docs/v1/migration");
// When handling a request with version 1.0, adds headers:
// Deprecation: @1704067200
// Sunset: Sun, 31 Dec 2024 23:59:59 GMT
// Link: <https://api.example.com/docs/v1/migration>; rel="deprecation"Configure API versioning in WebFlux applications.
public class ApiVersionConfigurer {
// Add version resolver
public ApiVersionConfigurer addVersionResolver(ApiVersionResolver resolver) { ... }
// Set version strategy
public ApiVersionConfigurer setVersionStrategy(ApiVersionStrategy strategy) { ... }
// Set deprecation handler
public ApiVersionConfigurer setDeprecationHandler(ApiVersionDeprecationHandler handler) { ... }
}Usage in configuration:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.accept.*;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;
@Configuration
@EnableWebFlux
public class ApiVersioningConfig implements WebFluxConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
// Add resolvers (header takes precedence)
configurer
.addVersionResolver(new HeaderApiVersionResolver("API-Version"))
.addVersionResolver(new QueryApiVersionResolver("version"))
.addVersionResolver(new PathApiVersionResolver("/v{version}/**"));
// Configure version strategy
DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy()
.addVersions("1.0", "1.1", "2.0", "2.1", "3.0")
.addDeprecatedVersions("1.0", "1.1")
.setLatestVersion("3.0");
configurer.setVersionStrategy(strategy);
// Configure deprecation handler
StandardApiVersionDeprecationHandler handler = new StandardApiVersionDeprecationHandler();
handler.setDeprecationDate("1.0", Instant.parse("2023-01-01T00:00:00Z"));
handler.setSunsetDate("1.0", Instant.parse("2024-12-31T23:59:59Z"));
handler.setDocumentationLink("1.0", "https://api.example.com/docs/migration");
configurer.setDeprecationHandler(handler);
}
}Access resolved API version in controller methods:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.result.method.annotation.ApiVersionMethodArgumentResolver;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/users")
public class VersionedUserController {
// Version-specific endpoints using @RequestMapping
@GetMapping(produces = "application/json;v=1.0")
public Mono<UserV1> getUserV1() {
return Mono.just(new UserV1());
}
@GetMapping(produces = "application/json;v=2.0")
public Mono<UserV2> getUserV2() {
return Mono.just(new UserV2());
}
// Access version as method parameter
@GetMapping
public Mono<String> getUser(@ApiVersion String version) {
return Mono.just("User data for version: " + version);
}
}
// Using RequestPredicates in functional routing
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
public class VersionedRoutes {
@Bean
public RouterFunction<ServerResponse> routes(UserHandler handler) {
return route()
.GET("/users", version("1.0"), handler::handleV1)
.GET("/users", version("2.0"), handler::handleV2)
.build();
}
}Request condition for API version matching in @RequestMapping.
public class VersionRequestCondition implements RequestCondition<VersionRequestCondition> {
public VersionRequestCondition(String version) { ... }
// Get version
public String getVersion() { ... }
@Override
public VersionRequestCondition combine(VersionRequestCondition other) { ... }
@Override
public VersionRequestCondition getMatchingCondition(ServerWebExchange exchange) { ... }
@Override
public int compareTo(VersionRequestCondition other, ServerWebExchange exchange) { ... }
}import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.accept.*;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import java.time.Instant;
import java.util.Map;
@Configuration
@EnableWebFlux
public class CompleteNegotiationConfig implements WebFluxConfigurer {
@Override
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
builder
// Accept header resolution
.headerResolver()
// Query parameter resolution
.parameterResolver("format")
// Media type mappings
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML)
.mediaType("html", MediaType.TEXT_HTML)
.mediaTypes(Map.of(
"pdf", MediaType.APPLICATION_PDF,
"csv", MediaType.parseMediaType("text/csv")
))
// Default content type
.defaultContentTypes(MediaType.APPLICATION_JSON);
}
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
// Multiple resolution strategies
configurer
.addVersionResolver(new HeaderApiVersionResolver("API-Version"))
.addVersionResolver(new QueryApiVersionResolver("version"))
.addVersionResolver(new PathApiVersionResolver("/api/v{version}/**"))
.addVersionResolver(new MediaTypeParamApiVersionResolver("v"));
// Version strategy
DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy()
.addVersions("1.0", "1.1", "1.2", "2.0", "2.1", "3.0")
.addDeprecatedVersions("1.0", "1.1")
.setLatestVersion("3.0");
configurer.setVersionStrategy(strategy);
// Deprecation handling
StandardApiVersionDeprecationHandler handler = new StandardApiVersionDeprecationHandler();
handler.setDeprecationDate("1.0", Instant.parse("2023-01-01T00:00:00Z"));
handler.setSunsetDate("1.0", Instant.parse("2024-06-30T23:59:59Z"));
handler.setDocumentationLink("1.0", "https://api.example.com/docs/v1-migration");
handler.setDeprecationDate("1.1", Instant.parse("2023-06-01T00:00:00Z"));
handler.setSunsetDate("1.1", Instant.parse("2024-12-31T23:59:59Z"));
handler.setDocumentationLink("1.1", "https://api.example.com/docs/v1.1-migration");
configurer.setDeprecationHandler(handler);
}
}