AWS Lambda处理程序抛出带有Scala泛型的ClassCastException

cod*_*dle 5 scala amazon-web-services aws-lambda

我正在使用其POJO处理程序构建一个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
Run Code Online (Sandbox Code Playgroud)

以下代码在上载到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
}
Run Code Online (Sandbox Code Playgroud)

现在,因为我正在使用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)
}
Run Code Online (Sandbox Code Playgroud)

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

public void handleRequest(com.amazonaws.services.lambda.runtime.events.SNSEvent, com.amazonaws.services.lambda.runtime.Context);
Run Code Online (Sandbox Code Playgroud)

运行时,javap PojoTest2.class您可以从该签名中看到的类型SNSEvent已被擦除为Object

public void handleRequest(java.lang.Object, com.amazonaws.services.lambda.runtime.Context);
Run Code Online (Sandbox Code Playgroud)

这看起来就像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}")
}
Run Code Online (Sandbox Code Playgroud)

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

// 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)
}
Run Code Online (Sandbox Code Playgroud)

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

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

我正在寻找各种工作来解决这个问题。

Ser*_*gGr 5

注意:我不适用于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);
Run Code Online (Sandbox Code Playgroud)

唯一有效的类型擦除是

public Object handleRequest(Object input, Context context);
Run Code Online (Sandbox Code Playgroud)

因为IO是不受约束的。

现在假设你做

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

此时,您说您有一个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;
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,您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;
    }
}
Run Code Online (Sandbox Code Playgroud)

此时返回类型是固定的,但参数仍然是未绑定的泛型。因此,编译器必须产生如下内容:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

所以现在当我们来到

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

您已覆盖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
    }
}
Run Code Online (Sandbox Code Playgroud)

或者,如果将树展平并在中显示所有(相关)方法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
    }
}
Run Code Online (Sandbox Code Playgroud)

这两种handleRequest方法都接受Object作为参数,这就是AWS必须承担的。由于您没有在其中重写该handleRequest方法PojoTest2(并且不必这样做就是继承层次结构的全部要点),因此编译器没有为此生成更具体的方法。

不幸的是,我看不到任何解决此问题的好方法。如果您想让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)
}
Run Code Online (Sandbox Code Playgroud)

这种方法的好处是您仍然可以在其中添加一些其他包装逻辑(例如日志记录)handleRequestImpl。但这仍然只能按照惯例进行。我没有办法强迫开发人员以正确的方式使用此代码。

如果您的整个观点Handler2只是将输出类型绑定OUnit而不添加任何包装逻辑,则可以执行此操作而无需将方法重命名为act

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

这样,您的子类仍将必须handleRequest使用绑定的特定类型来实现,Event并且编译器将不得不在那里生成特定的方法,因此不会发生此问题。