Starter for building WebFlux applications using Spring Framework's Reactive Web support
—
Spring Boot WebFlux provides comprehensive configuration options through Spring Boot properties, programmatic configuration classes, and customization interfaces. This enables fine-tuning of reactive web behavior, static resources, server settings, and WebFlux-specific features.
@ConfigurationProperties("spring.webflux")
public class WebFluxProperties {
private String basePath;
private String staticPathPattern = "/**";
private String webjarsPathPattern = "/webjars/**";
private final Format format = new Format();
private final Problemdetails problemdetails = new Problemdetails();
public static class Format {
private String date;
private String time;
private String dateTime = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
}
public static class Problemdetails {
private boolean enabled = false;
}
}@ConfigurationProperties("spring.http.codecs")
public class HttpCodecsProperties {
private DataSize maxInMemorySize = DataSize.ofBytes(262144); // 256KB
private boolean logRequestDetails = false;
public DataSize getMaxInMemorySize();
public void setMaxInMemorySize(DataSize maxInMemorySize);
public boolean isLogRequestDetails();
public void setLogRequestDetails(boolean logRequestDetails);
}public interface WebFluxConfigurer {
default void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {}
default void addCorsMappings(CorsRegistry registry) {}
default void configurePathMatching(PathMatchConfigurer configurer) {}
default void addResourceHandlers(ResourceHandlerRegistry registry) {}
default void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {}
default void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {}
default void addFormatters(FormatterRegistry registry) {}
default Validator getValidator() { return null; }
default MessageCodesResolver getMessageCodesResolver() { return null; }
default void configureViewResolvers(ViewResolverRegistry registry) {}
}@Configuration
@EnableWebFlux
public class WebFluxConfiguration implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024); // 2MB
configurer.defaultCodecs().enableLoggingRequestDetails(true);
// Custom JSON encoder/decoder
configurer.defaultCodecs().jackson2JsonEncoder(customJsonEncoder());
configurer.defaultCodecs().jackson2JsonDecoder(customJsonDecoder());
// Custom string encoder for CSV
configurer.customCodecs().register(new CsvEncoder());
configurer.customCodecs().register(new CsvDecoder());
// Configure multipart reader with DataBuffer handling
configurer.defaultCodecs().multipartReader(multipartHttpMessageReader());
// Configure server-sent events with streaming
configurer.defaultCodecs().serverSentEventHttpMessageReader(sseReader());
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(7)));
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:./uploads/")
.setCacheControl(CacheControl.noCache());
}
@Override
public void configurePathMatching(PathMatchConfigurer configurer) {
configurer.setUseCaseSensitiveMatch(false)
.setUseTrailingSlashMatch(true)
.addPathPrefix("/api/v1", HandlerTypePredicate.forAnnotation(RestController.class));
}
@Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
configurer.addCustomResolver(new CustomArgumentResolver());
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToDateConverter());
registry.addFormatter(new LocalDateTimeFormatter());
}
@Override
public Validator getValidator() {
return new LocalValidatorFactoryBean();
}
@Bean
public Jackson2JsonEncoder customJsonEncoder() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.registerModule(new JavaTimeModule());
return new Jackson2JsonEncoder(mapper);
}
@Bean
public Jackson2JsonDecoder customJsonDecoder() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new JavaTimeModule());
return new Jackson2JsonDecoder(mapper);
}
}public interface WebSessionManager {
Mono<WebSession> getSession(ServerWebExchange exchange);
default WebSessionIdResolver getSessionIdResolver() {
return new CookieWebSessionIdResolver();
}
}public class DefaultWebSessionManager implements WebSessionManager {
private WebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
private WebSessionStore sessionStore = new InMemoryWebSessionStore();
public void setSessionIdResolver(WebSessionIdResolver sessionIdResolver);
public WebSessionIdResolver getSessionIdResolver();
public void setSessionStore(WebSessionStore sessionStore);
public WebSessionStore getSessionStore();
@Override
public Mono<WebSession> getSession(ServerWebExchange exchange);
}public interface WebSessionStore {
Mono<WebSession> createWebSession();
Mono<WebSession> retrieveSession(String sessionId);
Mono<Void> removeSession(String sessionId);
Mono<WebSession> updateLastAccessTime(WebSession webSession);
}public class InMemoryWebSessionStore implements WebSessionStore {
private int maxSessions = 10000;
private Duration maxInactiveInterval = Duration.ofMinutes(30);
public void setMaxSessions(int maxSessions);
public int getMaxSessions();
public void setMaxInactiveInterval(Duration maxInactiveInterval);
public Duration getMaxInactiveInterval();
@Override
public Mono<WebSession> createWebSession();
@Override
public Mono<WebSession> retrieveSession(String sessionId);
@Override
public Mono<Void> removeSession(String sessionId);
@Override
public Mono<WebSession> updateLastAccessTime(WebSession webSession);
}@Configuration
public class SessionConfiguration {
@Bean
public WebSessionManager webSessionManager() {
DefaultWebSessionManager manager = new DefaultWebSessionManager();
// Configure session ID resolver
CookieWebSessionIdResolver resolver = new CookieWebSessionIdResolver();
resolver.setCookieName("JSESSIONID");
resolver.addCookieInitializer(builder -> {
builder.httpOnly(true)
.secure(true)
.sameSite("Strict")
.maxAge(Duration.ofHours(24));
});
manager.setSessionIdResolver(resolver);
// Configure session store
InMemoryWebSessionStore store = new InMemoryWebSessionStore();
store.setMaxSessions(5000);
store.setMaxInactiveInterval(Duration.ofMinutes(45));
manager.setSessionStore(store);
return manager;
}
}public interface LocaleContextResolver {
LocaleContext resolveLocaleContext(ServerWebExchange exchange);
void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext);
}public class AcceptHeaderLocaleContextResolver implements LocaleContextResolver {
private List<Locale> supportedLocales = new ArrayList<>();
private Locale defaultLocale;
public void setSupportedLocales(List<Locale> locales);
public List<Locale> getSupportedLocales();
public void setDefaultLocale(Locale defaultLocale);
public Locale getDefaultLocale();
@Override
public LocaleContext resolveLocaleContext(ServerWebExchange exchange);
@Override
public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext);
}public class FixedLocaleContextResolver implements LocaleContextResolver {
private Locale defaultLocale = Locale.getDefault();
public void setDefaultLocale(Locale defaultLocale);
public Locale getDefaultLocale();
@Override
public LocaleContext resolveLocaleContext(ServerWebExchange exchange);
@Override
public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext);
}@Configuration
public class LocaleConfiguration {
@Bean
public LocaleContextResolver localeContextResolver() {
AcceptHeaderLocaleContextResolver resolver = new AcceptHeaderLocaleContextResolver();
resolver.setSupportedLocales(Arrays.asList(
Locale.ENGLISH,
Locale.FRENCH,
Locale.GERMAN,
new Locale("es")
));
resolver.setDefaultLocale(Locale.ENGLISH);
return resolver;
}
}public interface WebFilter {
Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);
}public interface WebFilterChain {
Mono<Void> filter(ServerWebExchange exchange);
}public class OrderedHiddenHttpMethodFilter implements WebFilter, Ordered {
public static final int DEFAULT_ORDER = REQUEST_WRAPPER_FILTER_MAX_ORDER - 10000;
private int order = DEFAULT_ORDER;
private String methodParam = "_method";
public void setMethodParam(String methodParam);
public String getMethodParam();
public void setOrder(int order);
@Override
public int getOrder();
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);
}
public class ForwardedHeaderFilter implements WebFilter {
public static final int REQUEST_WRAPPER_FILTER_MAX_ORDER = 0;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);
}@Component
public class RequestLoggingFilter implements WebFilter {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
logger.info("Processing request: {} {}",
request.getMethod(),
request.getURI());
return chain.filter(exchange)
.doOnSuccess(aVoid -> {
ServerHttpResponse response = exchange.getResponse();
logger.info("Response status: {}", response.getStatusCode());
})
.doOnError(throwable -> {
logger.error("Request processing failed", throwable);
});
}
}
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders responseHeaders = response.getHeaders();
// Add CORS headers
responseHeaders.add("Access-Control-Allow-Origin", "*");
responseHeaders.add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
responseHeaders.add("Access-Control-Allow-Headers", "Content-Type, Authorization");
responseHeaders.add("Access-Control-Max-Age", "3600");
if (HttpMethod.OPTIONS.equals(request.getMethod())) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(exchange);
}
}
@Component
public class RequestTimingFilter implements WebFilter {
private static final String REQUEST_START_TIME = "requestStartTime";
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
exchange.getAttributes().put(REQUEST_START_TIME, System.currentTimeMillis());
return chain.filter(exchange)
.doFinally(signalType -> {
Long startTime = exchange.getAttribute(REQUEST_START_TIME);
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
ServerHttpRequest request = exchange.getRequest();
System.out.println(String.format("Request %s %s took %d ms",
request.getMethod(),
request.getURI(),
duration));
}
});
}
}public interface ServerCodecConfigurer extends CodecConfigurer {
ServerDefaultCodecs defaultCodecs();
void clone(Consumer<ServerCodecConfigurer> consumer);
interface ServerDefaultCodecs extends DefaultCodecs {
void multipartReader(HttpMessageReader<?> reader);
void multipartCodecs(Consumer<MultipartHttpMessageReader.Builder> consumer);
void serverSentEventHttpMessageReader(HttpMessageReader<?> reader);
}
}public interface DataBuffer {
DataBufferFactory factory();
int indexOf(IntPredicate predicate, int fromIndex);
int lastIndexOf(IntPredicate predicate, int fromIndex);
int readableByteCount();
int writableByteCount();
int capacity();
DataBuffer capacity(int capacity);
DataBuffer ensureCapacity(int capacity);
int readPosition();
DataBuffer readPosition(int readPosition);
int writePosition();
DataBuffer writePosition(int writePosition);
byte getByte(int index);
byte read();
DataBuffer read(byte[] destination);
DataBuffer read(byte[] destination, int offset, int length);
DataBuffer write(byte b);
DataBuffer write(byte[] source);
DataBuffer write(byte[] source, int offset, int length);
DataBuffer write(DataBuffer... buffers);
DataBuffer write(ByteBuffer... buffers);
DataBuffer slice(int index, int length);
DataBuffer retainedSlice(int index, int length);
ByteBuffer asByteBuffer();
ByteBuffer asByteBuffer(int index, int length);
InputStream asInputStream();
InputStream asInputStream(boolean releaseOnClose);
OutputStream asOutputStream();
String toString(Charset charset);
String toString(int index, int length, Charset charset);
}
public interface DataBufferFactory {
DataBuffer allocateBuffer();
DataBuffer allocateBuffer(int initialCapacity);
DataBuffer wrap(ByteBuffer byteBuffer);
DataBuffer wrap(byte[] bytes);
DataBuffer join(List<? extends DataBuffer> dataBuffers);
boolean isDirect();
}@Component
public class DataBufferProcessor {
private final DataBufferFactory bufferFactory;
public DataBufferProcessor(DataBufferFactory bufferFactory) {
this.bufferFactory = bufferFactory;
}
// Stream processing with DataBuffer
public Flux<DataBuffer> processLargeFile(String filePath) {
return DataBufferUtils.readInputStream(
() -> new FileInputStream(filePath),
bufferFactory,
4096
).map(this::processChunk);
}
private DataBuffer processChunk(DataBuffer buffer) {
// Process the data buffer
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
// Transform the data (example: to uppercase)
String content = new String(bytes, StandardCharsets.UTF_8);
String processed = content.toUpperCase();
// Release the original buffer
DataBufferUtils.release(buffer);
// Return new buffer with processed content
return bufferFactory.wrap(processed.getBytes(StandardCharsets.UTF_8));
}
// Streaming response with DataBuffer
public Mono<ServerResponse> streamLargeResponse() {
Flux<DataBuffer> dataStream = Flux.range(1, 1000)
.map(i -> "Data chunk " + i + "\n")
.map(s -> bufferFactory.wrap(s.getBytes(StandardCharsets.UTF_8)));
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(dataStream, DataBuffer.class);
}
// Multipart file handling with DataBuffer
public Mono<String> handleFileUpload(Flux<DataBuffer> content) {
return DataBufferUtils.join(content)
.map(buffer -> {
try {
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
return new String(bytes, StandardCharsets.UTF_8);
} finally {
DataBufferUtils.release(buffer);
}
});
}
}
@Component
public class CustomCodecConfiguration {
@Bean
public HttpMessageReader<MultiValueMap<String, Part>> multipartReader() {
return new MultipartHttpMessageReader();
}
@Bean
public HttpMessageReader<Object> sseReader() {
return new ServerSentEventHttpMessageReader();
}
@Bean
public Encoder<CustomData> customDataEncoder() {
return new AbstractEncoder<CustomData>(MediaType.APPLICATION_JSON) {
@Override
public Flux<DataBuffer> encode(Publisher<? extends CustomData> inputStream,
DataBufferFactory bufferFactory,
ResolvableType elementType,
MimeType mimeType,
Map<String, Object> hints) {
return Flux.from(inputStream)
.map(this::serialize)
.map(json -> bufferFactory.wrap(json.getBytes(StandardCharsets.UTF_8)));
}
private String serialize(CustomData data) {
// Custom serialization logic
return "{\"id\":\"" + data.getId() + "\",\"value\":\"" + data.getValue() + "\"}";
}
};
}
@Bean
public Decoder<CustomData> customDataDecoder() {
return new AbstractDecoder<CustomData>(MediaType.APPLICATION_JSON) {
@Override
public Flux<CustomData> decode(Publisher<DataBuffer> inputStream,
ResolvableType elementType,
MimeType mimeType,
Map<String, Object> hints) {
return Flux.from(inputStream)
.reduce(DataBuffer::write)
.map(buffer -> {
try {
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
String json = new String(bytes, StandardCharsets.UTF_8);
return deserialize(json);
} finally {
DataBufferUtils.release(buffer);
}
})
.flux();
}
private CustomData deserialize(String json) {
// Custom deserialization logic
// This is a simplified example - use proper JSON parsing in production
return new CustomData();
}
};
}
}
// Custom data class for the example
public class CustomData {
private String id;
private String value;
// constructors, getters, setters
public CustomData() {}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}@Configuration
public class AdvancedCodecConfiguration implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
// Configure maximum in-memory size for data buffers
configurer.defaultCodecs().maxInMemorySize(8 * 1024 * 1024); // 8MB
// Enable logging of request details
configurer.defaultCodecs().enableLoggingRequestDetails(true);
// Configure multipart handling
configurer.defaultCodecs().multipartCodecs(builder -> {
builder.maxInMemorySize(2 * 1024 * 1024) // 2MB per part
.maxHeadersSize(8192) // 8KB headers
.maxParts(20); // Maximum 20 parts
});
// Register custom encoders/decoders
configurer.customCodecs().register(new ProtobufEncoder());
configurer.customCodecs().register(new ProtobufDecoder());
// Configure Jackson JSON processing
configurer.defaultCodecs().jackson2JsonEncoder(
new Jackson2JsonEncoder(customObjectMapper())
);
configurer.defaultCodecs().jackson2JsonDecoder(
new Jackson2JsonDecoder(customObjectMapper())
);
// Configure string encoder/decoder with specific charsets
configurer.defaultCodecs().stringEncoder(
CharSequenceEncoder.textPlainOnly(List.of(StandardCharsets.UTF_8))
);
configurer.defaultCodecs().stringDecoder(
StringDecoder.textPlainOnly(List.of(StandardCharsets.UTF_8), false)
);
}
@Bean
public ObjectMapper customObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.registerModule(new JavaTimeModule());
return mapper;
}
@Bean
public DataBufferFactory dataBufferFactory() {
// Choose between DefaultDataBufferFactory and NettyDataBufferFactory
return new DefaultDataBufferFactory();
}
}public interface WebFluxRegistrations {
default RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return null;
}
default RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
return null;
}
}@Configuration
public class CustomWebFluxRegistrations implements WebFluxRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setOrder(0);
mapping.setUseCaseSensitiveMatch(false);
mapping.setUseTrailingSlashMatch(true);
return mapping;
}
@Override
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setIgnoreDefaultModelOnRedirect(true);
return adapter;
}
}@FunctionalInterface
public interface ResourceHandlerRegistrationCustomizer {
void customize(ResourceHandlerRegistration registration);
}@Configuration
public class ResourceConfiguration {
@Bean
public ResourceHandlerRegistrationCustomizer resourceCustomizer() {
return registration -> {
registration.setCacheControl(CacheControl.maxAge(Duration.ofDays(30)))
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"))
.addTransformer(new CssLinkResourceTransformer());
};
}
@Bean
public ResourceUrlProvider resourceUrlProvider() {
return new ResourceUrlProvider();
}
@Bean
public WebFluxConfigurer staticResourceConfigurer(ResourceUrlProvider resourceUrlProvider) {
return new WebFluxConfigurerAdapter() {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/assets/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)))
.resourceChain(true)
.addResolver(new VersionResourceResolver()
.addContentVersionStrategy("/**"))
.addTransformer(new AppCacheManifestTransformer());
}
};
}
}public interface ServerCodecConfigurer extends CodecConfigurer {
ServerDefaultCodecs defaultCodecs();
void clone(Consumer<ServerCodecConfigurer> consumer);
interface ServerDefaultCodecs extends DefaultCodecs {
void serverSentEventEncoder(Encoder<?> encoder);
void multipartReader(HttpMessageReader<?> reader);
void multipartCodecs();
}
}@Configuration
public class CodecConfiguration {
@Bean
public CodecCustomizer codecCustomizer() {
return configurer -> {
// Increase max in-memory size
configurer.defaultCodecs().maxInMemorySize(5 * 1024 * 1024); // 5MB
// Enable request logging
configurer.defaultCodecs().enableLoggingRequestDetails(true);
// Custom JSON codec with specific ObjectMapper
ObjectMapper mapper = createCustomObjectMapper();
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper));
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper));
// Custom protobuf codec
configurer.defaultCodecs().protobufDecoder(new ProtobufDecoder());
configurer.defaultCodecs().protobufEncoder(new ProtobufHttpMessageWriter());
// Server-sent events configuration
configurer.defaultCodecs().serverSentEventEncoder(new Jackson2JsonEncoder());
};
}
@Bean
public ObjectMapper createCustomObjectMapper() {
return JsonMapper.builder()
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.addModule(new JavaTimeModule())
.addModule(new Jdk8Module())
.build();
}
@Bean
public HttpMessageReader<?> customMultipartReader() {
return new MultipartHttpMessageReader(new DefaultPartHttpMessageReader());
}
@Bean
public HttpMessageWriter<?> customMultipartWriter() {
return new MultipartHttpMessageWriter();
}
}@Configuration
public class CorsConfiguration {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("http://localhost:*", "https://*.example.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
@Bean
public CorsWebFilter corsWebFilter() {
return new CorsWebFilter(corsConfigurationSource());
}
}@RestController
@CrossOrigin(origins = "http://localhost:3000", maxAge = 3600)
public class ApiController {
@CrossOrigin(origins = "https://admin.example.com")
@GetMapping("/admin/users")
public Flux<User> getAdminUsers() {
return userService.findAll();
}
@CrossOrigin(methods = {RequestMethod.POST, RequestMethod.PUT})
@PostMapping("/users")
public Mono<User> createUser(@RequestBody Mono<User> user) {
return user.flatMap(userService::save);
}
}@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestLoggingFilter implements WebFilter {
private final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String requestId = UUID.randomUUID().toString();
// Add request ID to response headers
exchange.getResponse().getHeaders().add("X-Request-ID", requestId);
logger.info("Request started: {} {} [{}]",
request.getMethod(), request.getURI(), requestId);
long startTime = System.currentTimeMillis();
return chain.filter(exchange)
.doFinally(signalType -> {
long duration = System.currentTimeMillis() - startTime;
ServerHttpResponse response = exchange.getResponse();
logger.info("Request completed: {} {} {} {}ms [{}]",
request.getMethod(), request.getURI(),
response.getStatusCode(), duration, requestId);
});
}
}@Component
public class AuthenticationFilter implements WebFilter {
private final AuthenticationService authService;
public AuthenticationFilter(AuthenticationService authService) {
this.authService = authService;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// Skip authentication for public endpoints
if (isPublicEndpoint(request.getPath().value())) {
return chain.filter(exchange);
}
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return handleUnauthorized(exchange);
}
String token = authHeader.substring(7);
return authService.validateToken(token)
.flatMap(user -> {
ServerWebExchange mutatedExchange = exchange.mutate()
.request(request.mutate()
.header("X-User-ID", user.getId())
.header("X-User-Role", user.getRole())
.build())
.build();
return chain.filter(mutatedExchange);
})
.switchIfEmpty(handleUnauthorized(exchange));
}
private boolean isPublicEndpoint(String path) {
return path.startsWith("/public/") ||
path.equals("/health") ||
path.equals("/metrics");
}
private Mono<Void> handleUnauthorized(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
String body = "{\"error\":\"UNAUTHORIZED\",\"message\":\"Authentication required\"}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}@Configuration
public class ValidationConfiguration {
@Bean
public LocalValidatorFactoryBean validator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setProviderClass(HibernateValidator.class);
validator.setMessageInterpolator(new ParameterMessageInterpolator());
return validator;
}
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
processor.setValidator(validator());
return processor;
}
@Bean
public WebFluxConfigurer validationConfigurer() {
return new WebFluxConfigurerAdapter() {
@Override
public Validator getValidator() {
return validator();
}
};
}
}public interface CreateValidation {}
public interface UpdateValidation {}
@RestController
@Validated
public class UserController {
@PostMapping("/users")
public Mono<User> createUser(@Validated(CreateValidation.class) @RequestBody Mono<User> user) {
return user.flatMap(userService::save);
}
@PutMapping("/users/{id}")
public Mono<User> updateUser(@PathVariable String id,
@Validated(UpdateValidation.class) @RequestBody Mono<User> user) {
return user.flatMap(u -> userService.update(id, u));
}
}
public class User {
@NotNull(groups = {CreateValidation.class, UpdateValidation.class})
@Size(min = 2, max = 50, groups = {CreateValidation.class, UpdateValidation.class})
private String name;
@NotNull(groups = CreateValidation.class)
@Email(groups = {CreateValidation.class, UpdateValidation.class})
private String email;
@Null(groups = CreateValidation.class) // ID should be null when creating
@NotNull(groups = UpdateValidation.class) // ID should be present when updating
private String id;
// Constructors, getters, setters...
}# WebFlux Configuration
spring.webflux.base-path=/api/v1
spring.webflux.static-path-pattern=/static/**
spring.webflux.webjars-path-pattern=/webjars/**
spring.webflux.hiddenmethod.filter.enabled=true
spring.webflux.format.date-time=yyyy-MM-dd'T'HH:mm:ss.SSSXXX
spring.webflux.problemdetails.enabled=true
# HTTP Codecs Configuration
spring.http.codecs.max-in-memory-size=2MB
spring.http.codecs.log-request-details=true
# Server Configuration
server.port=8080
server.address=0.0.0.0
server.http2.enabled=true
server.compression.enabled=true
server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml
server.compression.min-response-size=2048
# Netty-specific configuration
server.netty.connection-timeout=20s
server.netty.idle-timeout=60s
server.netty.max-keep-alive-requests=100
# SSL Configuration
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=password
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=spring
server.ssl.protocol=TLS
server.ssl.enabled-protocols=TLSv1.2,TLSv1.3
# Logging
logging.level.org.springframework.web.reactive=DEBUG
logging.level.org.springframework.http.server.reactive=DEBUG
logging.level.org.springframework.web.reactive.function.client=DEBUGspring:
webflux:
base-path: /api/v1
static-path-pattern: /static/**
format:
date-time: "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"
problemdetails:
enabled: true
http:
codecs:
max-in-memory-size: 2MB
log-request-details: true
server:
port: 8080
http2:
enabled: true
compression:
enabled: true
mime-types:
- text/html
- text/xml
- text/plain
- text/css
- application/json
- application/javascript
min-response-size: 2KB
netty:
connection-timeout: 20s
idle-timeout: 60s
max-keep-alive-requests: 100
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: password
key-store-type: PKCS12
key-alias: spring
protocol: TLS
enabled-protocols:
- TLSv1.2
- TLSv1.3
logging:
level:
org.springframework.web.reactive: DEBUG
org.springframework.http.server.reactive: DEBUG# application-dev.yml
spring:
webflux:
problemdetails:
enabled: true
http:
codecs:
log-request-details: true
server:
port: 8080
ssl:
enabled: false
logging:
level:
org.springframework.web.reactive: DEBUG
reactor.netty.http.server: DEBUG
root: INFO# application-prod.yml
spring:
webflux:
problemdetails:
enabled: false
http:
codecs:
log-request-details: false
server:
port: 8443
ssl:
enabled: true
key-store: file:/etc/ssl/app-keystore.p12
key-store-password: ${SSL_KEYSTORE_PASSWORD}
compression:
enabled: true
shutdown: graceful
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
endpoint:
health:
show-details: never
logging:
level:
org.springframework.web.reactive: WARN
root: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-boot--spring-boot-starter-webflux