SpringBoot WebFlux Functional RestAPIs

Reactive programming is about non-blocking applications. And Spring Framework 5 includes a new spring-webflux module, supports Reactive Streams for communicating backpressure across async components and libraries. So in the tutorial, JavaSampleApproach will guide you through the steps for creating a SpringBoot WebFlux Functional restful APIs.

Related posts:
SpringBoot WebFlux Annotation-based RestAPIs
Reactor – Simple Ways to create Flux/Mono
Spring WebClient with Spring Webflux | SpringBoot 2

I. Technologies

– Java: 1.8
– Maven: 3.3.9
– Spring Tool Suite: Version 3.9.0.RELEASE
– Spring Boot: 2.0.0.M4
– Spring Boot Starter Webflux

II. Spring WebFlux Functional

Spring Framework 5.0 supports WebFlux with fully asynchronous and non-blocking and does NOT require the Servlet API(Unlike Spring MVC).

Spring WebFlux supports 2 distinct programming models:
– Annotation-based with @Controller
– Functional with Java 8 lambda style

In the tutorial, we will introduce WebFlux with Functional.
For starting with WebFlux, SpringBoot supports a collection dependency: spring-boot-starter-webflux.

With Spring WebFlux Functional, we use {HandlerFunctions, RouterFunctions} to develop.

1. HandlerFunctions

HandlerFunctions will handle incoming HTTP requests through ServerRequest, and return a Mono


@Component
public class CustomerHandler {
	
	...
	
    public Mono getAll(ServerRequest request) {
		...
		return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(customers, Customer.class);
    }
    
    ...
    
    public Mono putCustomer(ServerRequest request) {
		...
		return responseMono
                .flatMap(cust -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(fromObject(cust)));
    }

    ...
	
    public Mono deleteCustomer(ServerRequest request) {
		...
		return responseMono
                .flatMap(strMono -> ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).body(fromObject(strMono)));
    }
}

2. RouterFunction

RouterFunction handle all incoming requests. It takes a ServerRequest, and returns a Mono. If a request matches a particular route, a handler function is returned; otherwise it returns an empty Mono.


@Configuration
public class RoutingConfiguration {
	
    @Bean
    public RouterFunction monoRouterFunction(CustomerHandler customerHandler) {
        return route(GET("/api/customer").and(accept(MediaType.APPLICATION_JSON)), customerHandler::getAll)
        		.andRoute(GET("/api/customer/{id}").and(accept(MediaType.APPLICATION_JSON)), customerHandler::getCustomer)
        		.andRoute(POST("/api/customer/post").and(accept(MediaType.APPLICATION_JSON)), customerHandler::postCustomer)
                .andRoute(PUT("/api/customer/put/{id}").and(accept(MediaType.APPLICATION_JSON)), customerHandler::putCustomer)
                .andRoute(DELETE("/api/customer/delete/{id}").and(accept(MediaType.APPLICATION_JSON)), customerHandler::deleteCustomer);
    }
    
}

III. Practice

In the tutorial, We create a SpringBoot project as below:

springboot webflux functional - project structure

Step to do:
– Create SpringBoot project
– Create data model
– Implement repository
– Implement Spring WebFlux APIs
– Run and check results

1. Create SpringBoot project

Using SpringToolSuite, create a SpringBoot project with Reactive Web dependency:

spring-webflux-reactive-select-reactive-web

Check pom.xml after creating:

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-webflux</artifactId>
	</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>

<build>
	<plugins>
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
		</plugin>
	</plugins>
</build>

<repositories>
	<repository>
		<id>spring-snapshots</id>
		<name>Spring Snapshots</name>
		<url>https://repo.spring.io/snapshot</url>
		<snapshots>
			<enabled>true</enabled>
		</snapshots>
	</repository>
	<repository>
		<id>spring-milestones</id>
		<name>Spring Milestones</name>
		<url>https://repo.spring.io/milestone</url>
		<snapshots>
			<enabled>false</enabled>
		</snapshots>
	</repository>
</repositories>

