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

Accurately Counting Steps with iWatch using CMPedometer

A Beginner-Friendly Guide to PyTorch and How it Works from Scratch

Robot Coin Collection-Dynamic Programming

How to Increase Your Website Conversions and Speed

Service Now Beginners Workshop Assignments

Nifty Data Analysis

How to Create ASP.NET Web Api project??

Why do we need a dedicated metaverse for Engineers & Creators?

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

Testing in Java — Unit Tests

Algorithms In Context #9: Auto-Completion

Naming Convention in Java

Exception Handling in Java