Blocking is a feature of classic servlet-based web frameworks like Spring MVC. Introduced in Spring 5, Spring WebFlux is a reactive framework that operates on servers like Netty and is completely non-blocking.
Two programming paradigms are supported by Spring WebFlux. Annotations (Aspect Oriented Programming) and WebFlux.fn (Functional Programming).
"Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions are used to route and handle requests and contracts are designed for immutability. It is an alternative to the annotation-based programming model but otherwise runs on the same Reactive Core foundation." Spring | Functional Endpoints
Project Description
As the title describe, this is a simple Songs API build using Spring, Docker and MongoDB, the endpoints are Functional Endpoints and will have the traditional ControllerAdvice as Exception handler.
Project Dependencies
- Java Version
21
- Spring Boot version
3.3.0-SNAPSHOT
with Spring Reactive Starter. - Spring Docker Support.
- Lombok (Optional).
Talking XML these are the project dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Coding time!
First, let's setup the docker compose file /compose.yaml
of the project (it should generated by spring via the docker support starter).
services:
mongodb:
image: 'mongo:7.0.5'
environment:
- 'MONGO_INITDB_DATABASE=songsDB'
- 'MONGO_INITDB_ROOT_PASSWORD=passw0rd'
- 'MONGO_INITDB_ROOT_USERNAME=root'
ports:
- '27017'
With that set, let's create the Song class:
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.UUID;
@Document
@Getter
@Setter
@AllArgsConstructor
@Builder
public class Song {
@Id
private UUID id;
private String title;
private String artist;
}
The SongRepository interface will be referring to the Song class in its DB ops:
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import java.util.UUID;
@Repository
public interface SongRepository extends ReactiveCrudRepository<Song, UUID> {
Flux<Song> findAllByArtist(final String artist);
}
Song Functional Endpoint and Handler
Now, it's time for the Song Router, it will be responsible for router the incoming requests for the /songs ressource:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
public class SongRouterConfig {
private final SongHandler handler;
public SongRouterConfig(SongHandler handler) {
this.handler = handler;
}
@Bean
public RouterFunction<ServerResponse> router() {
return route().path("/songs", builder -> builder
.GET("/artist", handler::findAllByArtist)
.GET(handler::findAll) // Get endpoints' order is important
.POST("/new", handler::create)
.DELETE("/{id}", handler::delete)
).build();
}
}
As you noticed the request are redirected to the SongHandler for a certain logic to be performed.
Note: If you having trouble understanding the syntax, make sure to know more about Java functional interfaces, lambda and method references.
The SongsHandler will act as Service as well, will perform a business logic and communicate with the SongRepository for operations with the database.
import io.daasrattale.webfluxmongofunctionalendpoints.song.exceptions.InvalidParamException;
import io.daasrattale.webfluxmongofunctionalendpoints.song.exceptions.InvalidUUIDException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.Optional;
import java.util.UUID;
@Service
public class SongHandler {
private final SongRepository repository;
public SongHandler(SongRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> findAll(final ServerRequest request) {
return ServerResponse
.ok()
.body(repository.findAll(), Song.class);
}
public Mono<ServerResponse> findAllByArtist(final ServerRequest request) {
return Mono.just(request.queryParam("artist"))
.switchIfEmpty(Mono.error(new InvalidParamException("artist")))
.map(Optional::get)
.map(repository::findAllByArtist)
.flatMap(songFlux -> ServerResponse
.ok()
.body(songFlux, Song.class));
}
public Mono<ServerResponse> create(final ServerRequest request) {
return request.bodyToMono(Song.class)
.switchIfEmpty(Mono.error(new RuntimeException("Song body not found"))) // you can use that or create a custom exception (recommended)
.doOnNext(song -> song.setId(UUID.randomUUID()))
.flatMap(song -> ServerResponse
.status(HttpStatus.CREATED)
.body(repository.save(song), Song.class)
);
}
public Mono<ServerResponse> delete(final ServerRequest request) {
return Mono.just(request.pathVariable("id"))
.map(UUID::fromString)
.doOnError(throwable -> {
throw new InvalidUUIDException(throwable);
})
.flatMap(songId -> ServerResponse
.ok()
.body(repository.deleteById(songId), Void.class)
);
}
}
Note: The
SongHandler
can be annotated with @Component, since it performs a business logic I see it better have the @Service annotation instead.
Exception Handling
As previously states, will be using the same old ControllerAdvice as Exception handler with two custom Exceptions as the following:
Custom Exceptions
import lombok.Getter;
@Getter
public class InvalidParamException extends RuntimeException {
private final String paramName;
public InvalidParamException(final String paramName) {
this.paramName = paramName;
}
}
import lombok.Getter;
@Getter
public class InvalidUUIDException extends RuntimeException {
private final Throwable cause;
public InvalidUUIDException(final Throwable cause) {
this.cause = cause;
}
}
Custom Exception Handler
import io.daasrattale.webfluxmongofunctionalendpoints.song.exceptions.InvalidUUIDException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.Map;
@ControllerAdvice
@Slf4j
public class SongExceptionHandler {
@ExceptionHandler(InvalidUUIDException.class)
public ResponseEntity<Map<String, ?>> handle(final InvalidUUIDException exception) {
return ResponseEntity
.badRequest()
.body(
Map.of(
"status", 400,
"message", "Invalid UUID",
"details", exception.getCause().getMessage()
)
);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, ?>> handle(final Exception exception) {
log.error("Unhandled Error, message: {}", exception.getMessage());
return ResponseEntity
.internalServerError()
.body(
Map.of(
"status", 500,
"message", "Unknown Error",
"details", exception.getMessage()
)
);
}
}
With all that been set, let's make use of our endpoint using Postman:
- Creating a new Song
- Getting songs by artist:
- Getting all songs:
- Deleting a song:
Sorry not a big fan of Madonna tbh :|
- Checking the result of the delete op:
Finally,
With that said, our functional songs endpoint will be good to go for further improvements and new features.
This is simple, in real industrial projects, I can assure you things get complicated with more layers, for "getting started" purposes I avoided the use of advanced concepts such as validation, DTO, etc.
You can find the full source here
Also find more content on my personal personal website.
Top comments (0)