Example implementation for Aws API Gateway

User can extend SpringBootApiGatewayRequestHandler instead of the
generic SpringBootRequestHandler. It ties the code to AWS and the
API Gateway, but at least it supports the incoming data fully.

Fixes gh-111, closes gh-136
This commit is contained in:
Dimitry Declercq
2018-01-13 11:28:41 +01:00
committed by Dave Syer
parent 0ce1a81bd4
commit 8c963bf456
6 changed files with 212 additions and 4 deletions

View File

@@ -20,7 +20,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<aws-lambda-events.version>1.2.1</aws-lambda-events.version>
<aws-lambda-events.version>2.0.2</aws-lambda-events.version>
</properties>
<dependencies>

View File

@@ -0,0 +1,96 @@
package org.springframework.cloud.function.adapter.aws;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.GenericMessage;
import java.util.HashMap;
import java.util.Map;
public class SpringBootApiGatewayRequestHandler extends SpringBootRequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
@Autowired
private ObjectMapper mapper;
@Autowired
private FunctionInspector inspector;
public SpringBootApiGatewayRequestHandler(Class<?> configurationClass) {
super(configurationClass);
}
public SpringBootApiGatewayRequestHandler() {
super();
}
protected Object convertEvent(APIGatewayProxyRequestEvent event) {
Object body = deserializeBody(event.getBody());
if (functionAcceptsMessage()) {
return new GenericMessage<>(body, getHeaders(event));
} else {
return body;
}
}
private boolean functionAcceptsMessage() {
return inspector.isMessage(function());
}
private Object deserializeBody(String json) {
try {
return mapper.readValue(json, getInputType());
} catch (Exception e) {
throw new IllegalStateException("Cannot convert event", e);
}
}
private MessageHeaders getHeaders(APIGatewayProxyRequestEvent event) {
Map<String, Object> headers = new HashMap<>();
if (event.getHeaders() != null) {
headers.putAll(event.getHeaders());
}
headers.put("request", event);
return new MessageHeaders(headers);
}
protected APIGatewayProxyResponseEvent convertOutput(Object output) {
if (functionReturnsMessage(output)) {
Message message = (Message) output;
return new APIGatewayProxyResponseEvent()
.withStatusCode((Integer) message.getHeaders().getOrDefault("statusCode", 200))
.withHeaders(toResponseHeaders(message.getHeaders()))
.withBody(serializeBody(message.getPayload()));
} else {
return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody(serializeBody(output));
}
}
private boolean functionReturnsMessage(Object output) {
return output instanceof Message;
}
private Map<String, String> toResponseHeaders(MessageHeaders messageHeaders) {
Map<String, String> responseHeaders = new HashMap<>();
messageHeaders.forEach((key, value) -> responseHeaders.put(key, value.toString()));
return responseHeaders;
}
private String serializeBody(Object body) {
try {
return mapper.writeValueAsString(body);
} catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot convert output", e);
}
}
}

View File

@@ -49,7 +49,7 @@ public class SpringBootRequestHandler<E, O> extends SpringFunctionInitializer im
private Object result(Object input, Flux<?> output) {
List<Object> result = new ArrayList<>();
for (Object value : output.toIterable()) {
result.add(value);
result.add(convertOutput(value));
}
if (isSingleValue(input) && result.size()==1) {
return result.get(0);
@@ -72,4 +72,8 @@ public class SpringBootRequestHandler<E, O> extends SpringFunctionInitializer im
return event;
}
protected O convertOutput(Object output) {
return (O) output;
}
}

View File

@@ -136,7 +136,7 @@ public class SpringFunctionInitializer implements Closeable {
return Object.class;
}
private Object function() {
protected Object function() {
return this.function != null ? this.function
: (this.consumer != null ? this.consumer : this.supplier);
}

View File

@@ -0,0 +1,108 @@
package org.springframework.cloud.function.adapter.aws;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import org.junit.Test;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
public class SpringBootApiGatewayRequestHandlerTests {
private SpringBootApiGatewayRequestHandler handler;
@Test
public void functionBean() {
handler = new SpringBootApiGatewayRequestHandler(FunctionConfig.class);
handler.initialize();
APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent();
request.setBody("{\"value\":\"foo\"}");
Object output = handler.handleRequest(request, null);
assertThat(output).isInstanceOf(APIGatewayProxyResponseEvent.class);
assertThat(((APIGatewayProxyResponseEvent) output).getStatusCode()).isEqualTo(200);
assertThat(((APIGatewayProxyResponseEvent) output).getBody()).isEqualTo("{\"value\":\"FOO\"}");
}
@Configuration
@Import({ContextFunctionCatalogAutoConfiguration.class,
JacksonAutoConfiguration.class})
protected static class FunctionConfig {
@Bean
public Function<Foo, Bar> function() {
return foo -> new Bar(foo.getValue().toUpperCase());
}
}
@Test
public void functionMessageBean() {
handler = new SpringBootApiGatewayRequestHandler(FunctionMessageConfig.class);
handler.initialize();
APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent();
request.setBody("{\"value\":\"foo\"}");
Object output = handler.handleRequest(request, null);
assertThat(output).isInstanceOf(APIGatewayProxyResponseEvent.class);
assertThat(((APIGatewayProxyResponseEvent) output).getStatusCode()).isEqualTo(200);
assertThat(((APIGatewayProxyResponseEvent) output).getHeaders().get("spring")).isEqualTo("cloud");
assertThat(((APIGatewayProxyResponseEvent) output).getBody()).isEqualTo("{\"value\":\"FOO\"}");
}
@Configuration
@Import({ContextFunctionCatalogAutoConfiguration.class,
JacksonAutoConfiguration.class})
protected static class FunctionMessageConfig {
@Bean
public Function<Message<Foo>, Message<Bar>> function() {
return (foo -> {
Map<String, Object> headers = Collections.singletonMap("spring", "cloud");
return new GenericMessage<>(
new Bar(foo.getPayload().getValue().toUpperCase()),
headers);
});
}
}
protected static class Foo {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
protected static class Bar {
private String value;
public Bar() {
}
public Bar(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}

View File

@@ -23,7 +23,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<wrapper.version>1.0.9.RELEASE</wrapper.version>
<aws-lambda-events.version>1.2.1</aws-lambda-events.version>
<aws-lambda-events.version>2.0.2</aws-lambda-events.version>
<reactor.version>3.1.2.RELEASE</reactor.version>
<spring-cloud-function.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-function.version>
<spring-cloud-stream-servlet.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-stream-servlet.version>