Repository: localstack/localstack

Issue:
Java "ClassNotFoundException" due to unzipped code passed to lambci/lambda
`awslocal lambda create-function --function-name helloworld --zip-file fileb://target/hello-world-0.1.jar --runtime java11 --handler hello.world.HelloWorldFunction::get --role whatever`

`awslocal lambda invoke --function-name helloworld out.txt && cat out.txt && rm out.txt`

This fails due to `Class not found: hello.world.HelloWorldFunction: java.lang.ClassNotFoundException`

The same function works fine when deploying to AWS with the same commands.

I dug in a bit, and I see that `/tmp/localstack/zipfile.e89d9c5d` has one file, `original_lambda_archive.zip`. When I unzip this and re-invoke the Lambda, it's successful.

According to https://github.com/lambci/docker-lambda, it's expecting unzipped.


Discussion:
If I'm in `/tmp/localstack/zipfile.e89d9c5d` and I run:

`docker run --rm -v "$PWD":/var/task lambci/lambda:java11 hello.world.HelloWorldFunction::get`

Then I get the same error.

If I unzip the zip file, then rerun, it's successful.
Issue may be here: https://github.com/localstack/localstack/blob/03c6f4bf250edd18eb818ed65090f508636b0bff/localstack/services/awslambda/lambda_api.py#L593

Which is at odds with https://github.com/lambci/docker-lambda

I'm going to try adding a `/lib` folder as a workaround for now, but I think all Java JARs should be unzipped.
I may be able to open a PR for this soon, but in the meantime, anyone who encounters this: there's a temporary workaround of adding `/src/main/resources/lib/empty.jar` to your Java project.
Thanks for reporting, and thanks for opening the pull request @sjmatta . Any chance you can share the your `hello-world-0.1.jar` JAR file? I just did a quick test, and it seems that uploading a JAR file directly in fact works:
```
$ aws lambda create-function --function-name test-java --runtime java8 --role r1  --handler cloud.localstack.sample.LambdaHandler --zip-file fileb://localstack/ext/java/target/localstack-utils-0.2.0-tests.jar
$ awslocal lambda invoke --function-name test-java /tmp/foobar
{
    "StatusCode": 200
}
```

We also have an integration test for direct upload of JAR files here: https://github.com/localstack/localstack/blob/77d4061526741d9063d388de0a183347b5d9c428/tests/integration/test_lambda.py#L520-L529

My suspicion would be that the zip file is actually the JAR file with a different file ending, in your case something like the following (please note that LocalStack should be able to add this ZIP file to the Java classpath when running the Lambda):
```
$ tar -tf /tmp/localstack/zipfile.e89d9c5d/original_lambda_archive.zip
```

The main reason we're trying to run Lambdas from JAR files directly without extracting is for performance reasons. We've had cases of 50+ MB JAR files with with thousands of classes, which can take in the order of up to half a minute to exract on some systems. Hence, my goal here would be to understand the root cause for this particular use case before we proceed with the PR in #1982. Thanks

Resolving patch:
diff --git a/localstack/services/awslambda/lambda_api.py b/localstack/services/awslambda/lambda_api.py
--- a/localstack/services/awslambda/lambda_api.py
+++ b/localstack/services/awslambda/lambda_api.py
@@ -51,8 +51,8 @@
 # List of Lambda runtime names. Keep them in this list, mainly to silence the linter
 LAMBDA_RUNTIMES = [LAMBDA_RUNTIME_PYTHON27, LAMBDA_RUNTIME_PYTHON36,
     LAMBDA_RUNTIME_DOTNETCORE2, LAMBDA_RUNTIME_DOTNETCORE21, LAMBDA_RUNTIME_NODEJS,
-    LAMBDA_RUNTIME_NODEJS610, LAMBDA_RUNTIME_NODEJS810, LAMBDA_RUNTIME_JAVA8, LAMBDA_RUNTIME_RUBY,
-    LAMBDA_RUNTIME_RUBY25]
+    LAMBDA_RUNTIME_NODEJS610, LAMBDA_RUNTIME_NODEJS810, LAMBDA_RUNTIME_JAVA8,
+    LAMBDA_RUNTIME_JAVA11, LAMBDA_RUNTIME_RUBY, LAMBDA_RUNTIME_RUBY25]
 
 # default timeout in seconds
 LAMBDA_DEFAULT_TIMEOUT = 3
@@ -535,11 +535,6 @@ def set_archive_code(code, lambda_name, zip_file_content=None):
     return tmp_dir
 
 
-def is_java_lambda(lambda_details):
-    runtime = getattr(lambda_details, 'runtime', lambda_details)
-    return runtime in [LAMBDA_RUNTIME_JAVA8, LAMBDA_RUNTIME_JAVA11]
-
-
 def set_function_code(code, lambda_name, lambda_cwd=None):
 
     def generic_handler(event, context):
@@ -572,8 +567,9 @@ def generic_handler(event, context):
 
     # Set the appropriate lambda handler.
     lambda_handler = generic_handler
+    is_java = lambda_executors.is_java_lambda(runtime)
 
-    if is_java_lambda(runtime):
+    if is_java:
         # The Lambda executors for Docker subclass LambdaExecutorContainers, which
         # runs Lambda in Docker by passing all *.jar files in the function working
         # directory as part of the classpath. Obtain a Java handler function below.
@@ -585,11 +581,11 @@ def generic_handler(event, context):
             raise ClientError(
                 'Uploaded Lambda code for runtime ({}) is not in Zip format'.format(runtime))
         # Unzipping should only be required for (1) non-Java Lambdas, or (2) zip files containing JAR files
-        if not is_java_lambda(runtime) or zip_contains_jar_entries(zip_file_content, 'lib/'):
+        if not is_java or zip_contains_jar_entries(zip_file_content, 'lib/'):
             unzip(tmp_file, lambda_cwd)
 
     # Obtain handler details for any non-Java Lambda function
-    if not is_java_lambda(runtime):
+    if not is_java:
 
         handler_file = get_handler_file_from_name(handler_name, runtime=runtime)
         handler_function = get_handler_function_from_name(handler_name, runtime=runtime)
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
@@ -68,6 +68,11 @@ def get_from_event(event, key):
         return None
 
 
+def is_java_lambda(lambda_details):
+    runtime = getattr(lambda_details, 'runtime', lambda_details)
+    return runtime in [LAMBDA_RUNTIME_JAVA8, LAMBDA_RUNTIME_JAVA11]
+
+
 class LambdaExecutor(object):
     """ Base class for Lambda executors. Subclasses must overwrite the _execute method """
 
@@ -208,7 +213,7 @@ def _execute(self, func_arn, func_details, event, context=None, version=None):
         command = ''
 
         # if running a Java Lambda, set up classpath arguments
-        if runtime == LAMBDA_RUNTIME_JAVA8:
+        if is_java_lambda(runtime):
             java_opts = Util.get_java_opts()
             stdin = None
             # copy executor jar into temp directory


Test patch: