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
人性啊!
我有一个非常简单的 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
人性啊!