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.