Repository: localstack/localstack

Issue:
ClassNotFoundException triggering Java 8 Lambda from SQS
<!-- Love localstack? Please consider supporting our collective:
👉  https://opencollective.com/localstack/donate -->

# Type of request: This is a ...

[x] bug report
[ ] feature request

# Detailed description
I'm testing out a lambda function locally. This is a rewrite of something that I had before, which I  had written in NodeJS previously. Now, I have a Scala lambda function, running in Java8. I have a CloudFormation template that I'm using to stand up everything that I need:

```json
{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Parameters": {
        "TwitchClientId": {
            "Type": "String"
        },
        "TwitchClientSecret": {
            "Type": "String"
        },
        "SpotifyClientId": {
            "Type": "String"
        },
        "SpotifyClientSecret": {
            "Type": "String"
        }
    },
    "Resources": {
        "SongRequestQueue": {
            "Type": "AWS::SQS::Queue",
            "Properties": {
                "MaximumMessageSize": 1024,
                "MessageRetentionPeriod": 3600,
                "QueueName": "song-requests",
                "VisibilityTimeout": 60,
                "DependsOn": "ConnectionsTable"
            }
        },
        "ConnectionsTable": {
            "Type": "AWS::DynamoDB::Table",
            "Properties": {
                "TableName": "connections",
                "BillingMode": "PAY_PER_REQUEST",
                "AttributeDefinitions": [
                    {
                        "AttributeName": "channelId",
                        "AttributeType": "S"
                    }
                ],
                "KeySchema": [
                    {
                        "AttributeName": "channelId",
                        "KeyType": "HASH"
                    }
                ],
                "TimeToLiveSpecification": {
                    "AttributeName": "expires",
                    "Enabled": true
                }
            }
        },
        "SongRequestFunction": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Description": "Lambda function that accepts messages from SQS, in order to queue songs to Spotify",
                "Handler": "com.github.saxypandabear.songrequests.spotify.Handler::handle",
                "Role": {
                    "Fn::GetAtt": [
                        "LambdaExecutionRole",
                        "Arn"
                    ]
                },
                "Code": {
                    "S3Bucket": "twitch-song-requests",
                    "S3Key": "queue-song-lambda.zip"
                },
                "Runtime": "java8",
                "MemorySize": 512,
                "Timeout": 30,
                "Environment": {
                    "Variables": {
                        "SpotifyClientId": {
                            "Ref": "SpotifyClientId"
                        },
                        "SpotifyClientSecret": {
                            "Ref": "SpotifyClientSecret"
                        },
                        "SpotifyApiUrl": "https://api.spotify.com/v1/me/player",
                        "SpotifyOauthUrl": "something",
                        "env": "localstack"
                    }
                },
                "DependsOn": "LambdaExecutionRole"
            }
        },
        "SongRequestListener": {
            "Type": "AWS::Lambda::EventSourceMapping",
            "Properties": {
                "BatchSize": 1,
                "Enabled": true,
                "EventSourceArn": {
                    "Fn::GetAtt": [
                        "SongRequestQueue",
                        "Arn"
                    ]
                },
                "FunctionName": {
                    "Fn::GetAtt": [
                        "SongRequestFunction",
                        "Arn"
                    ]
                },
                "DependsOn": "SongRequestFunction"
            }
        },
        "LambdaExecutionRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "lambda.amazonaws.com"
                                ]
                            },
                            "Action": [
                                "sts:AssumeRole"
                            ]
                        }
                    ]
                },
                "Path": "/",
                "Policies": [
                    {
                        "PolicyName": "accessLogs",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "logs:*"
                                    ],
                                    "Resource": "arn:aws:logs:*:*:*"
                                }
                            ]
                        }
                    },
                    {
                        "PolicyName": "accessQueue",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "sqs:ReceiveMessage",
                                        "sqs:DeleteMessage",
                                        "sqs:GetQueueAttributes"
                                    ],
                                    "Resource": {
                                        "Fn::GetAtt": [
                                            "SongRequestQueue",
                                            "Arn"
                                        ]
                                    }
                                }
                            ]
                        }
                    },
                    {
                        "PolicyName": "accessDynamo",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "dynamodb:GetItem"
                                    ],
                                    "Resource": {
                                        "Fn::GetAtt": [
                                            "ConnectionsTable",
                                            "Arn"
                                        ]
                                    }
                                }
                            ]
                        }
                    }
                ],
                "DependsOn": "SongRequestQueue"
            }
        }
    }
}
```

