Skip to content

Commit

Permalink
Add example of Spring WebClient usage (#418)
Browse files Browse the repository at this point in the history
- Add example of WebClient usage
- Add additional test case with WebClient usage

Signed-off-by: Dave Syer <dsyer@vmware.com>
  • Loading branch information
dsyer committed Nov 17, 2021
1 parent a94bc5c commit 9231e6d
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 103 deletions.
12 changes: 12 additions & 0 deletions docs/spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ public Mono<CloudEvent> event(@RequestBody Mono<CloudEvent> body) {
}
```

The `CodecCustomizer` also works on the client side, so you can use it anywhere that you use a `WebClient` (including in an MVC application). Here's a simple example of a Cloud Event HTTP client:

```java
WebClient client = ...; // Either WebClient.create() or @Autowired a WebClient.Builder
CloudEvent event = ...; // Create a CloudEvent
Mono<CloudEvent> response = client.post()
.uri("http://localhost:8080/events")
.bodyValue(event)
.retrieve()
.bodyToMono(CloudEvent.class);
```

### Messaging

Spring Messaging is applicable in a wide range of use cases including WebSockets, JMS, Apache Kafka, RabbitMQ and others. It is also a core part of the Spring Cloud Function and Spring Cloud Stream libraries, so those are natural tools to use to build applications that use Cloud Events. The core abstraction in Spring is the `Message` which carries headers and a payload, just like a `CloudEvent`. Since the mapping is quite direct it makes sense to have a set of converters for Spring applications, so you can consume and produce `CloudEvents`, by treating them as `Messages`. This project provides a converter that you can register in a Spring Messaging application:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@
import java.util.UUID;
import java.util.function.Function;

import io.cloudevents.CloudEvent;
import io.cloudevents.core.builder.CloudEventBuilder;
import io.cloudevents.spring.messaging.CloudEventMessageConverter;
import io.cloudevents.spring.webflux.CloudEventHttpMessageReader;
import io.cloudevents.spring.webflux.CloudEventHttpMessageWriter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.web.bind.annotation.RestController;

import io.cloudevents.CloudEvent;
import io.cloudevents.core.builder.CloudEventBuilder;
import io.cloudevents.spring.messaging.CloudEventMessageConverter;
import io.cloudevents.spring.webflux.CloudEventHttpMessageReader;
import io.cloudevents.spring.webflux.CloudEventHttpMessageWriter;

@SpringBootApplication
@RestController
public class DemoApplication {

public static void main(String[] args) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,110 +3,146 @@
import java.net.URI;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.web.reactive.server.WebTestClient;

import io.cloudevents.CloudEvent;
import io.cloudevents.core.builder.CloudEventBuilder;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class DemoApplicationTests {

@Autowired
private TestRestTemplate rest;

@LocalServerPort
private int port;

@Test
void echoWithCorrectHeaders() {

ResponseEntity<String> response = rest
.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/foos")) //
.header("ce-id", "12345") //
.header("ce-specversion", "1.0") //
.header("ce-type", "io.spring.event") //
.header("ce-source", "https://spring.io/events") //
.contentType(MediaType.APPLICATION_JSON) //
.body("{\"value\":\"Dave\"}"), String.class);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");

HttpHeaders headers = response.getHeaders();

assertThat(headers).containsKey("ce-id");
assertThat(headers).containsKey("ce-source");
assertThat(headers).containsKey("ce-type");

assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");

}

@Test
void structuredRequestResponseEvents() {

ResponseEntity<String> response = rest
.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/event")) //
.contentType(new MediaType("application", "cloudevents+json")) //
.body("{" //
+ "\"id\":\"12345\"," //
+ "\"specversion\":\"1.0\"," //
+ "\"type\":\"io.spring.event\"," //
+ "\"source\":\"https://spring.io/events\"," //
+ "\"data\":{\"value\":\"Dave\"}}"),
String.class);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");

HttpHeaders headers = response.getHeaders();

assertThat(headers).containsKey("ce-id");
assertThat(headers).containsKey("ce-source");
assertThat(headers).containsKey("ce-type");

assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");

}

@Test
void requestResponseEvents() {

ResponseEntity<String> response = rest
.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/event")) //
.header("ce-id", "12345") //
.header("ce-specversion", "1.0") //
.header("ce-type", "io.spring.event") //
.header("ce-source", "https://spring.io/events") //
.contentType(MediaType.APPLICATION_JSON) //
.body("{\"value\":\"Dave\"}"), String.class);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");

HttpHeaders headers = response.getHeaders();

assertThat(headers).containsKey("ce-id");
assertThat(headers).containsKey("ce-source");
assertThat(headers).containsKey("ce-type");

assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");

}
@Autowired
private WebTestClient rest;

@Test
void echoWithCorrectHeaders() {

rest.post().uri("/foos").header("ce-id", "12345") //
.header("ce-specversion", "1.0") //
.header("ce-type", "io.spring.event") //
.header("ce-source", "https://spring.io/events") //
.contentType(MediaType.APPLICATION_JSON) //
.bodyValue("{\"value\":\"Dave\"}") //
.exchange() //
.expectStatus().isOk() //
.expectHeader().exists("ce-id") //
.expectHeader().exists("ce-source") //
.expectHeader().exists("ce-type") //
.expectHeader().value("ce-id", value -> {
if (value.equals("12345"))
throw new IllegalStateException();
}) //
.expectHeader().valueEquals("ce-type", "io.spring.event.Foo") //
.expectHeader().valueEquals("ce-source", "https://spring.io/foos") //
.expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}");

}

@Test
void structuredRequestResponseEvents() {

rest.post().uri("/event") //
.contentType(new MediaType("application", "cloudevents+json")) //
.bodyValue("{" //
+ "\"id\":\"12345\"," //
+ "\"specversion\":\"1.0\"," //
+ "\"type\":\"io.spring.event\"," //
+ "\"source\":\"https://spring.io/events\"," //
+ "\"data\":{\"value\":\"Dave\"}}") //
.exchange() //
.expectStatus().isOk() //
.expectHeader().exists("ce-id") //
.expectHeader().exists("ce-source") //
.expectHeader().exists("ce-type") //
.expectHeader().value("ce-id", value -> {
if (value.equals("12345"))
throw new IllegalStateException();
}) //
.expectHeader().valueEquals("ce-type", "io.spring.event.Foo") //
.expectHeader().valueEquals("ce-source", "https://spring.io/foos") //
.expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}");

}

@Test
void structuredRequestResponseCloudEventToString() {

rest.post().uri("/event") //
.bodyValue(CloudEventBuilder.v1() //
.withId("12345") //
.withType("io.spring.event") //
.withSource(URI.create("https://spring.io/events")).withData("{\"value\":\"Dave\"}".getBytes()) //
.build()) //
.exchange() //
.expectStatus().isOk() //
.expectHeader().exists("ce-id") //
.expectHeader().exists("ce-source") //
.expectHeader().exists("ce-type") //
.expectHeader().value("ce-id", value -> {
if (value.equals("12345"))
throw new IllegalStateException();
}) //
.expectHeader().valueEquals("ce-type", "io.spring.event.Foo") //
.expectHeader().valueEquals("ce-source", "https://spring.io/foos") //
.expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}");

}

@Test
void structuredRequestResponseCloudEventToCloudEvent() {

rest.post().uri("/event") //
.accept(new MediaType("application", "cloudevents+json")) //
.bodyValue(CloudEventBuilder.v1() //
.withId("12345") //
.withType("io.spring.event") //
.withSource(URI.create("https://spring.io/events")) //
.withData("{\"value\":\"Dave\"}".getBytes()) //
.build()) //
.exchange() //
.expectStatus().isOk() //
.expectHeader().exists("ce-id") //
.expectHeader().exists("ce-source") //
.expectHeader().exists("ce-type") //
.expectHeader().value("ce-id", value -> {
if (value.equals("12345"))
throw new IllegalStateException();
}) //
.expectHeader().valueEquals("ce-type", "io.spring.event.Foo") //
.expectHeader().valueEquals("ce-source", "https://spring.io/foos") //
.expectBody(CloudEvent.class) //
.value(event -> assertThat(new String(event.getData().toBytes())) //
.isEqualTo("{\"value\":\"Dave\"}"));

}

@Test
void requestResponseEvents() {

rest.post().uri("/event").header("ce-id", "12345") //
.header("ce-specversion", "1.0") //
.header("ce-type", "io.spring.event") //
.header("ce-source", "https://spring.io/events") //
.contentType(MediaType.APPLICATION_JSON) //
.bodyValue("{\"value\":\"Dave\"}") //
.exchange() //
.expectStatus().isOk() //
.expectHeader().exists("ce-id") //
.expectHeader().exists("ce-source") //
.expectHeader().exists("ce-type") //
.expectHeader().value("ce-id", value -> {
if (value.equals("12345"))
throw new IllegalStateException();
}) //
.expectHeader().valueEquals("ce-type", "io.spring.event.Foo") //
.expectHeader().valueEquals("ce-source", "https://spring.io/foos") //
.expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}");

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.cloudevents.examples.spring;

import static org.assertj.core.api.Assertions.assertThat;

import java.net.URI;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.web.reactive.function.client.WebClient;

import io.cloudevents.CloudEvent;
import io.cloudevents.core.builder.CloudEventBuilder;
import reactor.core.publisher.Mono;

/**
* Test case to show example usage of WebClient and CloudEvent. The actual
* content of the request and response are asserted separately in
* {@link DemoApplicationTests}.
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class WebClientTests {

@Autowired
private WebClient.Builder rest;

@LocalServerPort
private int port;

private CloudEvent event;

@BeforeEach
void setUp() {
event = CloudEventBuilder.v1() //
.withId("12345") //
.withSource(URI.create("https://spring.io/events")) //
.withType("io.spring.event") //
.withData("{\"value\":\"Dave\"}".getBytes()) //
.build();
}

@Test
void echoWithCorrectHeaders() {

Mono<CloudEvent> result = rest.build() //
.post() //
.uri("http://localhost:" + port + "/event") //
.bodyValue(event) //
.exchangeToMono(response -> response.bodyToMono(CloudEvent.class));

assertThat(result.block().getData()).isEqualTo(event.getData());

}

}

0 comments on commit 9231e6d

Please sign in to comment.