Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AWS Lambda - Java Beans

I have a request that looks like the following:

package pricing

import scala.beans.BeanProperty

class Request(@BeanProperty var name: String, @BeanProperty var surname: String) {
  def this() = this(name="defName", surname="defSurname")
}

The handler is as follows:

package pricing

import com.amazonaws.services.lambda.runtime.{Context, RequestHandler}
import scala.collection.JavaConverters
import spray.json._


class ApiGatewayHandler extends RequestHandler[Request, ApiGatewayResponse] {

  import DefaultJsonProtocol._

  def handleRequest(input: Request, context: Context): ApiGatewayResponse = {
    val headers = Map("x-foo" -> "coucou")
    val msg = "Hello " + input.name
    val message = Map[String, String]("message" -> msg )
    ApiGatewayResponse(
      200,
      message.toJson.toString(),
      JavaConverters.mapAsJavaMap[String, Object](headers),
      true
    )
  }
}

which has been documented as:

functions:
  pricing:
    handler: pricing.ApiGatewayHandler
    events:
      - http:
          path: pricing/test
          method: get
          documentation:
            summary: "submit your name and surname, the API says hi"
            description: ".. well, the summary is pretty exhaustive"
            requestBody:
              description: "Send over name and surname"
            queryParams:
              - name: "name"
                description: "your 1st name"
              - name: "surname"
                description: ".. guess .. "
            methodResponses:
              - statusCode: "200"
                responseHeaders:
                  - name: "x-foo"
                    description: "you can foo in here"
                responseBody:
                  description: "You'll see a funny message here"
                responseModels:
                  "application/json": "HelloWorldResponse"

well, this is a copy and paste from one of the tutorials. And it is not working. I guess that the BeanProperty refers to body object properties; and this is what I can guess from the example here.

if I would like to have query strings?

A try was:

package pricing

import scala.beans.BeanProperty
import spray.json._

abstract class ApiGatewayGetRequest(
                                     @BeanProperty httpMethod: String,
                                     @BeanProperty headers: Map[String, String],
                                     @BeanProperty queryStringParameters: Map[String, String])


abstract class ApiGatewayPostRequest(
                                     @BeanProperty httpMethod: String,
                                     @BeanProperty headers: Map[String, String],
                                     @BeanProperty queryStringParameters: Map[String, String])

class HelloWorldRequest(
                         @BeanProperty httpMethod: String,
                         @BeanProperty headers: Map[String, String],
                         @BeanProperty queryStringParameters: Map[String, String]
                       ) extends ApiGatewayGetRequest(httpMethod, headers, queryStringParameters) {

  private def getParam(param: String): String =
    queryStringParameters get param match {
      case Some(s) => s
      case None => "default_" + param
    }

  def name: String = getParam("name")
  def surname: String = getParam("surname")

  def this() = this("GET", Map.empty, Map.empty)

}

Which results in:

 {
  "message":"Hello default_name"
 }

