Spring 5 : WebFlux Example With Spring Boot| Code Factory


Donate : Link

Medium Blog : Link

Applications : Link

The reactive-stack web framework, Spring WebFlux, has been added Spring 5.0. It is fully non-blocking, supports reactive streams back pressure, and runs on such servers as Netty, Undertow, and Servlet 3.1+ containers.

What is Spring WebFlux ?

Spring WebFlux is parallel version of Spring MVC and supports fully non-blocking reactive streams. It support the back pressure concept and uses Netty as inbuilt server to run reactive applications. If you are familiar with Spring MVC programming style, you can easily work on webflux also.

Spring webflux uses project reactor as reactive library. Reactor is a Reactive Streams library and, therefore, all of its operators support non-blocking back pressure. It is developed in close collaboration with Spring.

Spring WebFlux heavily uses two publishers :

Mono: Returns 0 or 1 element.

Mono<String> mono = Mono.just("CodeFactory");
Mono<String> mono = Mono.empty();

Flux: Returns 0…N elements. A Flux can be endless, meaning that it can keep emitting elements forever. Also it can return a sequence of elements and then send a completion notification when it has returned all of its elements.

Flux<String> flux = Flux.just("A", "B", "C");
Flux<String> flux = Flux.fromArray(new String[]{"A", "B", "C"});
Flux<String> flux = Flux.fromIterable(Arrays.asList("A", "B", "C"));

In Spring WebFlux, we call reactive apis/functions that return monos and fluxes and your controllers will return monos and fluxes. When you invoke an API that returns a mono or a flux, it will return immediately. The results of the function call will be delivered to you through the mono or flux when they become available.

To build a truly non-blocking application, we must aim to create/use all of it’s components as non-blocking i.e. client, controller, middle services and even the database. If one of them is blocking the requests, our aim will be defeated.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.7.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.spring5crud</groupId>
	<artifactId>spring5-crud</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring5-crud</name>
	<description>Demo project for Spring 5 with Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</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>
		
		<dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>
        
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
	</dependencies>

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

</project>

application.properties

spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 
    <appender name="STDOUT"
        class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n
            </pattern>
        </encoder>
    </appender>
 
    <logger name="org.springframework" level="DEBUG"
        additivity="false">
        <appender-ref ref="STDOUT" />
    </logger>
 
    <root level="ERROR">
        <appender-ref ref="STDOUT" />
    </root>
 
</configuration>

Spring5CrudApplication.java

package com.spring5crud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author code.factory
 *
 */
@SpringBootApplication
public class Spring5CrudApplication {

	public static void main(String[] args) {
		SpringApplication.run(Spring5CrudApplication.class, args);
	}

}

AppConfig.java

package com.spring5crud.configuration;

import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

/**
 * @author code.factory
 *
 */
@Configuration
public class AppConfig {

	@Bean
	public static PropertyPlaceholderConfigurer getPropertyPlaceholderConfigurer() {
		PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
		ppc.setLocation(new ClassPathResource("application.properties"));
		ppc.setIgnoreUnresolvablePlaceholders(true);
		return ppc;
	}

}

WebFluxConfig.java

package com.spring5crud.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;

/**
 * @author code.factory
 *
 */
@Configuration
@EnableWebFlux
public class WebFluxConfig implements WebFluxConfigurer {

}

EmployeeController.java

package com.spring5crud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.spring5crud.model.Employee;
import com.spring5crud.service.EmployeeService;

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

/**
 * @author code.factory
 * add Content-Type = application/json on every request
 */
@RestController
public class EmployeeController {

	@Autowired
	private EmployeeService employeeService;

	@RequestMapping(value = "/", method = RequestMethod.GET)
	@ResponseBody
	public ResponseEntity<Flux<Employee>> findAll() {
		Flux<Employee> emps = employeeService.findAll();
		HttpStatus status = emps != null ? HttpStatus.OK : HttpStatus.NOT_FOUND;
		return new ResponseEntity<Flux<Employee>>(emps, status);
	}
	
	@RequestMapping(value = "/{id}", method = RequestMethod.GET)
    @ResponseBody
    public ResponseEntity<Mono<Employee>> findById(@PathVariable("id") Integer id) {
        Mono<Employee> e = employeeService.findById(id);
        HttpStatus status = e.equals(Mono.empty()) ? HttpStatus.NOT_FOUND : HttpStatus.OK;
        return new ResponseEntity<Mono<Employee>>(e, status);
    }
	
