Skip to content

Commit a14f5ea

Browse files
author
Dave Syer
authored
Add a Spring sample with webflux (#314)
Signed-off-by: Dave Syer <[email protected]>
1 parent 5099b31 commit a14f5ea

File tree

7 files changed

+367
-0
lines changed

7 files changed

+367
-0
lines changed

examples/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ This directory includes some examples on how to use CloudEvents sdk-java:
1212
- [restful-ws-spring-boot](restful-ws-spring-boot) shows how to use the module
1313
`cloudevents-http-restful-ws` with Spring Boot and Jersey to receive and
1414
send CloudEvents through HTTP.
15+
- [spring-reactive](spring-reactive) shows how to use the module
16+
`cloudevents-spring` with Spring Boot and Webflux to receive and
17+
send CloudEvents through HTTP.
1518
- [vertx](vertx) shows how to use the module `cloudevents-http-vertx` to
1619
receive and send CloudEvents through HTTP using `vertx-web-client` and
1720
`vertx-core`.

examples/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<module>basic-http</module>
2121
<module>restful-ws-spring-boot</module>
2222
<module>amqp-proton</module>
23+
<module>spring-reactive</module>
2324
</modules>
2425

2526
</project>

examples/spring-reactive/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Spring Reactive + CloudEvents sample
2+
3+
## Build
4+
5+
```shell
6+
mvn package
7+
```
8+
9+
## Start HTTP Server
10+
11+
```shell
12+
mvn spring-boot:run
13+
```
14+
15+
You can try sending a request using curl, and it echos back a cloud event the same body and with new `ce-*` headers:
16+
17+
```shell
18+
curl -v -d '{"value": "Foo"}' \
19+
-H'Content-type: application/json' \
20+
-H'ce-id: 1' \
21+
-H'ce-source: cloud-event-example' \
22+
-H'ce-type: my.application.Foo' \
23+
-H'ce-specversion: 1.0' \
24+
http://localhost:8080/event
25+
```
26+
27+
It also accepts data in "structured" format:
28+
29+
```shell
30+
curl -v -H'Content-type: application/cloudevents+json' \
31+
-d '{"data": {"value": "Foo"},
32+
"ce-id: 1,
33+
"ce-source": "cloud-event-example"
34+
"ce-type": "my.application.Foo"
35+
"ce-specversion": "1.0"}' \
36+
http://localhost:8080/event
37+
```
38+
39+
The `/event endpoint is implemented like this (the request and response are modelled directly as a `CloudEvent`):
40+
41+
```java
42+
@PostMapping("/event")
43+
public Mono<CloudEvent> event(@RequestBody Mono<CloudEvent> body) {
44+
return ...;
45+
}
46+
```
47+
48+
and to make that work we need to install the codecs:
49+
50+
```java
51+
@Configuration
52+
public static class CloudEventHandlerConfiguration implements CodecCustomizer {
53+
54+
@Override
55+
public void customize(CodecConfigurer configurer) {
56+
configurer.customCodecs().register(new CloudEventHttpMessageReader());
57+
configurer.customCodecs().register(new CloudEventHttpMessageWriter());
58+
}
59+
60+
}
61+
```
62+
63+
The same feature in Spring MVC is provided by the `CloudEventHttpMessageConverter`.
64+
65+
66+
The `/foos` endpoint does the same thing. It doesn't use the `CloudEvent` data type directly, but instead models the request and response body as a `Foo` (POJO type):
67+
68+
```java
69+
@PostMapping("/foos")
70+
public ResponseEntity<Foo> echo(@RequestBody Foo foo, @RequestHeader HttpHeaders headers) {
71+
...
72+
}
73+
```
74+
75+
Note that this endpoint only accepts "binary" format cloud events (context in HTTP headers like in the first example above). It translates the `HttpHeaders` to `CloudEventContext` using a utility class provided by `cloudevents-spring`.

examples/spring-reactive/pom.xml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<artifactId>cloudevents-examples</artifactId>
7+
<groupId>io.cloudevents</groupId>
8+
<version>2.0.0-SNAPSHOT</version>
9+
</parent>
10+
<modelVersion>4.0.0</modelVersion>
11+
12+
<artifactId>cloudevents-spring-reactive-example</artifactId>
13+
14+
<properties>
15+
<spring-boot.version>2.4.0</spring-boot.version>
16+
</properties>
17+
18+
<dependencyManagement>
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.springframework.boot</groupId>
22+
<artifactId>spring-boot-dependencies</artifactId>
23+
<version>${spring-boot.version}</version>
24+
<type>pom</type>
25+
<scope>import</scope>
26+
</dependency>
27+
</dependencies>
28+
</dependencyManagement>
29+
30+
<dependencies>
31+
<dependency>
32+
<groupId>org.springframework.boot</groupId>
33+
<artifactId>spring-boot-starter-webflux</artifactId>
34+
</dependency>
35+
<dependency>
36+
<groupId>io.cloudevents</groupId>
37+
<artifactId>cloudevents-spring</artifactId>
38+
<version>${project.version}</version>
39+
</dependency>
40+
<dependency>
41+
<groupId>io.cloudevents</groupId>
42+
<artifactId>cloudevents-http-basic</artifactId>
43+
<version>${project.version}</version>
44+
</dependency>
45+
<dependency>
46+
<groupId>io.cloudevents</groupId>
47+
<artifactId>cloudevents-json-jackson</artifactId>
48+
<version>${project.version}</version>
49+
</dependency>
50+
<dependency>
51+
<groupId>org.springframework.boot</groupId>
52+
<artifactId>spring-boot-starter-test</artifactId>
53+
<scope>test</scope>
54+
</dependency>
55+
</dependencies>
56+
57+
<build>
58+
<plugins>
59+
<plugin>
60+
<groupId>org.springframework.boot</groupId>
61+
<artifactId>spring-boot-maven-plugin</artifactId>
62+
<version>${spring-boot.version}</version>
63+
</plugin>
64+
</plugins>
65+
</build>
66+
67+
</project>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.cloudevents.examples.spring;
2+
3+
import java.net.URI;
4+
import java.util.UUID;
5+
6+
import io.cloudevents.CloudEvent;
7+
import io.cloudevents.core.builder.CloudEventBuilder;
8+
import io.cloudevents.spring.http.CloudEventHttpUtils;
9+
import io.cloudevents.spring.webflux.CloudEventHttpMessageReader;
10+
import io.cloudevents.spring.webflux.CloudEventHttpMessageWriter;
11+
import reactor.core.publisher.Mono;
12+
13+
import org.springframework.boot.SpringApplication;
14+
import org.springframework.boot.autoconfigure.SpringBootApplication;
15+
import org.springframework.boot.web.codec.CodecCustomizer;
16+
import org.springframework.context.annotation.Configuration;
17+
import org.springframework.http.HttpHeaders;
18+
import org.springframework.http.ResponseEntity;
19+
import org.springframework.http.codec.CodecConfigurer;
20+
import org.springframework.web.bind.annotation.PostMapping;
21+
import org.springframework.web.bind.annotation.RequestBody;
22+
import org.springframework.web.bind.annotation.RequestHeader;
23+
import org.springframework.web.bind.annotation.RestController;
24+
25+
@SpringBootApplication
26+
@RestController
27+
public class DemoApplication {
28+
29+
public static void main(String[] args) throws Exception {
30+
SpringApplication.run(DemoApplication.class, args);
31+
}
32+
33+
@PostMapping("/foos")
34+
// Let Spring do the type conversion of request and response body
35+
public ResponseEntity<Foo> echo(@RequestBody Foo foo, @RequestHeader HttpHeaders headers) {
36+
CloudEvent attributes = CloudEventHttpUtils.fromHttp(headers) //
37+
.withId(UUID.randomUUID().toString()) //
38+
.withSource(URI.create("https://spring.io/foos")) //
39+
.withType("io.spring.event.Foo") //
40+
.build();
41+
HttpHeaders outgoing = CloudEventHttpUtils.toHttp(attributes);
42+
return ResponseEntity.ok().headers(outgoing).body(foo);
43+
}
44+
45+
@PostMapping("/event")
46+
// Use CloudEvent API and manual type conversion of request and response body
47+
public Mono<CloudEvent> event(@RequestBody Mono<CloudEvent> body) {
48+
return body.map(event -> CloudEventBuilder.from(event) //
49+
.withId(UUID.randomUUID().toString()) //
50+
.withSource(URI.create("https://spring.io/foos")) //
51+
.withType("io.spring.event.Foo") //
52+
.withData(event.getData().toBytes()) //
53+
.build());
54+
}
55+
56+
@Configuration
57+
public static class CloudEventHandlerConfiguration implements CodecCustomizer {
58+
59+
@Override
60+
public void customize(CodecConfigurer configurer) {
61+
configurer.customCodecs().register(new CloudEventHttpMessageReader());
62+
configurer.customCodecs().register(new CloudEventHttpMessageWriter());
63+
}
64+
65+
}
66+
67+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2019-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.cloudevents.examples.spring;
17+
18+
class Foo {
19+
20+
private String value;
21+
22+
public Foo() {
23+
}
24+
25+
public Foo(String value) {
26+
this.value = value;
27+
}
28+
29+
public String getValue() {
30+
return this.value;
31+
}
32+
33+
public void setValue(String value) {
34+
this.value = value;
35+
}
36+
37+
@Override
38+
public String toString() {
39+
return "Foo [value=" + this.value + "]";
40+
}
41+
42+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package io.cloudevents.examples.spring;
2+
3+
import java.net.URI;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.boot.test.context.SpringBootTest;
9+
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;
14+
import org.springframework.http.MediaType;
15+
import org.springframework.http.RequestEntity;
16+
import org.springframework.http.ResponseEntity;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
21+
public class DemoApplicationTests {
22+
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+
}
111+
112+
}

0 commit comments

Comments
 (0)