13.2 Practical WebFlux Usage (WebClient and Handlers)
Explore how to utilize Spring WebFlux in real-world projects, specifically focusing on WebClient for external API calls and detailed implementation examples of Functional Endpoints.
1. Advanced WebClient Usage
WebClient is a non-blocking HTTP client used for communication with external services in a WebFlux environment.
1) Creating a WebClient Instance
You can create an instance in two ways.
// Option 1: Simple creation (create)
WebClient client1 = WebClient.create("http://localhost:8080");
// Option 2: Creation with detailed settings (builder) - Recommended
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
2) GET / POST Request Examples
@Service
@RequiredArgsConstructor
public class ReactiveApiService {
private final WebClient webClient;
// GET Request: Retrieve a single item
public Mono<UserDto> getUser(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(UserDto.class);
}
// POST Request: Send data
public Mono<UserDto> createUser(UserDto userDto) {
return webClient.post()
.uri("/users")
.bodyValue(userDto)
.retrieve()
.bodyToMono(UserDto.class);
}
}
2. Detailed Functional Endpoints Example
The functional approach clearly separates the business logic (Handler) from path mapping (Router).
1) Handler Class (Business Logic)
@Component
public class UserHandler {
public Mono<ServerResponse> getAllUsers(ServerRequest request) {
Flux<UserDto> users = Flux.just(
new UserDto(1L, "User1"),
new UserDto(2L, "User2")
);
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(users, UserDto.class);
}
public Mono<ServerResponse> getUserById(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return ServerResponse.ok()
.bodyValue(new UserDto(id, "User" + id));
}
}
2) Router Class (Path Mapping)
@Configuration
public class UserRouter {
@Bean
public RouterFunction<ServerResponse> userRoute(UserHandler handler) {
return RouterFunctions.route()
.GET("/api/v2/users", handler::getAllUsers)
.GET("/api/v2/users/{id}", handler::getUserById)
.build();
}
}
3. Reactive Data Handling Example (R2DBC Example)
When integrating with a database, you must use R2DBC, a non-blocking library.
@Repository
public interface UserRepository extends ReactiveSortingRepository<User, Long> {
// Note that return types are Flux or Mono
Flux<User> findByName(String name);
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public Mono<User> saveUser(User user) {
return userRepository.save(user);
}
public Flux<User> findUsers(String name) {
return userRepository.findByName(name);
}
}
4. Error Handling
You can handle exceptions gracefully within reactive streams.
public Mono<String> callApiWithRetry() {
return webClient.get()
.uri("/some-error-api")
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.just("Fallback Data")) // Return default value upon error
.retry(3); // Retry 3 times upon error
}