Search code examples
javajacksonosgijiraapache-felix

Imported Jackson Annotations Ignored for POJOs in OSGi Dependency at Runtime


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?


Solution

  • 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.