<pluginRepositories>
	<pluginRepository>
		<id>spring-snapshots</id>
		<name>Spring Snapshots</name>
		<url>https://repo.spring.io/snapshot</url>
		<snapshots>
			<enabled>true</enabled>
		</snapshots>
	</pluginRepository>
	<pluginRepository>
		<id>spring-milestones</id>
		<name>Spring Milestones</name>
		<url>https://repo.spring.io/milestone</url>
		<snapshots>
			<enabled>false</enabled>
		</snapshots>
	</pluginRepository>
</pluginRepositories>

2. Create data model

Create a Customer data model:


package com.javasampleapproach.webflux.model;

public class Customer {
	private long custId;
	private String firstname;
	private String lastname;
	private int age;
	
	public Customer(){}
	
	public Customer(long custId, String firstname, String lastname, int age){
		this.custId = custId;
		this.firstname = firstname;
		this.lastname = lastname;
		this.age = age;
	}
 
	public long getCustId() {
		return custId;
	}
 
	public void setCustId(Long custId) {
		this.custId = custId;
	}
 
	public String getFirstname() {
		return firstname;
	}
 
	public void setFirstname(String firstname) {
		this.firstname = firstname;
	}
 
	public String getLastname() {
		return lastname;
	}
 
	public void setLastname(String lastname) {
		this.lastname = lastname;
	}
 
	public int getAge() {
		return age;
	}
 
	public void setAge(int age) {
		this.age = age;
	}
	
	@Override
	public String toString() {
		String info = String.format("custId = %d, firstname = %s, lastname = %s, age = %d", custId, firstname, lastname, age);
		return info;
	}
}

3. Implement repository

3.1 Define interface CustomerRepository


package com.javasampleapproach.webflux.repo;

import com.javasampleapproach.webflux.model.Customer;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public interface CustomerRepository {
	public Mono getCustomerById(Long id);
	public Flux getAllCustomers();
	public Mono saveCustomer(Mono customer);
	public Mono putCustomer(Long id, Mono customer);
	public Mono deleteCustomer(Long id);
}

3.2 Implement CustomerRepository


package com.javasampleapproach.webflux.repo.impl;

import java.util.HashMap;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.springframework.stereotype.Repository;

import com.javasampleapproach.webflux.model.Customer;
import com.javasampleapproach.webflux.repo.CustomerRepository;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Repository
public class CustomerRepositoryImpl implements CustomerRepository{
	private Map custStores = new HashMap();
	
	@PostConstruct
    public void initIt() throws Exception {
        custStores.put(Long.valueOf(1), new Customer(1, "Jack", "Smith", 20));
        custStores.put(Long.valueOf(2), new Customer(2, "Peter", "Johnson", 25));
    }

	@Override
	public Mono getCustomerById(Long id) {
		return Mono.just(custStores.get(id));
	}

	@Override
	public Flux getAllCustomers() {
		return Flux.fromIterable(this.custStores.values());
	}

	@Override
	public Mono saveCustomer(Mono monoCustomer) {
		Mono customerMono =  monoCustomer.doOnNext(customer -> {
            // do post
            custStores.put(customer.getCustId(), customer);
            
            // log on console
            System.out.println("########### POST:" + customer);
        });
		
		return customerMono.then();
	}
	
	@Override
	public Mono putCustomer(Long id, Mono monoCustomer) {
		Mono customerMono =  monoCustomer.doOnNext(customer -> {
			// reset customer.Id
			customer.setCustId(id);
			
			// do put
			custStores.put(id, customer);
			
			// log on console
			System.out.println("########### PUT:" + customer);
        });
		
		return customerMono;
	}
	
	@Override
	public Mono deleteCustomer(Long id) {
		// delete processing
    	custStores.remove(id);
    	return Mono.just("Delete Succesfully!");
	}
}

4. Implement Spring WebFlux APIs

4.1 RouterFunction


package com.javasampleapproach.webflux.functional.router;

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 com.javasampleapproach.webflux.functional.handler.CustomerHandler;

import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

import org.springframework.http.MediaType;

@Configuration
public class RoutingConfiguration {
	
