JAXB XML Adapters work via annotations but not via

2019-04-20 23:28发布

问题:

I understand all about how to use XMLAdapters to convert unmappable types, or just to change how certain objects are serialized/deserialized to XML. Everything works great if I use annotations (either package level or otherwise). The problem is that I am attempting to change the representations of third party objects which I cannot change the source code to (i.e. in order to inject annotations).

That shouldn't be a problem, considering that the Marshaller object has a method for manually adding adapters. Unfortunately, no matter what I do, I can't get the adapters set in this way to "kick in". For instance, I have a class representing a point in XYZ space (geocentric coordinates). In the XML I produce, I want this to be converted into lat/long/altitude (geodetic coordinates). Here are my classes:

Geocentric

package testJaxb;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class GeocentricCoordinate {
    // Units are in meters; see http://en.wikipedia.org/wiki/Geocentric_coordinates
    private double x;
    private double y;
    private double z;

    @XmlAttribute
    public double getX() {
        return x;
    }
    public void setX(double x) {
        this.x = x;
    }
    @XmlAttribute
    public double getY() {
        return y;
    }
    public void setY(double y) {
        this.y = y;
    }
    @XmlAttribute
    public double getZ() {
        return z;
    }
    public void setZ(double z) {
        this.z = z;
    }
}

Geodetic

package testJaxb;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
/**
 * @see http://en.wikipedia.org/wiki/Geodetic_system
 */
@XmlRootElement
public class GeodeticCoordinate {

    private double latitude;
    private double longitude;
    // Meters
    private double altitude;

    public GeodeticCoordinate() {
        this(0,0,0);
    }

    public GeodeticCoordinate(double latitude, double longitude, double altitude) {
        super();
        this.latitude = latitude;
        this.longitude = longitude;
        this.altitude = altitude;
    }

    @XmlAttribute
    public double getLatitude() {
        return latitude;
    }
    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }

    @XmlAttribute
    public double getLongitude() {
        return longitude;
    }

    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }

    @XmlAttribute
    public double getAltitude() {
        return altitude;
    }
    public void setAltitude(double altitude) {
        this.altitude = altitude;
    }



}

GeocentricToGeodeticLocationAdapter

package testJaxb;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.adapters.XmlAdapter;


/**
 * One of our systems uses xyz coordinates to represent locations. Consumers of our XML would much
 * prefer lat/lon/altitude.  This handles converting between xyz and lat lon alt.  
 * 
 * @author ndunn
 *
 */
public class GeocentricToGeodeticLocationAdapter extends XmlAdapter<GeodeticCoordinate,GeocentricCoordinate> {

    @Override
    public GeodeticCoordinate marshal(GeocentricCoordinate arg0) throws Exception {
        // TODO: do a real coordinate transformation
        GeodeticCoordinate coordinate = new GeodeticCoordinate();
        coordinate.setLatitude(45);
        coordinate.setLongitude(45);
        coordinate.setAltitude(1000);
        return coordinate;
    }

    @Override
    public GeocentricCoordinate unmarshal(GeodeticCoordinate arg0) throws Exception {
        // TODO do a real coordinate transformation
        GeocentricCoordinate gcc = new GeocentricCoordinate();
        gcc.setX(100);
        gcc.setY(200);
        gcc.setZ(300);
        return gcc;
    }
}

ObjectWithLocation field

package testJaxb; 
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class ObjectWithLocation {

    private GeocentricCoordinate location = new GeocentricCoordinate();

    public GeocentricCoordinate getLocation() {
        return location;
    }

    public void setLocation(GeocentricCoordinate location) {
        this.location = location;
    }


    public static void main(String[] args) {

        ObjectWithLocation object = new ObjectWithLocation();

        try {
            JAXBContext context = JAXBContext.newInstance(ObjectWithLocation.class, GeodeticCoordinate.class, GeocentricCoordinate.class);
            Marshaller marshaller = context.createMarshaller();

            marshaller.setAdapter(new GeocentricToGeodeticLocationAdapter());
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

            marshaller.marshal(object, System.out);

        }
        catch (JAXBException jaxb) {
            jaxb.printStackTrace();
        }
    }
}

Output:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<objectWithLocation>
    <location z="0.0" y="0.0" x="0.0"/>
</objectWithLocation>

By using an annotation (in my package-info.java file):

@javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters
({
@javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter(value=GeocentricToGeodeticLocationAdapter.class,type=GeocentricCoordinate.class),
})

package package testJaxb;

I get the following (desired) xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<objectWithLocation>
    <location longitude="45.0" latitude="45.0" altitude="1000.0"/>
</objectWithLocation>

So my question is twofold.

  1. Why does the adapter work when annotated, but not when explicitly set via the setAdapter method?
  2. How do I work around this problem when I have classes that I cannot annotate and whose package-info.java I cannot modify in order to add the annotations?

回答1:

The setAdapter(XmlAdapter) on Marshaller is used to pass in an initialized XmlAdapter for a property that is already annotated with @XmlJavaTypeAdapter. The link below is to an answer where I leverage this behaviour:

  • Using JAXB to cross reference XmlIDs from two XML files

If you want to map third party classes you could use EclipseLink JAXB (MOXy)'s XML mapping file (I'm the MOXy lead):

  • http://bdoughan.blogspot.com/2010/12/extending-jaxb-representing-annotations.html


回答2:

You always have to annotate with @XmlJavaTypeAdapter(...).

marshaller.setAdapter(...) is means to assign a custom initialized instance of your type adapter in case you have non default constructor initialisation.

Otherwise, if you have only one default constructor for your adapter, then you don't need to explicitly call .setAdapter(...) method.

Here is a great answer with more detailed explanation: JAXB: Isn't it possible to use an XmlAdapter without @XmlJavaTypeAdapter?

JAXB Runtime can only accept Adapter with No-args constructor .. (Obviously JAXBContext does not know about application specific model)

So thankfully there is an option :D

You can tell your unmarshaller to use given instance of UserAdapter rather than instating it by own its own.

public class Test {
public static void main(String... args) { 
    JAXBContext context = JAXBContext.getInstance(Event.class);
    Unmarshaller unmarshaller = context.createUnmarshaller();

      UserContext userContext = null; // fetch it from some where
      unmarshaller.setAdapter(UserAdapter.class, new UserAdapter(userContext));

      Event event = (Event) unmarshaller.unmarshal(..);
   }
}

setAdapter method is available on both Marshaller & Unmarshaller

Note: setAdapter on marshaller / unmarshaller does not mean that you don't have to use @XmlJavaTypeAdapter.