There's no errors creating the CF stack, but when I send a message to the SQS that the Lambda is listening on:
```bash
aws sqs send-message \
    --endpoint-url http://localhost:4566 \
    --message-body spotify:track:5Cjkfft7iRWJp4elZXgjkc \
    --message-attributes '{"channelId": {"DataType": "String", "StringValue": "REDACTED"}}' \
    --queue-url http://localhost:4566/000000000000/song-requests
```

I get this exception stacktrace from Localstack:
```
2021-03-28T02:24:47:WARNING:bootstrap.py: Thread run method <function LambdaExecutor.execute.<locals>.do_execute at 0x7efd3ba43700>(None) failed: Lambda process returned error status code: 1. Result: . Output:
Exception in thread "main" java.lang.ClassNotFoundException: com.github.saxypandabear.songrequests.spotify.Handler::handle
        at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:264)
        at cloud.localstack.LambdaExecutor.getHandler(LambdaExecutor.java:150)
        at cloud.localstack.LambdaExecutor.main(LambdaExecutor.java:64) Traceback (most recent call last):
  File "/opt/code/localstack/localstack/utils/bootstrap.py", line 521, in run
    result = self.func(self.params)
  File "/opt/code/localstack/localstack/services/awslambda/lambda_executors.py", line 159, in do_execute
    return _run(func_arn=func_arn)
  File "/opt/code/localstack/localstack/utils/cloudwatch/cloudwatch_util.py", line 146, in wrapped
    raise e
  File "/opt/code/localstack/localstack/utils/cloudwatch/cloudwatch_util.py", line 142, in wrapped
    result = func(*args, **kwargs)
  File "/opt/code/localstack/localstack/services/awslambda/lambda_executors.py", line 152, in _run
    raise e
  File "/opt/code/localstack/localstack/services/awslambda/lambda_executors.py", line 140, in _run
    result = self._execute(func_arn, func_details, event, context, version)
  File "/opt/code/localstack/localstack/services/awslambda/lambda_executors.py", line 297, in _execute
    result = self.run_lambda_executor(cmd, stdin, env_vars=environment, func_details=func_details)
  File "/opt/code/localstack/localstack/services/awslambda/lambda_executors.py", line 204, in run_lambda_executor
    raise Exception('Lambda process returned error status code: %s. Result: %s. Output:\n%s' %
Exception: Lambda process returned error status code: 1. Result: . Output:
Exception in thread "main" java.lang.ClassNotFoundException: com.github.saxypandabear.songrequests.spotify.Handler::handle
        at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:264)
        at cloud.localstack.LambdaExecutor.getHandler(LambdaExecutor.java:150)
        at cloud.localstack.LambdaExecutor.main(LambdaExecutor.java:64)
```

I saw some older issues, and this looks like the same issue described in #3138.  I thought that it was because my version of localstack that I was using was outdated, but I ran `pipenv update` to pull in the current latest version (0.12.7), and reran it, getting the same error.

...

## Expected behavior
The lambda should be able to intercept the SQS message and act on it.
...

## Actual behavior
The lambda fails to execute because the underlying localstack executor cannot find the handler class
...

# Steps to reproduce

## Command used to start LocalStack
`pipenv run localstack start`
...

