L'avvio di primavera @ResponseBody non serializza l'ID entità


95

Hai uno strano problema e non riesci a capire come affrontarlo. Avere un POJO semplice:

@Entity
@Table(name = "persons")
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "middle_name")
    private String middleName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "comment")
    private String comment;

    @Column(name = "created")
    private Date created;

    @Column(name = "updated")
    private Date updated;

    @PrePersist
    protected void onCreate() {
        created = new Date();
    }

    @PreUpdate
    protected void onUpdate() {
        updated = new Date();
    }

    @Valid
    @OrderBy("id")
    @OneToMany(mappedBy = "person", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<PhoneNumber> phoneNumbers = new ArrayList<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getMiddleName() {
        return middleName;
    }

    public void setMiddleName(String middleName) {
        this.middleName = middleName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    public Date getCreated() {
        return created;
    }

    public Date getUpdated() {
        return updated;
    }

    public List<PhoneNumber> getPhoneNumbers() {
        return phoneNumbers;
    }

    public void addPhoneNumber(PhoneNumber number) {
        number.setPerson(this);
        phoneNumbers.add(number);
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

@Entity
@Table(name = "phone_numbers")
public class PhoneNumber {

    public PhoneNumber() {}

    public PhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "phone_number")
    private String phoneNumber;

    @ManyToOne
    @JoinColumn(name = "person_id")
    private Person person;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

e rest endpoint:

@ResponseBody
@RequestMapping(method = RequestMethod.GET)
public List<Person> listPersons() {
    return personService.findAll();
}

Nella risposta json ci sono tutti i campi tranne Id, che mi serve sul lato front-end per modificare / eliminare la persona. Come posso configurare anche lo Spring Boot per serializzare l'ID?

Ecco come appare la risposta ora:

[{
  "firstName": "Just",
  "middleName": "Test",
  "lastName": "Name",
  "comment": "Just a comment",
  "created": 1405774380410,
  "updated": null,
  "phoneNumbers": [{
    "phoneNumber": "74575754757"
  }, {
    "phoneNumber": "575757547"
  }, {
    "phoneNumber": "57547547547"
  }]
}]

UPD Avere una mappatura ibernata bidirezionale, forse è in qualche modo correlata al problema.


Potresti darci ulteriori informazioni sulla tua configurazione primaverile? Quale Json Marshaller usi? Quello predefinito, Jackson, ...?
Ota

In realtà non esiste una configurazione speciale. Volevo provare lo spring boot :) Aggiunto spring-boot-starter-data-rest a pom e usando @EnableAutoConfiguration questo è tutto. Leggi un paio di tutorial e sembra che tutti debbano funzionare fuori dagli schemi. E lo è, tranne quel campo Id. Post aggiornato con risposta endpoint.
Konstantin

1
Nella primavera 4 dovresti anche usare @RestControllersulla classe controller e rimuovere @ResponseBodydai metodi. Inoltre suggerirei di avere classi DTO per gestire richieste / risposte json invece di oggetti di dominio.
Vaelyr

Risposte:


139

Recentemente ho avuto lo stesso problema ed è perché è così che spring-boot-starter-data-restfunziona di default. Vedi la mia domanda SO -> Durante l'utilizzo di Spring Data Rest dopo la migrazione di un'app a Spring Boot, ho osservato che le proprietà dell'entità con @Id non vengono più gestite in JSON

Per personalizzare il modo in cui si comporta, puoi estendere RepositoryRestConfigurerAdapterper esporre gli ID per classi specifiche.

import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;

@Configuration
public class RepositoryConfig extends RepositoryRestConfigurerAdapter {
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(Person.class);
    }
}

1
E non dimenticare di supportare ora getter e setter per l'id nella classe entità! .. (L'ho dimenticato e stavo cercando molto tempo per questo)
phil

3
chef era estremamente trovare RepositoryRestConfigurerAdapter inorg.springframework.data.rest.webmvc.config
Govind Singh

2
I rendimenti: The type RepositoryRestConfigurerAdapter is deprecated. Inoltre non sembra funzionare
GreenAsJade

2
Indeed RepositoryRestConfigurerAdapterè deprecato nelle ultime versioni, è necessario implementarlo RepositoryRestConfigurerdirettamente (utilizzare al implements RepositoryRestConfigurerposto di extends RepositoryRestConfigurerAdapter)
Yann39

48

Nel caso in cui sia necessario esporre gli identificatori per tutte le entità :

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;

import javax.persistence.EntityManager;
import javax.persistence.metamodel.Type;

@Configuration
public class RestConfiguration implements RepositoryRestConfigurer {

    @Autowired
    private EntityManager entityManager;

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(
                entityManager.getMetamodel().getEntities().stream()
                .map(Type::getJavaType)
                .toArray(Class[]::new));
    }
}

Si noti che nelle versioni di Spring Boot precedenti 2.1.0.RELEASEè necessario estendere il (ora deprecato) org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter invece di implementarlo RepositoryRestConfigurerdirettamente.


Se si desidera solo per esporre gli identificatori di entità che si estende o implementa specifici super classe o interfaccia :

    ...
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(
                entityManager.getMetamodel().getEntities().stream()
                .map(Type::getJavaType)
                .filter(Identifiable.class::isAssignableFrom)
                .toArray(Class[]::new));
    }

Se desideri esporre solo gli identificatori di entità con un'annotazione specifica :

    ...
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(
                entityManager.getMetamodel().getEntities().stream()
                .map(Type::getJavaType)
                .filter(c -> c.isAnnotationPresent(ExposeId.class))
                .toArray(Class[]::new));
    }

