4

Summary

Jackson annotations applied to POJOs exported through an OSGi depdendency does not work when the POJO is serialized in an importing bundle at Runtime. If the POJO is placed in the using bundle directly, or tested in a Unit Test (in either bundle), everything works as expected.

Does anyone know what could make the runtime serialization ignore the Jackson-annotations at runtime in the importing OSGi-bundle?


This is a looong question. I have tried to create an as simple example as possible. If anything is unclear, please let me know, and I will try to elaborate.

Contents

  • Inside Exporting Bundle

    • POJO
    • Unit test (which works)
    • Runtime test (which works)
  • Inside Importing Bundle

    • Unit test (which works)
    • Runtime test (which FAILS)
  • Runtime environment details

  • Bundle manifests (simplified)

Simplified example:

Let's assume we have want to serialize a simple POJO exported and imported over OSGi. The JSON-annotations should work both in the importing and exporting bundle, both runtime and during unit tests (runtime fails in the when imported).


Inside Exporting Bundle:

Both runtime and unit tests of the Jackson serialization works just fine in the bundle where the POJO itself is declared.

POJO to serialize

The @JsonProperty-annotation should make any serialized version of this POJO look something like {"correctSerializedName":"someName"} and not {"javaName":"someName"}:

package exporting.osgi.bundle.models;

import com.fasterxml.jackson.annotation.JsonProperty;

public class DependencyModel {

    private String javaName;

    @JsonProperty("correctSerializedName")
    public String getJavaName() {
        return javaName;
    }

    public DependencyModel(String javaName) {
        this.javaName = javaName;
    }
}

Correct behaviour: Unit test

import com.fasterxml.jackson.*;
import org.junit.jupiter.*;

class ExportingBundleTests {

    @Test
    void serialize_inDepdendencyProject_getsCorrectJsonName() {
        DependencyModel dependencyModel = new DependencyModel("name");

        ObjectMapper mapper = new ObjectMapper();
        String jsonString = mapper.writeValueAsString(dependencyModel);

        // Asserts True -> serialization works as expected
        assertEquals(jsonString, "{\"correctSerializedName\":\"name\"}");
    }    
}

Correct behaviour: Runtime

public void runtimeFromDepdendency() {
    DependencyModel dependencyModel = new DependencyModel("name");

    ObjectMapper mapper = new ObjectMapper();
    String jsonString = mapper.writeValueAsString(dependencyModel);

    // jsonString = {"correctSerializedName":"name"}
}

Inside the importing OSGi bundle

Correct behaviour: Unit test

import exporting.osgi.bundle.models.DependencyModel;
import com.fasterxml.jackson.*;
import org.junit.jupiter.api.*;

class ImportingBundleTests {

    @Test
    void serialize_inUsingProject_getsCorrectJsonName() {

        DependencyModel dependencyModel = new DependencyModel("name");

        ObjectMapper mapper = new ObjectMapper();
        String jsonString = mapper.writeValueAsString(dependencyModel);

        // Asserts True -> serialization works as expected
        assertEquals(jsonString, "{\"correctSerializedName\":\"name\"}");
    }
}

FAILS: Runtime

import exporting.osgi.bundle.models.DependencyModel;
import com.fasterxml.jackson.*;

public Response runsFromUsingProject() throws JsonProcessingException {
    DependencyModel dependencyModel = new DependencyModel("name");

    ObjectMapper mapper = new ObjectMapper();
    String jsonString = mapper.writeValueAsString(dependencyModel);

    // jsonString = {"javaName":"name"}   <---- WHICH IS WRONG
}

Runtime environment

  • Jira 7.2.2 OSGi (based on Apache Felix)
  • Maven 3.2.1
  • Java 1.8
  • Jackson 2.9.3

Exporting plugin Bundle Manifest (simplified)

Created-By: Apache Maven Bundle Plugin
Manifest-Version: 1.0
Build-Jdk: 1.8.0_111
Bundle-ManifestVersion: 2

Export-Package:
...
exporting.osgi.bundle;version="1.0.0";
    uses:="exporting.osgi.bundle.models",
...

Originally-Created-By: Apache Maven Bundle Plugin
Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"
Spring-Context: *
Tool: Bnd-2.4.1.201501161923

Importing plugin Bundle Manifest (simplified)

Created-By: Apache Maven Bundle Plugin
Manifest-Version: 1.0
Build-Jdk: 1.8.0_111
Bundle-ManifestVersion: 2
Archiver-Version: Plexus Archiver
Bundle-ClassPath: .,META-INF/lib/gt-epsg-hsql-18.1.jar,META-INF/lib/sq
lite-jdbc-3.8.11.1.jar

Import-Package: 
...
exporting.osgi.bundle.models
...
com.fasterxml.jackson.dataformat.xml;resolution:=optional,
com.fasterxml.jackson.dataformat.xml.deser;resolution:=optional,
com.fasterxml.jackson.dataformat.xml.ser;resolution:=optional,
org.codehaus.jackson;resolution:=optional,
org.codehaus.jackson.annotate;resolution:=optional,
org.codehaus.jackson.map;resolution:=optional,
org.codehaus.jackson.map.annotate;resolution:=optional,
...

Originally-Created-By: Apache Maven Bundle Plugin
Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"
Spring-Context: *
Tool: Bnd-2.4.1.201501161923

