Spring WebFlux is a reactive-stack web framework for building non-blocking, reactive web applications with Reactive Streams back pressure.
org.springframework:spring-webflux:7.0.1Installation:
Maven:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>7.0.1</version>
</dependency>Gradle:
implementation 'org.springframework:spring-webflux:7.0.1'import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/hello")
public Mono<String> hello() {
return Mono.just("Hello, WebFlux!");
}
@PostMapping("/users")
public Mono<User> createUser(@RequestBody User user) {
return userRepository.save(user);
}
}import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.server.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
@Bean
public RouterFunction<ServerResponse> routes() {
return route(GET("/hello"),
request -> ServerResponse.ok().bodyValue("Hello, WebFlux!"));
}import org.springframework.web.reactive.function.client.WebClient;
WebClient client = WebClient.create("https://api.example.com");
Mono<User> user = client.get()
.uri("/users/{id}", userId)
.retrieve()
.bodyToMono(User.class);// Annotations
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.config.EnableWebFlux;
// Functional Routing
import org.springframework.web.reactive.function.server.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.*;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
// WebClient
import org.springframework.web.reactive.function.client.WebClient;
// Reactive Types
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;
// Exception Handling
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
// WebSocket
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;
import org.springframework.web.reactive.socket.WebSocketMessage;Two Programming Models:
@RestController, @RequestMapping, method annotationsRouterFunction, HandlerFunction, composable routesRequest Processing Flow:
Request → DispatcherHandler → HandlerMapping → HandlerAdapter → Handler → Result Handler → ResponseCore Components:
DispatcherHandler: Central request dispatcherHandlerMapping: Routes requests to handlersHandlerAdapter: Invokes handlers, processes arguments/return valuesHandlerResultHandler: Writes handler results to responseReactive Foundation:
Mono<T> for 0-1 items, Flux<T> for 0-N items)Server Support: Netty (default), Tomcat, Jetty, Undertow, Servlet 3.1+
Configure WebFlux using @EnableWebFlux and WebFluxConfigurer:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://example.com");
}
}Key Configuration Areas:
Non-blocking HTTP client with fluent API:
WebClient client = WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Authorization", "Bearer " + token)
.build();
// GET request
Mono<User> user = client.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(User.class);
// POST request with error handling
Mono<Response> response = client.post()
.uri("/users")
.bodyValue(newUser)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
res -> Mono.error(new ClientException()))
.bodyToMono(Response.class);Features: Request/response filtering, timeout, retry, error handling, streaming
Composable, functional route definitions:
@Bean
public RouterFunction<ServerResponse> routes(UserHandler handler) {
return route()
.GET("/users/{id}", handler::getUser)
.GET("/users", handler::listUsers)
.POST("/users", handler::createUser)
.filter((request, next) -> {
// Logging, auth, etc.
return next.handle(request);
})
.build();
}Key Types: RouterFunction, HandlerFunction, ServerRequest, ServerResponse, RequestPredicate
Familiar Spring MVC-style controllers with reactive types:
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userService.findById(id);
}
@PostMapping
public Mono<User> createUser(@RequestBody @Valid User user) {
return userService.save(user);
}
}Method Arguments: @PathVariable, @RequestParam, @RequestBody, @RequestHeader, @CookieValue, ServerWebExchange, Principal, reactive types
Return Types: Mono<T>, Flux<T>, ResponseEntity, Rendering, void, plain objects
Bidirectional real-time communication:
public class ChatHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
return session.send(
session.receive()
.map(msg -> session.textMessage("Echo: " + msg.getPayloadAsText()))
);
}
}Features: Text/binary messages, ping/pong, handshake info, sub-protocols, multiple server implementations
Serve static resources with caching, versioning, transformation:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
.resourceChain(true)
.addResolver(new VersionResourceResolver()
.addContentVersionStrategy("/**"));
}Features: Version strategies (content hash, fixed), gzip/brotli encoding, CSS link transformation, WebJars support
Server-side template rendering:
@GetMapping("/users")
public Mono<Rendering> listUsers() {
return userService.findAll()
.collectList()
.map(users -> Rendering.view("userList")
.modelAttribute("users", users)
.build());
}Supported: FreeMarker, script templates (Nashorn, GraalVM JS), custom views
Resolve content types and API versions:
@Override
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
builder.headerResolver()
.parameterResolver("format")
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
}
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.addVersionResolver(new HeaderApiVersionResolver("API-Version"))
.setVersionStrategy(new DefaultApiVersionStrategy()
.addVersions("1.0", "2.0", "3.0")
.addDeprecatedVersions("1.0"));
}Version Resolution: Header, query parameter, path segment, media type parameter
Handle exceptions globally or per-controller:
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public Mono<ResponseEntity<ErrorResponse>> handleNotFound(NotFoundException ex) {
return Mono.just(ResponseEntity.status(404)
.body(new ErrorResponse("NOT_FOUND", ex.getMessage())));
}
}Approaches: @ExceptionHandler, ResponseEntityExceptionHandler, @ResponseStatus, functional onError(), DispatchExceptionHandler
@RestController
@RequestMapping("/api/users")
public class UserApiController {
private final UserService userService;
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getUser(@PathVariable String id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
public Mono<ResponseEntity<User>> createUser(@RequestBody @Valid User user) {
return userService.save(user)
.map(saved -> ResponseEntity
.created(URI.create("/api/users/" + saved.getId()))
.body(saved));
}
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidation(ValidationException ex) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("VALIDATION_ERROR", ex.getMessage()));
}
}@GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<Event>> streamEvents() {
return eventService.subscribeToEvents()
.map(event -> ServerSentEvent.builder(event)
.id(event.getId())
.event("message")
.build())
.onBackpressureBuffer(100);
}@PostMapping("/upload")
public Mono<String> handleUpload(@RequestPart("file") FilePart file,
@RequestPart("metadata") Metadata metadata) {
return file.transferTo(Path.of("/uploads/" + file.filename()))
.then(Mono.just("Upload successful"));
}@Bean
public WebFilter authenticationFilter() {
return (exchange, chain) -> {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !isValid(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange)
.contextWrite(Context.of("userId", extractUserId(token)));
};
}public interface ServerWebExchange {
ServerHttpRequest getRequest();
ServerHttpResponse getResponse();
Mono<WebSession> getSession();
Mono<? extends Principal> getPrincipal();
Map<String, Object> getAttributes();
}
public class ServerHttpRequest extends HttpRequest {
URI getURI();
HttpHeaders getHeaders();
MultiValueMap<String, HttpCookie> getCookies();
Flux<DataBuffer> getBody();
}
public class ServerHttpResponse extends HttpResponse {
void setStatusCode(HttpStatus status);
HttpHeaders getHeaders();
Mono<Void> writeWith(Publisher<? extends DataBuffer> body);
}public interface WebHandler {
Mono<Void> handle(ServerWebExchange exchange);
}
public class DispatcherHandler implements WebHandler {
// Central dispatcher - delegates to HandlerMapping → HandlerAdapter
}
public interface HandlerMapping {
Mono<Object> getHandler(ServerWebExchange exchange);
}
public interface HandlerAdapter {
boolean supports(Object handler);
Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler);
}
public class HandlerResult {
Object getHandler();
Object getReturnValue();
MethodParameter getReturnType();
BindingContext getBindingContext();
}@Target(ElementType.TYPE)
@Import(DelegatingWebFluxConfiguration.class)
public @interface EnableWebFlux { }
public interface WebFluxConfigurer {
void configureHttpMessageCodecs(ServerCodecConfigurer configurer);
void addFormatters(FormatterRegistry registry);
void addCorsMappings(CorsRegistry registry);
void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder);
void configureApiVersioning(ApiVersionConfigurer configurer);
void configurePathMatching(PathMatchConfigurer configurer);
void configureViewResolvers(ViewResolverRegistry registry);
void addResourceHandlers(ResourceHandlerRegistry registry);
void configureBlockingExecution(BlockingExecutionConfigurer configurer);
}Thread Safety:
subscribeOn() or publishOn() for blocking operationsServerWebExchange is NOT thread-safe - do not share across threadsBlocking Operations:
// WRONG - blocks event loop
@GetMapping("/users")
public Mono<User> getUser() {
User user = blockingJdbcCall(); // BAD!
return Mono.just(user);
}
// CORRECT - offload blocking work
@GetMapping("/users")
public Mono<User> getUser() {
return Mono.fromCallable(() -> blockingJdbcCall())
.subscribeOn(Schedulers.boundedElastic());
}Configure Blocking Scheduler:
@Override
public void configureBlockingExecution(BlockingExecutionConfigurer configurer) {
configurer.setExecutor(Schedulers.boundedElastic());
}Reactive Error Operators:
onErrorReturn(fallbackValue) - return default value on erroronErrorResume(fallbackPublisher) - switch to fallback publisheronErrorMap(mapper) - transform erroronErrorContinue((error, value) -> ...) - log and continueretry(n) - retry on errortimeout(duration) - fail if no emission within durationreturn webClient.get()
.uri("/data")
.retrieve()
.bodyToMono(Data.class)
.timeout(Duration.ofSeconds(5))
.retry(3)
.onErrorResume(TimeoutException.class, e ->
Mono.just(new Data("cached")))
.onErrorMap(WebClientException.class, e ->
new ServiceException("Failed to fetch data", e));Backpressure: WebFlux automatically handles backpressure - slow consumers signal upstream to slow down production
Memory:
ServerCodecConfigurer.defaultCodecs().maxInMemorySize()Flux<DataBuffer>) instead of loading into memoryConnection Pooling:
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(5))
.option(ChannelOption.SO_KEEPALIVE, true);
WebClient client = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();subscribeOn(Schedulers.boundedElastic())onErrorResume() or let them propagateDataBuffer instances or use try-with-resourcesThreadLocal - use Reactor Context insteadmaxInMemorySize for large uploads