当前位置: 首页 > 知识库问答 >
问题:

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

尹正奇
2023-03-14

我正在用他们的POJO处理程序构建一个AWS lambda函数,但是在请求处理程序接口上抽象会导致擦除类型。当这种情况发生时,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请求处理程序。以下是一个不起作用的小例子:

// 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时。类您可以从该签名中看到,SNSEvent的类型已被删除为对象

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。

我正在寻找解决这个问题的方法。

共有3个答案

裴嘉良
2023-03-14

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

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;
    }
}

相关:

  • 将地图转换为Java对象
  • 将贴图转换为对象Scala
邢焕
2023-03-14

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

这个答案对如何实现自定义抽象处理程序的创建有不同的理解,它不涉及使用AWSRequestHandler

我解决这个问题的方法是使用上下文边界和ClassTag如下:

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是将String事件转换为预期的TEent函数。在下面的示例中,我使用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,您也可以在GitHub中查看myLambdaHandler的完整实现

林德辉
2023-03-14

注意:我不为亚马逊或太阳/甲骨文工作,所以部分答案是猜测。

我认为JVM类型擦除、AWS如何尝试解决它和你想要做的事情之间存在着根本性的冲突。我也不认为你提到的错误是相关的。我认为Java的行为是一样的。

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

现在假设您开发了一个以JVM为目标的编译器(Scala或Java,Java会有更多的例子来说明这不是Scala特定的问题)。由于JVM不支持泛型,因此必须删除类型。您希望将它们擦除到涵盖所有可能参数的最窄类型,这样您在JVM级别仍然是类型安全的。

对于请求Handler.handle请求

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方法,编译器必须尊重它。但与此同时,它也必须尊重你的实现的请求处理程序。所以编译器要做的是添加一个“桥接方法”,即产生一个逻辑上等同于

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实际上并不是请求andler.handle请求的重写。事实上,您还拥有Handler1并不能改变任何事情。真正重要的是,在非泛型类中有一个重写,因此编译器必须在最终类中生成一个非泛型方法(即具有未擦除类型的方法)。现在您有两个方法,AWS可以理解,接受SNSEent的方法是最具体的方法,所以它代表了您真正的绑定。

现在假设您添加了通用中间类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)
}

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

如果Handler2的全部要点只是将输出类型O绑定到单元,而不添加任何包装逻辑,那么您只需将方法重命名为act

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

以这种方式,您的子类仍然必须实现具有绑定到事件的特定类型的处理请求,编译器必须在那里生成特定的方法,这样问题就不会发生。

 类似资料:
  • 我有一个使用泽西和MOXy的JAX-RS服务。我有一个处理程序,它返回类型的JSON或XML(取决于标头)表示,但是如果没有找到该项目,它应该返回一个404错误不同的类型。 在第一种情况下(返回Memo),它可以正常工作,但在第二种情况下(抛出带有MemoError实体的WebApplication ationExcture)则不能正常工作。 如果我将处理程序更改为返回一个,则第二种情况确实有效(

  • 我想制作一个,传递事件和一些参数。问题是函数没有得到元素。下面是一个例子: 必须在匿名函数之外定义。如何获取传递的元素以在匿名函数中使用?有办法做到这一点吗? 那么呢?我似乎根本无法通过传递事件,是吗? 使现代化 我似乎用“这个”解决了这个问题 其中包含我可以在函数中访问的。 addEventListener 但是我想知道:

  • 很简单的问题是,如何初始化可能引发构造函数异常的类成员变量? 显然,您不能在构造函数初始化器中尝试捕获异常,因此在构造函数中的try catch块中构造对象是否更合适? 这是一个顶级类,因此处理异常以让用户知道发生了什么并优雅地退出比让程序因异常而崩溃更重要? 但是,这不起作用,因为对象在构造函数内初始化之前初始化一次,因此如果对象引发异常,程序可能会崩溃。

  • 看看这个代码。 愚蠢的Scala编译器在这里显示错误: 错误:(22, 39) 类型不匹配;找到: mix.type (底层类型 SomeMix) 必需: T with SomeMix 大小写混合: SomeMix = 它不理解表达式I匹配到在某种混合已经是类型T。好吧,让我们帮助他。更改代码: 现在,它同意一切都是正确的,但显示警告: 警告:(22,17)抽象类型模式T未选中,因为它已通过擦除大

  • 问题内容: 我正在尝试使用scala json库Circe,将其包装在一个简单的特征中以提供往返于json的转换,我对此具有以下要求: 这样做的目的是简单地能够用任何对象调用JsonConverter并将其转换成json之类的东西,但是当我尝试对其进行编译时,我得到以下信息: 我当然可以拥有一个类,打算通过转换器放入的所有内容都继承自该类,但是我有一个印象,大约可以自动生成编码器/解码器? 问题答

  • 我正在尝试创建一个新的react应用程序: 安装似乎工作正常,但当我尝试的应用程序,我得到一个错误: C:\。。。\我的应用程序\node\u modules\react scripts\scripts\utils\verifyTypeScriptSetup。js:239 appTsConfig。编译器选项[选项]=值^ TypeError:无法在对象的verifyTypeScriptSetup(