【问题标题】:AWS Lambda handler throws a ClassCastException with Scala genericsAWS Lambda 处理程序使用 Scala 泛型引发 ClassCastException
【发布时间】:2019-06-03 12:29:02
【问题描述】:

我正在使用他们的 POJO handler 构建一个 AWS lambda 函数,但是通过 RequestHandler 接口进行抽象会导致类型被擦除。发生这种情况时,AWS 无法转换为我的 lambda 函数的输入类型:

java.util.LinkedHashMap cannot be cast to com.amazonaws.services.lambda.runtime.events.SNSEvent: java.lang.ClassCastException
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.amazonaws.services.lambda.runtime.events.SNSEvent

以下代码在上传到 AWS 时有效:

import com.amazonaws.services.lambda.runtime._
import com.amazonaws.services.lambda.runtime.events.SNSEvent

// Only working version
class PojoTest1 extends Handler1[SNSEvent]{
  override def handleRequest(input: SNSEvent, context: Context): Unit =
    println(s"message: ${input.getRecords.get(0).getSNS.getMessage}")
}

trait Handler1[Event] extends RequestHandler[Event, Unit]{
  override def handleRequest(input: Event, context: Context): Unit
}

现在因为我使用的是 Scala,所以我正在抽象出具有通用特征的 Java RequestHandler。以下是不起作用的缩小示例:

// Doesn't work
class PojoTest2 extends Handler2[SNSEvent]{
  override def act(input: SNSEvent): Unit =
    println(s"message: ${input.getRecords.get(0).getSNS.getMessage}")
}

trait Handler2[Event] extends RequestHandler[Event, Unit]{
  def act(input: Event): Unit
  override def handleRequest(input: Event, context: Context): Unit = act(input)
}

当我运行 javap PojoTest1.class 时,这是使一切正常的方法:

public void handleRequest(com.amazonaws.services.lambda.runtime.events.SNSEvent, com.amazonaws.services.lambda.runtime.Context);

当我运行javap PojoTest2.class时,你可以从这个签名中看到SNSEvent的类型已经被擦除为Object

public void handleRequest(java.lang.Object, com.amazonaws.services.lambda.runtime.Context);

这看起来与SI-8905 中描述的问题完全相同。不幸的是,发布的解决方法似乎也不起作用:

// Doesn't work
abstract class Handler3[T] extends Handler2[T]

class PojoTest3 extends Handler3[SNSEvent]{
  override def act(input: SNSEvent): Unit =
    println(s"message: ${input.getRecords.get(0).getSNS.getMessage}")
}

即使直接扩展抽象类也不会产生更好的结果:

// Doesn't work
class PojoTest4 extends Handler4[SNSEvent]{
  override def act(input: SNSEvent): Unit =
    println(s"message: ${input.getRecords.get(0).getSNS.getMessage}")
}

abstract class Handler4[Event] extends RequestHandler[Event, Unit] {
  def act(input: Event): Unit
  override def handleRequest(input: Event, context: Context): Unit = act(input)
}

当我在任何不起作用的类上使用 javap 时,我仍然会得到与擦除类型相同的方法签名。

我正在使用 Scala 2.12.7、sbt 1.1.2 和 sbt-assembly 0.14.8。

我正在寻找任何解决方法来解决这个问题。

