AWS Lambda 无法从 START_OBJECT 令牌中反序列化 `java.lang.String` 的实例

AWS Lambda Cannot deserialize instance of `java.lang.String` out of START_OBJECT token

我有一个非常简单的 Java 11 Lambda:

public class GetArticleHandler implements RequestHandler<APIGatewayV2ProxyRequestEvent, APIGatewayV2ProxyResponseEvent> {

    @Inject
    private GetArticleService getArticleService;

    @Override
    public APIGatewayV2ProxyResponseEvent handleRequest(APIGatewayV2ProxyRequestEvent req, Context context) {

        String path = req.getPath();

        Article article = getArticleService.get(path);

        return generateResponse(req, article);
    }

    private APIGatewayV2ProxyResponseEvent generateResponse(APIGatewayV2ProxyRequestEvent req, Article article) {
        APIGatewayV2ProxyResponseEvent res = new APIGatewayV2ProxyResponseEvent();
        res.setHeaders(Collections.singletonMap("timeStamp", String.valueOf(System.currentTimeMillis())));
        res.setStatusCode(200);
        res.setBody(article.toString());
        return res;
    }

}

它通过 CloudFormation 部署连接到 AWS API网关,使用以下模板(请注意,这是该模板的摘录):

Resources:
  UTableArticle:
    Type: AWS::DynamoDB::Table
    Properties:
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
      TableName: !Sub ${AWS::StackName}-Article
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
    
  FunctionAssumeRoleRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
                
  DynamoActionsPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action:
              - dynamodb:BatchGetItem
              - dynamodb:GetRecords
              - dynamodb:GetShardIterator
              - dynamodb:Query
              - dynamodb:GetItem
              - dynamodb:Scan
              - dynamodb:BatchWriteItem
              - dynamodb:PutItem
              - dynamodb:UpdateItem
              - dynamodb:DeleteItem
            Effect: Allow
            Resource:
              - !GetAtt [ UTableArticle, Arn ]
              - !Ref AWS::NoValue
        Version: "2012-10-17"
      PolicyName: DynamoActionsPolicy
      Roles:
        - !Ref FunctionAssumeRoleRole
  BFunctionGetArticle:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref ArtefactRepositoryBucket
        S3Key: !Join [ '', [!Ref ArtefactRepositoryKeyPrefix, '.zip' ] ]
      Handler: !Ref 'GetArticleHandler'
      Role: !GetAtt [ FunctionAssumeRoleRole, Arn ]
      Runtime: java11
      Environment:
        Variables:
          TABLE_NAME: !Ref UTableArticle
          PRIMARY_KEY: id
    DependsOn:
      - DynamoActionsPolicy
      - FunctionAssumeRoleRole
  BFunctionGWPermissionGetIdArticle:
    Type: AWS::Lambda::Permission
    DependsOn:
      - BlogRestApi
      - BFunctionGetArticle
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt [ BFunctionGetArticle, Arn ]
      Principal: apigateway.amazonaws.com
      SourceArn: !Join ['', ['arn:', !Ref 'AWS::Partition', ':execute-api:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':', !Ref BlogRestApi, '/*/GET/article/{id}'] ]
  BlogRestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: Article
  AGWDeploymentArticle:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId: !Ref BlogRestApi
      Description: Automatically created by the RestApi construct
    DependsOn:
      - MethodArticleIdGet
      - MethodArticleIdPatch
      - ResourceArticleId
      - MethodArticleGet
      - MethodArticlePost
      - ResourceArticle
  BAGDeploymentStageProdArticle:
    Type: AWS::ApiGateway::Stage
    Properties:
      RestApiId: !Ref BlogRestApi
      DeploymentId: !Ref AGWDeploymentArticle
      StageName: prod
  ResourceArticle:
    Type: AWS::ApiGateway::Resource
    Properties:
      ParentId: !GetAtt [ BlogRestApi, RootResourceId ]
      PathPart: article
      RestApiId: !Ref BlogRestApi
  MethodArticleGet:
    Type: AWS::ApiGateway::Method
    Properties:
      HttpMethod: GET
      ResourceId: !Ref ResourceArticle
      RestApiId: !Ref BlogRestApi
      AuthorizationType: NONE
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Join [ "", ['arn:', !Ref 'AWS::Partition', ':apigateway:', !Ref 'AWS::Region', ':lambda:path/2015-03-31/functions/', !GetAtt [ BFunctionListArticles, Arn ], '/invocations' ] ]
  ResourceArticleId:
    Type: AWS::ApiGateway::Resource
    Properties:
      ParentId: !Ref ResourceArticle
      PathPart: "{id}"
      RestApiId: !Ref BlogRestApi
  MethodArticleIdGet:
    Type: AWS::ApiGateway::Method
    Properties:
      HttpMethod: GET
      ResourceId: !Ref ResourceArticleId
      RestApiId: !Ref BlogRestApi
      AuthorizationType: NONE
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Join [ "", ['arn:', !Ref 'AWS::Partition', ':apigateway:', !Ref 'AWS::Region', ':lambda:path/2015-03-31/functions/', !GetAtt [ BFunctionGetArticle, Arn ], '/invocations' ] ]

CloudFromation 部署正确,我可以通过 cURL 对整个部署进行调用,或者我可以转到 API 网关资源并在那里进行测试。在任何一种情况下,对 Lambda 的调用都会在入口时卡在 Jackson 反序列化中,并且在 CloudWatch 的日志中,我收到错误:

An error occurred during JSON parsing: java.lang.RuntimeException
java.lang.RuntimeException: An error occurred during JSON parsing
Caused by: java.io.UncheckedIOException: com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token
 at [Source: (ByteArrayInputStream); line: 1, column: 1]
    at com.amazonaws.services.lambda.runtime.serialization.factories.JacksonFactory$InternalSerializer.fromJson(JacksonFactory.java:182)
Caused by: com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token
 at [Source: (ByteArrayInputStream); line: 1, column: 1]
    at com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
    at com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1442)
    at com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1216)
    at com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1126)
    at com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.std.StringDeserializer.deserialize(StringDeserializer.java:63)
    at com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.std.StringDeserializer.deserialize(StringDeserializer.java:10)
    at com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:1719)
    at com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1228)
    at com.amazonaws.services.lambda.runtime.serialization.factories.JacksonFactory$InternalSerializer.fromJson(JacksonFactory.java:180)

这个错误似乎告诉我 Jackson 正试图将 API 网关事件反序列化为一个字符串(当然,它不是)。鉴于我已将 Lambda 指定为: GetArticleHandler implements RequestHandler<APIGatewayV2ProxyRequestEvent, APIGatewayV2ProxyResponseEvent> 我预计 Jackson 会尝试将 API 网关事件反序列化为 APIGatewayV2ProxyRequestEvent。但无论我如何指定 RequestHandler(例如,我尝试指定 Map<String,Object>),它都会一直尝试将事件反序列化,就好像它是一个字符串一样。谁能告诉我这是怎么回事?有什么我想念的吗?

很难追踪到这一点,但归结为需要向 AWS::ApiGateway::Method 提供 RequestTemplate。我这样做的方式是在 CloudFormation 模板中:

  MethodArticleIdGet:
    Type: AWS::ApiGateway::Method
    Properties:
      HttpMethod: GET
      ResourceId: !Ref ResourceArticleId
      RestApiId: !Ref BlogRestApi
      AuthorizationType: NONE
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        RequestTemplates:
          application/json: !Ref ParamRequestMappingTemplate
        Uri: !Join [ "", ['arn:', !Ref 'AWS::Partition', ':apigateway:', !Ref 'AWS::Region', ':lambda:path/2015-03-31/functions/', !GetAtt [ BFunctionGetArticle, Arn ], '/invocations' ] ]

然后添加 ParamRequestMappingTemplate:

Parameters:

  <snip>

  ParamRequestMappingTemplate:
    Type: String
    Description: 'Read from resources/templates'

  <snip>

这样我就可以通过 cloudformation deploy 调用中的 --parameter-overrides 输入参数,并引用 .vsl 文件,其中包含:

#set($allParams = $input.params())
{
    #foreach($type in $allParams.keySet())
    #set($params = $allParams.get($type))
    "$type" : {
      #foreach($paramName in $params.keySet())
      "$paramName" : "$util.escapeJavaScript($params.get($paramName))"
      #if($foreach.hasNext),#end
      #end
    }
    #if($foreach.hasNext),#end
    #end
}

这是对 AWS 脚本的修改,将所有 headers、路径参数和查询参数作为映射元素传递。

然后我将请求参数建模如下class:

public class RequestParams {
    String path;

    Map<String, String> header;

    Map<String, String> queryString;
}

然后改造了 Lamabda 和处理程序方法:

public class GetArticleHandler implements RequestHandler<RequestParams, Response<Article>> {

    Injector injector = Guice.createInjector(new GetArticleHandlerModule());

    private GetArticleService getArticleService = injector.getInstance(GetArticleService.class);

    public void setGetArticleService(GetArticleService getArticleService) {
        this.getArticleService = getArticleService;
    }

    @Override
    public Response<Article> handleRequest(RequestParams params, Context context) {
        LOGGER.init(context, "GetArticle", null);

        LOGGER.info(this, params.getPath());

        Article article = getArticleService.get(params.getPath());

        return new Response<>(article);
    }
}

有了这个,错误就消失了。

虽然,需要注意的是API网关层也要求响应建模为:

public class Response<B> {
    @JsonProperty("isBase64Encoded")
    boolean isBase64Encoded;
    int statusCode;
    Map<String, String> headers;
    B body;

    public Response(B body) {
        this.setBase64Encoded(false);
        this.setStatusCode(200);
        this.setHeaders(Map.of("Access-Control-Allow-Origin", "*"));
        this.setBody(body);
    }
}

Jackson 序列化后的响应顽固地输出:

Tue May 26 08:25:04 UTC 2020 : Endpoint response body before transformations: {"statusCode":200,"headers":{"Access-Control-Allow-Origin":"*"},"body":{"tags":[]},"base64Encoded":false}

换句话说,无论我做什么,它总是将 isBase64Encoded 序列化为 base64Encoded

这会导致错误:

Tue May 26 08:25:04 UTC 2020 : Execution failed due to configuration error: Malformed Lambda proxy response
Tue May 26 08:25:04 UTC 2020 : Method completed with status: 502

人性啊!