## Client code (AWS SDK code snippet, or sequence of "awslocal" commands)
1. Create CF stack
```bash
# Using the above CF template json
aws cloudformation create-stack \
    --region us-east-1 \
    --endpoint-url http://localhost:4566 \
    --stack-name song-requests \
    --template-body file://services.json \
    --parameters ParameterKey=TwitchClientId,ParameterValue="$TWITCH_CLIENT_ID" \
                 ParameterKey=TwitchClientSecret,ParameterValue="$TWITCH_CLIENT_SECRET" \
                 ParameterKey=SpotifyClientId,ParameterValue="$SPOTIFY_CLIENT_ID" \
                 ParameterKey=SpotifyClientSecret,ParameterValue="$SPOTIFY_CLIENT_SECRET"
```
2. Send message to SQS
```bash
aws sqs send-message \
    --endpoint-url http://localhost:4566 \
    --message-body spotify:track:5Cjkfft7iRWJp4elZXgjkc \
    --message-attributes '{"channelId": {"DataType": "String", "StringValue": "REDACTED"}}' \
    --queue-url http://localhost:4566/000000000000/song-requests
```
...

feature request: Support Class::method format for lambda handler
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Feature description

For the following lambda code:

```java
class Handler {
  public static String handle(Object event, Object context) {
    return "Hello world!";
  }
}
```

And deployment with `--handler Handler::handle`, we get the following error from LocalStack:

```
An error occurred (InternalFailure) when calling the Invoke operation (reached max retries: 4): Error executing Lambda function: Lambda process returned error status code: 1. Output:
Exception in thread "main" java.lang.ClassNotFoundException: Handler::handle
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:264)
	at cloud.localstack.LambdaExecutor.getHandler(LambdaExecutor.java:137)
	at cloud.localstack.LambdaExecutor.main(LambdaExecutor.java:52)
```

This works when done to real AWS.

The reason is that LocalStack treats `Handler::handle` as a FQCN, rather than as a FQCN::method.

The feature request is for LocalStack to support this double colon syntax for specifying the handler. This will enable the newer form as well as make usage from other JVM languages (such as Clojure or Scala) easier.

### 🧑‍💻 Implementation

Should be fairly simple, at worst split by `::` and use `clazz.getMethod()` on the parts. But I'm not a primary Java dev so not sure if there's a more robust way.

### Anything else?

There has been multiple issues opened for this, but they were all either closed without resolution or abandoned once the symptom was handled (by removing the `::` and implementing the interface).

eg. #865 #3793


Discussion:
Have you tried to wait for aprox. 5-10 minutes? I have almost the same problem.. I have an S3 bucket that triggers a Lambda service.. If I Invoke the service right after deployment I get almost the same error.. If I wait for a while then everything works well..
If that's what works, I'd like to understand why. I can try it tonight and see
Waited 25 minutes and got the same issue. Don't think that solves it.
I'm also seeing this issue.
I got it working.

Replace `"Handler": "com.github.saxypandabear.songrequests.spotify.Handler::handle",` with `"Handler": "com.github.saxypandabear.songrequests.spotify.Handler",`, and possibly change the function name to `handleRequest()`.

Though localstack should support specifying a function name with that notation.
I'll try it out when I get a chance. Thanks for looking into it!


Resolving patch:
diff --git a/localstack/constants.py b/localstack/constants.py
--- a/localstack/constants.py
+++ b/localstack/constants.py
@@ -28,7 +28,7 @@
 LOCALHOST_HOSTNAME = "localhost.localstack.cloud"
 
 # version of the Maven dependency with Java utility code
-LOCALSTACK_MAVEN_VERSION = "0.2.14"
+LOCALSTACK_MAVEN_VERSION = "0.2.16"
 
 # map of default service APIs and ports to be spun up (fetch map from localstack_client)
 DEFAULT_SERVICE_PORTS = localstack_client.config.get_service_ports()
diff --git a/localstack/services/awslambda/lambda_executors.py b/localstack/services/awslambda/lambda_executors.py
--- a/localstack/services/awslambda/lambda_executors.py
+++ b/localstack/services/awslambda/lambda_executors.py
@@ -1165,7 +1165,7 @@ def _execute_in_custom_runtime(
         )
 
         # store log output - TODO get live logs from `process` above?
