All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazonaws.services.lambda.invoke.LambdaInvokerFactory Maven / Gradle / Ivy

/*
 * Copyright 2015-2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
package com.amazonaws.services.lambda.invoke;

import com.amazonaws.services.lambda.AWSLambda;
import com.amazonaws.services.lambda.AWSLambdaAsyncClientBuilder;
import com.amazonaws.services.lambda.model.InvocationType;
import com.amazonaws.services.lambda.model.InvokeRequest;
import com.amazonaws.services.lambda.model.InvokeResult;
import com.amazonaws.services.lambda.model.LogType;
import com.amazonaws.util.Base64;
import com.amazonaws.util.BinaryUtils;
import com.amazonaws.util.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;

/**
 * A factory for objects that implement a user-supplied interface by invoking a remote Lambda
 * function.
 * 

*

 * public class Request {
 *     // Standard POJO stuff here modeling the input your Lambda function
 *     // expects.
 * }
 *
 * public class Result {
 *     // More standard POJO stuff here modeling the output your Lambda
 *     // function produces.
 * }
 *
 * public interface LambdaFunctions {
 *
 *     @LambdaFunction
 *     Result doSomeStuff(Request request);
 * }
 * LambdaFunctions functions = LambdaInvokerFactory.builder()
 *                             .lambdaClient(AWSLambdaSyncClientBuilder.standard()
 *                                  .withCredentials(new ProfileCredentialsProvider("myprofile"))
 *                                  .build())
 *                             .build(LambdaFunctions.class);
 * Request request = new Request(...);
 * Result result = functions.doSomeStuff(request);
 * 
*/ public final class LambdaInvokerFactory { static final ObjectMapper DEFAULT_MAPPER = new ObjectMapper() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); /** * Creates a new Lambda invoker implementing the given interface and wrapping the given {@code AWSLambda} client. * * @param interfaceClass the interface to implement * @param awsLambda the lambda client to use for making remote calls * @deprecated Use {@link LambdaInvokerFactory#builder()} to configure invoker factory. */ @Deprecated public static T build(Class interfaceClass, AWSLambda awsLambda) { return build(interfaceClass, awsLambda, new LambdaInvokerFactoryConfig()); } /** * Creates a new Lambda invoker implementing the given interface and wrapping the given {@code AWSLambda} client. * * @param interfaceClass the interface to implement * @param awsLambda the lambda client to use for making remote calls * @param config configuration for the LambdaInvokerFactory * @deprecated Use {@link LambdaInvokerFactory#builder()} to configure invoker factory. */ @Deprecated public static T build(Class interfaceClass, AWSLambda awsLambda, LambdaInvokerFactoryConfig config) { final Object proxy = Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new LambdaInvocationHandler(interfaceClass, awsLambda, config)); return interfaceClass.cast(proxy); } /** * @return An instance of {@link Builder} to configure an invoker factory and build proxies for * invoking remote lambda functions. */ public static Builder builder() { return new Builder(); } public static class Builder { private LambdaFunctionNameResolver functionNameResolver; private String functionAlias; private String functionVersion; private AWSLambda lambda; private ObjectMapper objectMapper; /** * Sets a new Function name resolver to override the default behavior. * * @param functionNameResolver Implementation of {@link LambdaFunctionNameResolver} * @return The current object for method chaining. */ public Builder lambdaFunctionNameResolver( LambdaFunctionNameResolver functionNameResolver) { this.functionNameResolver = functionNameResolver; return this; } private LambdaFunctionNameResolver resolveFunctionNameResolver() { return functionNameResolver == null ? new DefaultLambdaFunctionNameResolver() : functionNameResolver; } /** * Sets the ObjectMapper used to (de-)serialize payload if you do not wish to use the default mapper. * (see {@link ObjectMapper} for configuration options) * *

The FAIL_ON_UNKNOWN_PROPERTIES property MUST be disabled for any custom object mapper so that * the response unmarshalling is forward compatible with any new fields your Lambda function may return but might * not be modeled.

* * @return This current object for method chaining. */ public Builder objectMapper(ObjectMapper objectMapper) { if (objectMapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) { throw new IllegalArgumentException("FAIL_ON_UNKNOWN_PROPERTIES must be disabled on any custom ObjectMapper used"); } this.objectMapper = objectMapper; return this; } private ObjectMapper resolveObjectMapper() { return null != this.objectMapper ? this.objectMapper : DEFAULT_MAPPER; } /** * Sets the function alias to invoke. See Versioning * & Aliases for more information on aliases. * * @return This current object for method chaining. */ public Builder functionAlias(String functionAlias) { this.functionAlias = functionAlias; return this; } /** * Sets the function version to invoke. See Versioning * & Aliases for more information on function versions. * * @return This current object for method chaining. */ public Builder functionVersion(String functionVersion) { this.functionVersion = functionVersion; return this; } /** * Sets the client to use to call AWS Lambda. If not set a default client is used (see {@link * AWSLambdaAsyncClientBuilder#defaultClient()}). * * @param lambda Client instance to use. * @return This current object for method chaining. */ public Builder lambdaClient(AWSLambda lambda) { this.lambda = lambda; return this; } private AWSLambda resolveLambdaClient() { return lambda == null ? AWSLambdaAsyncClientBuilder.defaultClient() : lambda; } /** * Build a remote proxy of the given interface to make calls to AWS Lambda. * * @param interfaceClass Interface class to proxy. * @param Interface type. * @return This current object for method chaining. */ public T build(Class interfaceClass) { return LambdaInvokerFactory.build(interfaceClass, resolveLambdaClient(), getConfiguration()); } private LambdaInvokerFactoryConfig getConfiguration() { return new LambdaInvokerFactoryConfig(resolveFunctionNameResolver(), resolveObjectMapper(), functionAlias, functionVersion); } } private LambdaInvokerFactory() { } private static class LambdaInvocationHandler implements InvocationHandler { private final AWSLambda awsLambda; private final Log log; private final LambdaInvokerFactoryConfig config; private final ObjectMapper mapper; public LambdaInvocationHandler(Class interfaceClass, AWSLambda awsLambda, LambdaInvokerFactoryConfig config) { this.awsLambda = awsLambda; this.log = LogFactory.getLog(interfaceClass); this.config = config; this.mapper = config.getObjectMapper(); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("toString")) { return this.toString(); } else if (method.getName().equals("hashCode")) { return this.hashCode(); } LambdaFunction annotation = validateInterfaceMethod(method, args); InvokeRequest invokeRequest = buildInvokeRequest(method, annotation, args == null ? null : args[0]); InvokeResult invokeResult = awsLambda.invoke(invokeRequest); return processInvokeResult(method, invokeResult); } /** * Verifies that the given method is annotated appropriately. */ private LambdaFunction validateInterfaceMethod(Method method, Object[] args) { LambdaFunction annotation = method.getAnnotation(LambdaFunction.class); if (annotation == null) { throw new LambdaSerializationException("No LambdaFunction annotation for method " + method.getName()); } if (annotation.invocationType() != InvocationType.RequestResponse && annotation.logType() != LogType.None) { throw new LambdaSerializationException("InvocationType must be RequestResponse if LogType " + "is set"); } if (args != null && args.length > 1) { throw new LambdaSerializationException("LambdaFunctions take either 0 or 1 arguments"); } return annotation; } /** * Builds an InvokeRequest from the given method, its {@code LambdaFunction} annotation, and * the input parameter (if any). */ private InvokeRequest buildInvokeRequest(Method method, LambdaFunction annotation, Object input) { InvokeRequest invokeRequest = new InvokeRequest(); String functionName = config.getLambdaFunctionNameResolver().getFunctionName(method, annotation, config); invokeRequest.setFunctionName(functionName); if (hasQualifier()) { invokeRequest.setQualifier(getQualifier()); } invokeRequest.setInvocationType(annotation.invocationType()); invokeRequest.setLogType(annotation.logType()); if (input != null) { try { String payload = mapper.writer().writeValueAsString(input); if (log.isDebugEnabled()) { log.debug("Serialized request object to '" + payload + "'"); } invokeRequest.setPayload(payload); } catch (JsonProcessingException ex) { throw new LambdaSerializationException("Failed to serialize request object to JSON", ex); } } return invokeRequest; } private boolean hasQualifier() { return getQualifier() != null; } private String getQualifier() { return config.getFunctionAlias() == null ? config.getFunctionVersion() : config.getFunctionAlias(); } /** * Process the result of invoking a remote function. If the response includes server-side * logs, dump them into our logs; if it includes a server-side error indication, parse it * into a corresponding {@code Exception} type, otherwise parse the result payload into a * Java object suitable for returning from this method. */ private Object processInvokeResult(Method method, InvokeResult invokeResult) throws Throwable { if (invokeResult.getLogResult() != null && log.isInfoEnabled()) { try { String decoded = new String(Base64.decode(invokeResult.getLogResult()), StringUtils.UTF8); log.info(method.getName() + " log:\n\t" + decoded.replaceAll("\n", "\n\t")); } catch (Exception ex) { log.warn("Error decoding log result '" + invokeResult.getLogResult() + "'", ex); } } String functionError = invokeResult.getFunctionError(); if (functionError == null) { // Success. return getObjectFromPayload(method, invokeResult); } else { throw getExceptionFromPayload(method, invokeResult); } } /** * Reads a Java object suitable for returning from the given method from the payload of the * given {@code InvokeResult} (or returns {@code null} if the method has no return value or * the response contains no payload). * * @throws LambdaSerializationException * on error deserializing */ private Object getObjectFromPayload(Method method, InvokeResult invokeResult) { try { return getObjectFromPayload(method.getGenericReturnType(), invokeResult.getPayload()); } catch (IOException ex) { throw new LambdaSerializationException("Failed to parse Lambda function result", ex); } } /** * Unmarshall the exception from the response payload. The invoker factory supports unmarshalling into custom exceptions * that are declared on the method signature (in the interface the invoker factory proxies). * * @param method Method being proxied * @param invokeResult Result from AWS Lambda. * @return Exception to throw back to the caller. May either be a custom exception declared in the interface, a generic * exception unmarshalled from the payload, or a very generic exception if we can't unmarshall the payload. */ private Throwable getExceptionFromPayload(Method method, InvokeResult invokeResult) { try { LambdaFunctionException error = getObjectFromPayload(LambdaFunctionException.class, invokeResult.getPayload()); error.setFunctionError(invokeResult.getFunctionError()); error.fillInStackTrace(method.getDeclaringClass()); return getExceptionToThrow(method, error); } catch (Exception ex) { log.warn("Error parsing exception information from response payload", ex); return new LambdaFunctionException("Unexpected error executing Lambda function", invokeResult.getFunctionError()); } } /** * Get the correct exception to throw back to the caller. * * @param method Interface method we are proxying * @param error Unmarshalled error payload * @return A custom exception if the error matches any thrown by the interface method or the original {@link * LambdaFunctionException} if none matches. */ private Throwable getExceptionToThrow(Method method, LambdaFunctionException error) { final String type = error.getType(); final Constructor constructor = findConstructor(findCustomExceptionClass(method, type)); if (constructor != null) { try { final Throwable toReturn = (Throwable) constructor.newInstance(error.getMessage()); toReturn.setStackTrace(error.getStackTrace()); return toReturn; } catch (Exception ex) { log.warn("Error constructing custom exception", ex); } } return error; } /** * Search the method throws clause to find an exception that matches the error type. * * @param method We only consider exception types that are explicitly declared in the interface for the proxied method. * @param type Error type returned by AWS Lambda. * @return Custom exception class to create or null to use default exception. */ private Class findCustomExceptionClass(Method method, String type) { if (type != null) { for (Class exceptionType : method.getExceptionTypes()) { if (exceptionType.getName().equals(type) || exceptionType.getSimpleName().equals(type)) { return exceptionType; } } } return null; } /** * For custom exceptions we expect to find a accessible constructor that takes a String parameter (for the error message) * * @param type Exception class * @return Applicable constructor or null if not found. */ private Constructor findConstructor(Class type) { if (type == null) { return null; } for (Constructor constructor : type.getConstructors()) { Class[] params = constructor.getParameterTypes(); if (params != null && params.length == 1 && String.class.equals(params[0])) { return constructor; } } return null; } private T getObjectFromPayload(Class type, ByteBuffer payload) throws IOException { return type.cast(getObjectFromPayload((Type) type, payload)); } private Object getObjectFromPayload(Type type, ByteBuffer payload) throws IOException { if (type == void.class || payload.remaining() == 0) { return null; } JavaType javaType = mapper.getTypeFactory().constructType(type); return mapper.reader(javaType).readValue(BinaryUtils.copyAllBytesFrom(payload)); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy