Spring 5 — WebClient and WebTestClient Tutorial | Code Factory

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
  1. Go to http://start.spring.io.
  2. Set Group and Artifact details.
  3. Add Reactive Web dependency in the dependencies section.
  4. Click Generate to generate and download the project.
WebClient webClient = WebClient.create();
WebClient webClient = WebClient.create("https://api.github.com");
WebClient webClient = WebClient.builder()
.baseUrl("https://api.github.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
.defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
.build();
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri("/user/repos")
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToFlux(GithubRepo.class);
}
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri("/user/repos")
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.exchange()
.flatMapMany(clientResponse -> clientResponse.bodyToFlux(GithubRepo.class));
}
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri("/user/repos?sort={sortField}&direction={sortDirection}",
"updated", "desc")
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToFlux(GithubRepo.class);
}
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri(uriBuilder -> uriBuilder.path("/user/repos")
.queryParam("sort", "updated")
.queryParam("direction", "desc")
.build())
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToFlux(GithubRepo.class);
}
public Mono<GithubRepo> createGithubRepository(String username, String token, 
RepoRequest createRepoRequest) {
return webClient.post()
.uri("/user/repos")
.body(Mono.just(createRepoRequest), RepoRequest.class)
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToMono(GithubRepo.class);
}
public Mono<GithubRepo> createGithubRepository(String username, String token, 
RepoRequest createRepoRequest) {
return webClient.post()
.uri("/user/repos")
.syncBody(createRepoRequest)
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToMono(GithubRepo.class);
}
public Mono<GithubRepo> createGithubRepository(String username, String token, 
RepoRequest createRepoRequest) {
return webClient.post()
.uri("/user/repos")
.body(BodyInserters.fromObject(createRepoRequest))
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToMono(GithubRepo.class);
}
  1. The ClientRequest and
  2. The next ExchangeFilterFunction in the filter chain.