【问题讨论】:

    标签: scala amazon-web-services aws-lambda


    【解决方案1】:

    注意:我不为 Amazon 或 Sun/Oracle 工作,所以部分答案是猜测。

    我认为 JVM 类型擦除、AWS 如何尝试解决它以及您尝试做什么之间存在根本冲突。我也不认为您引用的错误是相关的。我认为 Java 的行为是相同的。

    从 AWS 的角度来看,AFAIU 的问题是这样的:有一个不同类型的事件流和一堆处理程序。您需要决定给定处理程序可以处理哪些事件。显而易见的解决方案是查看handleRequest 方法的签名并使用参数的类型。不幸的是,JVM 类型系统并不真正支持泛型,因此您必须寻找最具体的方法(见进一步)并假设该方法是真正的交易。

    现在假设您开发了一个针对 JVM 的编译器(Scala 或 Java,进一步的示例将在 Java 中显示这不是 Scala 特定的问题)。由于 JVM 不支持泛型,因此您必须擦除您的类型。并且您希望将它们擦除为涵盖所有可能参数的最窄类型,以便您在 JVM 级别仍然是类型安全的。

    对于RequestHandler.handleRequest

    public O handleRequest(I input, Context context);
    

    唯一有效的类型擦除是

    public Object handleRequest(Object input, Context context);
    

    因为IO 未绑定。

    现在假设你这样做了

    public class PojoTest1 implements RequestHandler<SNSEvent, Void> {
        @Override
        public Void handleRequest(SNSEvent input, Context context) {
            // whatever
            return null;
        }
    }
    

    此时你说你有一个带有这个非泛型签名的handleRequest 方法,编译器必须尊重它。但同时它也必须尊重您的implements RequestHandler。所以编译器要做的就是添加一个“桥接方法”,即产生一个逻辑上等价于

    的代码
    public class PojoTest1 implements RequestHandler {
        // bridge-method
        @Override
        public Object handleRequest(Object input, Context context) {
            // call the real method casting the argument
            return handleRequest((SNSEvent)input, context);
        }
    
        // your original method
        public Void handleRequest(SNSEvent input, Context context) {
            // whatever
            return null;
        }
    }
    

    请注意您的handleRequest 并不是真正的RequestHandler.handleRequest 的覆盖。您还拥有Handler1 的事实并没有改变任何事情。真正重要的是你的非泛型类中有一个override,因此编译器必须在你的最终类中生成一个非泛型方法(即没有擦除类型的方法)。现在您有两种方法,AWS 可以理解采用 SNSEvent 的方法是最具体的方法,因此它代表了您的真实界限。

    现在假设您确实添加了通用中间类Handler2

    public abstract class Handler2<E> implements RequestHandler<E, Void> {
        protected abstract void act(E input);
    
        @Override
        public Void handleRequest(E input, Context context) {
            act(input);
            return null;
        }
    }
    

    此时返回类型是固定的,但参数仍然是未绑定的泛型。所以编译器必须产生这样的东西:

    public abstract class Handler2 implements RequestHandler {
        protected abstract void act(Object input);
    
        // bridge-method
        @Override
        public Object handleRequest(Object input, Context context) {
            // In Java or Scala you can't distinguish between methods basing
            // only on return type but JVM can easily do it. This is again
            // call of the other ("your") handleRequest method
            return handleRequest(input, context);
        }
    
        public Void handleRequest(Object input, Context context) {
            act(input);
            return null;
        }
    }
    

    所以现在我们来

    public class PojoTest2 extends Handler2<SNSEvent> {
        @Override
        protected void act(SNSEvent input) {
            // whatever
        }
    }
    

    您已覆盖 act 但未覆盖 handleRequest。因此编译器不必生成特定的handleRequest 方法,它也不需要。它只生成一个特定的act。所以生成的代码是这样的:

    public class PojoTest2 extends Handler2 {
        // Bridge-method
        @Override
        protected void act(Object input) {
            act((SNSEvent)input); // call the "real" method
        }
    
        protected void act(SNSEvent input) {
            // whatever
        }
    }
    

    或者,如果您将树展平并在 PojoTest2 中显示所有(相关)方法,它看起来像这样:

    public class PojoTest2 extends Handler2 {
    
        // bridge-method
        @Override
        public Object handleRequest(Object input, Context context) {
            // In Java or Scala you can't distinguish between methods basing
            // only on return type but JVM can easily do it. This is again
            // call of the other ("your") handleRequest method
            return handleRequest(input, context);
        }
    
        public Void handleRequest(Object input, Context context) {
            act(input);
            return null;
        }
    
        // Bridge-method
        @Override
        protected void act(Object input) {
            act((SNSEvent)input); // call the "real" method
        }
    
        protected void act(SNSEvent input) {
            // whatever
        }
    }
    

    handleRequest 两种方法都只接受 Object 作为参数,这是 AWS 必须假设的。由于您没有覆盖 PojoTest2 中的 handleRequest 方法(而不必这样做是继承层次结构的重点),编译器没有为它生成更具体的方法。

    不幸的是,我没有看到任何解决此问题的好方法。如果您希望 AWS 识别 I 泛型参数的边界,则必须在层次结构中真正知道该边界的位置覆盖 handleRequest

    你可以尝试做这样的事情:

    // Your _non-generic_ sub-class has to have the following implementation of handleRequest:
    // def handleRequestImpl(input: EventType, context: Context): Unit = handleRequestImpl(input, context)
    trait UnitHandler[Event] extends RequestHandler[Event, Unit]{
         def act(input: Event): Unit
    
         protected def handleRequestImpl(input: Event, context: Context): Unit = act(input)
    }
    

    这种方法的好处是您仍然可以将一些额外的包装逻辑(例如日志记录)放入您的handleRequestImpl。但这仍然只能按照惯例起作用。我认为没有办法强制开发人员以正确的方式使用此代码。

    如果您的Handler2 的全部要点只是将输出类型O 绑定到Unit 而不添加任何包装逻辑,您可以这样做而无需将方法重命名为act

    trait UnitHandler[Event] extends RequestHandler[Event, Unit]{
         override def handleRequest(input: Event, context: Context): Unit
    }
    

    以这种方式,您的子类仍然必须实现 handleRequest 并将特定类型绑定到 Event 并且编译器必须在那里生成特定方法,因此问题不会发生。

    【讨论】:

      【解决方案2】:

      正如@SergGr 所说,JVM 中没有真正的泛型。所有类型都替换为其边界或对象。

      这个答案对如何创建不涉及使用 AWS RequestHandler 的自定义抽象处理程序有不同的看法。

      我解决这个问题的方法是像这样使用context boundsClassTag

      abstract class LambdaHandler[TEvent: ClassTag, TResponse<: Any] {
      
        def lambdaHandler(inputStream: InputStream, outputStream: OutputStream, context: Context): Unit = {
          val json = Source.fromInputStream(inputStream).mkString
          log.debug(json)
          val event = decodeEvent(json)
          val response = handleRequest(event, context)
          // do things with the response ...
          outputStream.close()
        }
      
        def decodeEvent(json: String): TEvent = jsonDecode[TEvent](json)
      }
      

      其中jsonDecode 是将字符串事件转换为预期的TEvent 的函数。在以下示例中,我使用json4s,但您可以使用任何您想要的反序列化方法:

      def jsonDecode[TEvent: ClassTag](json: String): TEvent = {
        val mapper = Mapper.default
        jsonDecode(mapper)
      }
      

      最终,你将能够编写这样的函数

      // AwsProxyRequest and AwsProxyResponse are classes from the com.amazonaws.serverless aws-serverless-java-container-core package
      class Function extends LambdaHandler[AwsProxyRequest, AwsProxyResponse] {
        def handleRequest(request: AwsProxyRequest, context: Context): AwsProxyResponse = {
          // handle request and retun an AwsProxyResponse
        }
      }
      

      或自定义 SNS 处理程序,其中 TEvent 是 SNS 消息的自定义类型:

      // SNSEvent is a class from the com.amazonaws aws-lambda-java-events package
      abstract class SnsHandler[TEvent: ClassTag] extends LambdaHandler[TEvent, Unit]{
        override def decodeEvent(json: String): TEvent = {
          val event: SNSEvent = jsonDecode[SNSEvent](json)
          val message: String = event.getRecords.get(0).getSNS.getMessage
          jsonDecode[TEvent](message)
        }
      }
      

      如果您使用这种方法,直接开箱即用,您会很快意识到反序列化 JSON 有效负载的边缘情况非常多,因为在 您从 AWS 事件中获得的类型。因此,您必须微调 jsonDecode 方法以满足您的需求。

      或者,使用为您处理这些步骤的现有库。我知道有一个用于 Scala(但尚未使用)的库,名为 aws-lambda-scala 或者你可以看看我的LambdaHandler in GitHub的完整实现

      【讨论】:

        【解决方案3】:

        您可以通过一种不需要让每个子类都调用基类方法的方式来解决这个问题。基本上,基类需要在运行时被告知请求的具体类型是什么,然后才能将其转换为该类型的对象。所以创建一个抽象方法来提供信息:

        public abstract class BaseHandler<RequestType, ResponseType> implements RequestHandler<Map<String, Object>, ResponseType>  {
            @Override
            public ResponseType handleRequest(Map<String, Object> request, Context context) {
                // Convert the Map to Request Object for the sub classes
                final ObjectMapper mapper = new ObjectMapper();
                final RequestType requestPojo = mapper.convertValue(request, this.getRequestType());
                return this.handle(requestPojo, context);
            }
        
            protected abstract ResponseType handle(RequestType request, Context context);
            protected abstract Class<RequestType> getRequestType();
        }
        

        那么每个基类只需要实现抽象方法即可。这并不完全理想,因为 getRequestType 是一种必须拥有的奇怪方法,但我认为它比这里的其他答案要干净一些。

        示例基类:

        public class SubHandler extends BaseHandler<SNSEvent, Void> {
            @Override
            protected Void handle(SNSEvent input, Context context) {
                // Handler implementation goes here
            }
        
            @Override
            protected Class<SNSEvent> getRequestType() {
                // strange method needed, but fairly obvious implementation since the generic requires the typing to be correct.
                return SNSEvent.class;
            }
        }
        

        相关:

        【讨论】:

        • 这其实很不错!当然,getRequestType 有点笨拙,但我喜欢它是机械强制的。
        猜你喜欢
        • 2017-04-18
        • 2021-05-20
        • 2021-06-16
        • 2018-03-10
        • 2016-08-06
        • 2016-09-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多