AWS Lambda handler throws a ClassCastException with Scala generics
Note: I don't work for Amazon or Sun/Oracle so parts of the answer is a speculation.
I think there is a fundamental conflict between JVM type erasure, how AWS tries to work it around and what you are trying to do. I also don't think that the bug you referenced is relevant. I think the behavior is the same for Java.
AFAIU from the AWS point of view the problem looks like this: there is a stream of events of different types and a bunch of handlers. You need to decide which events a given handler can handle. The obvious solution is to look at the signature of the handleRequest
method and use the argument's type. Unfortunately JVM type system doesn't really supports generics so you have to look for the most specific method (see further) and assume that that method is the real deal.
Now assume you develop a compiler that targets JVM (Scala or Java, further examples will be in Java to show that this is not a Scala-specific issue). Since JVM doesn't support generics you have to erasure your types. And you want to erase them to the most narrow type that covers all possible arguments so you are still type-safe at the JVM level.
For the RequestHandler.handleRequest
public O handleRequest(I input, Context context);
the only valid type erasure is
public Object handleRequest(Object input, Context context);
because I
and O
are unbound.
Now assume you do
public class PojoTest1 implements RequestHandler<SNSEvent, Void> {
@Override
public Void handleRequest(SNSEvent input, Context context) {
// whatever
return null;
}
}
At this point you say that you have a handleRequest
method with this non-generic signature and the compiler has to respect it. But at the same time it has to respect your implements RequestHandler
as well. So what the compiler has to do is to add a "bridge method" i.e. to produce a code logically equivalent to
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;
}
}
Note how your handleRequest
is not really an override of the RequestHandler.handleRequest
. The fact that you also have Handler1
doesn't change anything. What is really important is that you have an override
in your non-generic class so the compiler has to generate a non-generic method (i.e. a method with not erased types) in your final class. Now you have two methods and AWS can understand that the one that takes SNSEvent
is the most specific one so it is represents your real bound.
Now assume you do add your generic intermediate class 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;
}
}
At this point the return type is fixed but the argument is still an unbound generic. So compiler has to produce something like this:
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;
}
}
So now when we come to
public class PojoTest2 extends Handler2<SNSEvent> {
@Override
protected void act(SNSEvent input) {
// whatever
}
}
you have overridden act
but not handleRequest
. Thus the compiler doesn't have to generate a specific handleRequest
method and it doesn't. It only generates a specific act
. So the generated code looks like this:
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
}
}
Or if you flatten the tree and show all (relevant) methods in PojoTest2
, it looks like this:
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
}
}
Both of the handleRequest
methods accept just Object
as a parameter and this is what AWS has to assume. Since you didn't override the handleRequest
method in PojoTest2
(and not having to do so is the whole point of your inheritance hierarchy), the compiler didn't produce a more specific method for it.
Unfortunately I don't see any good workaround for this problem. If you want AWS to recognize the bound of the I
generic parameter, you have to override handleRequest
at the place in hierarchy where this bound becomes really known.
You may try do something like this:
// 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)
}
The benefit of this approach is that you can still put some additional wrapping logic (such as logging) into your handleRequestImpl
. But still this will work only by convention. I see no way to force developers to use this code in the correct way.
If the whole point of your Handler2
is just bind the output type O
to Unit
without adding any wrapping logic, you can just do this without renaming the method to act
:
trait UnitHandler[Event] extends RequestHandler[Event, Unit]{
override def handleRequest(input: Event, context: Context): Unit
}
In such way your sub-classes still will have to implement handleRequest
with specific types bound to Event
and compiler will have to produce specific methods there so the issue will not happen.
As @SergGr said, there are no real generics in the JVM. All types are replaced with their bounds or objects.
This answer has a different take on how to achieve the creation of custom abstract handlers which doesn't involve using the AWS RequestHandler
.
The way I have solved this is by using context bounds and ClassTag
like this:
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)
}
where jsonDecode
is a function that turns the String event to the expected TEvent
. In the following example I use json4s but you can use any de/serialization method you want:
def jsonDecode[TEvent: ClassTag](json: String): TEvent = {
val mapper = Mapper.default
jsonDecode(mapper)
}
In the end, you will be able to write functions like this
// 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
}
}
Or custom SNS handlers where TEvent is the custom type of the SNS message:
// 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)
}
}
If you use this method, straight out of the box, you will quickly realize that there are a large number of edge cases deserializing the JSON payloads because there are inconsistencies in the
types that you get from AWS events. Therefore, you will have to fine tune the jsonDecode
method to suit your needs.
Alternatively, use an existing library that takes care of these steps for you. There is one library that I know of for Scala (but have not used) called aws-lambda-scala
or you can take a look at the full implementation of my LambdaHandler
in GitHub