Annotazione di esempio:

import java.lang.annotation.*;

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

Se uno dovesse utilizzare il primo blocco di codice, in quale directory e file potrebbe andare?
David Krider

1
@DavidKrider: Dovrebbe essere nel proprio file, in una qualsiasi directory coperta da Component Scan . Qualsiasi pacchetto secondario sotto la tua app principale (con l' @SpringBootApplicationannotazione) dovrebbe andare bene.
lcnicolau

Field entityManager in com.myapp.api.config.context.security.RepositoryRestConfig required a bean of type 'javax.persistence.EntityManager' that could not be found.
Dimitri Kopriwa

Ho provato ad aggiungere un @ConditionalOnBean(EntityManager.class)in MyRepositoryRestConfigurerAdapter, ma il metodo non viene chiamato e l'id non è ancora esposto. Uso i dati di primavera con i dati di primavera mybatis: github.com/hatunet/spring-data-mybatis
Dimitri Kopriwa

@DimitriKopriwa: EntityManagerfa parte della specifica JPA e MyBatis non implementa JPA (dai un'occhiata a MyBatis segue JPA? ). Quindi, penso che dovresti configurare tutte le entità una per una, usando il config.exposeIdsFor(...)metodo come nella risposta accettata .
lcnicolau

24

La risposta di @ eric-peladan non ha funzionato immediatamente, ma era abbastanza vicina, forse ha funzionato per le versioni precedenti di Spring Boot. Ora invece è così che dovrebbe essere configurato, correggimi se sbaglio:

import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;

@Configuration
public class RepositoryConfiguration extends RepositoryRestConfigurerAdapter {

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(User.class);
        config.exposeIdsFor(Comment.class);
    }
}

3
Confermato che questa soluzione funziona bene con Spring Boot v1.3.3.RELEASE a differenza di quella proposta da @ eric-peladan.
Poliakoff

4

Con Spring Boot devi estendere SpringBootRepositoryRestMvcConfiguration
se usi RepositoryRestMvcConfiguration la configurazione definita in application.properties potrebbe non funzionare

@Configuration
public class MyConfiguration extends SpringBootRepositoryRestMvcConfiguration  {

@Override
protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
    config.exposeIdsFor(Project.class);
}
}

Ma per una necessità temporanea puoi usare la proiezione per includere l' ID nella serializzazione come:

@Projection(name = "allparam", types = { Person.class })
public interface ProjectionPerson {
Integer getIdPerson();
String getFirstName();
String getLastName();

}


Nella primavera del boot 2, questo non funziona. La classe RepositoryRestMvcConfiguration non dispone di configureRepositoryRestConfiguration da sovrascrivere.
pitchblack408

4

La classe RepositoryRestConfigurerAdapterè stata deprecata dalla 3.1, implementata RepositoryRestConfigurerdirettamente.

@Configuration
public class RepositoryConfiguration implements RepositoryRestConfigurer  {
	@Override
	public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
		config.exposeIdsFor(YouClass.class);
		RepositoryRestConfigurer.super.configureRepositoryRestConfiguration(config);
	}
}

Carattere: https://docs.spring.io/spring-data/rest/docs/current-SNAPSHOT/api/org/springframework/data/rest/webmvc/config/RepositoryRestConfigurer.html


2

Modo semplice: rinomina la tua variabile private Long id;inprivate Long Id;

Per me va bene. Puoi leggere di più al riguardo qui


13
oh, amico ... è un tale odore di codice ... davvero, non farlo
Igor Donin

@IgorDonin lo dice alla comunità Java che ama lo sniffing e il crawling della semantica da nomi di variabili / classi / funzioni ... sempre e ovunque.
EralpB

2
@Component
public class EntityExposingIdConfiguration extends RepositoryRestConfigurerAdapter {

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        try {
            Field exposeIdsFor = RepositoryRestConfiguration.class.getDeclaredField("exposeIdsFor");
            exposeIdsFor.setAccessible(true);
            ReflectionUtils.setField(exposeIdsFor, config, new ListAlwaysContains());
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    class ListAlwaysContains extends ArrayList {

        @Override
        public boolean contains(Object o) {
            return true;
        }
    }
}

3
Benvenuto in SO! Quando pubblichi il codice nelle tue risposte, è utile spiegare come il tuo codice risolve il problema dell'OP :)
Joel

1

Hm, ok sembra che ho trovato la soluzione. La rimozione di spring-boot-starter-data-rest dal file pom e l'aggiunta di @JsonManagedReference a phoneNumbers e @JsonBackReference a person fornisce l'output desiderato. Json in risposta non è più abbastanza stampato ma ora ha l'ID. Non so cosa si comporta sotto il cofano con questa dipendenza ma non mi piace :)


Serve per esporre i tuoi repository Spring Data come endpoint REST. Se non vuoi quella funzione puoi lasciarla fuori. L'id ha a che fare con un Jackson Moduleregistrato da SDR, credo.
Dave Syer

1
Le API RESTful non dovrebbero esporre ID chiave surrogata perché non significano nulla per i sistemi esterni. Nell'architettura RESTful l'ID è l'URL canonico per quella risorsa.
Chris DaMour

4
L'ho letto prima, ma onestamente non capisco come posso collegare front-end e back-end senza Id. Devo passare l'ID a orm per l'operazione di cancellazione / aggiornamento. Forse hai qualche collegamento, come può essere eseguito in modo vero RESTful :)
Konstantin
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.