Skip to content

Commit

Permalink
Merge pull request #4596 from Instabridge/invokelocal-pojo
Browse files Browse the repository at this point in the history
InvokeLocal/Java: support POJO input and different handler names
  • Loading branch information
horike37 committed Jan 13, 2018
2 parents 5d8acc3 + db08b03 commit 4b71faf
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 9 deletions.
21 changes: 14 additions & 7 deletions lib/plugins/aws/invokeLocal/index.js
Expand Up @@ -118,10 +118,10 @@ class AwsInvokeLocal {
|| this.serverless.service.provider.runtime
|| 'nodejs4.3';
const handler = this.options.functionObj.handler;
const handlerPath = handler.split('.')[0];
const handlerName = handler.split('.')[1];

if (runtime.startsWith('nodejs')) {
const handlerPath = handler.split('.')[0];
const handlerName = handler.split('.')[1];
return this.invokeLocalNodeJs(
handlerPath,
handlerName,
Expand All @@ -130,6 +130,8 @@ class AwsInvokeLocal {
}

if (runtime === 'python2.7' || runtime === 'python3.6') {
const handlerPath = handler.split('.')[0];
const handlerName = handler.split('.')[1];
return this.invokeLocalPython(
process.platform === 'win32' ? 'python.exe' : runtime,
handlerPath,
Expand All @@ -139,9 +141,12 @@ class AwsInvokeLocal {
}

if (runtime === 'java8') {
const className = handler.split('::')[0];
const handlerName = handler.split('::')[1] || 'handleRequest';
return this.invokeLocalJava(
'java',
handler,
className,
handlerName,
this.serverless.service.package.artifact,
this.options.data,
this.options.context);
Expand Down Expand Up @@ -177,11 +182,12 @@ class AwsInvokeLocal {
});
}

callJavaBridge(artifactPath, className, input) {
callJavaBridge(artifactPath, className, handlerName, input) {
return new BbPromise((resolve) => fs.statAsync(artifactPath).then(() => {
const java = spawn('java', [
`-DartifactPath=${artifactPath}`,
`-DclassName=${className}`,
`-DhandlerName=${handlerName}`,
'-jar',
path.join(__dirname, 'java', 'target', 'invoke-bridge-1.0.jar'),
]);
Expand All @@ -201,7 +207,7 @@ class AwsInvokeLocal {
}));
}

invokeLocalJava(runtime, className, artifactPath, event, customContext) {
invokeLocalJava(runtime, className, handlerName, artifactPath, event, customContext) {
const timeout = Number(this.options.functionObj.timeout)
|| Number(this.serverless.service.provider.timeout)
|| 6;
Expand All @@ -220,7 +226,7 @@ class AwsInvokeLocal {
const executablePath = path.join(javaBridgePath, 'target');

return new BbPromise(resolve => fs.statAsync(executablePath)
.then(() => this.callJavaBridge(artifactPath, className, input))
.then(() => this.callJavaBridge(artifactPath, className, handlerName, input))
.then(resolve)
.catch(() => {
const mvn = spawn('mvn', [
Expand All @@ -235,7 +241,8 @@ class AwsInvokeLocal {
mvn.stderr.on('data', (buf) => this.serverless.cli.consoleLog(`mvn - ${buf.toString()}`));
mvn.stdin.end();

mvn.on('close', () => this.callJavaBridge(artifactPath, className, input).then(resolve));
mvn.on('close', () => this.callJavaBridge(artifactPath, className, handlerName, input)
.then(resolve));
}));
}

Expand Down
6 changes: 6 additions & 0 deletions lib/plugins/aws/invokeLocal/index.test.js
Expand Up @@ -379,6 +379,7 @@ describe('AwsInvokeLocal', () => {
expect(invokeLocalJavaStub.calledWithExactly(
'java',
'handler.hello',
'handleRequest',
undefined,
{},
undefined
Expand Down Expand Up @@ -588,6 +589,7 @@ describe('AwsInvokeLocal', () => {
awsInvokeLocalMocked.callJavaBridge(
__dirname,
'com.serverless.Handler',
'handleRequest',
'{}'
).then(() => {
expect(writeChildStub.calledOnce).to.be.equal(true);
Expand Down Expand Up @@ -625,13 +627,15 @@ describe('AwsInvokeLocal', () => {
awsInvokeLocal.invokeLocalJava(
'java',
'com.serverless.Handler',
'handleRequest',
__dirname,
{}
).then(() => {
expect(callJavaBridgeStub.calledOnce).to.be.equal(true);
expect(callJavaBridgeStub.calledWithExactly(
__dirname,
'com.serverless.Handler',
'handleRequest',
JSON.stringify({
event: {},
context: {
Expand Down Expand Up @@ -694,13 +698,15 @@ describe('AwsInvokeLocal', () => {
awsInvokeLocalMocked.invokeLocalJava(
'java',
'com.serverless.Handler',
'handleRequest',
__dirname,
{}
).then(() => {
expect(callJavaBridgeMockedStub.calledOnce).to.be.equal(true);
expect(callJavaBridgeMockedStub.calledWithExactly(
__dirname,
'com.serverless.Handler',
'handleRequest',
JSON.stringify({
event: {},
context: {
Expand Down
Expand Up @@ -4,6 +4,8 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
Expand All @@ -16,12 +18,14 @@
public class InvokeBridge {
private File artifact;
private String className;
private String handlerName;
private Object instance;
private Class clazz;

private InvokeBridge() {
this.artifact = new File(new File("."), System.getProperty("artifactPath"));
this.className = System.getProperty("className");
this.handlerName = System.getProperty("handlerName");

try {
HashMap<String, Object> parsedInput = parseInput(getInput());
Expand Down Expand Up @@ -56,9 +60,52 @@ private Object getInstance() throws Exception {
}

private Object invoke(HashMap<String, Object> event, Context context) throws Exception {
Method[] methods = this.clazz.getDeclaredMethods();
Method method = findHandlerMethod(this.clazz, this.handlerName);
Class requestClass = method.getParameterTypes()[0];

Object request = event;
if (!requestClass.isAssignableFrom(event.getClass())) {
request = requestClass.newInstance();
PropertyDescriptor[] properties = Introspector.getBeanInfo(requestClass).getPropertyDescriptors();
for(int i=0; i < properties.length; i++) {
if (properties[i].getWriteMethod() == null) continue;
String propertyName = properties[i].getName();
if (event.containsKey(propertyName)) {
properties[i].getWriteMethod().invoke(request, event.get(propertyName));
}
}
}

if (method.getParameterCount() == 1) {
return method.invoke(this.instance, request);
} else if (method.getParameterCount() == 2) {
return method.invoke(this.instance, request, context);
} else {
throw new NoSuchMethodException("Handler should take 1 or 2 arguments: " + method);
}
}

private Method findHandlerMethod(Class clazz, String handlerName) throws Exception {
Method candidateMethod = null;
for(Method method: clazz.getDeclaredMethods()) {
if (method.getName().equals(handlerName) && !method.isBridge()) {
// Select the method with the largest number of parameters
// If two or more methods have the same number of parameters, AWS Lambda selects the method that has
// the Context as the last parameter.
// If none or all of these methods have the Context parameter, then the behavior is undefined.
int paramCount = method.getParameterCount();
boolean lastParamIsContext = paramCount >= 1 && method.getParameterTypes()[paramCount-1].getName().equals("com.amazonaws.services.lambda.runtime.Context");
if (candidateMethod == null || paramCount > candidateMethod.getParameterCount() || (paramCount == candidateMethod.getParameterCount() && lastParamIsContext)) {
candidateMethod = method;
}
}
}

if (candidateMethod == null) {
throw new NoSuchMethodException("Could not find handler for " + handlerName + " in " + clazz.getName());
}

return methods[1].invoke(this.instance, event, context);
return candidateMethod;
}

private HashMap<String, Object> parseInput(String input) throws IOException {
Expand Down

0 comments on commit 4b71faf

Please sign in to comment.