Skip to content

Commit ac06548

Browse files
authored
feat: Add CustomPojoSerializer (#1758)
* feat: Add CustomPojoSerializer This PR modifies `micronaut-function-aws` to provide an implementation of `com.amazonaws.services.lambda.runtime.CustomPojoSerializer` which is loaded via SPI. This `CustomPojoSerialization` avoids your Micronaut function to pay a double hit on performances when using a serialization library inside the Lambda function. * create custom exception
1 parent 6b77ced commit ac06548

13 files changed

+369
-5
lines changed

aws-lambda-events-serde/build.gradle.kts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ plugins {
44

55
dependencies {
66
annotationProcessor(mnSerde.micronaut.serde.processor)
7-
implementation(mnSerde.micronaut.serde.jackson)
8-
implementation(libs.managed.aws.lambda.events)
9-
testImplementation(libs.assertj.core)
7+
api(libs.managed.aws.lambda.events)
8+
api(mnSerde.micronaut.serde.jackson)
109
implementation(libs.managed.aws.lambda.java.serialization)
10+
testImplementation(libs.assertj.core)
1111
}
1212

1313
micronautBuild {

function-aws/build.gradle.kts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ plugins {
55
dependencies {
66
api(mn.micronaut.function)
77
api(libs.managed.aws.lambda.core)
8+
implementation(mn.micronaut.json.core)
89
testImplementation(mnMongo.micronaut.mongo.sync)
910
testImplementation(platform(libs.testcontainers.bom))
1011
testImplementation(libs.testcontainers.spock)
1112
testImplementation(libs.testcontainers.mongodb)
1213
testImplementation(libs.testcontainers)
13-
testRuntimeOnly(mn.micronaut.jackson.databind)
14+
testImplementation(projects.micronautAwsLambdaEventsSerde)
1415
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2017-2023 original 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.micronaut.function.aws;
17+
18+
import java.io.IOException;
19+
20+
/**
21+
* Exception raised when serialization with {@link JsonMapperCustomPojoSerializer} fails.
22+
* @author Sergio del Amo
23+
* @since 4.0.0
24+
*/
25+
public class CustomPojoSerializerException extends RuntimeException {
26+
public CustomPojoSerializerException(IOException e) {
27+
super(e);
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2017-2023 original 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.micronaut.function.aws;
17+
18+
import com.amazonaws.services.lambda.runtime.CustomPojoSerializer;
19+
import io.micronaut.core.type.Argument;
20+
import io.micronaut.json.JsonMapper;
21+
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.io.OutputStream;
25+
import java.lang.reflect.Type;
26+
27+
/**
28+
* Provides an implementation of {@link CustomPojoSerializer} which is loaded via SPI. This implementation avoids paying a double hit on performance when using a serialization library inside the Lambda function.
29+
* @author Sergio del Amo
30+
* @since 4.0.0
31+
*/
32+
public class JsonMapperCustomPojoSerializer implements CustomPojoSerializer {
33+
private JsonMapper jsonMapper;
34+
35+
public JsonMapperCustomPojoSerializer() {
36+
this.jsonMapper = JsonMapper.createDefault();
37+
}
38+
39+
@Override
40+
public <T> T fromJson(InputStream input, Type type) {
41+
try {
42+
return (T) jsonMapper.readValue(input, Argument.of(type));
43+
} catch (IOException e) {
44+
throw new CustomPojoSerializerException(e);
45+
}
46+
}
47+
48+
@Override
49+
public <T> T fromJson(String input, Type type) {
50+
try {
51+
return (T) jsonMapper.readValue(input, Argument.of(type));
52+
} catch (IOException e) {
53+
throw new CustomPojoSerializerException(e);
54+
}
55+
}
56+
57+
@Override
58+
public <T> void toJson(T value, OutputStream output, Type type) {
59+
Argument<T> argumentType = (Argument<T>) Argument.of(type);
60+
try {
61+
jsonMapper.writeValue(output, argumentType, value);
62+
} catch (IOException e) {
63+
throw new CustomPojoSerializerException(e);
64+
}
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.micronaut.function.aws.JsonMapperCustomPojoSerializer
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package io.micronaut.function.aws
2+
3+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
4+
import com.amazonaws.services.lambda.runtime.CustomPojoSerializer
5+
import io.micronaut.serde.annotation.Serdeable
6+
import spock.lang.Specification
7+
8+
import java.nio.charset.StandardCharsets
9+
10+
class JsonMapperCustomPojoSerializerSpec extends Specification {
11+
12+
void "via SPI you can load JsonMapperCustomPojoSerializer as CustomPojoSerializer"() {
13+
given:
14+
File f = new File("src/test/resources/api-gateway-proxy.json")
15+
16+
expect:
17+
f.exists()
18+
19+
when:
20+
ServiceLoader<CustomPojoSerializer> loader = ServiceLoader.load(CustomPojoSerializer.class);
21+
Iterator<CustomPojoSerializer> iterator = loader.iterator();
22+
23+
then:
24+
iterator.hasNext()
25+
26+
when:
27+
CustomPojoSerializer customPojoSerializer = iterator.next()
28+
29+
then:
30+
customPojoSerializer instanceof JsonMapperCustomPojoSerializer
31+
32+
when:
33+
APIGatewayProxyRequestEvent event = customPojoSerializer.fromJson(f.newInputStream(), APIGatewayProxyRequestEvent.class)
34+
35+
then:
36+
assertApiGatewayProxyRequestEvent(event)
37+
38+
when:
39+
event = customPojoSerializer.fromJson(f.text, APIGatewayProxyRequestEvent.class)
40+
41+
then:
42+
assertApiGatewayProxyRequestEvent(event)
43+
44+
when:
45+
ByteArrayOutputStream baos = new ByteArrayOutputStream()
46+
customPojoSerializer.toJson(new Book(title: "Building Microservices"), baos, Book.class)
47+
48+
then:
49+
'{"title":"Building Microservices"}' == new String(baos.toByteArray(), StandardCharsets.UTF_8)
50+
}
51+
52+
@Serdeable
53+
static class Book {
54+
String title
55+
}
56+
57+
void assertApiGatewayProxyRequestEvent(APIGatewayProxyRequestEvent event) {
58+
assert "eyJ0ZXN0IjoiYm9keSJ9" == event.body
59+
assert "/{proxy+}" == event.resource
60+
assert "/path/to/resource" == event.path
61+
assert "POST" == event.httpMethod
62+
assert event.isBase64Encoded
63+
assert [foo: "bar"] == event.queryStringParameters
64+
assert [foo: ["bar"]] == event.multiValueQueryStringParameters
65+
assert [proxy: "/path/to/resource"] == event.pathParameters
66+
assert [baz: "qux"] == event.stageVariables
67+
assert [
68+
"Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
69+
"Accept-Encoding" : "gzip, deflate, sdch",
70+
"Accept-Language" : "en-US,en;q=0.8",
71+
"Cache-Control" : "max-age=0",
72+
"CloudFront-Forwarded-Proto" : "https",
73+
"CloudFront-Is-Desktop-Viewer": "true",
74+
"CloudFront-Is-Mobile-Viewer" : "false",
75+
"CloudFront-Is-SmartTV-Viewer": "false",
76+
"CloudFront-Is-Tablet-Viewer" : "false",
77+
"CloudFront-Viewer-Country" : "US",
78+
"Host" : "1234567890.execute-api.us-east-1.amazonaws.com",
79+
"Upgrade-Insecure-Requests" : "1",
80+
"User-Agent" : "Custom User Agent String",
81+
"Via" : "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
82+
"X-Amz-Cf-Id" : "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
83+
"X-Forwarded-For" : "127.0.0.1, 127.0.0.2",
84+
"X-Forwarded-Port" : "443",
85+
"X-Forwarded-Proto" : "https"
86+
] == event.headers
87+
assert ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"] == event.multiValueHeaders.get("Accept")
88+
assert ["gzip, deflate, sdch"] == event.multiValueHeaders.get("Accept-Encoding")
89+
assert ["en-US,en;q=0.8"] == event.multiValueHeaders.get("Accept-Language")
90+
assert ["max-age=0"] == event.multiValueHeaders.get("Cache-Control")
91+
assert ["https"] == event.multiValueHeaders.get("CloudFront-Forwarded-Proto")
92+
assert ["true"] == event.multiValueHeaders.get("CloudFront-Is-Desktop-Viewer")
93+
assert ["false"] == event.multiValueHeaders.get("CloudFront-Is-Mobile-Viewer")
94+
assert ["false"] == event.multiValueHeaders.get("CloudFront-Is-SmartTV-Viewer")
95+
assert ["false"] == event.multiValueHeaders.get("CloudFront-Is-Tablet-Viewer")
96+
assert ["US"] == event.multiValueHeaders.get("CloudFront-Viewer-Country")
97+
assert ["0123456789.execute-api.us-east-1.amazonaws.com"] == event.multiValueHeaders.get("Host")
98+
assert ["1"] == event.multiValueHeaders.get("Upgrade-Insecure-Requests")
99+
assert ["Custom User Agent String"] == event.multiValueHeaders.get("User-Agent")
100+
assert ["1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"] == event.multiValueHeaders.get("Via")
101+
assert ["cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="] == event.multiValueHeaders.get("X-Amz-Cf-Id")
102+
assert ["127.0.0.1, 127.0.0.2"] == event.multiValueHeaders.get("X-Forwarded-For")
103+
assert ["443"] == event.multiValueHeaders.get("X-Forwarded-Port")
104+
assert ["https"] == event.multiValueHeaders.get("X-Forwarded-Proto")
105+
assert "123456789012" == event.requestContext.accountId
106+
assert "123456" == event.requestContext.resourceId
107+
assert "prod" == event.requestContext.stage
108+
assert "c6af9ac6-7b61-11e6-9a41-93e8deadbeef" == event.requestContext.requestId
109+
//assert "09/Apr/2015:12:34:56 +0000" == event.requestContext.requestTime
110+
//assert 1428582896000 == event.requestContext.requestTimeEpoch
111+
assert null == event.requestContext.identity.cognitoIdentityPoolId
112+
assert null == event.requestContext.identity.accountId
113+
assert null == event.requestContext.identity.cognitoIdentityId
114+
assert null == event.requestContext.identity.caller
115+
assert null == event.requestContext.identity.accessKey
116+
assert "127.0.0.1" == event.requestContext.identity.sourceIp
117+
assert null == event.requestContext.identity.cognitoAuthenticationType
118+
assert null == event.requestContext.identity.cognitoAuthenticationProvider
119+
assert null == event.requestContext.identity.userArn
120+
assert "Custom User Agent String" == event.requestContext.identity.userAgent
121+
assert null == event.requestContext.identity.user
122+
assert "/prod/path/to/resource" == event.requestContext.path
123+
assert "/{proxy+}" == event.requestContext.resourcePath
124+
assert "POST" == event.requestContext.httpMethod
125+
assert "1234567890" == event.requestContext.apiId
126+
//assert "HTTP/1.1" == event.requestContext.protocol
127+
}
128+
}

function-aws/src/test/groovy/io/micronaut/function/aws/MicronautRequestHandlerSpec.groovy

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package io.micronaut.function.aws
1818
import com.amazonaws.services.lambda.runtime.Context
1919
import groovy.transform.Canonical
2020
import io.micronaut.context.env.Environment
21+
import io.micronaut.serde.annotation.Serdeable
2122
import spock.lang.Specification
2223

2324
import jakarta.inject.Inject
@@ -72,6 +73,7 @@ class MicronautRequestHandlerSpec extends Specification {
7273
}
7374
}
7475

76+
@Serdeable
7577
@Canonical
7678
static class Point {
7779
Integer x,y

function-aws/src/test/groovy/io/micronaut/function/aws/MicronautRequestStreamHandlerSpec.groovy

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package io.micronaut.function.aws
1818
import com.amazonaws.services.lambda.runtime.Context
1919
import io.micronaut.context.env.Environment
2020
import io.micronaut.function.FunctionBean
21+
import io.micronaut.serde.annotation.Serdeable
2122
import spock.lang.Specification
2223

2324
import java.util.function.Function
@@ -92,6 +93,7 @@ class MicronautRequestStreamHandlerSpec extends Specification{
9293
output.toString() == 'value 20'
9394
}
9495

96+
@Serdeable
9597
static class Book {
9698
String title
9799
}

0 commit comments

Comments
 (0)