4

I work in a project with an old version of Jersey (1.19) along with old Jackson (1.19.13). I would like to switch, to new Jackson (2.x) deserialization, but only for new endpoints (and old ones gradually) because migration in one step to Jackson 2 and/or Jersey 2 would be really difficult (oh, the monolith!).

I've seen some topics about how to provide custom-configured ObjectMapper with Jackson providers in Jersey, or how to install new Jackson in 1.x Jersey but that's not I'm looking for, at least not all.

What I imagine as a solution, is (preferably) annotating my new JAX-RS endpoint with something like @UseJackson2 , or having some base class with some magic retrieving ObjectMapper from the correct package, for this particular endpoint and extending it later - in other words forcing given endpoint to use other (de)serialization provider than normally.

I've seen examples with providers for differently configured ObjectMappers (like here Using Jackson in Jersey with multiple configured ObjectMappers), but in my case, the ObjectMappers would come from completely different packages/maven artifacts.

Michał Wróbel
  • 558
  • 1
  • 4
  • 13
  • 1
    I would try creating my own MessageBodyReader/Writer and use that to delegate calls to the 1.x reader/writer or the 2.x reader/writer. Check the `Annotation[]` argument to see whether your `@UseJackson2` annotation is present or not. This is just an idea. Not something I have every tried. – Paul Samsotha Nov 02 '17 at 15:03
  • 1
    Or you can try to extend the 2.x reader/writer and override isReadable and isWritable and check for the annotation. Make sure this provider has priority over the 1.x one. – Paul Samsotha Nov 02 '17 at 15:08
  • From what I understand here (https://docs.oracle.com/javaee/7/api/javax/ws/rs/ext/MessageBodyReader.html) though, the annotations collection represents Annotations on the parameter to be deserialized, not the enclosing JAX-RS endpoint class. (So for instance `void put(@Jackson2 TheType deserializeMe) ` . So code would be easily polluted as annotation would have to be put on every payload parameter. (And how to apply it to response?) Or I did get something wrong. Interesting find anyway. – Michał Wróbel Nov 02 '17 at 15:27
  • Is it possible to get, let's say some '(de)serialization context' , to know for instance from what endpoint class the deserialization came from? – Michał Wróbel Nov 02 '17 at 15:29
  • _"And how to apply it to response"_. Looks like it would need to go on the method itself (writer checks the method for annotation, reader checks parameter). So two locations. I know, ugly. Not pretty solution. _"Is it possible to get, let's say some '(de)serialization context' , to know for instance from what endpoint class the deserialization came from?"_. I was thinking the same thing. In Jersey 2.x there is `ResourceInfo` you can inject. I don't work much with 1.x so I am not sure. – Paul Samsotha Nov 02 '17 at 15:32
  • 1
    So you can inject `@Context ExtendedUriInfo` into the reader/writer. You can get all the information you need there. – Paul Samsotha Nov 02 '17 at 16:00
  • You can do `if (info.getMatchedMethod().getDeclaringResource().isAnnotationPresent(Jackson2.class)` – Paul Samsotha Nov 02 '17 at 16:10
  • in this scenario we have one reader/writer per (de)serialized class?, or do you think it would be simple to write a reader/writer for , which would behave correctly (i.e. taking @JsonDeserialize annotation into account, collections.. ) ? – Michał Wróbel Nov 02 '17 at 16:16
  • I would just extend the 2.x reader writer (see https://github.com/FasterXML/jackson-jaxrs-providers). This is what you would extend https://github.com/FasterXML/jackson-jaxrs-providers/blob/master/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JacksonJaxbJsonProvider.java/. Override the `isReadable` and `isWritable`. Return false if the annotation isn't present or return super call. I'm playing around with it right now. Trying it working. – Paul Samsotha Nov 02 '17 at 16:22
  • Yeah I don't know. I'm getting weird behavior where the reader/writer will not register if I use the Jackson 2 provider in any way (extending or composition). Weird. Not sure why this is happening. – Paul Samsotha Nov 02 '17 at 17:14

2 Answers2

3

What you can do is create a MessageBodyReader/Writer that handles the 2.x version of Jackson. The isReadable and isWritable methods determine which entities it can handle. What you can do to check is inject Jersey's ExtendedUriInfo into the provider and check the resource class for your @Jackson2 annotation. If the annotation is not present than your provider ignores the entity and the runtime moves on to the next provider and checks if it can handle it; in this case the 1.x provider.

Instead of completely creating your own, you would just extend the provider that Jackson already provides in it artifact

<dependency>
    <groupId>com.fasterxml.jackson.jaxrs</groupId>
    <artifactId>jackson-jaxrs-json-provider</artifactId>
    <version>2.9.2</version>
</dependency>

You would extend the JacksonJaxbJsonProvider.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Jackson2 {
}

@Provider
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class MyProvider extends JacksonJaxbJsonProvider {

    @Context
    private ExtendedUriInfo info;

    @Override
    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        if (!info.getMatchedMethod().getDeclaringResource().isAnnotationPresent(Jackson2.class)) {
            return false;
        }
        return super.isReadable(type, genericType, annotations, mediaType);
    }



    @Override
    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        if (!info.getMatchedMethod().getDeclaringResource().isAnnotationPresent(Jackson2.class)) {
            return false;
        }
        return super.isWriteable(type, genericType, annotations, mediaType);
    }
}

Then just annotate your resource classes with @Jackson2.

I had problems earlier while trying to get this to work, because I was using Jackson 2.8.4. It seams the whole 2.8 line has a problem. Not sure what it is, but with 2.8, my provider would not register at all. I tested with minor versions 2.2-2.9 (excluding 2.8) and they all work.

