Add Spring Cloud Function sample (#356)

* Add Spring Cloud Function sample

Signed-off-by: Dave Syer <dsyer@vmware.com>

* Fix example curl command with structured event

Signed-off-by: Dave Syer <dsyer@vmware.com>
This commit is contained in:
Dave Syer 2021-03-08 14:08:16 +00:00 committed by GitHub
parent 47bed5616d
commit a419d8bba3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 376 additions and 5 deletions

View File

@ -188,3 +188,6 @@ Check out the integration tests and samples:
- [spring-rsocket](https://github.com/cloudevents/sdk-java/tree/master/examples/spring-rsocket)
shows how to receive and send CloudEvents through RSocket using Spring Boot.
- [spring-cloud-function](https://github.com/cloudevents/sdk-java/tree/master/examples/spring-function)
shows how to consume and process CloudEvents via Spring Cloud Function.

View File

@ -27,6 +27,7 @@
<module>amqp-proton</module>
<module>spring-reactive</module>
<module>spring-rsocket</module>
<module>spring-function</module>
</modules>
</project>

View File

@ -0,0 +1,75 @@
# Spring Reactive + CloudEvents sample
## Build
```shell
mvn package
```
## Start HTTP Server
```shell
mvn spring-boot:run
```
You can try sending a request using curl, and it echos back a cloud event the same body and with new `ce-*` headers:
```shell
curl -v -d '{"value": "Foo"}' \
-H'Content-type: application/json' \
-H'ce-id: 1' \
-H'ce-source: cloud-event-example' \
-H'ce-type: my.application.Foo' \
-H'ce-specversion: 1.0' \
http://localhost:8080/event
```
It also accepts data in "structured" format:
```shell
curl -v -H'Content-type: application/cloudevents+json' \
-d '{"data": {"value": "Foo"},
"id: 1,
"source": "cloud-event-example"
"type": "my.application.Foo"
"specversion": "1.0"}' \
http://localhost:8080/event
```
The `/event endpoint is implemented like this (the request and response are modelled directly as a `CloudEvent`):
```java
@PostMapping("/event")
public Mono<CloudEvent> event(@RequestBody Mono<CloudEvent> body) {
return ...;
}
```
and to make that work we need to install the codecs:
```java
@Configuration
public static class CloudEventHandlerConfiguration implements CodecCustomizer {
@Override
public void customize(CodecConfigurer configurer) {
configurer.customCodecs().register(new CloudEventHttpMessageReader());
configurer.customCodecs().register(new CloudEventHttpMessageWriter());
}
}
```
The same feature in Spring MVC is provided by the `CloudEventHttpMessageConverter`.
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):
```java
@PostMapping("/foos")
public ResponseEntity<Foo> echo(@RequestBody Foo foo, @RequestHeader HttpHeaders headers) {
...
}
```
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`.

View File

@ -0,0 +1,72 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloudevents-examples</artifactId>
<groupId>io.cloudevents</groupId>
<version>2.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloudevents-spring-function-example</artifactId>
<properties>
<spring-boot.version>2.4.3</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.cloudevents</groupId>
<artifactId>cloudevents-spring</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.cloudevents</groupId>
<artifactId>cloudevents-http-basic</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.cloudevents</groupId>
<artifactId>cloudevents-json-jackson</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,66 @@
package io.cloudevents.examples.spring;
import java.net.URI;
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;
@SpringBootApplication
@RestController
public class DemoApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public Function<CloudEvent, CloudEvent> events() {
return event -> CloudEventBuilder.from(event)
.withId(UUID.randomUUID().toString())
.withSource(URI.create("https://spring.io/foos"))
.withType("io.spring.event.Foo")
.withData(event.getData().toBytes())
.build();
}
/**
* Configure a MessageConverter for Spring Cloud Function to pick up and use to
* convert to and from CloudEvent and Message.
*/
@Configuration
public static class CloudEventMessageConverterConfiguration {
@Bean
public CloudEventMessageConverter cloudEventMessageConverter() {
return new CloudEventMessageConverter();
}
}
/**
* Configure an HTTP reader and writer so that we can process CloudEvents over
* HTTP via Spring Webflux.
*/
@Configuration
public static class CloudEventHandlerConfiguration implements CodecCustomizer {
@Override
public void customize(CodecConfigurer configurer) {
configurer.customCodecs().register(new CloudEventHttpMessageReader());
configurer.customCodecs().register(new CloudEventHttpMessageWriter());
}
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2019-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudevents.examples.spring;
class Foo {
private String value;
public Foo() {
}
public Foo(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Foo [value=" + this.value + "]";
}
}

View File

@ -0,0 +1,112 @@
package io.cloudevents.examples.spring;
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 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");
}
}

View File

@ -29,10 +29,10 @@ It also accepts data in "structured" format:
```shell
curl -v -H'Content-type: application/cloudevents+json' \
-d '{"data": {"value": "Foo"},
"ce-id: 1,
"ce-source": "cloud-event-example"
"ce-type": "my.application.Foo"
"ce-specversion": "1.0"}' \
"id: 1,
"source": "cloud-event-example"
"type": "my.application.Foo"
"specversion": "1.0"}' \
http://localhost:8080/event
```
@ -72,4 +72,4 @@ public ResponseEntity<Foo> echo(@RequestBody Foo foo, @RequestHeader HttpHeaders
}
```
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`.
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`.