8000 Allow specification of custom handler methods in Local lambdas (#75) · localstack/localstack-java-utils@8fbf4ff · GitHub
[go: up one dir, main page]

Skip to content

Commit 8fbf4ff

Browse files
authored
Allow specification of custom handler methods in Local lambdas (#75)
* first version of lambda handler function specification support * add more comments, make logger field of lambda context transient to allow gson serialization * minor fixes to make it release ready * bump version to 0.2.16
1 parent 138cddd commit 8fbf4ff

File tree

7 files changed

+124
-24
lines changed

7 files changed

+124
-24
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Simply add the following dependency to your `pom.xml` file:
6060
<dependency>
6161
<groupId>cloud.localstack</groupId>
6262
<artifactId>localstack-utils</artifactId>
63-
<version>0.2.15</version>
63+
<version>0.2.16</version>
6464
</dependency>
6565
```
6666

@@ -108,6 +108,7 @@ make build
108108

109109
## Change Log
110110

111+
* v0.2.16: Add support for :: notation for Java Lambda handler specification, fix failing QLDB tests, fix failing tests with Jexter rules/extensions
111112
* v0.2.15: Fix Kinesis CBOR tests; fix project setup and classpath for SDK v1/v2 utils; fix awaiting results in tests using async clients; refactor classpath setup for v1/v2 SDKs; fall back to using edge port if port mapping cannot be determined from container
112113
* v0.2.14: Add ability to get handler class name through `_HANDLER` environment variable like on real AWS and Lambci environment
113114
* v0.2.11: Enable specification of "platform" when configuring container

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<groupId>cloud.localstack</groupId>
55
<artifactId>localstack-utils</artifactId>
66
<packaging>jar</packaging>
7-
<version>0.2.15</version>
7+
<version>0.2.16</version>
88
<name>localstack-utils</name>
99

1010
<description>Java utilities for the LocalStack platform.</description>

src/main/java/cloud/localstack/LambdaContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class LambdaContext implements Context {
2222
private static final String TODAY = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
2323
private static final String CONTAINER_ID = UUID.randomUUID().toString();
2424

25-
private final Logger LOG = Logger.getLogger(LambdaContext.class.getName());
25+
private transient final Logger LOG = Logger.getLogger(LambdaContext.class.getName());
2626

2727
private final String requestId;
2828

src/main/java/cloud/localstack/LambdaExecutor.java

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import cloud.localstack.awssdkv1.lambda.KinesisEventParser;
55
import cloud.localstack.awssdkv1.lambda.S3EventParser;
66

7+
import cloud.localstack.lambda_handler.HandlerNameParseResult;
8+
import cloud.localstack.lambda_handler.MultipleMatchingHandlersException;
9+
import cloud.localstack.lambda_handler.NoMatchingHandlerException;
710
import com.amazonaws.services.lambda.runtime.Context;
811
import com.amazonaws.services.lambda.runtime.RequestHandler;
912
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
@@ -20,6 +23,7 @@
2023
import java.io.ByteArrayOutputStream;
2124
import java.io.OutputStream;
2225
import java.lang.reflect.InvocationTargetException;
26+
import java.lang.reflect.Method;
2327
import java.lang.reflect.ParameterizedType;
2428
import java.lang.reflect.Type;
2529
import java.nio.charset.StandardCharsets;
@@ -58,21 +62,25 @@ public static void main(String[] args) throws Exception {
5862
List<Map<String,Object>> records = (List<Map<String, Object>>) get(map, "Records");
5963
Object inputObject = map;
6064

61-
Object handler;
65+
String handlerName;
6266
if (args.length == 2) {
63-
handler = getHandler(args[0]);
67+
handlerName = args[0];
6468
} else {
6569
String handlerEnvVar = System.getenv("_HANDLER");
6670
if (handlerEnvVar == null) {
6771
System.err.println("Handler must be provided by '_HANDLER' environment variable");
6872
System.exit(1);
6973
}
70-
handler = getHandler(handlerEnvVar);
74+
handlerName = handlerEnvVar;
7175
}
76+
HandlerNameParseResult parseResult = parseHandlerName(handlerName);
77+
Object handler = getHandler(parseResult.getClassName());
78+
String handlerMethodName = parseResult.getHandlerMethod();
79+
Method handlerMethod = handlerMethodName != null ? getHandlerMethodByName(handler, handlerMethodName) : null;
7280
if (records == null) {
73-
Optional<Object> deserialisedInput = getInputObject(reader, fileContent, handler);
74-
if (deserialisedInput.isPresent()) {
75-
inputObject = deserialisedInput.get();
81+
Optional<Object> deserializedInput = getInputObject(reader, fileContent, handler, handlerMethod);
82+
if (deserializedInput.isPresent()) {
83+
inputObject = deserializedInput.get();
7684
}
7785
} else {
7886
if (records.stream().anyMatch(record -> record.containsKey("kinesis") || record.containsKey("Kinesis"))) {
@@ -92,7 +100,7 @@ public static void main(String[] args) throws Exception {
92100
snsRecord.setTimestamp(new DateTime());
93101
r.setSns(snsRecord);
94102
}
95-
} else if (records.stream().filter(record -> record.containsKey("dynamodb")).count() > 0) {
103+
} else if (records.stream().anyMatch(record -> record.containsKey("dynamodb"))) {
96104
inputObject = DDBEventParser.parse(records);
97105
} else if (records.stream().anyMatch(record -> record.containsK F438 ey("s3"))) {
98106
inputObject = S3EventParser.parse(records);
@@ -102,9 +110,14 @@ public static void main(String[] args) throws Exception {
102110
}
103111

104112
Context ctx = new LambdaContext(UUID.randomUUID().toString());
105-
if (handler instanceof RequestHandler) {
106-
Object result = ((RequestHandler<Object, ?>) handler).handleRequest(inputObject, ctx);
107-
// try turning the output into json
113+
if (handlerMethod != null || handler instanceof RequestHandler) {
114+
Object result;
115+
if (handlerMethod != null) {
116+
// use reflection to load handler method from class
117+
result = handlerMethod.invoke(handler, inputObject, ctx);
118+
} else {
119+
result = ((RequestHandler<Object, ?>) handler).handleRequest(inputObject, ctx);
120+
}
108121
try {
109122
result = new ObjectMapper().writeValueAsString(result);
110123
} catch (JsonProcessingException jsonException) {
@@ -115,36 +128,89 @@ public static void main(String[] args) throws Exception {
115128
} else if (handler instanceof RequestStreamHandler) {
116129
OutputStream os = new ByteArrayOutputStream();
117130
((RequestStreamHandler) handler).handleRequest(
118-
new StringInputStream(fileContent), os, ctx);
131+
new StringInputStream(fileContent), os, ctx);
119132
System.out.println(os);
120133
}
121134
}
122135

123-
private static Optional<Object> getInputObject(ObjectMapper mapper, String objectString, Object handler) {
136+
/**
137+
* Returns the method matching the specified name implemented in the given handler object class
138+
* @param handler Handler the method in question belongs to
139+
* @param handlerMethodName Name of the method we are looking for in the handler
140+
* @return Method object for the method with the given method name
141+
* @throws MultipleMatchingHandlersException Thrown when multiple methods in the given handler exist for the given name
142+
* @throws NoMatchingHandlerException Thrown if no method in the handler is matching the given name
143+
*/
144+
private static Method getHandlerMethodByName(Object handler, String handlerMethodName) throws MultipleMatchingHandlersException, NoMatchingHandlerException {
145+
List<Method> handlerMethods = Arrays.stream(handler.getClass().getMethods())
146+
.filter(method -> method.getName().equals(handlerMethodName))
147+
.collect(Collectors.toList());
148+
if (handlerMethods.size() > 1) {
149+
throw new MultipleMatchingHandlersException("Multiple matching headers: " + handlerMethods);
150+
} else if (handlerMethods.isEmpty()) {
151+
throw new NoMatchingHandlerException("No matching handlers for method name: "
152+
+ handlerMethodName);
153+
}
154+
return handlerMethods.get(0);
155+
}
156+
157+
/**
158+
* Getting the input object for the handler function.
159+
* @param mapper ObjectMapper that maps the objectString into the target parameter type
160+
* @param objectString Object we got from the lambda invocation
161+
* @param handler Handler object we need to get the correct input type
162+
* @param handlerMethod Handler method we need to get the correct input type
163+
* @return Optional of the input object for the lambda handler
164+
*/
165+
private static Optional<Object> getInputObject(ObjectMapper mapper, String objectString, Object handler, Method handlerMethod) {
124166
Optional<Object> inputObject = Optional.empty();
125167
try {
126-
Optional<Type> handlerInterface = Arrays.stream(handler.getClass().getGenericInterfaces())
127-
.filter(genericInterface ->
128-
((ParameterizedType) genericInterface).getRawType().equals(RequestHandler.class))
129-
.findFirst();
130-
if (handlerInterface.isPresent()) {
131-
Class<?> handlerInputType = Class.forName(((ParameterizedType) handlerInterface.get())
132-
.getActualTypeArguments()[0].getTypeName());
168+
if (handlerMethod != null) {
169+
Class<?> handlerInputType = Class.forName(handlerMethod.getParameterTypes()[0].getName());
133170
inputObject = Optional.of(mapper.readerFor(handlerInputType).readValue(objectString));
171+
} else {
172+
Optional<Type> handlerInterface = Arrays.stream(handler.getClass().getGenericInterfaces())
173+
.filter(genericInterface ->
174+
((ParameterizedType) genericInterface).getRawType().equals(RequestHandler.class))
175+
.findFirst();
176+
if (handlerInterface.isPresent()) {
177+
Class<?> handlerInputType = Class.forName(((ParameterizedType) handlerInterface.get())
178+
.getActualTypeArguments()[0].getTypeName());
179+
inputObject = Optional.of(mapper.readerFor(handlerInputType).readValue(objectString));
180+
}
134181
}
135182
} catch (Exception genericException) {
136183
// do nothing
137184
}
138185
return inputObject;
139186
}
140187

188+
/**
189+
* Parses the handler name
190+
* Depending on the string, the result handlerMethod can be null
191+
* @param handlerName Handler name in the format "java.package.class::handlerMethodName" or "java.package.class"
192+
* @return Result containing the class name, and the handler method if specified
193+
*/
194+
private static HandlerNameParseResult parseHandlerName(String handlerName) {
195+
String[] split = handlerName.split("::", 2);
196+
String className = split[0];
197+
String handlerMethod = split.length > 1 ? split[1] : null;
198+
return new HandlerNameParseResult(className, handlerMethod);
199+
}
200+
201+
202+
/**
203+
* Returns a instance of the class specified by handler name
204+
* @param handlerName name (including package information) of the class to load and instantiate
205+
* @return New object of handlerName class
206+
*/
141207
private static Object getHandler(String handlerName) throws NoSuchMethodException, IllegalAccessException,
142-
InvocationTargetException, InstantiationException, ClassNotFoundException {
208+
InvocationTargetException, InstantiationException, ClassNotFoundException {
143209
Class<?> clazz = Class.forName(handlerName);
144210
return clazz.getConstructor().newInstance();
145211
}
146212

147-
public static <T> T get(Map<String,T> map, String key) {
213+
public static <T> T get(Map<String, T> map, String key) {
148214
T result = map.get(key);
149215
if (result != null) {
150216
return result;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package cloud.localstack.lambda_handler;
2+
3+
public class HandlerNameParseResult {
4+
private final String className;
5+
private final String handlerMethod;
6+
7+
public HandlerNameParseResult(String className, String handlerMethod) {
8< ED5F code class="diff-text syntax-highlighted-line addition">+
this.className = className;
9+
this.handlerMethod = handlerMethod;
10+
}
11+
12+
public String getClassName() {
13+
return className;
14+
}
15+
16+
public String getHandlerMethod() {
17+
return handlerMethod;
18+
}
19+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package cloud.localstack.lambda_handler;
2+
3+
public class MultipleMatchingHandlersException extends Exception {
4+
public MultipleMatchingHandlersException(String message) {
5+
super(message);
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package cloud.localstack.lambda_handler;
2+
3+
public class NoMatchingHandlerException extends Exception {
4+
public NoMatchingHandlerException(String message) {
5+
super(message);
6+
}
7+
}

0 commit comments

Comments
 (0)
0