WebClient webClient = WebClient.builder()
.baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
.filter(ExchangeFilterFunctions
.basicAuthentication(username, token))
.build();
WebClient webClient = WebClient.builder()
.baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
.filter(ExchangeFilterFunctions
.basicAuthentication(username, token))
.filter(logRequest())
.build();
private ExchangeFilterFunction logRequest() {
return (clientRequest, next) -> {
logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers()
.forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return next.exchange(clientRequest);
};
}
private ExchangeFilterFunction logRequest() {
ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers()
.forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
private ExchangeFilterFunction logResposneStatus() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
logger.info("Response Status {}", clientResponse.statusCode());
return Mono.just(clientResponse);
});
}
public Flux<GithubRepo> listGithubRepositories() {
return webClient.get()
.uri("/user/repos?sort={sortField}&direction={sortDirection}",
"updated", "desc")
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new MyCustomClientException())
)
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new MyCustomServerException())
)
.bodyToFlux(GithubRepo.class);
}
@ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<String> handleWebClientResponseException(WebClientResponseException ex) {
logger.error("Error from WebClient - Status {}, Body {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex);
return ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString());
}
package com.example;import org.assertj.core.api.Assertions;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import com.example.payload.GithubRepo;
import com.example.payload.RepoRequest;
import reactor.core.publisher.Mono;/**
* @author code.factory
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class Spring5WebclientApplicationTests {
@Autowired
private WebTestClient webTestClient;
@Test
public void test1CreateGithubRepository() {
RepoRequest repoRequest = new RepoRequest("test-webclient-repository",
"Repository created for testing WebClient");
webTestClient.post().uri("/api/repos")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.body(Mono.just(repoRequest), RepoRequest.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody()
.jsonPath("$.name").isNotEmpty()
.jsonPath("$.name").isEqualTo("test-webclient-repository");
}
@Test
public void test2GetAllGithubRepositories() {
webTestClient.get().uri("/api/repos")
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBodyList(GithubRepo.class);
}
@Test
public void test3GetSingleGithubRepository() {
webTestClient.get().uri("/api/repos/{repo}", "test-webclient-repository")
.exchange()
.expectStatus().isOk()
.expectBody()
.consumeWith(response -> Assertions.assertThat(response.getResponseBody()).isNotNull());
}
@Test
public void test4EditGithubRepository() {
RepoRequest newRepoDetails = new RepoRequest("updated-webclient-repository", "Updated name and description");
webTestClient.patch().uri("/api/repos/{repo}", "test-webclient-repository")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.body(Mono.just(newRepoDetails), RepoRequest.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody()
.jsonPath("$.name").isEqualTo("updated-webclient-repository");
}
@Test
public void test5DeleteGithubRepository() {
webTestClient.delete().uri("/api/repos/{repo}", "updated-webclient-repository")
.exchange()
.expectStatus().isOk();
}
}
<?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.3.1.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>spring5-webclient</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring5-webclient</name>
<description>Demo project for Spring Boot 5 WebClient</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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
app.github.username=GIT_USERNAME
app.github.token=GIT_TOKEN
package com.example;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author code.factory
*
*/
@SpringBootApplication
public class Spring5WebclientApplication {
public static void main(String[] args) {
SpringApplication.run(Spring5WebclientApplication.class, args);
}
}
package com.example;import com.example.config.AppProperties;
import com.example.payload.RepoRequest;
import com.example.payload.GithubRepo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author code.factory
*
*/
@Service
public class GithubClient {
private static final String GITHUB_V3_MIME_TYPE = "application/vnd.github.v3+json";
private static final String GITHUB_API_BASE_URL = "https://api.github.com";
private static final String USER_AGENT = "Spring 5 WebClient";
private static final Logger logger = LoggerFactory.getLogger(GithubClient.class);
private final WebClient webClient; @Autowired
public GithubClient(AppProperties appProperties) {
this.webClient = WebClient.builder().baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
.defaultHeader(HttpHeaders.USER_AGENT, USER_AGENT)
.filter(ExchangeFilterFunctions.basicAuthentication(appProperties.getGithub().getUsername(),
appProperties.getGithub().getToken()))
.filter(logRequest()).build();
}
public Flux<GithubRepo> listGithubRepositories() {
return webClient.get().uri("/user/repos?sort={sortField}&direction={sortDirection}", "updated", "desc")
.exchange()
.flatMapMany(clientResponse -> clientResponse.bodyToFlux(GithubRepo.class));
}
public Mono<GithubRepo> createGithubRepository(RepoRequest createRepoRequest) {
return webClient.post().uri("/user/repos")
.body(Mono.just(createRepoRequest), RepoRequest.class)
.retrieve()
.bodyToMono(GithubRepo.class);
}
public Mono<GithubRepo> getGithubRepository(String owner, String repo) {
return webClient.get().uri("/repos/{owner}/{repo}", owner, repo)
.retrieve()
.bodyToMono(GithubRepo.class);
}
public Mono<GithubRepo> editGithubRepository(String owner, String repo, RepoRequest editRepoRequest) {
return webClient.patch().uri("/repos/{owner}/{repo}", owner, repo)
.body(BodyInserters.fromObject(editRepoRequest))
.retrieve()
.bodyToMono(GithubRepo.class);
}
public Mono<Void> deleteGithubRepository(String owner, String repo) {
return webClient.delete().uri("/repos/{owner}/{repo}", owner, repo)
.retrieve()
.bodyToMono(Void.class);
}
private ExchangeFilterFunction logRequest() {
return (clientRequest, next) -> {
logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers()
.forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return next.exchange(clientRequest);
};
}
}
package com.example;import com.example.config.AppProperties;
import com.example.payload.RepoRequest;
import com.example.payload.GithubRepo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.validation.Valid;/**
* @author code.factory
*
*/
@RestController
@RequestMapping("/api")
public class GithubController {
@Autowired
private GithubClient githubClient;
@Autowired
private AppProperties appProperties;
private static final Logger logger = LoggerFactory.getLogger(GithubController.class); @GetMapping("/repos")
public Flux<GithubRepo> listGithubRepositories() {
return githubClient.listGithubRepositories();
}
@PostMapping("/repos")
public Mono<GithubRepo> createGithubRepository(@RequestBody RepoRequest repoRequest) {
return githubClient.createGithubRepository(repoRequest);
}
@GetMapping("/repos/{repo}")
public Mono<GithubRepo> getGithubRepository(@PathVariable String repo) {
return githubClient.getGithubRepository(appProperties.getGithub().getUsername(), repo);
}
@PatchMapping("/repos/{repo}")
public Mono<GithubRepo> editGithubRepository(@PathVariable String repo, @Valid @RequestBody RepoRequest repoRequest) {
return githubClient.editGithubRepository(appProperties.getGithub().getUsername(), repo, repoRequest);
}
@DeleteMapping("/repos/{repo}")
public Mono<Void> deleteGithubRepository(@PathVariable String repo) {
return githubClient.deleteGithubRepository(appProperties.getGithub().getUsername(), repo);
}
@ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<String> handleWebClientResponseException(WebClientResponseException ex) {
logger.error("Error from WebClient - Status {}, Body {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex);
return ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString());
}
}
package com.example.config;import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author code.factory
*
*/
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private final Github github = new Github();
public static class Github {
private String username;
private String token;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
public Github getGithub() {
return github;
}
}
package com.example.payload;import com.fasterxml.jackson.annotation.JsonProperty;/**
* @author code.factory
*
*/
public class GithubRepo {
private Long id;
private String name; @JsonProperty("full_name")
private String fullName;
private String description; @JsonProperty("private")
private String isPrivate;
@JsonProperty("fork")
private String isFork;
private String url; @JsonProperty("html_url")
private String htmlUrl;
@JsonProperty("git_url")
private String gitUrl;
@JsonProperty("forks_count")
private Long forksCount;
@JsonProperty("stargazers_count")
private Long stargazersCount;
@JsonProperty("watchers_count")
private Long watchersCount;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getIsPrivate() {
return isPrivate;
}
public void setIsPrivate(String isPrivate) {
this.isPrivate = isPrivate;
}
public String getIsFork() {
return isFork;
}
public void setIsFork(String isFork) {
this.isFork = isFork;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getHtmlUrl() {
return htmlUrl;
}
public void setHtmlUrl(String htmlUrl) {
this.htmlUrl = htmlUrl;
}
public String getGitUrl() {
return gitUrl;
}
public void setGitUrl(String gitUrl) {
this.gitUrl = gitUrl;
}
public Long getForksCount() {
return forksCount;
}
public void setForksCount(Long forksCount) {
this.forksCount = forksCount;
}
public Long getStargazersCount() {
return stargazersCount;
}
public void setStargazersCount(Long stargazersCount) {
this.stargazersCount = stargazersCount;
}
public Long getWatchersCount() {
return watchersCount;
}
public void setWatchersCount(Long watchersCount) {
this.watchersCount = watchersCount;
}
}
package com.example.payload;import javax.validation.constraints.NotBlank;import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author code.factory
*
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RepoRequest {
@NotBlank
private String name;
private String description; @JsonProperty("private")
private Boolean isPrivate;
public RepoRequest() { } public RepoRequest(@NotBlank String name, String description) {
this.name = name;
this.description = description;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Boolean getPrivate() {
return isPrivate;
}
public void setPrivate(Boolean aPrivate) {
isPrivate = aPrivate;
}
@Override
public String toString() {
return "RepoRequest [name=" + name + ", description=" + description + ", isPrivate=" + isPrivate + "]";
}
}
package com.example;import org.assertj.core.api.Assertions;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import com.example.payload.GithubRepo;
import com.example.payload.RepoRequest;
import reactor.core.publisher.Mono;/**
* @author code.factory
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class Spring5WebclientApplicationTests {
@Autowired
private WebTestClient webTestClient;
@Test
public void test1CreateGithubRepository() {
RepoRequest repoRequest = new RepoRequest("test-webclient-repository",
"Repository created for testing WebClient");
webTestClient.post().uri("/api/repos")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.body(Mono.just(repoRequest), RepoRequest.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody()
.jsonPath("$.name").isNotEmpty()
.jsonPath("$.name").isEqualTo("test-webclient-repository");
}
@Test
public void test2GetAllGithubRepositories() {
webTestClient.get().uri("/api/repos")
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBodyList(GithubRepo.class);
}
@Test
public void test3GetSingleGithubRepository() {
webTestClient.get().uri("/api/repos/{repo}", "test-webclient-repository")
.exchange()
.expectStatus().isOk()
.expectBody()
.consumeWith(response -> Assertions.assertThat(response.getResponseBody()).isNotNull());
}
@Test
public void test4EditGithubRepository() {
RepoRequest newRepoDetails = new RepoRequest("updated-webclient-repository", "Updated name and description");
webTestClient.patch().uri("/api/repos/{repo}", "test-webclient-repository")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.body(Mono.just(newRepoDetails), RepoRequest.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody()
.jsonPath("$.name").isEqualTo("updated-webclient-repository");
}
@Test
public void test5DeleteGithubRepository() {
webTestClient.delete().uri("/api/repos/{repo}", "updated-webclient-repository")
.exchange()
.expectStatus().isOk();
}
}

--

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Software Testing with Java — Advanced Topics

5 Things To Do When Considering Transitioning into the Tech Industry

Getting Started with IndexedDB for Big Data Storage

Bombardata Kharkiv, due missili cadono a pochi metri da una donna, illesa

Another week of learning — some fairly personal notes

Benchmarking batch JDBC queries

Python scraping: find company name and job title from id.indeed.com

Automate payment collection using the GoCardless integration from Zoho

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Code Factory

Code Factory

More from Medium

Top gadgets and gear from CES 2022

9 Reasons To Go With Java Web Development Services!

ACID(SQL) vs BASE(No-SQL) properties

ACID vs BASE properties (SQL vs NoSQL)

Oracle DBMS Scheduler