-        store_lambda_logs(lambda_function, log_output)
+        # store_lambda_logs(lambda_function, log_output)
 
         if return_code != 0:
             raise InvocationException(
@@ -1245,6 +1245,8 @@ def do_execute():
         for stream in (c.stdout(), c.stderr()):
             if stream:
                 log_output += ("\n" if log_output else "") + stream
+        if isinstance(result, InvocationResult) and result.log_output:
+            log_output += "\n" + result.log_output
         log_output += "\nEND RequestId: %s" % request_id
         log_output += "\nREPORT RequestId: %s Duration: %s ms" % (
             request_id,


Test patch:
diff --git a/tests/integration/lambdas/java/.gitignore b/tests/integration/lambdas/java/lambda_echo/.gitignore
similarity index 100%
rename from tests/integration/lambdas/java/.gitignore
rename to tests/integration/lambdas/java/lambda_echo/.gitignore
diff --git a/tests/integration/lambdas/java/README.md b/tests/integration/lambdas/java/lambda_echo/README.md
similarity index 93%
rename from tests/integration/lambdas/java/README.md
rename to tests/integration/lambdas/java/lambda_echo/README.md
--- a/tests/integration/lambdas/java/README.md
+++ b/tests/integration/lambdas/java/lambda_echo/README.md
@@ -1,3 +1,3 @@
-`lambda-function-with-lib-0.0.1.jar` was generated using `mvn package` and 
+`lambda-function-with-lib-0.0.1.jar` was generated using `mvn package` and
 committed to the repo. Generation of the .jar _could_ be added to the build
 process, but this seems adequate.
diff --git a/tests/integration/lambdas/java/build.gradle b/tests/integration/lambdas/java/lambda_echo/build.gradle
similarity index 100%
rename from tests/integration/lambdas/java/build.gradle
rename to tests/integration/lambdas/java/lambda_echo/build.gradle
diff --git a/tests/integration/lambdas/java/build/distributions/lambda-function-built-by-gradle.zip b/tests/integration/lambdas/java/lambda_echo/build/distributions/lambda-function-built-by-gradle.zip
similarity index 100%
rename from tests/integration/lambdas/java/build/distributions/lambda-function-built-by-gradle.zip
rename to tests/integration/lambdas/java/lambda_echo/build/distributions/lambda-function-built-by-gradle.zip
diff --git a/tests/integration/lambdas/java/lambda-function-with-lib-0.0.1.jar b/tests/integration/lambdas/java/lambda_echo/lambda-function-with-lib-0.0.1.jar
similarity index 100%
rename from tests/integration/lambdas/java/lambda-function-with-lib-0.0.1.jar
rename to tests/integration/lambdas/java/lambda_echo/lambda-function-with-lib-0.0.1.jar
diff --git a/tests/integration/lambdas/java/pom.xml b/tests/integration/lambdas/java/lambda_echo/pom.xml
similarity index 100%
rename from tests/integration/lambdas/java/pom.xml
rename to tests/integration/lambdas/java/lambda_echo/pom.xml
diff --git a/tests/integration/lambdas/java/src/main/java/cloud/localstack/sample/LambdaHandlerWithLib.java b/tests/integration/lambdas/java/lambda_echo/src/main/java/cloud/localstack/sample/LambdaHandlerWithLib.java
similarity index 100%
rename from tests/integration/lambdas/java/src/main/java/cloud/localstack/sample/LambdaHandlerWithLib.java
rename to tests/integration/lambdas/java/lambda_echo/src/main/java/cloud/localstack/sample/LambdaHandlerWithLib.java
diff --git a/tests/integration/lambdas/java/lambda_multiple_handlers/build.gradle b/tests/integration/lambdas/java/lambda_multiple_handlers/build.gradle
new file mode 100755
--- /dev/null
+++ b/tests/integration/lambdas/java/lambda_multiple_handlers/build.gradle
@@ -0,0 +1,42 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ *
+ * This generated file contains a sample Java project to get you started.
+ * For more details take a look at the Java Quickstart chapter in the Gradle
+ * user guide available at https://docs.gradle.org/5.0/userguide/tutorial_java_projects.html
+ */
+
+plugins {
+    // Apply the java plugin to add support for Java
+    id 'java'
+
+    // Apply the application plugin to add support for building an application
+    id 'application'
+}
+
+repositories {
+    // Use jcenter for resolving your dependencies.
+    // You can declare any Maven/Ivy/file repository here.
+    mavenCentral()
+}
+
+dependencies {
+    // This dependency is found on compile classpath of this component and consumers.
+    implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
+    implementation 'com.google.code.gson:gson:2.8.6'
+}
+
+// Define the main class for the application
+mainClassName = 'cloud.localstack.sample.LambdaHandlerWithInterfaceAndCustom'
+
+// create a single Jar with all dependencies
+task buildZip(type: Zip) {
+    from compileJava
+    from processResources
+    into('lib') {
+        from configurations.runtimeClasspath
+    }
+    archiveName 'lambda-function-with-multiple-handlers.zip'
+}
+
+build.dependsOn buildZip
diff --git a/tests/integration/lambdas/java/lambda_multiple_handlers/build/distributions/lambda-function-with-multiple-handlers.zip b/tests/integration/lambdas/java/lambda_multiple_handlers/build/distributions/lambda-function-with-multiple-handlers.zip
new file mode 100644
Binary files /dev/null and b/tests/integration/lambdas/java/lambda_multiple_handlers/build/distributions/lambda-function-with-multiple-handlers.zip differ
diff --git a/tests/integration/lambdas/java/lambda_multiple_handlers/src/main/java/cloud/localstack/sample/LambdaHandlerWithInterfaceAndCustom.java b/tests/integration/lambdas/java/lambda_multiple_handlers/src/main/java/cloud/localstack/sample/LambdaHandlerWithInterfaceAndCustom.java
new file mode 100644
--- /dev/null
+++ b/tests/integration/lambdas/java/lambda_multiple_handlers/src/main/java/cloud/localstack/sample/LambdaHandlerWithInterfaceAndCustom.java
@@ -0,0 +1,34 @@
+package cloud.localstack.sample;
+
+import java.util.Map;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.LambdaLogger;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+public class LambdaHandlerWithInterfaceAndCustom implements RequestHandler<Map<String,String>, String> {
+  Gson gson = new GsonBuilder().setPrettyPrinting().create();
+
+  public String handleRequestCustom(Map<String,String> event, Context context)
+  {
+    LambdaLogger logger = context.getLogger();
+    logger.log("CUSTOM HANDLER");
+    logger.log("ENV: " + gson.toJson(System.getenv()));
+    logger.log("EVENT: " + gson.toJson(event));
+    logger.log("EVENT CLASS: " + event.getClass());
+    logger.log("CONTEXT: " + gson.toJson(context));
+    return "CUSTOM";
+  }
+  public String handleRequest(Map<String,String> event, Context context) {
+    LambdaLogger logger = context.getLogger();
+    logger.log("INTERFACE HANDLER");
+    logger.log("ENV: " + gson.toJson(System.getenv()));
+    logger.log("EVENT: " + gson.toJson(event));
+    logger.log("EVENT CLASS: " + event.getClass());
+    logger.log("CONTEXT: " + gson.toJson(context));
+    return "INTERFACE";
+  }
+}
diff --git a/tests/integration/test_lambda.py b/tests/integration/test_lambda.py
--- a/tests/integration/test_lambda.py
+++ b/tests/integration/test_lambda.py
@@ -82,7 +82,16 @@
     LOCALSTACK_ROOT_FOLDER, "localstack", "infra", "localstack-utils-tests.jar"
 )
 TEST_LAMBDA_JAVA_WITH_LIB = os.path.join(
-    THIS_FOLDER, "lambdas", "java", "lambda-function-with-lib-0.0.1.jar"
+    THIS_FOLDER, "lambdas", "java", "lambda_echo", "lambda-function-with-lib-0.0.1.jar"
+)
+TEST_LAMBDA_JAVA_MULTIPLE_HANDLERS = os.path.join(
+    THIS_FOLDER,
+    "lambdas",
+    "java",
+    "lambda_multiple_handlers",
+    "build",
+    "distributions",
+    "lambda-function-with-multiple-handlers.zip",
 )
 TEST_LAMBDA_ENV = os.path.join(THIS_FOLDER, "lambdas", "lambda_environment.py")
 TEST_LAMBDA_PYTHON3_MULTIPLE_CREATE1 = os.path.join(
@@ -1732,7 +1741,7 @@ def setUpClass(cls):
             func_name=TEST_LAMBDA_NAME_JAVA_STREAM,
             zip_file=cls.test_java_jar,
             runtime=LAMBDA_RUNTIME_JAVA8,
-            handler="cloud.localstack.sample.LambdaStreamHandler",
+            handler="cloud.localstack.awssdkv1.sample.LambdaStreamHandler",
         )
 
         # deploy lambda - Java with serializable input object
@@ -1740,7 +1749,7 @@ def setUpClass(cls):
             func_name=TEST_LAMBDA_NAME_JAVA_SERIALIZABLE,
             zip_file=cls.test_java_zip,
             runtime=LAMBDA_RUNTIME_JAVA8,
-            handler="cloud.localstack.sample.SerializedInputLambdaHandler",
+            handler="cloud.localstack.awssdkv1.sample.SerializedInputLambdaHandler",
         )
 
         # deploy lambda - Java with Kinesis input object