As illustrated above, the @JsonProperty-annotation is ignored at runtime in the importing depdendency, but everything else works as expected. Again, I don't think this is a Jackson-version issue, although I might have overlooked something.

Have I perhaps missed some fundamental OSGi-behaviour?

Tormod Haugene
  • 3,538
  • 2
  • 29
  • 47
  • 1
    It sounds like you're using two dynamically-loaded P2 plugins inside Jira. Is it possible that your dependent plugin could be using an earlier version of the same class (previously loaded in an earlier incarnation of the first plugin) that did not have that annotation present? Restarting Jira after loading both plugins can help you decide if this theory holds water. – Scott Dudley Jun 12 '18 at 17:42
  • 1
    As you are using Jackson 2, why does your importing bundle import classes from old Jackson 1 (org.codehaus)? Also, are you deploying the jackson bundles to your OSGi container? If not, I recommend you try that, and ensure that both your importing and exporting bundle have `Import-Package` on the relevant jackson packages. The three most central bundles are: `jackson-annotations`, `jackson-core` and `jackson-databind`. If both your bundles agree on which jackson classes to use, I think it should work. – gjoranv Jun 12 '18 at 19:37
  • 1
    I think @gjoranv is on to something. The OP likely doesn't have control over the container (end users can typically only add new OSGi bundles to Jira, but not touch what's available in the main container itself). However, if the OP is bundling Jackson 2 in each of the two plugins individually, the annotation classes in each will be different and I imagine it will function as described in the problem. In this case, if the OP adjusts his code to use the Jackson 1 implementation that is already included in Jira, both plugins will share the same classes and it should work. – Scott Dudley Jun 12 '18 at 20:34
  • Thanks for the insight, Scott and Gjoranv! I will answer each comment in order: (1, Scott - *restarting Jira*) I tried to restart Jira/OSGi, but the error still persisted unfortunately. (2, Gjoranv - *Codehaus imports*) Good question! The Jira platform itself uses Jackson 1, but it still shouldn't be needed in the "importing bundle" (we are trying to use Jackson 2 as standard). I will try to see if I can find the code that imports the Codehaus version, and switch it over to FasterXml. – Tormod Haugene Jun 13 '18 at 07:39
  • (3, Gjoranv/Scott *exposing Jackson 2 to OSGi*): Your assumption is correct, I currently have a *compile* dependency to Jackson 2.9.3 in *both bundles*. Are you suggesting that it might help to export the Jackson packages from the first bundle, importing those into the second bundle, and then change the mvn-dependency in the importing bundle to *provided*? Could you please elaborate why this would be different (I'm not that familiar with how Jackson works at a detailed level). Regarding switching to Jackson 1; although this is possible, I find the feature set/syntax in v. 2 much better.. :-) – Tormod Haugene Jun 13 '18 at 07:47
  • 1
    The simplest and best would be to deploy the jackson2 bundles along with your own bundles, and make sure both your bundles have `Import-Package` (use provided scope). Exporting the jackson2 packages from one of the bundles, and importing it the other should work the same way, but will give you the extra work of figuring out which packages to export. The correct set of packages are already exported from the jackson2 bundles. Why does it matter? i suspect (from experience) that the problem is that your two bundles are using different classes for the same jackson annotations. – gjoranv Jun 13 '18 at 08:53
  • 1
    When the jackson framework scans classes for annotations, it looks for the specific annotation class instance that it uses. If a bundle is using its own instance of the same class, the annotation will not be recognized. In OSGi, each bundle has a separate class loader, and each class loader can contain its own instance (but only one) of a given class. – gjoranv Jun 13 '18 at 08:58
  • @gjoranv, thank you for the explanation! It seems like your suspicion was spot on: I tried exporting/importing `com.fasterxml.jackson` from the first bundle to the other, and that worked! :-) I'll do some research on how to best expose Jackson to OSGi, since I believe I will have to do it manually due to the overlay Jira has for Apache Felix. Either way, I'd be struggling with this for quite some time if you and Scott hadn't stepped in, so thanks again! Also, if you'd like to post the suggestion as an answer I'll accept that afterwards and update it with some concrete examples. ;-) – Tormod Haugene Jun 13 '18 at 11:10
  • @TormodHaugene, glad I could help! – gjoranv Jun 13 '18 at 15:10

2 Answers2

2

Based on my comments and your feedback:

The problem is that your two bundles have different instances of the Jackson annotation classes. When the Jackson framework scans classes for annotations, it looks for the specific annotation class instance that it uses. If a bundle has its own instance of the same class, the annotation will not be recognised. (In OSGi, each bundle has a separate class loader, and each class loader can contain its own instance of a given class.)

You can resolve this in two ways:

  1. Deploy jackson2 as separate bundles to your OSGi container, and ensure that both your importing and exporting bundle have Import-Package on the relevant jackson packages. The most central bundles are: jackson-annotations, jackson-core and jackson-databind. In this case, use scope provided for the jackson2 dependencies in both your bundles.
  2. Export the jackson2 packages from one of your bundles, and import it in the other. This will give you the extra work of figuring out and maintaining which packages to export. In this case, use scope compile for jackson2 in the exporting bundle, and scope provided in the importing bundle.
gjoranv
  • 4,376
  • 3
  • 21
  • 37
0

In my case there were Jackson imports that I wasn't taking into account. I just added them in the pom after I saw your comment. It worked perfectly.