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:
committed by
Dave Syer
parent
0ce1a81bd4
commit
8c963bf456
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user