Skip to content

Commit a419d8b

Browse files
author
Dave Syer
authored
Add Spring Cloud Function sample (#356)
* Add Spring Cloud Function sample Signed-off-by: Dave Syer <[email protected]> * Fix example curl command with structured event Signed-off-by: Dave Syer <[email protected]>
1 parent 47bed56 commit a419d8b

File tree

8 files changed

+376
-5
lines changed

8 files changed

+376
-5
lines changed

docs/spring.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,6 @@ Check out the integration tests and samples:
188188

189189
- [spring-rsocket](https://github.com/cloudevents/sdk-java/tree/master/examples/spring-rsocket)
190190
shows how to receive and send CloudEvents through RSocket using Spring Boot.
191+
192+
- [spring-cloud-function](https://github.com/cloudevents/sdk-java/tree/master/examples/spring-function)
193+
shows how to consume and process CloudEvents via Spring Cloud Function.

examples/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<module>amqp-proton</module>
2828
<module>spring-reactive</module>
2929
<module>spring-rsocket</module>
30+
<module>spring-function</module>
3031
</modules>
3132

3233
</project>

examples/spring-function/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+
"id: 1,
33+
"source": "cloud-event-example"
34+
"type": "my.application.Foo"
35+
"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-function/pom.xml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.1.0-SNAPSHOT</version>
9+
</parent>
10+
<modelVersion>4.0.0</modelVersion>
11+
12+
<artifactId>cloudevents-spring-function-example</artifactId>
13+
14+
<properties>
15+
<spring-boot.version>2.4.3</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.cloud</groupId>
33+
<artifactId>spring-cloud-function-web</artifactId>
34+
<version>3.1.1</version>
35+
</dependency>
36+
<dependency>
37+
<groupId>org.springframework.boot</groupId>
38+
<artifactId>spring-boot-starter-webflux</artifactId>
39+
</dependency>
40+
<dependency>
41+
<groupId>io.cloudevents</groupId>
42+
<artifactId>cloudevents-spring</artifactId>
43+
<version>${project.version}</version>
44+
</dependency>
45+
<dependency>
46+
<groupId>io.cloudevents</groupId>
47+
<artifactId>cloudevents-http-basic</artifactId>
48+
<version>${project.version}</version>
49+
</dependency>
50+
<dependency>
51+
<groupId>io.cloudevents</groupId>
52+
<artifactId>cloudevents-json-jackson</artifactId>
53+
<version>${project.version}</version>
54+
</dependency>
55+
<dependency>
56+
<groupId>org.springframework.boot</groupId>
57+
<artifactId>spring-boot-starter-test</artifactId>
58+
<scope>test</scope>
59+
</dependency>
60+
</dependencies>
61+
62+
<build>
63+
<plugins>
64+
<plugin>
65+
<groupId>org.springframework.boot</groupId>
66+
<artifactId>spring-boot-maven-plugin</artifactId>
67+
<version>${spring-boot.version}</version>
68+
</plugin>
69+
</plugins>
70+
</build>
71+
72+
</project>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.cloudevents.examples.spring;
2+
3+
import java.net.URI;
4+
import java.util.UUID;
5+
import java.util.function.Function;
6+
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+
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.Bean;
17+
import org.springframework.context.annotation.Configuration;
18+
import org.springframework.http.codec.CodecConfigurer;
19+
import org.springframework.web.bind.annotation.RestController;
20+
21+
@SpringBootApplication
22+
@RestController
23+
public class DemoApplication {
24+
25+
public static void main(String[] args) throws Exception {
26+
SpringApplication.run(DemoApplication.class, args);
27+
}
28+
29+
@Bean
30+
public Function<CloudEvent, CloudEvent> events() {
31+
return event -> CloudEventBuilder.from(event)
32+
.withId(UUID.randomUUID().toString())
33+
.withSource(URI.create("https://spring.io/foos"))
34+
.withType("io.spring.event.Foo")
35+
.withData(event.getData().toBytes())
36+
.build();
37+
}
38+
39+
/**
40+
* Configure a MessageConverter for Spring Cloud Function to pick up and use to
41+
* convert to and from CloudEvent and Message.
42+
*/
43+
@Configuration
44+
public static class CloudEventMessageConverterConfiguration {
45+
@Bean
46+
public CloudEventMessageConverter cloudEventMessageConverter() {
47+
return new CloudEventMessageConverter();
48+
}
49+
}
50+
51+
/**
52+
* Configure an HTTP reader and writer so that we can process CloudEvents over
53+
* HTTP via Spring Webflux.
54+
*/
55+
@Configuration
56+
public static class CloudEventHandlerConfiguration implements CodecCustomizer {
57+
58+
@Override
59+
public void customize(CodecConfigurer configurer) {
60+
configurer.customCodecs().register(new CloudEventHttpMessageReader());
61+
configurer.customCodecs().register(new CloudEventHttpMessageWriter());
62+
}
63+
64+
}
65+
66+
}
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)