This commit is contained in:
Jonathan Cook
2019-10-23 15:01:44 +02:00
parent db85c8f275
commit 684ec0d2e3
20486 changed files with 1642483 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
#folders#
.idea
/target
/neoDb*
/data
/src/main/webapp/WEB-INF/classes
*/META-INF/*
# Packaged files #
*.jar
*.war
*.ear
+12
View File
@@ -0,0 +1,12 @@
## Spring 5 Reactive Project
This module contains articles about reactive Spring 5
- [Spring WebClient vs. RestTemplate](https://www.baeldung.com/spring-webclient-resttemplate)
- [Validation for Functional Endpoints in Spring 5](https://www.baeldung.com/spring-functional-endpoints-validation)
- [Logging a Reactive Sequence](https://www.baeldung.com/spring-reactive-sequence-logging)
- [Testing Reactive Streams Using StepVerifier and TestPublisher](https://www.baeldung.com/reactive-streams-step-verifier-test-publisher)
- [Debugging Reactive Streams in Spring 5](https://www.baeldung.com/spring-debugging-reactive-streams)
- [Static Content in Spring WebFlux](https://www.baeldung.com/spring-webflux-static-content)
- [Spring WebClient Filters](https://www.baeldung.com/spring-webclient-filters)
- More articles: [[<-- prev]](/spring-5-reactive)
+77
View File
@@ -0,0 +1,77 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.baeldung</groupId>
<artifactId>spring-5-reactive-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-5-reactive-2</name>
<packaging>jar</packaging>
<description>spring 5 sample project about new features</description>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../parent-boot-2</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectreactor</groupId>
<artifactId>reactor-spring</artifactId>
<version>${reactor-spring.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>${wiremock.version}</version>
<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>
<configuration>
<mainClass>com.baeldung.webclient.WebClientApplication</mainClass>
<layout>JAR</layout>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<reactor-spring.version>1.0.1.RELEASE</reactor-spring.version>
<wiremock.version>2.24.0</wiremock.version>
</properties>
</project>
@@ -0,0 +1,34 @@
package com.baeldung.debugging.consumer;
import java.util.Collections;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Hooks;
@SpringBootApplication(exclude = MongoReactiveAutoConfiguration.class)
@EnableScheduling
public class ConsumerDebuggingApplication {
public static void main(String[] args) {
Hooks.onOperatorDebug();
SpringApplication app = new SpringApplication(ConsumerDebuggingApplication.class);
app.setDefaultProperties(Collections.singletonMap("server.port", "8082"));
app.run(args);
}
@Bean
public SecurityWebFilterChain debuggingConsumerSpringSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange()
.anyExchange()
.permitAll();
http.csrf().disable();
return http.build();
}
}
@@ -0,0 +1,154 @@
package com.baeldung.debugging.consumer.chronjobs;
import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import com.baeldung.debugging.consumer.model.Foo;
import com.baeldung.debugging.consumer.model.FooDto;
import com.baeldung.debugging.consumer.service.FooService;
import reactor.core.publisher.Flux;
@Component
public class ChronJobs {
private static Logger logger = LoggerFactory.getLogger(ChronJobs.class);
private WebClient client = WebClient.create("http://localhost:8081");
@Autowired
private FooService service;
@Scheduled(fixedRate = 10000)
public void consumeInfiniteFlux() {
Flux<Foo> fluxFoo = client.get()
.uri("/functional-reactive/periodic-foo")
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(FooDto.class)
.delayElements(Duration.ofMillis(100))
.map(dto -> {
logger.debug("process 1 with dto id {} name{}", dto.getId(), dto.getName());
return new Foo(dto);
});
Integer random = ThreadLocalRandom.current()
.nextInt(0, 3);
switch (random) {
case 0:
logger.info("process 1 with approach 1");
service.processFoo(fluxFoo);
break;
case 1:
logger.info("process 1 with approach 1 EH");
service.processUsingApproachOneWithErrorHandling(fluxFoo);
break;
default:
logger.info("process 1 with approach 2");
service.processFooInAnotherScenario(fluxFoo);
break;
}
}
@Scheduled(fixedRate = 20000)
public void consumeFiniteFlux2() {
Flux<Foo> fluxFoo = client.get()
.uri("/functional-reactive/periodic-foo-2")
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(FooDto.class)
.delayElements(Duration.ofMillis(100))
.map(dto -> {
logger.debug("process 2 with dto id {} name{}", dto.getId(), dto.getName());
return new Foo(dto);
});
Integer random = ThreadLocalRandom.current()
.nextInt(0, 3);
switch (random) {
case 0:
logger.info("process 2 with approach 1");
service.processFoo(fluxFoo);
break;
case 1:
logger.info("process 2 with approach 1 EH");
service.processUsingApproachOneWithErrorHandling(fluxFoo);
break;
default:
logger.info("process 2 with approach 2");
service.processFooInAnotherScenario(fluxFoo);
break;
}
}
@Scheduled(fixedRate = 20000)
public void consumeFiniteFlux3() {
Flux<Foo> fluxFoo = client.get()
.uri("/functional-reactive/periodic-foo-2")
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(FooDto.class)
.delayElements(Duration.ofMillis(100))
.map(dto -> {
logger.debug("process 3 with dto id {} name{}", dto.getId(), dto.getName());
return new Foo(dto);
});
logger.info("process 3 with approach 3");
service.processUsingApproachThree(fluxFoo);
}
@Scheduled(fixedRate = 20000)
public void consumeFiniteFluxWithCheckpoint4() {
Flux<Foo> fluxFoo = client.get()
.uri("/functional-reactive/periodic-foo-2")
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(FooDto.class)
.delayElements(Duration.ofMillis(100))
.map(dto -> {
logger.debug("process 4 with dto id {} name{}", dto.getId(), dto.getName());
return new Foo(dto);
});
logger.info("process 4 with approach 4");
service.processUsingApproachFourWithCheckpoint(fluxFoo);
}
@Scheduled(fixedRate = 20000)
public void consumeFiniteFluxWitParallelScheduler() {
Flux<Foo> fluxFoo = client.get()
.uri("/functional-reactive/periodic-foo-2")
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(FooDto.class)
.delayElements(Duration.ofMillis(100))
.map(dto -> {
logger.debug("process 5-parallel with dto id {} name{}", dto.getId(), dto.getName());
return new Foo(dto);
});
logger.info("process 5-parallel with approach 5-parallel");
service.processUsingApproachFivePublishingToDifferentParallelThreads(fluxFoo);
}
@Scheduled(fixedRate = 20000)
public void consumeFiniteFluxWithSingleSchedulers() {
Flux<Foo> fluxFoo = client.get()
.uri("/functional-reactive/periodic-foo-2")
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(FooDto.class)
.delayElements(Duration.ofMillis(100))
.map(dto -> {
logger.debug("process 5-single with dto id {} name{}", dto.getId(), dto.getName());
return new Foo(dto);
});
logger.info("process 5-single with approach 5-single");
service.processUsingApproachFivePublishingToDifferentSingleThreads(fluxFoo);
}
}
@@ -0,0 +1,23 @@
package com.baeldung.debugging.consumer.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Hooks;
@RestController
public class ReactiveConfigsToggleRestController {
@GetMapping("/debug-hook-on")
public String setReactiveDebugOn() {
Hooks.onOperatorDebug();
return "DEBUG HOOK ON";
}
@GetMapping("/debug-hook-off")
public String setReactiveDebugOff() {
Hooks.resetOnOperatorDebug();
return "DEBUG HOOK OFF";
}
}
@@ -0,0 +1,27 @@
package com.baeldung.debugging.consumer.model;
import java.util.concurrent.ThreadLocalRandom;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Foo {
private Integer id;
private String formattedName;
private Integer quantity;
public Foo(FooDto dto) {
this.id = (ThreadLocalRandom.current()
.nextInt(0, 100) == 0) ? null : dto.getId();
this.formattedName = dto.getName();
this.quantity = ThreadLocalRandom.current()
.nextInt(0, 10);
}
}
@@ -0,0 +1,16 @@
package com.baeldung.debugging.consumer.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class FooDto {
private Integer id;
private String name;
}
@@ -0,0 +1,45 @@
package com.baeldung.debugging.consumer.service;
import java.util.concurrent.ThreadLocalRandom;
import com.baeldung.debugging.consumer.model.Foo;
import reactor.core.publisher.Flux;
public class FooNameHelper {
public static Flux<Foo> concatAndSubstringFooName(Flux<Foo> flux) {
flux = concatFooName(flux);
flux = substringFooName(flux);
return flux;
}
public static Flux<Foo> concatFooName(Flux<Foo> flux) {
flux = flux.map(foo -> {
String processedName = null;
Integer random = ThreadLocalRandom.current()
.nextInt(0, 80);
processedName = (random != 0) ? foo.getFormattedName() : foo.getFormattedName() + "-bael";
foo.setFormattedName(processedName);
return foo;
});
return flux;
}
public static Flux<Foo> substringFooName(Flux<Foo> flux) {
return flux.map(foo -> {
String processedName;
Integer random = ThreadLocalRandom.current()
.nextInt(0, 100);
processedName = (random == 0) ? foo.getFormattedName()
.substring(10, 15)
: foo.getFormattedName()
.substring(0, 5);
foo.setFormattedName(processedName);
return foo;
});
}
}
@@ -0,0 +1,31 @@
package com.baeldung.debugging.consumer.service;
import java.util.concurrent.ThreadLocalRandom;
import com.baeldung.debugging.consumer.model.Foo;
import reactor.core.publisher.Flux;
public class FooQuantityHelper {
public static Flux<Foo> processFooReducingQuantity(Flux<Foo> flux) {
flux = flux.map(foo -> {
Integer result;
Integer random = ThreadLocalRandom.current()
.nextInt(0, 90);
result = (random == 0) ? result = 0 : foo.getQuantity() + 2;
foo.setQuantity(result);
return foo;
});
return divideFooQuantity(flux);
}
public static Flux<Foo> divideFooQuantity(Flux<Foo> flux) {
return flux.map(foo -> {
Integer result = Math.round(5 / foo.getQuantity());
foo.setQuantity(result);
return foo;
});
}
}
@@ -0,0 +1,26 @@
package com.baeldung.debugging.consumer.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.baeldung.debugging.consumer.model.Foo;
import reactor.core.publisher.Flux;
public class FooReporter {
private static Logger logger = LoggerFactory.getLogger(FooReporter.class);
public static Flux<Foo> reportResult(Flux<Foo> input, String approach) {
return input.map(foo -> {
if (foo.getId() == null)
throw new IllegalArgumentException("Null id is not valid!");
logger.info("Reporting for approach {}: Foo with id '{}' name '{}' and quantity '{}'", approach, foo.getId(), foo.getFormattedName(), foo.getQuantity());
return foo;
});
}
public static Flux<Foo> reportResult(Flux<Foo> input) {
return reportResult(input, "default");
}
}
@@ -0,0 +1,120 @@
package com.baeldung.debugging.consumer.service;
import static com.baeldung.debugging.consumer.service.FooNameHelper.concatAndSubstringFooName;
import static com.baeldung.debugging.consumer.service.FooNameHelper.substringFooName;
import static com.baeldung.debugging.consumer.service.FooQuantityHelper.divideFooQuantity;
import static com.baeldung.debugging.consumer.service.FooQuantityHelper.processFooReducingQuantity;
import static com.baeldung.debugging.consumer.service.FooReporter.reportResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.baeldung.debugging.consumer.model.Foo;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
@Component
public class FooService {
private static Logger logger = LoggerFactory.getLogger(FooService.class);
public void processFoo(Flux<Foo> flux) {
flux = FooNameHelper.concatFooName(flux);
flux = FooNameHelper.substringFooName(flux);
flux = flux.log();
flux = FooReporter.reportResult(flux);
flux = flux.doOnError(error -> {
logger.error("The following error happened on processFoo method!", error);
});
flux.subscribe();
}
public void processFooInAnotherScenario(Flux<Foo> flux) {
flux = FooNameHelper.substringFooName(flux);
flux = FooQuantityHelper.divideFooQuantity(flux);
flux.subscribe();
}
public void processUsingApproachOneWithErrorHandling(Flux<Foo> flux) {
logger.info("starting approach one w error handling!");
flux = concatAndSubstringFooName(flux);
flux = concatAndSubstringFooName(flux);
flux = substringFooName(flux);
flux = processFooReducingQuantity(flux);
flux = processFooReducingQuantity(flux);
flux = processFooReducingQuantity(flux);
flux = reportResult(flux, "ONE w/ EH");
flux = flux.doOnError(error -> {
logger.error("Approach 1 with Error Handling failed!", error);
});
flux.subscribe();
}
public void processUsingApproachThree(Flux<Foo> flux) {
logger.info("starting approach three!");
flux = concatAndSubstringFooName(flux);
flux = reportResult(flux, "THREE");
flux = flux.doOnError(error -> {
logger.error("Approach 3 failed!", error);
});
flux.subscribe();
}
public void processUsingApproachFourWithCheckpoint(Flux<Foo> flux) {
logger.info("starting approach four!");
flux = concatAndSubstringFooName(flux);
flux = flux.checkpoint("CHECKPOINT 1");
flux = concatAndSubstringFooName(flux);
flux = divideFooQuantity(flux);
flux = flux.checkpoint("CHECKPOINT 2", true);
flux = reportResult(flux, "FOUR");
flux = concatAndSubstringFooName(flux).doOnError(error -> {
logger.error("Approach 4 failed!", error);
});
flux.subscribe();
}
public void processUsingApproachFourWithInitialCheckpoint(Flux<Foo> flux) {
logger.info("starting approach four!");
flux = concatAndSubstringFooName(flux);
flux = flux.checkpoint("CHECKPOINT 1", true);
flux = concatAndSubstringFooName(flux);
flux = divideFooQuantity(flux);
flux = reportResult(flux, "FOUR");
flux = flux.doOnError(error -> {
logger.error("Approach 4-2 failed!", error);
});
flux.subscribe();
}
public void processUsingApproachFivePublishingToDifferentParallelThreads(Flux<Foo> flux) {
logger.info("starting approach five-parallel!");
flux = concatAndSubstringFooName(flux).publishOn(Schedulers.newParallel("five-parallel-foo"))
.log();
flux = concatAndSubstringFooName(flux);
flux = divideFooQuantity(flux);
flux = reportResult(flux, "FIVE-PARALLEL").publishOn(Schedulers.newSingle("five-parallel-bar"));
flux = concatAndSubstringFooName(flux).doOnError(error -> {
logger.error("Approach 5-parallel failed!", error);
});
flux.subscribeOn(Schedulers.newParallel("five-parallel-starter"))
.subscribe();
}
public void processUsingApproachFivePublishingToDifferentSingleThreads(Flux<Foo> flux) {
logger.info("starting approach five-single!");
flux = flux.log()
.subscribeOn(Schedulers.newSingle("five-single-starter"));
flux = concatAndSubstringFooName(flux).publishOn(Schedulers.newSingle("five-single-foo"));
flux = concatAndSubstringFooName(flux);
flux = divideFooQuantity(flux);
flux = reportResult(flux, "FIVE-SINGLE").publishOn(Schedulers.newSingle("five-single-bar"));
flux = concatAndSubstringFooName(flux).doOnError(error -> {
logger.error("Approach 5-single failed!", error);
});
flux.subscribe();
}
}
@@ -0,0 +1,29 @@
package com.baeldung.debugging.server;
import java.util.Collections;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.reactive.config.EnableWebFlux;
@EnableWebFlux
@SpringBootApplication
public class ServerDebuggingApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(ServerDebuggingApplication.class);
app.setDefaultProperties(Collections.singletonMap("server.port", "8081"));
app.run(args);
}
@Bean
public SecurityWebFilterChain debuggingServerSpringSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange()
.anyExchange()
.permitAll();
return http.build();
}
}
@@ -0,0 +1,47 @@
package com.baeldung.debugging.server.handlers;
import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.baeldung.debugging.server.model.Foo;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Component
public class ServerHandler {
private static Logger logger = LoggerFactory.getLogger(ServerHandler.class);
public Mono<ServerResponse> useHandler(final ServerRequest request) {
// there are chances that something goes wrong here...
return ServerResponse.ok()
.contentType(MediaType.TEXT_EVENT_STREAM)
.body(Flux.interval(Duration.ofSeconds(1))
.map(sequence -> {
logger.info("retrieving Foo. Sequence: {}", sequence);
if (ThreadLocalRandom.current()
.nextInt(0, 50) == 1) {
throw new RuntimeException("There was an error retrieving the Foo!");
}
return new Foo(sequence, "name" + sequence);
}), Foo.class);
}
public Mono<ServerResponse> useHandlerFinite(final ServerRequest request) {
return ServerResponse.ok()
.contentType(MediaType.TEXT_EVENT_STREAM)
.body(Flux.range(0, 50)
.map(sequence -> {
return new Foo(new Long(sequence), "theFooNameNumber" + sequence);
}), Foo.class);
}
}
@@ -0,0 +1,13 @@
package com.baeldung.debugging.server.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Foo {
private Long id;
private String name;
}
@@ -0,0 +1,22 @@
package com.baeldung.debugging.server.routers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.baeldung.debugging.server.handlers.ServerHandler;
@Configuration
public class ServerRouter {
@Bean
public RouterFunction<ServerResponse> responseRoute(@Autowired ServerHandler handler) {
return RouterFunctions.route(RequestPredicates.GET("/functional-reactive/periodic-foo"), handler::useHandler)
.andRoute(RequestPredicates.GET("/functional-reactive/periodic-foo-2"), handler::useHandlerFinite);
}
}
@@ -0,0 +1,28 @@
package com.baeldung.staticcontent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import java.util.Collections;
@SpringBootApplication
public class StaticContentApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(StaticContentApplication.class);
app.setDefaultProperties(Collections.singletonMap("server.port", "8084"));
app.run(args);
}
@Bean
public SecurityWebFilterChain staticContentSpringSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange()
.anyExchange()
.permitAll();
return http.build();
}
}
@@ -0,0 +1,35 @@
package com.baeldung.staticcontent;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
@Configuration
public class StaticContentConfig {
@Bean
public RouterFunction<ServerResponse> htmlRouter(@Value("classpath:/public/index.html") Resource html) {
return route(
GET("/"),
request -> ok()
.contentType(MediaType.TEXT_HTML)
.syncBody(html)
);
}
@Bean
public RouterFunction<ServerResponse> imgRouter() {
return RouterFunctions.resources("/img/**", new ClassPathResource("img/"));
}
}
@@ -0,0 +1,24 @@
package com.baeldung.validations.functional;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@SpringBootApplication
public class FunctionalValidationsApplication {
public static void main(String[] args) {
SpringApplication.run(FunctionalValidationsApplication.class, args);
}
@Bean
public SecurityWebFilterChain functionalValidationsSpringSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange()
.anyExchange()
.permitAll();
http.csrf().disable();
return http.build();
}
}
@@ -0,0 +1,45 @@
package com.baeldung.validations.functional.handlers;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import reactor.core.publisher.Mono;
public abstract class AbstractValidationHandler<T, U extends Validator> {
private final Class<T> validationClass;
private final U validator;
protected AbstractValidationHandler(Class<T> clazz, U validator) {
this.validationClass = clazz;
this.validator = validator;
}
abstract protected Mono<ServerResponse> processBody(T validBody, final ServerRequest originalRequest);
public final Mono<ServerResponse> handleRequest(final ServerRequest request) {
return request.bodyToMono(this.validationClass)
.flatMap(body -> {
Errors errors = new BeanPropertyBindingResult(body, this.validationClass.getName());
this.validator.validate(body, errors);
if (errors == null || errors.getAllErrors()
.isEmpty()) {
return processBody(body, request);
} else {
return onValidationErrors(errors, body, request);
}
});
}
protected Mono<ServerResponse> onValidationErrors(Errors errors, T invalidBody, final ServerRequest request) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, errors.getAllErrors()
.toString());
}
}
@@ -0,0 +1,43 @@
package com.baeldung.validations.functional.handlers;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import com.baeldung.validations.functional.model.CustomRequestEntity;
import com.baeldung.validations.functional.validators.CustomRequestEntityValidator;
import reactor.core.publisher.Mono;
@Component
public class FunctionalHandler {
public Mono<ServerResponse> handleRequest(final ServerRequest request) {
Validator validator = new CustomRequestEntityValidator();
Mono<String> responseBody = request.bodyToMono(CustomRequestEntity.class)
.map(body -> {
Errors errors = new BeanPropertyBindingResult(body, CustomRequestEntity.class.getName());
validator.validate(body, errors);
if (errors == null || errors.getAllErrors()
.isEmpty()) {
return String.format("Hi, %s [%s]!", body.getName(), body.getCode());
} else {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, errors.getAllErrors()
.toString());
}
});
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(responseBody, String.class);
}
}
@@ -0,0 +1,29 @@
package com.baeldung.validations.functional.handlers.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.baeldung.validations.functional.handlers.AbstractValidationHandler;
import com.baeldung.validations.functional.model.AnnotatedRequestEntity;
import reactor.core.publisher.Mono;
@Component
public class AnnotatedRequestEntityValidationHandler extends AbstractValidationHandler<AnnotatedRequestEntity, Validator> {
private AnnotatedRequestEntityValidationHandler(@Autowired Validator validator) {
super(AnnotatedRequestEntity.class, validator);
}
@Override
protected Mono<ServerResponse> processBody(AnnotatedRequestEntity validBody, ServerRequest originalRequest) {
String responseBody = String.format("Hi, %s. Password: %s!", validBody.getUser(), validBody.getPassword());
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(responseBody), String.class);
}
}
@@ -0,0 +1,37 @@
package com.baeldung.validations.functional.handlers.impl;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.baeldung.validations.functional.handlers.AbstractValidationHandler;
import com.baeldung.validations.functional.model.CustomRequestEntity;
import com.baeldung.validations.functional.validators.CustomRequestEntityValidator;
import reactor.core.publisher.Mono;
@Component
public class CustomRequestEntityValidationHandler extends AbstractValidationHandler<CustomRequestEntity, CustomRequestEntityValidator> {
private CustomRequestEntityValidationHandler() {
super(CustomRequestEntity.class, new CustomRequestEntityValidator());
}
@Override
protected Mono<ServerResponse> processBody(CustomRequestEntity validBody, ServerRequest originalRequest) {
String responseBody = String.format("Hi, %s [%s]!", validBody.getName(), validBody.getCode());
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(responseBody), String.class);
}
@Override
protected Mono<ServerResponse> onValidationErrors(Errors errors, CustomRequestEntity invalidBody, final ServerRequest request) {
return ServerResponse.badRequest()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(String.format("Custom message showing the errors: %s", errors.getAllErrors()
.toString())), String.class);
}
}
@@ -0,0 +1,28 @@
package com.baeldung.validations.functional.handlers.impl;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.baeldung.validations.functional.handlers.AbstractValidationHandler;
import com.baeldung.validations.functional.model.OtherEntity;
import com.baeldung.validations.functional.validators.OtherEntityValidator;
import reactor.core.publisher.Mono;
@Component
public class OtherEntityValidationHandler extends AbstractValidationHandler<OtherEntity, OtherEntityValidator> {
private OtherEntityValidationHandler() {
super(OtherEntity.class, new OtherEntityValidator());
}
@Override
protected Mono<ServerResponse> processBody(OtherEntity validBody, ServerRequest originalRequest) {
String responseBody = String.format("Other object with item %s and quantity %s!", validBody.getItem(), validBody.getQuantity());
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(responseBody), String.class);
}
}
@@ -0,0 +1,23 @@
package com.baeldung.validations.functional.model;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class AnnotatedRequestEntity {
@NotNull
private String user;
@NotNull
@Size(min = 4, max = 7)
private String password;
}
@@ -0,0 +1,17 @@
package com.baeldung.validations.functional.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class CustomRequestEntity {
private String name;
private String code;
}
@@ -0,0 +1,17 @@
package com.baeldung.validations.functional.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class OtherEntity {
private String item;
private Integer quantity;
}
@@ -0,0 +1,29 @@
package com.baeldung.validations.functional.routers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.baeldung.validations.functional.handlers.FunctionalHandler;
import com.baeldung.validations.functional.handlers.impl.AnnotatedRequestEntityValidationHandler;
import com.baeldung.validations.functional.handlers.impl.CustomRequestEntityValidationHandler;
import com.baeldung.validations.functional.handlers.impl.OtherEntityValidationHandler;
@Configuration
public class ValidationsRouters {
@Bean
public RouterFunction<ServerResponse> validationsRouter(@Autowired CustomRequestEntityValidationHandler dryHandler,
@Autowired FunctionalHandler complexHandler,
@Autowired OtherEntityValidationHandler otherHandler,
@Autowired AnnotatedRequestEntityValidationHandler annotatedEntityHandler) {
return RouterFunctions.route(RequestPredicates.POST("/complex-handler-functional-validation"), complexHandler::handleRequest)
.andRoute(RequestPredicates.POST("/dry-functional-validation"), dryHandler::handleRequest)
.andRoute(RequestPredicates.POST("/other-dry-functional-validation"), otherHandler::handleRequest)
.andRoute(RequestPredicates.POST("/annotated-functional-validation"), annotatedEntityHandler::handleRequest);
}
}
@@ -0,0 +1,29 @@
package com.baeldung.validations.functional.validators;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.baeldung.validations.functional.model.CustomRequestEntity;
public class CustomRequestEntityValidator implements Validator {
private static final int MINIMUM_CODE_LENGTH = 6;
@Override
public boolean supports(Class<?> clazz) {
return CustomRequestEntity.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "code", "field.required");
CustomRequestEntity request = (CustomRequestEntity) target;
if (request.getCode() != null && request.getCode()
.trim()
.length() < MINIMUM_CODE_LENGTH) {
errors.rejectValue("code", "field.min.length", new Object[] { Integer.valueOf(MINIMUM_CODE_LENGTH) }, "The code must be at least [" + MINIMUM_CODE_LENGTH + "] characters in length.");
}
}
}
@@ -0,0 +1,27 @@
package com.baeldung.validations.functional.validators;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.baeldung.validations.functional.model.OtherEntity;
public class OtherEntityValidator implements Validator {
private static final int MIN_ITEM_QUANTITY = 1;
@Override
public boolean supports(Class<?> clazz) {
return OtherEntity.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "item", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "quantity", "field.required");
OtherEntity request = (OtherEntity) target;
if (request.getQuantity() != null && request.getQuantity() < MIN_ITEM_QUANTITY) {
errors.rejectValue("quantity", "field.min.length", new Object[] { Integer.valueOf(MIN_ITEM_QUANTITY) }, "There must be at least one item");
}
}
}
@@ -0,0 +1,13 @@
package com.baeldung.webclient;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Tweet {
private String text;
private String username;
}
@@ -0,0 +1,20 @@
package com.baeldung.webclient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
public class TweetsSlowServiceController {
@GetMapping("/slow-service-tweets")
private List<Tweet> getAllTweets() throws Exception {
Thread.sleep(2000L); // delay
return Arrays.asList(
new Tweet("RestTemplate rules", "@user1"),
new Tweet("WebClient is better", "@user2"),
new Tweet("OK, both are useful", "@user1"));
}
}
@@ -0,0 +1,23 @@
package com.baeldung.webclient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@SpringBootApplication
public class WebClientApplication {
public static void main(String[] args) {
SpringApplication.run(WebClientApplication.class, args);
}
@Bean
public SecurityWebFilterChain functionalValidationsSpringSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange()
.anyExchange()
.permitAll();
http.csrf().disable();
return http.build();
}
}
@@ -0,0 +1,60 @@
package com.baeldung.webclient;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import java.util.List;
@Slf4j
@RestController
public class WebController {
private static final int DEFAULT_PORT = 8080;
@Setter
private int serverPort = DEFAULT_PORT;
@GetMapping("/tweets-blocking")
public List<Tweet> getTweetsBlocking() {
log.info("Starting BLOCKING Controller!");
final String uri = getSlowServiceUri();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<List<Tweet>> response = restTemplate.exchange(
uri, HttpMethod.GET, null,
new ParameterizedTypeReference<List<Tweet>>(){});
List<Tweet> result = response.getBody();
result.forEach(tweet -> log.info(tweet.toString()));
log.info("Exiting BLOCKING Controller!");
return result;
}
@GetMapping(value = "/tweets-non-blocking", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Tweet> getTweetsNonBlocking() {
log.info("Starting NON-BLOCKING Controller!");
Flux<Tweet> tweetFlux = WebClient.create()
.get()
.uri(getSlowServiceUri())
.retrieve()
.bodyToFlux(Tweet.class);
tweetFlux.subscribe(tweet -> log.info(tweet.toString()));
log.info("Exiting NON-BLOCKING Controller!");
return tweetFlux;
}
private String getSlowServiceUri() {
return "http://localhost:" + serverPort + "/slow-service-tweets";
}
}
@@ -0,0 +1,57 @@
package com.baeldung.webclient.filter;
import java.io.PrintStream;
import java.net.URI;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
public class WebClientFilters {
private static final Logger LOG = LoggerFactory.getLogger(WebClientFilters.class);
public static ExchangeFilterFunction demoFilter() {
ExchangeFilterFunction filterFunction = (clientRequest, nextFilter) -> {
LOG.info("WebClient fitler executed");
return nextFilter.exchange(clientRequest);
};
return filterFunction;
}
public static ExchangeFilterFunction countingFilter(AtomicInteger getCounter) {
ExchangeFilterFunction countingFilter = (clientRequest, nextFilter) -> {
HttpMethod httpMethod = clientRequest.method();
if (httpMethod == HttpMethod.GET) {
getCounter.incrementAndGet();
}
return nextFilter.exchange(clientRequest);
};
return countingFilter;
}
public static ExchangeFilterFunction urlModifyingFilter(String version) {
ExchangeFilterFunction urlModifyingFilter = (clientRequest, nextFilter) -> {
String oldUrl = clientRequest.url()
.toString();
URI newUrl = URI.create(oldUrl + "/" + version);
ClientRequest filteredRequest = ClientRequest.from(clientRequest)
.url(newUrl)
.build();
return nextFilter.exchange(filteredRequest);
};
return urlModifyingFilter;
}
public static ExchangeFilterFunction loggingFilter(PrintStream printStream) {
ExchangeFilterFunction loggingFilter = (clientRequest, nextFilter) -> {
printStream.print("Sending request " + clientRequest.method() + " " + clientRequest.url());
return nextFilter.exchange(clientRequest);
};
return loggingFilter;
}
}
@@ -0,0 +1,21 @@
package com.baeldung.webflux.logging;
import reactor.core.publisher.Flux;
public class WebFluxLoggingExample {
public static void main(String[] args) {
Flux<Integer> reactiveStream = Flux.range(1, 5).log();
reactiveStream.subscribe();
reactiveStream = Flux.range(1, 5).log().take(3);
reactiveStream.subscribe();
reactiveStream = Flux.range(1, 5).take(3).log();
reactiveStream.subscribe();
}
}
@@ -0,0 +1,3 @@
# Use in Static content Example
spring.webflux.static-path-pattern = /assets/**
spring.resources.static-locations = classpath:/assets/
@@ -0,0 +1,65 @@
package com.baeldung.debugging.consumer;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.baeldung.debugging.consumer.model.Foo;
import com.baeldung.debugging.consumer.service.FooService;
import com.baeldung.debugging.consumer.utils.ListAppender;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Hooks;
public class ConsumerFooServiceIntegrationTest {
FooService service = new FooService();
@BeforeEach
public void clearLogList() {
Hooks.onOperatorDebug();
ListAppender.clearEventList();
}
@Test
public void givenFooWithNullId_whenProcessFoo_thenLogsWithDebugTrace() {
Foo one = new Foo(1, "nameverylong", 8);
Foo two = new Foo(null, "nameverylong", 4);
Flux<Foo> flux = Flux.just(one, two);
service.processFoo(flux);
Collection<String> allLoggedEntries = ListAppender.getEvents()
.stream()
.map(ILoggingEvent::getFormattedMessage)
.collect(Collectors.toList());
Collection<String> allSuppressedEntries = ListAppender.getEvents()
.stream()
.map(ILoggingEvent::getThrowableProxy)
.flatMap(t -> {
return Optional.ofNullable(t)
.map(IThrowableProxy::getSuppressed)
.map(Arrays::stream)
.orElse(Stream.empty());
})
.map(IThrowableProxy::getMessage)
.collect(Collectors.toList());
assertThat(allLoggedEntries).anyMatch(entry -> entry.contains("The following error happened on processFoo method!"))
.anyMatch(entry -> entry.contains("| onSubscribe"))
.anyMatch(entry -> entry.contains("| cancel()"));
assertThat(allSuppressedEntries).anyMatch(entry -> entry.contains("Assembly trace from producer"))
.anyMatch(entry -> entry.contains("Error has been observed by the following operator(s)"));
}
}
@@ -0,0 +1,49 @@
package com.baeldung.debugging.consumer;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec;
import com.baeldung.debugging.consumer.service.FooService;
public class ConsumerFooServiceLiveTest {
FooService service = new FooService();
private static final String BASE_URL = "http://localhost:8082";
private static final String DEBUG_HOOK_ON = BASE_URL + "/debug-hook-on";
private static final String DEBUG_HOOK_OFF = BASE_URL + "/debug-hook-off";
private static WebTestClient client;
@BeforeAll
public static void setup() {
client = WebTestClient.bindToServer()
.baseUrl(BASE_URL)
.build();
}
@Test
public void whenRequestingDebugHookOn_thenObtainExpectedMessage() {
ResponseSpec response = client.get()
.uri(DEBUG_HOOK_ON)
.exchange();
response.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("DEBUG HOOK ON");
}
@Test
public void whenRequestingDebugHookOff_thenObtainExpectedMessage() {
ResponseSpec response = client.get()
.uri(DEBUG_HOOK_OFF)
.exchange();
response.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("DEBUG HOOK OFF");
}
}
@@ -0,0 +1,25 @@
package com.baeldung.debugging.consumer.utils;
import java.util.ArrayList;
import java.util.List;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
public class ListAppender extends AppenderBase<ILoggingEvent> {
static private List<ILoggingEvent> events = new ArrayList<>();
@Override
protected void append(ILoggingEvent eventObject) {
events.add(eventObject);
}
public static List<ILoggingEvent> getEvents() {
return events;
}
public static void clearEventList() {
events.clear();
}
}
@@ -0,0 +1,39 @@
package com.baeldung.staticcontent;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("assets-custom-location")
public class StaticContentCustomLocationIntegrationTest {
@Autowired
private WebTestClient client;
@Test
public void whenRequestingHtmlFileFromCustomLocation_thenReturnThisFileAndTextHtmlContentType() throws Exception {
client.get()
.uri("/assets/index.html")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_VALUE);
}
@Test
public void whenRequestingMissingStaticResource_thenReturnNotFoundStatus() throws Exception {
client.get()
.uri("/assets/unknown.png")
.exchange()
.expectStatus().isNotFound();
}
}
@@ -0,0 +1,46 @@
package com.baeldung.staticcontent;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class StaticContentDefaultLocationIntegrationTest {
@Autowired
private WebTestClient client;
@Test
public void whenRequestingHtmlFileFromDefaultLocation_thenReturnThisFileAndTextHtmlContentType() throws Exception {
client.get()
.uri("/index.html")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_VALUE);
}
@Test
public void whenRequestingPngImageFromImgLocation_thenReturnThisFileAndImagePngContentType() throws Exception {
client.get()
.uri("/img/example-image.png")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE);
}
@Test
public void whenRequestingMissingStaticResource_thenReturnNotFoundStatus() throws Exception {
client.get()
.uri("/unknown.png")
.exchange()
.expectStatus().isNotFound();
}
}
@@ -0,0 +1,34 @@
package com.baeldung.stepverifier;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import java.time.Duration;
public class PostExecutionUnitTest {
Flux<Integer> source = Flux.<Integer>create(emitter -> {
emitter.next(1);
emitter.next(2);
emitter.next(3);
emitter.complete();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
emitter.next(4);
}).filter(number -> number % 2 == 0);
@Test
public void droppedElements() {
StepVerifier.create(source)
.expectNext(2)
.expectComplete()
.verifyThenAssertThat()
.hasDropped(4)
.tookLessThan(Duration.ofMillis(1500));
}
}
@@ -0,0 +1,39 @@
package com.baeldung.stepverifier;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
public class StepByStepUnitTest {
Flux<String> source = Flux.just("John", "Monica", "Mark", "Cloe", "Frank", "Casper", "Olivia", "Emily", "Cate")
.filter(name -> name.length() == 4)
.map(String::toUpperCase);
@Test
public void shouldReturnForLettersUpperCaseStrings() {
StepVerifier
.create(source)
.expectNext("JOHN")
.expectNextMatches(name -> name.startsWith("MA"))
.expectNext("CLOE", "CATE")
.expectComplete()
.verify();
}
@Test
public void shouldThrowExceptionAfterFourElements() {
Flux<String> error = source.concatWith(
Mono.error(new IllegalArgumentException("Our message"))
);
StepVerifier
.create(error)
.expectNextCount(4)
.expectErrorMatches(throwable -> throwable instanceof IllegalArgumentException &&
throwable.getMessage().equals("Our message")
).verify();
}
}
@@ -0,0 +1,51 @@
package com.baeldung.stepverifier;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import reactor.test.publisher.TestPublisher;
public class TestingTestPublisherUnitTest {
@Test
public void testPublisher() {
TestPublisher
.<String>create()
.next("First", "Second", "Third")
.error(new RuntimeException("Message"));
}
@Test
public void nonCompliant() {
TestPublisher
.createNoncompliant(TestPublisher.Violation.ALLOW_NULL)
.emit("1", "2", null, "3");
}
@Test
public void testPublisherInAction() {
final TestPublisher<String> testPublisher = TestPublisher.create();
UppercaseConverter uppercaseConverter = new UppercaseConverter(testPublisher.flux());
StepVerifier.create(uppercaseConverter.getUpperCase())
.then(() -> testPublisher.emit("aA", "bb", "ccc"))
.expectNext("AA", "BB", "CCC")
.verifyComplete();
}
}
class UppercaseConverter {
private final Flux<String> source;
UppercaseConverter(Flux<String> source) {
this.source = source;
}
Flux<String> getUpperCase() {
return source
.map(String::toUpperCase);
}
}
@@ -0,0 +1,22 @@
package com.baeldung.stepverifier;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import java.time.Duration;
public class TimeBasedUnitTest {
@Test
public void simpleExample() {
StepVerifier
.withVirtualTime(() -> Flux.interval(Duration.ofSeconds(1)).take(2))
.expectSubscription()
.expectNoEvent(Duration.ofSeconds(1))
.expectNext(0L)
.thenAwait(Duration.ofSeconds(1))
.expectNext(1L)
.verifyComplete();
}
}
@@ -0,0 +1,108 @@
package com.baeldung.validations.functional;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec;
import com.baeldung.validations.functional.model.AnnotatedRequestEntity;
import com.baeldung.validations.functional.model.CustomRequestEntity;
import reactor.core.publisher.Mono;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class FunctionalEndpointValidationsLiveTest {
private static final String BASE_URL = "http://localhost:8080";
private static final String COMPLEX_EP_URL = BASE_URL + "/complex-handler-functional-validation";
private static final String DRY_EP_URL = BASE_URL + "/dry-functional-validation";
private static final String ANNOTATIONS_EP_URL = BASE_URL + "/annotated-functional-validation";
private static WebTestClient client;
@BeforeAll
public static void setup() {
client = WebTestClient.bindToServer()
.baseUrl(BASE_URL)
.build();
}
@Test
public void whenRequestingDryEPWithInvalidBody_thenObtainBadRequest() {
CustomRequestEntity body = new CustomRequestEntity("name", "123");
ResponseSpec response = client.post()
.uri(DRY_EP_URL)
.body(Mono.just(body), CustomRequestEntity.class)
.exchange();
response.expectStatus()
.isBadRequest();
}
@Test
public void whenRequestingComplexEPWithInvalidBody_thenObtainBadRequest() {
CustomRequestEntity body = new CustomRequestEntity("name", "123");
ResponseSpec response = client.post()
.uri(COMPLEX_EP_URL)
.body(Mono.just(body), CustomRequestEntity.class)
.exchange();
response.expectStatus()
.isBadRequest();
}
@Test
public void whenRequestingAnnotatedEPWithInvalidBody_thenObtainBadRequest() {
AnnotatedRequestEntity body = new AnnotatedRequestEntity("user", "passwordlongerthan7digits");
ResponseSpec response = client.post()
.uri(ANNOTATIONS_EP_URL)
.body(Mono.just(body), AnnotatedRequestEntity.class)
.exchange();
response.expectStatus()
.isBadRequest();
}
@Test
public void whenRequestingDryEPWithValidBody_thenObtainBadRequest() {
CustomRequestEntity body = new CustomRequestEntity("name", "1234567");
ResponseSpec response = client.post()
.uri(DRY_EP_URL)
.body(Mono.just(body), CustomRequestEntity.class)
.exchange();
response.expectStatus()
.isOk();
}
@Test
public void whenRequestingComplexEPWithValidBody_thenObtainBadRequest() {
CustomRequestEntity body = new CustomRequestEntity("name", "1234567");
ResponseSpec response = client.post()
.uri(COMPLEX_EP_URL)
.body(Mono.just(body), CustomRequestEntity.class)
.exchange();
response.expectStatus()
.isOk();
}
@Test
public void whenRequestingAnnotatedEPWithValidBody_thenObtainBadRequest() {
AnnotatedRequestEntity body = new AnnotatedRequestEntity("user", "12345");
ResponseSpec response = client.post()
.uri(ANNOTATIONS_EP_URL)
.body(Mono.just(body), AnnotatedRequestEntity.class)
.exchange();
response.expectStatus()
.isOk();
}
}
@@ -0,0 +1,51 @@
package com.baeldung.webclient;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = WebClientApplication.class)
public class WebControllerIntegrationTest {
@LocalServerPort
int randomServerPort;
@Autowired
private WebTestClient testClient;
@Autowired
private WebController webController;
@Before
public void setup() {
webController.setServerPort(randomServerPort);
}
@Test
public void whenEndpointWithBlockingClientIsCalled_thenThreeTweetsAreReceived() {
testClient.get()
.uri("/tweets-blocking")
.exchange()
.expectStatus()
.isOk()
.expectBodyList(Tweet.class)
.hasSize(3);
}
@Test
public void whenEndpointWithNonBlockingClientIsCalled_thenThreeTweetsAreReceived() {
testClient.get()
.uri("/tweets-non-blocking")
.exchange()
.expectStatus()
.isOk()
.expectBodyList(Tweet.class)
.hasSize(3);
}
}
@@ -0,0 +1,145 @@
package com.baeldung.webclient.filter;
import static com.baeldung.webclient.filter.WebClientFilters.countingFilter;
import static com.baeldung.webclient.filter.WebClientFilters.loggingFilter;
import static com.baeldung.webclient.filter.WebClientFilters.urlModifyingFilter;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URI;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
import org.springframework.web.reactive.function.client.WebClient;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
public class FilteredWebClientUnitTest {
private static final String PATH = "/filter/test";
@Rule
public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort()
.dynamicHttpsPort());
@Test
public void whenNoUrlModifyingFilter_thenPathUnchanged() {
stubFor(get(urlPathEqualTo(PATH)).willReturn(aResponse().withStatus(200)
.withBody("done")));
WebClient webClient = WebClient.create();
String actual = sendGetRequest(webClient);
assertThat(actual).isEqualTo("done");
verify(getRequestedFor(urlPathEqualTo(PATH)));
}
@Test
public void whenUrlModifyingFilter_thenPathModified() {
stubFor(get(urlPathEqualTo(PATH + "/1.0")).willReturn(aResponse().withStatus(200)
.withBody("done")));
WebClient webClient = WebClient.builder()
.filter(urlModifyingFilter("1.0"))
.build();
String actual = sendGetRequest(webClient);
assertThat(actual).isEqualTo("done");
verify(getRequestedFor(urlPathEqualTo(PATH + "/1.0")));
}
@Test
public void givenCountingFilter_whenGet_thenIncreaseCounter() {
stubFor(get(urlPathEqualTo(PATH)).willReturn(aResponse().withStatus(200)
.withBody("done")));
AtomicInteger counter = new AtomicInteger(10);
WebClient webClient = WebClient.builder()
.filter(countingFilter(counter))
.build();
String actual = sendGetRequest(webClient);
assertThat(actual).isEqualTo("done");
assertThat(counter.get()).isEqualTo(11);
}
@Test
public void givenCountingFilter_whenPost_thenDoNotIncreaseCounter() {
stubFor(post(urlPathEqualTo(PATH)).willReturn(aResponse().withStatus(200)
.withBody("done")));
AtomicInteger counter = new AtomicInteger(10);
WebClient webClient = WebClient.builder()
.filter(countingFilter(counter))
.build();
String actual = sendPostRequest(webClient);
assertThat(actual).isEqualTo("done");
assertThat(counter.get()).isEqualTo(10);
}
@Test
public void testLoggingFilter() throws IOException {
stubFor(get(urlPathEqualTo(PATH)).willReturn(aResponse().withStatus(200)
.withBody("done")));
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos);) {
WebClient webClient = WebClient.builder()
.filter(loggingFilter(ps))
.build();
String actual = sendGetRequest(webClient);
assertThat(actual).isEqualTo("done");
assertThat(baos.toString()).isEqualTo("Sending request GET " + getUrl());
}
}
@Test
public void testBasicAuthFilter() {
stubFor(get(urlPathEqualTo(PATH)).willReturn(aResponse().withStatus(200)
.withBody("authorized")));
WebClient webClient = WebClient.builder()
.filter(ExchangeFilterFunctions.basicAuthentication("user", "password"))
.build();
String actual = sendGetRequest(webClient);
assertThat(actual).isEqualTo("authorized");
verify(getRequestedFor(urlPathEqualTo(PATH)).withHeader("Authorization", containing("Basic")));
}
private String sendGetRequest(WebClient webClient) {
return webClient.get()
.uri(getUrl())
.retrieve()
.bodyToMono(String.class)
.block();
}
private String sendPostRequest(WebClient webClient) {
return webClient.post()
.uri(URI.create(getUrl()))
.retrieve()
.bodyToMono(String.class)
.block();
}
private String getUrl() {
return "http://localhost:" + wireMockRule.port() + PATH;
}
}
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Baeldung: Static Content in Spring WebFlux</title>
</head>
<body>
Example HTML file
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include
resource="org/springframework/boot/logging/logback/base.xml" />
<appender name="LISTAPPENDER"
class="com.baeldung.debugging.consumer.utils.ListAppender">
</appender>
<logger
name="com.baeldung.debugging.consumer.service.FooService">
<appender-ref ref="LISTAPPENDER" />
</logger>
<root level="info">
<appender-ref ref="CONSOLE" />
<appender-ref ref="LISTAPPENDER" />
</root>
</configuration>
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Baeldung: Static Content in Spring WebFlux</title>
</head>
<body>
Example HTML file
</body>
</html>