As far as priority, I'm not sure about how Jersey determines precedence. If the 1.x provider were to be called first, then this solution would all fall apart. One way around this would be to use composition instead of inheritance, where you would Just determine which reader/writer (1.x or 2.x) to use inside your readFrom and writeTo methods. The 1.x version is also JacksonJaxbJsonProvider but it used the codehaus packaging. From what I tested though, my provider always gets called first, so this may not be needed. Unfortunately, I cannot confirm that this is by design.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • have to try it. as about the priorities maybe this annotation would make it? https://dennis-xlc.gitbooks.io/restful-java-with-jax-rs-2-0-2rd-edition/en/part1/chapter12/ordering_filters_and_interceptors.html – Michał Wróbel Nov 03 '17 at 09:41
  • I also started to wonder if the JacksonJaxbJsonProvider which is extended won't be called next? (undesirable) – Michał Wróbel Nov 03 '17 at 09:59
  • about priority : I just discovered following piece of code in MessageBodyFactory (for Jersey 1.19) : `initWriters(customWriterProviders, customWriterListProviders, providerServices.getProviders(MessageBodyWriter.class)); initWriters(writerProviders, writerListProviders, providerServices.getServices(MessageBodyWriter.class)); ` - this probably explains why your provider gets called first. – Michał Wróbel Nov 03 '17 at 10:55
  • Unfortunately after returning false from custom provder, the default Jackson 2.x JacksonJaxbJsonProvider gets called so it quite ruins the concept, I am now looking for a way to remove this provider from consideration but have no clue yet. – Michał Wróbel Nov 03 '17 at 10:56
  • 1
    It shouldn't be called. It should not even be registered, unless you are using classpath scanning. If so, yes the provider will be registered because it's annotated with `@Provider`. That's the only way I could see it being registered. If you're using classpath scan, you should change to just package scan. – Paul Samsotha Nov 03 '17 at 13:59
  • 1
    Or actually (though I didn't not experience this when testing - maybe just a class loading discrepancy), it might be because of the [services files](https://github.com/FasterXML/jackson-jaxrs-providers/tree/master/json/src/main/resources/META-INF/services). Jersey will use these to [service load](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) the providers. You can try removing those from the jar. – Paul Samsotha Nov 03 '17 at 14:06
  • 1
    Yeah, I just tested, it's definitely a class loading (order) problem. I just change my (maven) dependencies around, and I did face the same problem. Originally I had the 1.x jersey-son first, but switched it to after the jackson-jaxrs-json-provider, and yup, same problem. You will just need to remove the services files so it isn't automatically registered. – Paul Samsotha Nov 03 '17 at 14:12
  • After removing it from `jackson-jaxrs-json-provider` jar in Tomcat it indeed works. But removing it from mvn dependency during the build could be tricky.. – Michał Wróbel Nov 03 '17 at 15:19
  • Haven't tried it, but [this looks promising](https://stackoverflow.com/a/20539586/2587435) – Paul Samsotha Nov 03 '17 at 15:29
  • Yep, I just read the same thing, probably will give it a try. But maybe some way of removing the provider from jersey post-init, could be clearer? But from what I've seen in MessageBodyFactory the init process is not well, quite open-closed principle like. Maybe using reflection.. but that's also not clean. – Michał Wróbel Nov 03 '17 at 15:33
  • In my test, I created 2 providers for 1.x and 2.x respectively and registered both. This works fine. – Leon Feb 13 '19 at 05:55
  • @Leon and you used this method of checking for an annotation in both providers? – Paul Samsotha Feb 13 '19 at 06:19
  • @PaulSamsotha I pasted the code as it's long. I did not use annotation. – Leon Feb 13 '19 at 09:48
1

Based on the solution of Paul Samsotha, creaing 2 providers worked fine for me.

public class Main {
  public static void main(final String[] args) {
    ResourceConfig config = new ResourceConfig();
    config.register(Service1.class)
        .register(Service2.class)
        .register(JacksonV1Provider.class)
        .register(JacksonV2Provider.class);
    GrizzlyHttpServerFactory.createHttpServer(URI.create("http://0.0.0.0:8123"), config);
  }
}

@Provider
public class JacksonV1Provider extends org.codehaus.jackson.jaxrs.JacksonJsonProvider {
  @Context
  private ExtendedUriInfo uriInfo;

  @Override
  public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
    return uriInfo.getPath().contains("/v1/");
  }

  @Override
  public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
    return uriInfo.getPath().contains("/v1/");
  }
}

@Provider
public class JacksonV2Provider extends com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider {
  @Context
  private ExtendedUriInfo uriInfo;

  @Override
  public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
    return uriInfo.getPath().contains("/v2/");
  }

  @Override
  public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
    return uriInfo.getPath().contains("/v2/");
  }
}

dependencies {
    compile group: 'org.glassfish.jersey.containers', name: 'jersey-container-grizzly2-http', version: '2.17'
    compile 'org.glassfish.jersey.media:jersey-media-json-jackson:2.17'

    compile group: "org.codehaus.jackson", name: "jackson-mapper-asl", version: "$jackson1_version"
    compile group: "org.codehaus.jackson", name: "jackson-jaxrs", version: "$jackson1_version"
    compile group: "org.codehaus.jackson", name: "jackson-xc", version: "$jackson1_version"

    compile "com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:$jackson2_vervion"
    compile "com.fasterxml.jackson.core:jackson-core:$jackson2_vervion"
    compile "com.fasterxml.jackson.core:jackson-annotations:$jackson2_vervion"
}
Leon
  • 3,124
  • 31
  • 36