0

I have a MessageBodyReader with the following class:

@Provider
@Consumes(MediaType.APPLICATION_JSON)
public class TransactionMessageBodyReader implements MessageBodyReader<Transaction<Customer>> 

boolean isReadable(Class<?> type,
                       Type genericType,
                       Annotation[] annotations,
                       MediaType mediaType)

And I have the following REST endpoint:

@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Path("trans")
public Response checkStatus(Transaction<Customer> transaction) { ... }

In my debugger I have a breakpoint on isReadable and it hits the breakpoint before calling the checkStatus method. Good so far. I can see in my debugger that the genericType says Transaction<Customer> ... in other words, it knows about the type information. How can it possibly know the type information when Java type erasure erases it to just be Transaction at runtime? I see that the genericType is made up of an ParameterizedType which I believe is used to pass type information around (to get around the type erasure problem). However, how is Jersey populating the genericType automatically? Because I never specified type information except in the method headers that I posted above.

Also, a second question: if Jersey 'automatically' knows so much about my types, why do I need a MessageBodyReader at all? Isn't there some easier way to use generics with Jersey/REST endpoints and make it "just work"?

KyleM
  • 4,445
  • 9
  • 46
  • 78
  • Actually, it is because there is the `MessageBodyReader` implementation that it magically works. See also http://stackoverflow.com/questions/19860393/java-generics-obtaining-actual-type-of-generic-parameter – Tunaki May 13 '16 at 21:06
  • @Tunaki I know, but why? How does the MessageBodyReader actually know the type information? (It is also type erased - right?). – KyleM May 13 '16 at 21:07
  • I suspect this of using the standard trick: type erasure erases generics from _objects_ but not _classes_. – Louis Wasserman May 13 '16 at 21:09
  • There's a dark magic trick as described here: http://stackoverflow.com/a/15999255/1743880. – Tunaki May 13 '16 at 21:10
  • @Tunaki I kind of get it but not fully. I didn't create an anonymous class anywhere. In the example given, the author creates an anonymous class and passes it to a method. I didn't do that anywhere. So wouldn't my type still be erased at runtime? – KyleM May 13 '16 at 21:20
  • Not exactly but you did implement an interface with the parameterized type (so it's conceptually the same thing). The Spring framework uses the same techniques to get the actual parameterized type. I found this other related answer http://stackoverflow.com/a/2226038/1743880 – Tunaki May 13 '16 at 21:26
  • It should _"just work"_. Can you post a small, minimal project on github of it _not working_. – Paul Samsotha May 14 '16 at 00:35
  • @peeskillet My Transaction class is defined as Transaction. I found that using jersey-media-json-jackson I get a ClassCastException "Person cannot be cast to Customer" so clearly it isn't creating the expected type, it's creating the base class type instead. When I switched to using Genson instead, that problem went away. In summary... I tried to make an example for you but the problem didn't replicate. However the problem definitely exists when using certain JARs, maybe it's a bad configuration. – KyleM May 17 '16 at 16:47

1 Answers1

4

Take a look at the javadoc.

genericType - the type of instance to be produced. E.g. if the message body is to be converted into a method parameter, this will be the formal type of the method parameter as returned by Method.getGenericParameterTypes.

Generic parameterization in classes, methods, and fields is maintained in the corresponding .class files. This is obviously necessary for compilation. You wouldn't be able to package and use Java classes as a library if this wasn't the case.

Reflection exposes this type information (and more).

Jersey basically scans your classes for handler methods, ones annotated with @POST and the like and retrieves corresponding Method objects. Once it has that, it can use Method#getGenericParameterTypes()

Returns an array of Type objects that represent the formal parameter types, in declaration order, of the executable represented by this object. Returns an array of length 0 if the underlying executable takes no parameters.

If a formal parameter type is a parameterized type, the Type object returned for it must accurately reflect the actual type parameters used in the source code.

On other words, the Type has to represent both the parameter's raw type and its parameterization, if it has one.

So Jersey retrieves those types and feeds them into your MessageBodyReader. The expectation is that your implementation should be able to handle the genericType.

I don't know Jersey that well, but if it follows the same strategy as Spring MVC, it will simply iterate over all the MessageBodyReader instances that were registered. It doesn't actually care about its type (eg. your TransactionMessageBodyReader), that's why it relies on isReadable. It could theoretically keep a mapping of types to MessageBodyReader implementations, but that becomes unwieldy in the end and less flexible. In any case, an application doesn't typically have that many types/formats it can serialize to, so iterating a few objects isn't that big of a deal.


Since Java 8, you also have access to Executable#getParameters (Method is a subtype of Executable).


Here's an example of what Jersey could do

public class StackOverflow {
    public static void main(String[] args) throws Exception {
        Method method = StackOverflow.class.getDeclaredMethod("method", List.class);
        Type[] parameterTypes = method.getGenericParameterTypes();
        for (Type parameterType : parameterTypes) {
            System.out.println(parameterType.getClass());
            System.out.println(parameterType);
            ParameterizedType parameterizedType = (ParameterizedType) parameterType;
            System.out
                    .println(parameterizedType.getRawType().getTypeName() + "<" + parameterizedType.getActualTypeArguments()[0].getTypeName() + ">");
        }
    }

    public String method(List<StackOverflow> parameter) {
        return "example";

    }
}

prints

class sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
java.util.List<com.example.StackOverflow>
java.util.List<com.example.StackOverflow>
Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • Thanks. So if Jersey can 'see' all the method types, why do I need to write a MessageBodyReader at all? Without one, Jersey does not seem to know the generic type parameters. Other posters in this discussion have said that libraries use tricks like anonymous classes to discover the type parameters. So it doesn't seem to be as simple as just 'it's in the class file so it's available'. – KyleM May 13 '16 at 22:12
  • @KyleM Read the type and do what with it? Jersey might have a default implementation for handling JSON, XML, and standard formats. But once you need something more complicated or less common, you need your own implementation. – Sotirios Delimanolis May 13 '16 at 22:15
  • My point is that from the checkStatus() method I posted above, Jersey has no way of knowing what the generic type parameter is. Hence the necessity of the MessageBodyReader... – KyleM May 13 '16 at 22:18
  • @KyleM It does a have way, as stated in the answer. The `MessageBodyReader` is for defining a _Contract for a provider that supports the conversion of a stream to a Java type._ – Sotirios Delimanolis May 13 '16 at 22:19
  • @KyleM If you have a parameter of type `MyPojo`, where/how should Jersey generate a value of that type in order to invoke your method? That's what `MessageBodyReader` gives you. – Sotirios Delimanolis May 13 '16 at 22:20
  • Jersey automatically generates simple types. It's only when you start using generic parameters that it stops working. I can ascertain from this discussion that it has to do with Jersey not knowing what the generic type parameter is using the @Consumes method alone. Once you implement the MessageBodyReader, this somehow clues Jersey in to what the Generic type parameter is. That is the mechanism I am trying to understand better. Sorry if I'm misunderstanding you. – KyleM May 13 '16 at 22:27
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/111901/discussion-between-kylem-and-sotirios-delimanolis). – KyleM May 13 '16 at 22:27