suggesting that the class has been initialized with an empty map in place of the queryStringParameters which was however submitted correctly

 Mon Sep 25 20:45:22 UTC 2017 : Endpoint request body after
 transformations:
 {"resource":"/pricing/test","path":"/pricing/test","httpMethod":"GET","headers":null,"queryStringParameters":{"name":"ciao", "surname":"bonjour"},"pathParameters":null,"stageVariables":null,
 ...

Note: I am following this path because I feel it would be convenient and expressive to replace the Map in @BeanProperty queryStringParameters: Map[String, String] with a type T, for example

case class Person(@beanProperty val name: String, @beanProperty val surname: String)

However, the code above looks at {"name":"ciao", "surname":"bonjour"} as a String, without figuring out that it should deserialize that String.

EDIT

I have also tried to replace the scala map with a java.util.Map[String, String] without success

like image 571
NoIdeaHowToFixThis Avatar asked Sep 25 '17 19:09

NoIdeaHowToFixThis


People also ask

Does Beanstalk support Lambda?

You can even use Elastic Beanstalk and Lambda together. A lightweight application on EC2, managed with Elastic Beanstalk, can use Lambda for its heavy-duty processing.

Can I use Java in AWS Lambda?

You can run Java code in AWS Lambda. Lambda provides runtimes for Java that run your code to process events. Your code runs in an Amazon Linux environment that includes AWS credentials from an AWS Identity and Access Management (IAM) role that you manage. Lambda supports the following Java runtimes.

Does AWS Lambda support Java 11?

You can use your existing tools such as Eclipse or IntelliJ IDEA to author Java code and use Maven for packaging your Java code, making it easy to integrate AWS Lambda into your existing development processes. To get started, just upload your code through the AWS Lambda console or CLI and select the Java 11 runtime.

Is Lambda deprecated?

Lambda does not block invocations of functions that use deprecated runtime versions. Function invocations continue indefinitely after the runtime version reaches end of support.


1 Answers

By default, Serverless enables proxy integration between the lambda and API Gateway. What this means for you is that API Gateway is going to pass an object containing all the metadata about the request into your handler, as you have noticed:

Mon Sep 25 20:45:22 UTC 2017 : Endpoint request body after transformations: {"resource":"/pricing/test","path":"/pricing/test","httpMethod":"GET","headers":null,"queryStringParameters":{"name":"ciao", "surname":"bonjour"},"pathParameters":null,"stageVariables":null, ...

This clearly doesn't map to your model which has just the fields name and surname in it. There are several ways you could go about solving this.

1. Adapt your model

Your attempt with the HelloWorldRequest class does actually work if you make your class a proper POJO by making the fields mutable (and thus creating the setters for them):

class HelloWorldRequest(
                         @BeanProperty var httpMethod: String,
                         @BeanProperty var headers: java.util.Map[String, String],
                         @BeanProperty var queryStringParameters: java.util.Map[String, String]
                       ) extends ApiGatewayGetRequest(httpMethod, headers, queryStringParameters) {

AWS Lambda documentation states:

The get and set methods are required in order for the POJOs to work with AWS Lambda's built in JSON serializer.

Also keep in mind that Scala's Map is not supported.

2. Use a custom request template

If you don't need the metadata, then instead of changing your model you can make API Gateway pass only the data you need into the lambda using mapping templates.

In order to do this, you need to tell Serverless to use plain lambda integration (instead of proxy) and specify a custom request template.

Amazon API Gateway documentation has an example request template which is almost perfect for your problem. Tailoring it a little bit, we get

functions:
  pricing:
    handler: pricing.ApiGatewayHandler
    events:
      - http:
          path: pricing/test
          method: get
          integration: lambda
          request:
            template:
              application/json: |
                #set($params = $input.params().querystring)
                {
                #foreach($paramName in $params.keySet())
                  "$paramName" : "$util.escapeJavaScript($params.get($paramName))"
                  #if($foreach.hasNext),#end
                #end
                }

This template will make a JSON out of the query string parameters, and it will now be the input of the lambda:

Endpoint request body after transformations: { "name" : "ciao" }

Which maps properly to your model.

Note that disabling proxy integration also changes the response format. You will notice that now your API returns your response model directly:

{"statusCode":200,"body":"{\"message\":\"Hello ciao\"}","headers":{"x-foo":"coucou"},"base64Encoded":true}

You can fix this by either modifying your code to return only the body, or by adding a custom response template:

          response:
            template: $input.path('$.body')

This will transform the output into what you expect, but will blatantly ignore the statusCode and headers. You would need to make a more complex response configuration to handle those.

3. Do the mapping yourself

Instead of extending RequestHandler and letting AWS Lambda map the JSON to a POJO, you can instead extend RequestStreamHandler, which will provide you an InputStream and an OutputStream, so you can do the (de)serialization with the JSON serializer of your choice.

like image 181
izstas Avatar answered Oct 21 '22 02:10

izstas