Лямбда-обработчик AWS генерирует исключение ClassCastException с обобщениями Scala - PullRequest
0 голосов
/ 08 января 2019

Я создаю лямбда-функцию AWS с их обработчиком POJO , но абстрагирование по интерфейсу RequestHandler приводит к удалению типа. Когда это происходит, AWS не может привести к типу ввода моей лямбда-функции:

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.

Я ищу какой-нибудь способ обойти это.

Ответы [ 2 ]

0 голосов
/ 05 февраля 2019

Как сказал @SergGr, в JVM нет настоящих обобщений. Все типы заменены их связями или объектами.

Этот ответ имеет другой взгляд на то, как добиться создания пользовательских обработчиков абстрактных данных, которые не связаны с использованием AWS RequestHandler.

Я решил это, используя границы контекста и 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 в ожидаемое 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 в GitHub

0 голосов
/ 09 января 2019

Примечание : я не работаю на Amazon или Sun / Oracle, поэтому часть ответа - это предположение.

Я думаю, что существует фундаментальный конфликт между стиранием типа JVM, тем, как AWS пытается обойти это, и тем, что вы пытаетесь сделать. Я также не думаю, что ошибка, на которую вы ссылались, актуальна. Я думаю, что поведение для Java такое же.

AFAIU с точки зрения AWS проблема выглядит следующим образом: есть поток событий разных типов и куча обработчиков. Вам необходимо решить, какие события может обработать данный обработчик. Очевидное решение - посмотреть на сигнатуру метода 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);

потому что I и O не связаны.

Теперь предположим, что вы делаете

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 должен принять. Поскольку вы не переопределили метод 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)
}

Преимущество этого подхода состоит в том, что вы все равно можете добавить некоторую дополнительную логику обертывания (такую ​​как ведение журнала) в handleRequestImpl. Но все же это будет работать только по соглашению. Я не вижу способа заставить разработчиков правильно использовать этот код.

Если весь смысл вашего Handler2 просто привязать тип вывода O к Unit без добавления логики переноса, вы можете просто сделать это без переименования метода в act:

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

Таким образом, ваши подклассы по-прежнему должны будут реализовывать handleRequest с конкретными типами, привязанными к Event, и компилятор должен будет создавать специальные методы там, чтобы проблема не возникала.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...