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:
parent
47bed5616d
commit
a419d8bba3
|
@ -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.
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<module>amqp-proton</module>
|
||||
<module>spring-reactive</module>
|
||||
<module>spring-rsocket</module>
|
||||
<module>spring-function</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -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`.
|
|
@ -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>
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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`.
|
||||
|
|
Loading…
Reference in New Issue