	@RequestMapping(value = { "/create" }, method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    @ResponseBody
    public void create(@RequestBody Employee e) {
        employeeService.create(e);
    }

	@RequestMapping(value = "/update", method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.OK)
    public ResponseEntity<Mono<Employee>> update(@RequestBody Employee emp) {
		Mono<Employee> e = employeeService.findById(emp.getId());
        HttpStatus status = e.equals(Mono.empty()) ? HttpStatus.NOT_FOUND : HttpStatus.OK;
		if(e.equals(Mono.empty())) {
			return new ResponseEntity<Mono<Employee>>(Mono.empty(), status);
		}
        return new ResponseEntity<Mono<Employee>>(employeeService.update(emp), status);
    }
	
	@RequestMapping(value = "/delete/{id}", method = RequestMethod.DELETE)
    @ResponseStatus(HttpStatus.OK)
    public ResponseEntity<Mono<Void>> delete(@PathVariable("id") Integer id) {
		Mono<Employee> e = employeeService.findById(id);
		HttpStatus status = e.equals(Mono.empty()) ? HttpStatus.NOT_FOUND : HttpStatus.OK;
		if(e.equals(Mono.empty())) {
			return new ResponseEntity<Mono<Void>>(Mono.empty(), status);
		}
		return new ResponseEntity<Mono<Void>>(employeeService.delete(id), status);
    }
}

Employee.java

package com.spring5crud.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/**
 * @author code.factory
 *
 */
@Entity
public class Employee {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	int id;
	String name;
	long salary;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public long getSalary() {
		return salary;
	}

	public void setSalary(long salary) {
		this.salary = salary;
	}

	@Override
	public String toString() {
		return "id=" + id + ", name=" + name + ", salary=" + salary;
	}
}

EmployeeRepository.java

package com.spring5crud.Repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.spring5crud.model.Employee;

/**
 * @author code.factory
 *
 */
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {

}

EmployeeService.java

package com.spring5crud.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.spring5crud.Repository.EmployeeRepository;
import com.spring5crud.model.Employee;

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

/**
 * @author code.factory
 *
 */
@Service
public class EmployeeService implements IEmployeeService {

	@Autowired
	private EmployeeRepository employeeRepository;

	@Override
	public void create(Employee e) {
		employeeRepository.save(e);
	}

	@Override
	public Mono<Employee> findById(Integer id) {
		return Mono.justOrEmpty(employeeRepository.findById(id));
	}

	@Override
	public Flux<Employee> findAll() {
		return Flux.fromIterable(employeeRepository.findAll());
	}

	@Override
	public Mono<Employee> update(Employee e) {
		return Mono.just(employeeRepository.save(e));
	}

	@Override
	public Mono<Void> delete(Integer id) {
		employeeRepository.deleteById(id);
		return Mono.empty();
	}

}

IEmployeeService.java

package com.spring5crud.service;

import com.spring5crud.model.Employee;

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

/**
 * @author code.factory
 *
 */
public interface IEmployeeService {

	void create(Employee e);

	Mono<Employee> findById(Integer id);

	Flux<Employee> findAll();

	Mono<Employee> update(Employee e);

	Mono<Void> delete(Integer id);

}

Note : use Content-Type = application/json

HTTP POST http://localhost:8080/create

HTTP GET http://localhost:8080/

HTTP PUT http://localhost:8080/update

HTTP DELETE http://localhost:8080/delete/3

Notice that I am testing the API with Postman which is a blocking client. It will display the result only when It has collected the response for both employees.

To verify the non-blocking response feature, hit the URL in chrome browser directly. The results will appear one by one, as and when they are available in form of events (text/event-stream). To better view the result, consider adding a delay to controller API.

Do some small change in EmployeeController#findAll() method, add produces = MediaType.TEXT_EVENT_STREAM_VALUE

@RequestMapping(value = "/", method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
	@ResponseBody
	public ResponseEntity<Flux<Employee>> findAll() {
		Flux<Employee> emps = employeeService.findAll();
		HttpStatus status = emps != null ? HttpStatus.OK : HttpStatus.NOT_FOUND;
		return new ResponseEntity<Flux<Employee>>(emps, status);
	}

Hit URL http://localhost:8080/ in browser.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s