Skip to content

Commit d543b3b

Browse files
committed
added example function code
added support for multiple interface implementation injection aws sdk v2 autodiscovery feature
1 parent 3b06182 commit d543b3b

18 files changed

+1055
-79
lines changed

README.md

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
### Introduction
2+
3+
Simple Java library to build native AWS Lambda functions that limit cold start issues for Java runtime. It uses Java 11 and adds support for AWS SDK V2, Jackson for JSON serialization and Guice as JSR-330 Dependency Injection provider.
4+
5+
Please note that this is still proof of concept.
6+
7+
### Usage
8+
9+
rapid-lambda still requires you to implement `RequestHandler` interface from `aws-lambda-java-core` that contains your function logic. You still should use event classes from `aws-lambda-java-events` if you consumes events from other AWS services. All events have reflection configuration already prepared to be used by `native-image` plugin.
10+
11+
Example bootstrap code:
12+
```java
13+
public class Function {
14+
15+
public static void main(String[] args) {
16+
FunctionBootstrap.build(new MeasurementHandler(), APIGatewayProxyRequestEvent.class, FunctionConfiguration.newConfiguration(new Configuration())).bootstrap();
17+
}
18+
19+
}
20+
```
21+
22+
Please see `example` directory for more details.
23+
24+
`native-image` requires reflection configuration for your classes that will be deserialized via ObjectMapper or injected via JSR-330 annotations.
25+
26+
In order to build executable you need to run the following command (`JAVA_HOME` points to GraalVM location):
27+
28+
```shell script
29+
mvn clean package
30+
$JAVA_HOME/bin/native-image -H:ReflectionConfigurationFiles=reflection-config.json -jar target/native-function-jar-with-dependencies.jar function.o
31+
```
32+
33+
### Deployment on AWS Lambda
34+
35+
AWS Lambda custom runtime uses Amazon Linux 2 so in order to build native binary it has to be done on that OS e.g. on EC2 instance.
36+
37+
### AWS SDK V2
38+
39+
Supported clients
40+
* DynamoDB
41+
* Lambda
42+
* S3
43+
* SQS
44+
* SNS
45+
* SSM
46+
* EC2
47+
48+
In order to start using AWS Clients you need just to add maven dependency
49+
50+
```xml
51+
<dependency>
52+
<groupId>software.amazon.awssdk</groupId>
53+
<artifactId>dynamodb</artifactId>
54+
<version>2.10.76</version>
55+
</dependency>
56+
```
57+
58+
and the just inject client in your service classes.
59+
60+
```java
61+
@Inject
62+
private DynamoDbClient dynamoDbClient;
63+
```
64+
65+
### Dependency Injection
66+
67+
rapid-lambda partially supports JSR-330 Dependency Injection provided by Google implementation named Guice.
68+
69+
#### Available to inject
70+
71+
* HttpClient from Java 11
72+
* ObjectMapper
73+
* AWS SDK clients (if added as dependency)
74+
75+
#### Configuration:
76+
77+
Create binding configuration
78+
79+
```java
80+
public class GlobalModule extends AbstractModule {
81+
82+
@Override
83+
protected void configure() {
84+
bind(HttpClient.class).toInstance(HttpClient.newHttpClient());
85+
}
86+
}
87+
```
88+
89+
then HttpClient can be injected via @Inject annotation
90+
91+
```java
92+
@Inject
93+
private HttpClient httpClient;
94+
```
95+
96+
Injecting multiple instances of single interface:
97+
```java
98+
public class Configuration extends AbstractModule {
99+
@Override
100+
protected void configure() {
101+
Multibinder<NotificationService> multibinder = Multibinder.newSetBinder(binder(), NotificationService.class);
102+
multibinder.addBinding().toInstance(new SnsNotificationService());
103+
multibinder.addBinding().toInstance(new LambdaNotificationService());
104+
}
105+
}
106+
```
107+
108+
injection via
109+
```java
110+
@Inject
111+
private Set<NotificationService> notificationServices;
112+
```
113+
114+
##### Not supported:
115+
116+
binding interfaces to implementation classes e.g.
117+
```java
118+
bind(Service.class).to(ServiceImpl.class);
119+
```
120+
121+
It requires creating proxies via CGLIB that is not supported by native-image plugin. As alternative you might use direct binding to new class instances e.g.
122+
```java
123+
bind(Service.class).annotatedWith(Names.named("service1")).toInstance(new Service1());
124+
bind(Service.class).annotatedWith(Names.named("service2")).toInstance(new Service2());
125+
```
126+
127+
then it can be autowired with @Named annotation
128+
```java
129+
@Inject
130+
@Named("service1")
131+
private Service service;
132+
```
133+
134+
As an alternative to Guice you might want to try Dagger 2 https://github.com/google/dagger as compile-time dependency injection library.
135+
136+
#### Native-image
137+
Most of AWS services require HTTPS communication with their endpoints. In order to make it work with native binares generated by native-image libsunec.so and cacerts are added to custom runtime archive. More details https://quarkus.io/guides/native-and-ssl#the-sunec-library-and-friends
138+
139+
#### Debuging
140+
141+
native-image plugin is still a new tool in GraalVM portfolio and there is still room to improve. Many well-known libraries and frameworks from Java world are not supported. However there are some tricks that you may want to try if you face any issues with your code. You need to remember that your application is no longer pure AWS Lambda function but standalone Java application. GraalVM authors provide great tool to record all required configuration to build native binary called native-image-agent. You can launch your application as normal Java application with following configuration and it will create configuration files (Lambda endpoints simulator required, look above):
142+
```shell script
143+
java -agentlib:native-image-agent=config-output-dir=/tmp/native-image-config -jar target/native-function-jar-with-dependencies.jar
144+
```
145+
146+
#### Simulating Lambda endpoints
147+
148+
Please read the following documentation https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html
149+
150+
The quick way to write such simulator is to use SparkJava (http://sparkjava.com/) or Javalin (https://javalin.io/).
151+
152+
Example code using Javalin:
153+
154+
Maven dependency
155+
```xml
156+
<dependency>
157+
<groupId>io.javalin</groupId>
158+
<artifactId>javalin</artifactId>
159+
<version>3.8.0</version>
160+
</dependency>
161+
<dependency>
162+
<groupId>org.slf4j</groupId>
163+
<artifactId>slf4j-simple</artifactId>
164+
<version>1.7.28</version>
165+
</dependency>
166+
```
167+
168+
LambdaRuntimeSimulator.java
169+
```java
170+
public class LambdaRuntimeSimulator {
171+
172+
private static final BlockingQueue<String> events = new LinkedBlockingQueue<>();
173+
174+
public static void main(String[] args) {
175+
Javalin app = Javalin.create().start(7000);
176+
app.get("/2018-06-01/runtime/init/error", LambdaRuntimeSimulator::logResponse);
177+
app.get("/2018-06-01/runtime/invocation/next", LambdaRuntimeSimulator::nextInvocation);
178+
app.post("/2018-06-01/runtime/invocation/:invocationId/response", LambdaRuntimeSimulator::logResponse);
179+
app.post("/2018-06-01/runtime/invocation/:invocationId/error", LambdaRuntimeSimulator::logResponse);
180+
app.post("/events", LambdaRuntimeSimulator::newEvent);
181+
}
182+
183+
private static void nextInvocation(Context ctx) throws InterruptedException {
184+
ctx.header("Lambda-Runtime-Aws-Request-Id", UUID.randomUUID().toString());
185+
ctx.result(events.take());
186+
}
187+
188+
private static void logResponse(Context ctx) {
189+
System.out.println(String.format("Response from %s, body: %s", ctx.fullUrl(), ctx.body()));
190+
}
191+
192+
private static void newEvent(Context ctx) {
193+
final String body = ctx.body();
194+
if (nonNull(body) && !body.isBlank()) {
195+
events.add(body);
196+
}
197+
}
198+
199+
}
200+
```
201+
202+
This code exposes `/events` endpoint that accepts next Lambda event.

example/pom.xml

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>org.example</groupId>
8+
<artifactId>rapid-lambda-example</artifactId>
9+
<version>1.0-SNAPSHOT</version>
10+
11+
<dependencies>
12+
<dependency>
13+
<groupId>dev.jozefowicz</groupId>
14+
<artifactId>rapid-lambda</artifactId>
15+
<version>1.0.0</version>
16+
</dependency>
17+
18+
<dependency>
19+
<groupId>software.amazon.awssdk</groupId>
20+
<artifactId>dynamodb</artifactId>
21+
<version>2.10.76</version>
22+
</dependency>
23+
<dependency>
24+
<groupId>software.amazon.awssdk</groupId>
25+
<artifactId>sns</artifactId>
26+
<version>2.10.76</version>
27+
</dependency>
28+
<dependency>
29+
<groupId>software.amazon.awssdk</groupId>
30+
<artifactId>lambda</artifactId>
31+
<version>2.10.76</version>
32+
</dependency>
33+
</dependencies>
34+
35+
<build>
36+
<finalName>native-function</finalName>
37+
<plugins>
38+
<plugin>
39+
<groupId>org.apache.maven.plugins</groupId>
40+
<artifactId>maven-compiler-plugin</artifactId>
41+
<version>3.8.1</version>
42+
<configuration>
43+
<source>11</source>
44+
<target>11</target>
45+
</configuration>
46+
</plugin>
47+
<plugin>
48+
<groupId>org.apache.maven.plugins</groupId>
49+
<artifactId>maven-assembly-plugin</artifactId>
50+
<version>3.2.0</version>
51+
<configuration>
52+
<descriptorRefs>
53+
<descriptorRef>jar-with-dependencies</descriptorRef>
54+
</descriptorRefs>
55+
<archive>
56+
<manifest>
57+
<mainClass>com.example.Function</mainClass>
58+
</manifest>
59+
</archive>
60+
</configuration>
61+
<executions>
62+
<execution>
63+
<id>make-assembly</id>
64+
<phase>package</phase>
65+
<goals>
66+
<goal>single</goal>
67+
</goals>
68+
</execution>
69+
</executions>
70+
</plugin>
71+
</plugins>
72+
</build>
73+
74+
</project>

example/reflection-config.json

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[
2+
{
3+
"name": "com.example.Measurement",
4+
"methods": [
5+
{
6+
"name": "<init>",
7+
"parameterTypes": []
8+
}
9+
],
10+
"allDeclaredConstructors" : true,
11+
"allPublicConstructors" : true,
12+
"allDeclaredMethods" : true,
13+
"allPublicMethods" : true,
14+
"allDeclaredClasses" : true,
15+
"allPublicClasses" : true
16+
},
17+
{
18+
"name":"com.example.Configuration",
19+
"allDeclaredMethods":true
20+
},
21+
{
22+
"name":"com.example.DynamoDbMeasurementRepository",
23+
"allDeclaredFields":true,
24+
"allDeclaredMethods":true
25+
},
26+
{
27+
"name":"com.example.SnsNotificationService",
28+
"allDeclaredFields":true,
29+
"allDeclaredMethods":true
30+
},
31+
{
32+
"name":"com.example.LambdaNotificationService",
33+
"allDeclaredFields":true,
34+
"allDeclaredMethods":true
35+
},
36+
{
37+
"name":"com.example.MeasurementHandler",
38+
"allDeclaredFields":true,
39+
"allDeclaredMethods":true
40+
}
41+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.example;
2+
3+
import com.google.inject.AbstractModule;
4+
import com.google.inject.multibindings.Multibinder;
5+
6+
public class Configuration extends AbstractModule {
7+
@Override
8+
protected void configure() {
9+
bind(MeasurementRepository.class).toInstance(new DynamoDbMeasurementRepository());
10+
Multibinder<NotificationService> multibinder = Multibinder.newSetBinder(binder(), NotificationService.class);
11+
multibinder.addBinding().toInstance(new SnsNotificationService());
12+
multibinder.addBinding().toInstance(new LambdaNotificationService());
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.example;
2+
3+
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
4+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
5+
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
6+
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
7+
import software.amazon.awssdk.services.dynamodb.model.ScanResponse;
8+
9+
import javax.inject.Inject;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.stream.Collectors;
13+
14+
import static java.util.Objects.nonNull;
15+
16+
public class DynamoDbMeasurementRepository implements MeasurementRepository {
17+
18+
private final String tableName = System.getenv("TABLE_NAME");
19+
20+
@Inject
21+
private DynamoDbClient dynamoDbClient;
22+
23+
@Override
24+
public void persist(final Measurement measurement) {
25+
final PutItemRequest putItemRequest = PutItemRequest
26+
.builder()
27+
.tableName(tableName)
28+
.item(buildItem(measurement))
29+
.build();
30+
dynamoDbClient.putItem(putItemRequest);
31+
}
32+
33+
@Override
34+
public List<Measurement> findAll() {
35+
final ScanResponse response = dynamoDbClient.scan(ScanRequest.builder().tableName(tableName).build());
36+
return response.items().stream().map(this::buildMeasurement).collect(Collectors.toList());
37+
}
38+
39+
private Map<String, AttributeValue> buildItem(final Measurement measurement) {
40+
return Map.of(
41+
"serialNumber", AttributeValue.builder().s(measurement.getSerialNumber()).build(),
42+
"pressure", AttributeValue.builder().n(Double.toString(measurement.getPressure())).build(),
43+
"temperature", AttributeValue.builder().n(Double.toString(measurement.getTemperature())).build()
44+
);
45+
}
46+
47+
private Measurement buildMeasurement(final Map<String, AttributeValue> item) {
48+
final Measurement measurement = new Measurement();
49+
measurement.setSerialNumber(item.get("serialNumber").s());
50+
final AttributeValue pressure = item.get("pressure");
51+
if (nonNull(pressure)) {
52+
measurement.setPressure(Double.valueOf(pressure.n()));
53+
}
54+
final AttributeValue temperature = item.get("temperature");
55+
if (nonNull(temperature)) {
56+
measurement.setTemperature(Double.valueOf(temperature.n()));
57+
}
58+
return measurement;
59+
}
60+
}

0 commit comments

Comments
 (0)