Skip to content

Commit 9231e6d

Browse files
author
Dave Syer
authored
Add example of Spring WebClient usage (#418)
- Add example of WebClient usage - Add additional test case with WebClient usage Signed-off-by: Dave Syer <[email protected]>
1 parent a94bc5c commit 9231e6d

File tree

4 files changed

+207
-103
lines changed

4 files changed

+207
-103
lines changed

docs/spring.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ public Mono<CloudEvent> event(@RequestBody Mono<CloudEvent> body) {
115115
}
116116
```
117117

118+
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:
119+
120+
```java
121+
WebClient client = ...; // Either WebClient.create() or @Autowired a WebClient.Builder
122+
CloudEvent event = ...; // Create a CloudEvent
123+
Mono<CloudEvent> response = client.post()
124+
.uri("http://localhost:8080/events")
125+
.bodyValue(event)
126+
.retrieve()
127+
.bodyToMono(CloudEvent.class);
128+
```
129+
118130
### Messaging
119131

120132
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:

examples/spring-function/src/main/java/io/cloudevents/examples/spring/DemoApplication.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,20 @@
44
import java.util.UUID;
55
import java.util.function.Function;
66

7-
import io.cloudevents.CloudEvent;
8-
import io.cloudevents.core.builder.CloudEventBuilder;
9-
import io.cloudevents.spring.messaging.CloudEventMessageConverter;
10-
import io.cloudevents.spring.webflux.CloudEventHttpMessageReader;
11-
import io.cloudevents.spring.webflux.CloudEventHttpMessageWriter;
12-
137
import org.springframework.boot.SpringApplication;
148
import org.springframework.boot.autoconfigure.SpringBootApplication;
159
import org.springframework.boot.web.codec.CodecCustomizer;
1610
import org.springframework.context.annotation.Bean;
1711
import org.springframework.context.annotation.Configuration;
1812
import org.springframework.http.codec.CodecConfigurer;
19-
import org.springframework.web.bind.annotation.RestController;
13+
14+
import io.cloudevents.CloudEvent;
15+
import io.cloudevents.core.builder.CloudEventBuilder;
16+
import io.cloudevents.spring.messaging.CloudEventMessageConverter;
17+
import io.cloudevents.spring.webflux.CloudEventHttpMessageReader;
18+
import io.cloudevents.spring.webflux.CloudEventHttpMessageWriter;
2019

2120
@SpringBootApplication
22-
@RestController
2321
public class DemoApplication {
2422

2523
public static void main(String[] args) throws Exception {

examples/spring-reactive/src/test/java/io/cloudevents/examples/spring/DemoApplicationTests.java

Lines changed: 131 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -3,110 +3,146 @@
33
import java.net.URI;
44

55
import org.junit.jupiter.api.Test;
6-
76
import org.springframework.beans.factory.annotation.Autowired;
87
import org.springframework.boot.test.context.SpringBootTest;
98
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
10-
import org.springframework.boot.test.web.client.TestRestTemplate;
11-
import org.springframework.boot.web.server.LocalServerPort;
12-
import org.springframework.http.HttpHeaders;
13-
import org.springframework.http.HttpStatus;
149
import org.springframework.http.MediaType;
15-
import org.springframework.http.RequestEntity;
16-
import org.springframework.http.ResponseEntity;
10+
import org.springframework.test.web.reactive.server.WebTestClient;
1711

12+
import io.cloudevents.CloudEvent;
13+
import io.cloudevents.core.builder.CloudEventBuilder;
1814
import static org.assertj.core.api.Assertions.assertThat;
1915

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

23-
@Autowired
24-
private TestRestTemplate rest;
25-
26-
@LocalServerPort
27-
private int port;
28-
29-
@Test
30-
void echoWithCorrectHeaders() {
31-
32-
ResponseEntity<String> response = rest
33-
.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/foos")) //
34-
.header("ce-id", "12345") //
35-
.header("ce-specversion", "1.0") //
36-
.header("ce-type", "io.spring.event") //
37-
.header("ce-source", "https://spring.io/events") //
38-
.contentType(MediaType.APPLICATION_JSON) //
39-
.body("{\"value\":\"Dave\"}"), String.class);
40-
41-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
42-
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");
43-
44-
HttpHeaders headers = response.getHeaders();
45-
46-
assertThat(headers).containsKey("ce-id");
47-
assertThat(headers).containsKey("ce-source");
48-
assertThat(headers).containsKey("ce-type");
49-
50-
assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
51-
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
52-
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");
53-
54-
}
55-
56-
@Test
57-
void structuredRequestResponseEvents() {
58-
59-
ResponseEntity<String> response = rest
60-
.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/event")) //
61-
.contentType(new MediaType("application", "cloudevents+json")) //
62-
.body("{" //
63-
+ "\"id\":\"12345\"," //
64-
+ "\"specversion\":\"1.0\"," //
65-
+ "\"type\":\"io.spring.event\"," //
66-
+ "\"source\":\"https://spring.io/events\"," //
67-
+ "\"data\":{\"value\":\"Dave\"}}"),
68-
String.class);
69-
70-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
71-
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");
72-
73-
HttpHeaders headers = response.getHeaders();
74-
75-
assertThat(headers).containsKey("ce-id");
76-
assertThat(headers).containsKey("ce-source");
77-
assertThat(headers).containsKey("ce-type");
78-
79-
assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
80-
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
81-
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");
82-
83-
}
84-
85-
@Test
86-
void requestResponseEvents() {
87-
88-
ResponseEntity<String> response = rest
89-
.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/event")) //
90-
.header("ce-id", "12345") //
91-
.header("ce-specversion", "1.0") //
92-
.header("ce-type", "io.spring.event") //
93-
.header("ce-source", "https://spring.io/events") //
94-
.contentType(MediaType.APPLICATION_JSON) //
95-
.body("{\"value\":\"Dave\"}"), String.class);
96-
97-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
98-
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");
99-
100-
HttpHeaders headers = response.getHeaders();
101-
102-
assertThat(headers).containsKey("ce-id");
103-
assertThat(headers).containsKey("ce-source");
104-
assertThat(headers).containsKey("ce-type");
105-
106-
assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
107-
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
108-
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");
109-
110-
}
19+
@Autowired
20+
private WebTestClient rest;
21+
22+
@Test
23+
void echoWithCorrectHeaders() {
24+
25+
rest.post().uri("/foos").header("ce-id", "12345") //
26+
.header("ce-specversion", "1.0") //
27+
.header("ce-type", "io.spring.event") //
28+
.header("ce-source", "https://spring.io/events") //
29+
.contentType(MediaType.APPLICATION_JSON) //
30+
.bodyValue("{\"value\":\"Dave\"}") //
31+
.exchange() //
32+
.expectStatus().isOk() //
33+
.expectHeader().exists("ce-id") //
34+
.expectHeader().exists("ce-source") //
35+
.expectHeader().exists("ce-type") //
36+
.expectHeader().value("ce-id", value -> {
37+
if (value.equals("12345"))
38+
throw new IllegalStateException();
39+
}) //
40+
.expectHeader().valueEquals("ce-type", "io.spring.event.Foo") //
41+
.expectHeader().valueEquals("ce-source", "https://spring.io/foos") //
42+
.expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}");
43+
44+
}
45+
46+
@Test
47+
void structuredRequestResponseEvents() {
48+
49+
rest.post().uri("/event") //
50+
.contentType(new MediaType("application", "cloudevents+json")) //
51+
.bodyValue("{" //
52+
+ "\"id\":\"12345\"," //
53+
+ "\"specversion\":\"1.0\"," //
54+
+ "\"type\":\"io.spring.event\"," //
55+
+ "\"source\":\"https://spring.io/events\"," //
56+
+ "\"data\":{\"value\":\"Dave\"}}") //
57+
.exchange() //
58+
.expectStatus().isOk() //
59+
.expectHeader().exists("ce-id") //
60+
.expectHeader().exists("ce-source") //
61+
.expectHeader().exists("ce-type") //
62+
.expectHeader().value("ce-id", value -> {
63+
if (value.equals("12345"))
64+
throw new IllegalStateException();
65+
}) //
66+
.expectHeader().valueEquals("ce-type", "io.spring.event.Foo") //
67+
.expectHeader().valueEquals("ce-source", "https://spring.io/foos") //
68+
.expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}");
69+
70+
}
71+
72+
@Test
73+
void structuredRequestResponseCloudEventToString() {
74+
75+
rest.post().uri("/event") //
76+
.bodyValue(CloudEventBuilder.v1() //
77+
.withId("12345") //
78+
.withType("io.spring.event") //
79+
.withSource(URI.create("https://spring.io/events")).withData("{\"value\":\"Dave\"}".getBytes()) //
80+
.build()) //
81+
.exchange() //
82+
.expectStatus().isOk() //
83+
.expectHeader().exists("ce-id") //
84+
.expectHeader().exists("ce-source") //
85+
.expectHeader().exists("ce-type") //
86+
.expectHeader().value("ce-id", value -> {
87+
if (value.equals("12345"))
88+
throw new IllegalStateException();
89+
}) //
90+
.expectHeader().valueEquals("ce-type", "io.spring.event.Foo") //
91+
.expectHeader().valueEquals("ce-source", "https://spring.io/foos") //
92+
.expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}");
93+
94+
}
95+
96+
@Test
97+
void structuredRequestResponseCloudEventToCloudEvent() {
98+
99+
rest.post().uri("/event") //
100+
.accept(new MediaType("application", "cloudevents+json")) //
101+
.bodyValue(CloudEventBuilder.v1() //
102+
.withId("12345") //
103+
.withType("io.spring.event") //
104+
.withSource(URI.create("https://spring.io/events")) //
105+
.withData("{\"value\":\"Dave\"}".getBytes()) //
106+
.build()) //
107+
.exchange() //
108+
.expectStatus().isOk() //
109+
.expectHeader().exists("ce-id") //
110+
.expectHeader().exists("ce-source") //
111+
.expectHeader().exists("ce-type") //
112+
.expectHeader().value("ce-id", value -> {
113+
if (value.equals("12345"))
114+
throw new IllegalStateException();
115+
}) //
116+
.expectHeader().valueEquals("ce-type", "io.spring.event.Foo") //
117+
.expectHeader().valueEquals("ce-source", "https://spring.io/foos") //
118+
.expectBody(CloudEvent.class) //
119+
.value(event -> assertThat(new String(event.getData().toBytes())) //
120+
.isEqualTo("{\"value\":\"Dave\"}"));
121+
122+
}
123+
124+
@Test
125+
void requestResponseEvents() {
126+
127+
rest.post().uri("/event").header("ce-id", "12345") //
128+
.header("ce-specversion", "1.0") //
129+
.header("ce-type", "io.spring.event") //
130+
.header("ce-source", "https://spring.io/events") //
131+
.contentType(MediaType.APPLICATION_JSON) //
132+
.bodyValue("{\"value\":\"Dave\"}") //
133+
.exchange() //
134+
.expectStatus().isOk() //
135+
.expectHeader().exists("ce-id") //
136+
.expectHeader().exists("ce-source") //
137+
.expectHeader().exists("ce-type") //
138+
.expectHeader().value("ce-id", value -> {
139+
if (value.equals("12345"))
140+
throw new IllegalStateException();
141+
}) //
142+
.expectHeader().valueEquals("ce-type", "io.spring.event.Foo") //
143+
.expectHeader().valueEquals("ce-source", "https://spring.io/foos") //
144+
.expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}");
145+
146+
}
111147

112148
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.cloudevents.examples.spring;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.net.URI;
6+
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.boot.test.context.SpringBootTest;
11+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
12+
import org.springframework.boot.web.server.LocalServerPort;
13+
import org.springframework.web.reactive.function.client.WebClient;
14+
15+
import io.cloudevents.CloudEvent;
16+
import io.cloudevents.core.builder.CloudEventBuilder;
17+
import reactor.core.publisher.Mono;
18+
19+
/**
20+
* Test case to show example usage of WebClient and CloudEvent. The actual
21+
* content of the request and response are asserted separately in
22+
* {@link DemoApplicationTests}.
23+
*/
24+
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
25+
public class WebClientTests {
26+
27+
@Autowired
28+
private WebClient.Builder rest;
29+
30+
@LocalServerPort
31+
private int port;
32+
33+
private CloudEvent event;
34+
35+
@BeforeEach
36+
void setUp() {
37+
event = CloudEventBuilder.v1() //
38+
.withId("12345") //
39+
.withSource(URI.create("https://spring.io/events")) //
40+
.withType("io.spring.event") //
41+
.withData("{\"value\":\"Dave\"}".getBytes()) //
42+
.build();
43+
}
44+
45+
@Test
46+
void echoWithCorrectHeaders() {
47+
48+
Mono<CloudEvent> result = rest.build() //
49+
.post() //
50+
.uri("http://localhost:" + port + "/event") //
51+
.bodyValue(event) //
52+
.exchangeToMono(response -> response.bodyToMono(CloudEvent.class));
53+
54+
assertThat(result.block().getData()).isEqualTo(event.getData());
55+
56+
}
57+
58+
}

0 commit comments

Comments
 (0)