@@ -1748,7 +1757,7 @@ def setUpClass(cls):
             func_name=TEST_LAMBDA_NAME_JAVA_KINESIS,
             zip_file=cls.test_java_zip,
             runtime=LAMBDA_RUNTIME_JAVA8,
-            handler="cloud.localstack.sample.KinesisLambdaHandler",
+            handler="cloud.localstack.awssdkv1.sample.KinesisLambdaHandler",
         )
 
     @classmethod
@@ -1804,6 +1813,7 @@ def test_java_runtime_with_lib(self):
                 THIS_FOLDER,
                 "lambdas",
                 "java",
+                "lambda_echo",
                 "build",
                 "distributions",
                 "lambda-function-built-by-gradle.zip",
@@ -1948,6 +1958,44 @@ def test_trigger_java_lambda_through_sns(self):
         s3_client.delete_bucket(Bucket=bucket_name)
 
 
+def test_java_custom_handler_method_specification(lambda_client, create_lambda_function):
+    java_handler_multiple_handlers = load_file(TEST_LAMBDA_JAVA_MULTIPLE_HANDLERS, mode="rb")
+    expected = ['.*"echo": "echo".*']
+
+    function_name_custom_handler = "lambda_custom_handler_%s" % short_uid()
+    create_lambda_function(
+        func_name=function_name_custom_handler,
+        zip_file=java_handler_multiple_handlers,
+        runtime=LAMBDA_RUNTIME_JAVA11,
+        handler="cloud.localstack.sample.LambdaHandlerWithInterfaceAndCustom::handleRequestCustom",
+    )
+
+    result = lambda_client.invoke(
+        FunctionName=function_name_custom_handler, Payload=b'{"echo":"echo"}'
+    )
+    result_data = result["Payload"].read()
+
+    assert 200 == result["StatusCode"]
+    assert "CUSTOM" == to_str(result_data).strip('"\n ')
+    _check_lambda_logs(function_name_custom_handler, expected_lines=expected)
+
+    function_name_interface = "lambda_interface_%s" % short_uid()
+    create_lambda_function(
+        func_name=function_name_interface,
+        zip_file=java_handler_multiple_handlers,
+        runtime=LAMBDA_RUNTIME_JAVA11,
+        handler="cloud.localstack.sample.LambdaHandlerWithInterfaceAndCustom",
+    )
+
+    result = lambda_client.invoke(FunctionName=function_name_interface, Payload=b'{"echo":"echo"}')
+    result_data = result["Payload"].read()
+
+    assert 200 == result["StatusCode"]
+    assert "INTERFACE" == to_str(result_data).strip('"\n ')
+
+    _check_lambda_logs(function_name_interface, expected_lines=expected)
+
+
 class TestDockerBehaviour(LambdaTestBase):
     @classmethod
     def setUpClass(cls):