    @Bean
    public RouterFunction monoRouterFunction(CustomerHandler customerHandler) {
        return route(GET("/api/customer").and(accept(MediaType.APPLICATION_JSON)), customerHandler::getAll)
        		.andRoute(GET("/api/customer/{id}").and(accept(MediaType.APPLICATION_JSON)), customerHandler::getCustomer)
        		.andRoute(POST("/api/customer/post").and(accept(MediaType.APPLICATION_JSON)), customerHandler::postCustomer)
                .andRoute(PUT("/api/customer/put/{id}").and(accept(MediaType.APPLICATION_JSON)), customerHandler::putCustomer)
                .andRoute(DELETE("/api/customer/delete/{id}").and(accept(MediaType.APPLICATION_JSON)), customerHandler::deleteCustomer);
    }
    
}

4.2 CustomerHandler


package com.javasampleapproach.webflux.functional.handler;

import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

import org.springframework.http.MediaType;

import com.javasampleapproach.webflux.model.Customer;
import com.javasampleapproach.webflux.repo.CustomerRepository;

import static org.springframework.web.reactive.function.BodyInserters.fromObject;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Component
public class CustomerHandler {
	
	private final CustomerRepository customerRepository;

	public CustomerHandler(CustomerRepository repository) {
		this.customerRepository = repository;
	}
	
	/**
	 * GET ALL Customers
	 */
    public Mono getAll(ServerRequest request) {
    	// fetch all customers from repository
    	Flux customers = customerRepository.getAllCustomers();
    	
    	// build response
		return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(customers, Customer.class);
    }
    
    /**
     * GET a Customer by ID 
     */
    public Mono getCustomer(ServerRequest request) {
    	// parse path-variable
    	long customerId = Long.valueOf(request.pathVariable("id"));
    	
    	// build notFound response 
		Mono notFound = ServerResponse.notFound().build();
		
		// get customer from repository 
		Mono customerMono = customerRepository.getCustomerById(customerId);
		
		// build response
		return customerMono
                .flatMap(customer -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(fromObject(customer)))
                .switchIfEmpty(notFound);
    }
    
    /**
     * POST a Customer
     */
    public Mono postCustomer(ServerRequest request) {
    	Mono customer = request.bodyToMono(Customer.class);
        return ServerResponse.ok().build(customerRepository.saveCustomer(customer));
    }
    
    /**
     *	PUT a Customer
     */
    public Mono putCustomer(ServerRequest request) {
    	// parse id from path-variable
    	long customerId = Long.valueOf(request.pathVariable("id"));
    	
    	// get customer data from request object
    	Mono customer = request.bodyToMono(Customer.class);
    	
		// get customer from repository 
		Mono responseMono = customerRepository.putCustomer(customerId, customer);
		
		// build response
		return responseMono
                .flatMap(cust -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(fromObject(cust)));
    }

    /**
     *	DELETE a Customer
     */
    public Mono deleteCustomer(ServerRequest request) {
    	// parse id from path-variable
    	long customerId = Long.valueOf(request.pathVariable("id"));
    	
    	// get customer from repository 
    	Mono responseMono = customerRepository.deleteCustomer(customerId);
    	
    	// build response
		return responseMono
                .flatMap(strMono -> ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).body(fromObject(strMono)));
    }
}

5. Run and check results

Build and run the SpringBoot project with commandlines: {mvn clean install, mvn spring-boot:run}.

– Make a GET all customer request: http://localhost:8080/api/customer

springboot webflux functional - get all customer

– Make a GET customer request: http://localhost:8080/api/customer/1

springboot webflux functional - get a customer by id

– Make a POST request: http://localhost:8080/api/customer/post

springboot webflux functional - post a customer

– Make a PUT request: http://localhost:8080/api/customer/put/3

springboot webflux functional - put a customer

– Make a DELETE request: http://localhost:8080/api/customer/delete/1

springboot webflux functional - delete a customer

– Make a GET all customers request: http://localhost:8080/api/customer

springboot webflux functional - final get all customer to check

IV. Sourcecode

SpringWebFluxFunctional

One thought on “SpringBoot WebFlux Functional RestAPIs”

  1. I wonder if you can comment on how to work with exceptions here?
    Eg if I call customer # which does not exist – will get null pointer exception
    in CustomerRepositoryImpl, here
    public Mono getCustomerById(Long id) {

Leave a Reply

Your email address will not be published. Required fields are marked *