Erfenis met Jackson

1. Overzicht

In dit artikel gaan we kijken naar het werken met klassenhiërarchieën in Jackson.

Twee typische use-cases zijn het opnemen van metagegevens van het subtype en het negeren van eigenschappen die zijn geërfd van superklassen. We gaan die twee scenario's beschrijven en een paar omstandigheden waarin een speciale behandeling van subtypen nodig is.

2. Opname van subtype-informatie

Er zijn twee manieren om type-informatie toe te voegen bij het serialiseren en deserialiseren van gegevensobjecten, namelijk globale standaardtypering en annotaties per klasse.

2.1. Globaal standaard typen

De volgende drie Java-klassen zullen worden gebruikt om de globale opname van type metadata te illustreren.

Voertuig superklasse:

openbare abstracte klasse Vehicle {private String make; privé String-model; beschermd voertuig (merk string, model string) {this.make = make; this.model = model; } // no-arg constructor, getters en setters}

Auto subklasse:

openbare klasse Auto verlengt Voertuig {privé int zitplaatsenCapaciteit; privé dubbele topSpeed; openbare auto (merk String, model String, int seatCapacity, dubbele topSpeed) {super (merk, model); this.seatingCapacity = seatCapacity; this.topSpeed ​​= topSpeed; } // no-arg constructor, getters en setters}

Vrachtauto subklasse:

openbare klasse Truck breidt voertuig {private double payloadCapacity; openbare vrachtwagen (merk string, model string, dubbele laadcapaciteit) {super (merk, model); this.payloadCapacity = payloadCapacity; } // no-arg constructor, getters en setters}

Globaal standaard typen maakt het mogelijk dat type-informatie slechts één keer wordt gedeclareerd door het in te schakelen op een ObjectMapper voorwerp. Metagegevens van dat type worden vervolgens op alle aangewezen typen toegepast. Hierdoor is het erg handig om deze methode te gebruiken voor het toevoegen van type metadata, zeker als het om een ​​groot aantal typen gaat. Het nadeel is dat het volledig gekwalificeerde Java-typenamen gebruikt als type-ID's, en dus ongeschikt is voor interacties met niet-Java-systemen, en alleen van toepassing is op verschillende vooraf gedefinieerde soorten typen.

De Voertuig structuur hierboven wordt gebruikt om een ​​instantie van te vullen Vloot klasse:

openbare klasse Fleet {private List-voertuigen; // getters en setters}

Om metagegevens van het type in te sluiten, moeten we de typefunctie inschakelen op het ObjectMapper object dat later zal worden gebruikt voor serialisatie en deserialisatie van data-objecten:

ObjectMapper.enableDefaultTyping (ObjectMapper.DefaultTyping toepasbaarheid, JsonTypeInfo.As includeAs)

De toepasbaarheid parameter bepaalt de typen die type-informatie vereisen, en de omvattenAs parameter is het mechanisme voor het opnemen van type metagegevens. Bovendien zijn er twee andere varianten van de enableDefaultTyping methode worden verstrekt:

  • ObjectMapper.enableDefaultTyping (ObjectMapper.DefaultTyping toepasbaarheid): stelt de beller in staat het toepasbaarheid, Tijdens het gebruik WRAPPER_ARRAY als de standaardwaarde voor omvattenAs
  • ObjectMapper.enableDefaultTyping (): toepassingen OBJECT_AND_NON_CONCRETE als de standaardwaarde voor toepasbaarheid en WRAPPER_ARRAY als de standaardwaarde voor omvattenAs

Laten we eens kijken hoe het werkt. Om te beginnen moeten we een ObjectMapper object en schakel het standaard typen in:

ObjectMapper-mapper = nieuwe ObjectMapper (); mapper.enableDefaultTyping ();

De volgende stap is het instantiëren en vullen van de datastructuur die aan het begin van deze subsectie is geïntroduceerd. De code om dit te doen zal later in de volgende subsecties worden hergebruikt. Voor het gemak en voor hergebruik noemen we het de voertuig instantiatie blok.

Auto auto = nieuwe auto ("Mercedes-Benz", "S500", 5, 250,0); Truck truck = nieuwe Truck ("Isuzu", "NQR", 7500.0); Lijst voertuigen = nieuwe ArrayList (); voertuigen.add (auto); voertuigen.add (vrachtwagen); Vloot serializedFleet = nieuwe vloot (); serializedFleet.setVehicles (voertuigen);

Die bevolkte objecten worden vervolgens geserialiseerd:

String jsonDataString = mapper.writeValueAsString (serializedFleet);

De resulterende JSON-tekenreeks:

{"vehicles": ["java.util.ArrayList", [["org.baeldung.jackson.inheritance.Car", {"make": "Mercedes-Benz", "model": "S500", "seatsCapacity" : 5, "topSpeed": 250.0}], ["org.baeldung.jackson.inheritance.Truck", {"make": "Isuzu", "model": "NQR", "payloadCapacity": 7500.0}]]] }

Tijdens deserialisatie worden objecten hersteld uit de JSON-reeks met behoud van typegegevens:

Fleet deserializedFleet = mapper.readValue (jsonDataString, Fleet.class);

De opnieuw gemaakte objecten zullen dezelfde concrete subtypen zijn als vóór de serialisering:

assertThat (deserializedFleet.getVehicles (). get (0), instanceOf (Car.class)); assertThat (deserializedFleet.getVehicles (). get (1), instanceOf (Truck.class));

2.2. Aantekeningen per klas

Annotatie per klasse is een krachtige methode om type-informatie op te nemen en kan erg handig zijn voor complexe gebruikssituaties waarbij een aanzienlijk niveau van aanpassing nodig is. Dit kan echter alleen worden bereikt ten koste van complicaties. Annotaties per klasse hebben voorrang op de algemene standaardtypering als de typegegevens op beide manieren zijn geconfigureerd.

Om van deze methode gebruik te maken, moet het supertype worden geannoteerd met @JsonTypeInfo en verschillende andere relevante annotaties. In deze subsectie wordt een datamodel gebruikt dat lijkt op het Voertuig structuur in het vorige voorbeeld om annotaties per klas te illustreren. De enige verandering is de toevoeging van annotaties aan Voertuig abstracte klasse, zoals hieronder weergegeven:

@JsonTypeInfo (gebruik = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonSubTypes ({@Type (waarde = Auto.klasse, naam = "auto"), @Type (waarde = Truck.class, name = "truck")}) openbare abstracte klasse Voertuig {// velden, constructeurs, getters en setters}

Data-objecten worden gemaakt met behulp van de voertuig instantiatie blok geïntroduceerd in de vorige subsectie, en vervolgens geserialiseerd:

String jsonDataString = mapper.writeValueAsString (serializedFleet);

De serialisatie levert de volgende JSON-structuur op:

{"vehicles": [{"type": "car", "make": "Mercedes-Benz", "model": "S500", "seatCapacity": 5, "topSpeed": 250.0}, {"type" : "truck", "make": "Isuzu", "model": "NQR", "payloadCapacity": 7500.0}]}

Die tekenreeks wordt gebruikt om gegevensobjecten opnieuw te maken:

Fleet deserializedFleet = mapper.readValue (jsonDataString, Fleet.class);

Ten slotte wordt de hele voortgang gevalideerd:

assertThat (deserializedFleet.getVehicles (). get (0), instanceOf (Car.class)); assertThat (deserializedFleet.getVehicles (). get (1), instanceOf (Truck.class));

3. Negeren van eigenschappen van een supertype

Soms moeten sommige eigenschappen die zijn geërfd van superklassen worden genegeerd tijdens serialisering of deserialisatie. Dit kan worden bereikt door een van de drie methoden: annotaties, mix-ins en annotatie-introspectie.

3.1. Annotaties

Er zijn twee veelgebruikte Jackson-annotaties om eigenschappen te negeren, namelijk @JsonIgnore en @JsonIgnoreProperties. De eerste wordt rechtstreeks toegepast op typeleden, waarbij Jackson wordt verteld de overeenkomstige eigenschap te negeren bij het serialiseren of deserialiseren. Dit laatste wordt op elk niveau gebruikt, inclusief type en type lid, om eigenschappen weer te geven die moeten worden genegeerd.

@JsonIgnoreProperties is krachtiger dan de andere omdat het ons in staat stelt eigenschappen te negeren die zijn geërfd van supertypes waarover we geen controle hebben, zoals typen in een externe bibliotheek. Bovendien stelt deze annotatie ons in staat om veel eigenschappen tegelijk te negeren, wat in sommige gevallen kan leiden tot meer begrijpelijke code.

De volgende klassenstructuur wordt gebruikt om het gebruik van annotaties te demonstreren:

openbare abstracte klasse Vehicle {private String make; privé String-model; beschermd voertuig (merk string, model string) {this.make = make; this.model = model; } // no-arg constructor, getters and setters} @JsonIgnoreProperties ({"model", "seatCapacity"}) openbare abstracte klasse Auto breidt voertuig uit {private int seatCapacity; @JsonIgnore privé dubbele topSpeed; beschermde auto (merk String, model String, int seatCapacity, dubbele topSpeed) {super (merk, model); this.seatingCapacity = seatCapacity; this.topSpeed ​​= topSpeed; } // no-arg constructor, getters and setters} public class Sedan breidt auto uit {public Sedan (merk string, string model, int seatCapacity, double topSpeed) {super (merk, model, seatCapacity, topSpeed); } // no-arg constructor} public class Crossover breidt Car {private double towingCapacity; openbare Crossover (merk String, String model, int seatCapacity, double topSpeed, double towingCapacity) {super (merk, model, seatCapacity, topSpeed); this.towingCapacity = towingCapacity; } // no-arg constructor, getters en setters}

Zoals je kunt zien, @JsonIgnore vertelt Jackson om te negeren Car.topSpeed eigendom, terwijl @JsonIgnoreProperties negeert de Voertuigmodel en AutostoelenCapaciteit degenen.

Het gedrag van beide annotaties wordt gevalideerd door de volgende test. Eerst moeten we instantiëren ObjectMapper en dataklassen, gebruik die dan ObjectMapper instantie om gegevensobjecten te serialiseren:

ObjectMapper-mapper = nieuwe ObjectMapper (); Berline sedan = nieuwe Berline ("Mercedes-Benz", "S500", 5, 250.0); Crossover-crossover = nieuwe Crossover ("BMW", "X6", 5, 250.0, 6000.0); Lijst voertuigen = nieuwe ArrayList (); voertuigen.add (sedan); vehicles.add (crossover); String jsonDataString = mapper.writeValueAsString (voertuigen);

jsonDataString bevat de volgende JSON-array:

[{"make": "Mercedes-Benz"}, {"make": "BMW", "towingCapacity": 6000.0}]

Ten slotte zullen we de aanwezigheid of afwezigheid van verschillende eigenschapsnamen in de resulterende JSON-string bewijzen:

assertThat (jsonDataString, containsString ("make")); assertThat (jsonDataString, not (containsString ("model"))); assertThat (jsonDataString, niet (bevatString ("seatCapacity"))); assertThat (jsonDataString, not (containsString ("topSpeed"))); assertThat (jsonDataString, containsString ("towingCapacity"));

3.2. Mix-ins

Mix-ins stellen ons in staat om gedrag toe te passen (zoals het negeren van eigenschappen bij het serialiseren en deserialiseren) zonder de noodzaak om direct annotaties op een klasse toe te passen. Dit is vooral handig bij het omgaan met klassen van derden, waarin we de code niet rechtstreeks kunnen wijzigen.

Deze subsectie hergebruikt de klasse-overervingsketen die in de vorige is geïntroduceerd, behalve dat de @JsonIgnore en @JsonIgnoreProperties annotaties op de Auto klasse is verwijderd:

openbare abstracte klasse Auto verlengt Voertuig {privé int zitplaatsenCapaciteit; privé dubbele topSpeed; // velden, constructeurs, getters en setters}

Om de werking van mix-ins te demonstreren, zullen we negeren Voertuig. Merk en Car.topSpeed properties en gebruik vervolgens een test om er zeker van te zijn dat alles werkt zoals verwacht.

De eerste stap is het declareren van een mix-in-type:

privé abstracte klasse CarMixIn {@JsonIgnore public String make; @JsonIgnore openbare String topSpeed; }

Vervolgens wordt de mix-in gebonden aan een dataklasse via een ObjectMapper voorwerp:

ObjectMapper-mapper = nieuwe ObjectMapper (); mapper.addMixIn (Car.class, CarMixIn.class);

Daarna instantiëren we data-objecten en serialiseren we ze in een string:

Berline sedan = nieuwe Berline ("Mercedes-Benz", "S500", 5, 250.0); Crossover-crossover = nieuwe Crossover ("BMW", "X6", 5, 250.0, 6000.0); Lijst voertuigen = nieuwe ArrayList (); voertuigen.add (sedan); vehicles.add (crossover); String jsonDataString = mapper.writeValueAsString (voertuigen);

jsonDataString bevat nu de volgende JSON:

[{"model": "S500", "seatCapacity": 5}, {"model": "X6", "seatCapacity": 5, "towingCapacity": 6000.0}]

Laten we tot slot het resultaat verifiëren:

assertThat (jsonDataString, not (containsString ("make"))); assertThat (jsonDataString, containsString ("model")); assertThat (jsonDataString, containsString ("seatCapacity")); assertThat (jsonDataString, not (containsString ("topSpeed"))); assertThat (jsonDataString, containsString ("towingCapacity"));

3.3. Annotatie Introspectie

Annotatie-introspectie is de krachtigste methode om supertype-eigenschappen te negeren, omdat het gedetailleerde aanpassingen mogelijk maakt met de AnnotationIntrospector.hasIgnoreMarker API.

Deze onderafdeling maakt gebruik van dezelfde klassenhiërarchie als de voorgaande. In dit geval zullen we Jackson vragen om te negeren Voertuigmodel, Crossover.towingCapacity en alle eigendommen gedeclareerd in het Auto klasse. Laten we beginnen met de declaratie van een klasse die de extensie JacksonAnnotatieIntrospector koppel:

klasse IgnoranceIntrospector breidt JacksonAnnotationIntrospector {public boolean hasIgnoreMarker (AnnotatedMember m)}

De introspector negeert alle eigenschappen (dat wil zeggen, hij behandelt ze alsof ze zijn gemarkeerd als genegeerd via een van de andere methoden) die voldoen aan de set voorwaarden die in de methode zijn gedefinieerd.

De volgende stap is het registreren van een exemplaar van het OnwetendheidIntrospector klas met een ObjectMapper voorwerp:

ObjectMapper-mapper = nieuwe ObjectMapper (); mapper.setAnnotationIntrospector (nieuwe IgnoranceIntrospector ());

Nu maken en serialiseren we data-objecten op dezelfde manier als in paragraaf 3.2. De inhoud van de nieuw geproduceerde string is:

[{"make": "Mercedes-Benz"}, {"make": "BMW"}]

Ten slotte zullen we controleren of de introspector werkte zoals bedoeld:

assertThat (jsonDataString, containsString ("make")); assertThat (jsonDataString, not (containsString ("model"))); assertThat (jsonDataString, niet (bevatString ("seatCapacity"))); assertThat (jsonDataString, not (containsString ("topSpeed"))); assertThat (jsonDataString, niet (bevatString ("towingCapacity")));

4. Subtype-afhandelingsscenario's

In dit gedeelte worden twee interessante scenario's behandeld die relevant zijn voor het afhandelen van subklassen.

4.1. Conversie tussen subtypen

Jackson staat toe dat een object wordt geconverteerd naar een ander type dan het originele. In feite kan deze conversie plaatsvinden tussen alle compatibele typen, maar het is het nuttigst wanneer deze wordt gebruikt tussen twee subtypen van dezelfde interface of klasse om waarden en functionaliteit te beveiligen.

Om de conversie van een type naar een ander te demonstreren, zullen we het Voertuig hiërarchie overgenomen uit sectie 2, met de toevoeging van de @JsonIgnore annotatie op eigendommen in Auto en Vrachtauto om incompatibiliteit te voorkomen.

openbare klasse Auto breidt voertuig uit {@JsonIgnore privé int zitplaatsenCapaciteit; @JsonIgnore privé dubbele topSpeed; // constructors, getters and setters} public class Truck breidt voertuig uit {@JsonIgnore private double payloadCapacity; // constructeurs, getters en setters}

De volgende code controleert of een conversie is geslaagd en dat het nieuwe object gegevenswaarden van het oude behoudt:

ObjectMapper-mapper = nieuwe ObjectMapper (); Auto auto = nieuwe auto ("Mercedes-Benz", "S500", 5, 250,0); Truck truck = mapper.convertValue (auto, Truck.class); assertEquals ("Mercedes-Benz", truck.getMake ()); assertEquals ("S500", truck.getModel ());

4.2. Deserialisatie zonder No-arg Constructors

Jackson maakt standaard gegevensobjecten opnieuw met behulp van constructors zonder argumenten. Dit is in sommige gevallen onhandig, zoals wanneer een klasse niet-standaard constructors heeft en gebruikers no-arg moeten schrijven om aan de eisen van Jackson te voldoen. Het is zelfs nog lastiger in een klassenhiërarchie waar een constructor zonder argumenten moet worden toegevoegd aan een klasse en alle hogere in de overervingsketen. In deze gevallen, creator methoden kom te hulp.

In deze sectie wordt een objectstructuur gebruikt die vergelijkbaar is met die in sectie 2, met enkele wijzigingen in constructors. In het bijzonder worden alle constructors zonder argumenten verwijderd en worden constructeurs van concrete subtypes geannoteerd met @RTLnieuws en @JsonProperty om ze scheppende methoden te maken.

public class Auto breidt voertuig uit {@JsonCreator public Car (@JsonProperty ("make") String make, @JsonProperty ("model") String model, @JsonProperty ("seats") int seatCapacity, @JsonProperty ("topSpeed") double topSpeed ) {super (merk, model); this.seatingCapacity = seatCapacity; this.topSpeed ​​= topSpeed; } // fields, getters and setters} public class Truck breidt voertuig uit {@JsonCreator public Truck (@JsonProperty ("make") String make, @JsonProperty ("model") String model, @JsonProperty ("payload") double payloadCapacity) {super (merk, model); this.payloadCapacity = payloadCapacity; } // velden, getters en setters}

Een test zal verifiëren dat Jackson kan omgaan met objecten die geen constructors zonder argumenten hebben:

ObjectMapper-mapper = nieuwe ObjectMapper (); mapper.enableDefaultTyping (); Auto auto = nieuwe auto ("Mercedes-Benz", "S500", 5, 250,0); Truck truck = nieuwe Truck ("Isuzu", "NQR", 7500.0); Lijst voertuigen = nieuwe ArrayList (); voertuigen.add (auto); voertuigen.add (vrachtwagen); Vloot serializedFleet = nieuwe vloot (); serializedFleet.setVehicles (voertuigen); String jsonDataString = mapper.writeValueAsString (serializedFleet); mapper.readValue (jsonDataString, Fleet.class);

5. Conclusie

Deze tutorial heeft verschillende interessante use-cases behandeld om Jackson's ondersteuning voor type-overerving te demonstreren, met een focus op polymorfisme en onwetendheid over supertype-eigenschappen.

De implementatie van al deze voorbeelden en codefragmenten is te vinden in een GitHub-project.