diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js index cf6e362b268..ecc355cf91d 100644 --- a/lib/plugins/aws/invokeLocal/index.js +++ b/lib/plugins/aws/invokeLocal/index.js @@ -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, @@ -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, @@ -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); @@ -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'), ]); @@ -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; @@ -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', [ @@ -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)); })); } diff --git a/lib/plugins/aws/invokeLocal/index.test.js b/lib/plugins/aws/invokeLocal/index.test.js index d2542acaeb6..c2e83743507 100644 --- a/lib/plugins/aws/invokeLocal/index.test.js +++ b/lib/plugins/aws/invokeLocal/index.test.js @@ -379,6 +379,7 @@ describe('AwsInvokeLocal', () => { expect(invokeLocalJavaStub.calledWithExactly( 'java', 'handler.hello', + 'handleRequest', undefined, {}, undefined @@ -588,6 +589,7 @@ describe('AwsInvokeLocal', () => { awsInvokeLocalMocked.callJavaBridge( __dirname, 'com.serverless.Handler', + 'handleRequest', '{}' ).then(() => { expect(writeChildStub.calledOnce).to.be.equal(true); @@ -625,6 +627,7 @@ describe('AwsInvokeLocal', () => { awsInvokeLocal.invokeLocalJava( 'java', 'com.serverless.Handler', + 'handleRequest', __dirname, {} ).then(() => { @@ -632,6 +635,7 @@ describe('AwsInvokeLocal', () => { expect(callJavaBridgeStub.calledWithExactly( __dirname, 'com.serverless.Handler', + 'handleRequest', JSON.stringify({ event: {}, context: { @@ -694,6 +698,7 @@ describe('AwsInvokeLocal', () => { awsInvokeLocalMocked.invokeLocalJava( 'java', 'com.serverless.Handler', + 'handleRequest', __dirname, {} ).then(() => { @@ -701,6 +706,7 @@ describe('AwsInvokeLocal', () => { expect(callJavaBridgeMockedStub.calledWithExactly( __dirname, 'com.serverless.Handler', + 'handleRequest', JSON.stringify({ event: {}, context: { diff --git a/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java index ab962942669..879c52f2fc5 100644 --- a/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java +++ b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java @@ -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; @@ -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 parsedInput = parseInput(getInput()); @@ -56,9 +60,52 @@ private Object getInstance() throws Exception { } private Object invoke(HashMap 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 parseInput(String input) throws IOException {