Spring boot @ResponseBody nie serializuje identyfikatora jednostki

96

Masz dziwny problem i nie potrafię sobie z nim poradzić. Miej proste POJO:

@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);
    }
}

i punkt końcowy odpoczynku:

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

W odpowiedzi json są wszystkie pola z wyjątkiem Id, których potrzebuję po stronie interfejsu, aby edytować / usunąć osobę. Jak mogę skonfigurować rozruch sprężynowy, aby również serializować identyfikator?

Tak wygląda teraz odpowiedź:

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

UPD Mają dwukierunkowe mapowanie hibernacji, być może jest to w jakiś sposób związane z problemem.

Konstantin
źródło
Czy mógłbyś podać nam więcej informacji na temat Twojej konfiguracji wiosennej? Jakiego JSON Marshaller używasz? Domyślny, jackson, ...?
Ota,
Właściwie nie ma specjalnej konfiguracji. Chciałem wypróbować Spring Boot :) Dodano spring-boot-starter-data-rest do pom i użycie @EnableAutoConfiguration to wszystko. Przeczytaj kilka samouczków i wydaje się, że wszystkie muszą działać po wyjęciu z pudełka. I tak jest, z wyjątkiem tego pola Id. Zaktualizowany post z odpowiedzią punktu końcowego.
Konstantin
1
Wiosną 4 powinieneś również użyć @RestControllerklasy kontrolera i usunąć @ResponseBodyz metod. Sugerowałbym również posiadanie klas DTO do obsługi żądań / odpowiedzi json zamiast obiektów domeny.
Vaelyr,

Odpowiedzi:

141

Niedawno miałem ten sam problem i to dlatego, że tak spring-boot-starter-data-restdziała domyślnie. Zobacz moje pytanie SO -> Podczas korzystania z Spring Data Rest po migracji aplikacji do Spring Boot, zauważyłem, że właściwości jednostki z @Id nie są już kierowane do JSON

Aby dostosować sposób działania, możesz rozszerzyć, RepositoryRestConfigurerAdapteraby ujawnić identyfikatory dla określonych klas.

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);
    }
}
Patrick Grimard
źródło
1
I nie zapomnij obsługiwać teraz getter i setter dla id w klasie encji! .. (zapomniałem o tym i szukałem dużo czasu na to)
phil
3
nie można znaleźć RepositoryRestConfigurerAdapter worg.springframework.data.rest.webmvc.config
Govind Singh
2
Plony: The type RepositoryRestConfigurerAdapter is deprecated. Wygląda na to, że nie działa
GreenAsJade
2
Rzeczywiście, RepositoryRestConfigurerAdapterw najnowszych wersjach jest przestarzałe, należy je wdrożyć RepositoryRestConfigurerbezpośrednio ( implements RepositoryRestConfigurerzamiast extends RepositoryRestConfigurerAdapter)
Yann39
49

W przypadku konieczności ujawnienia identyfikatorów dla wszystkich podmiotów :

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));
    }
}

Zauważ, że w wersjach Spring Boot wcześniejszych 2.1.0.RELEASEmusisz rozszerzyć (teraz przestarzałe) org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter zamiast RepositoryRestConfigurerbezpośrednio implementować .


Jeśli chcesz ujawnić tylko identyfikatory jednostek, które rozszerzają lub implementują określoną superklasę lub interfejs :

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

Jeśli chcesz ujawnić tylko identyfikatory jednostek z określoną adnotacją :

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

Przykładowa adnotacja:

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExposeId {}
lcnicolau
źródło
Gdyby ktoś użył pierwszego bloku kodu, do jakiego katalogu i pliku mógłby się on znaleźć?
David Krider
1
@DavidKrider: Powinien znajdować się we własnym pliku, w dowolnym katalogu objętym skanowaniem składników . Każdy pakiet podrzędny poniżej głównej aplikacji (z @SpringBootApplicationadnotacją) powinien działać.
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
Próbowałem dodać @ConditionalOnBean(EntityManager.class)in MyRepositoryRestConfigurerAdapter, ale metoda nie jest wywoływana, a identyfikatory nadal nie są widoczne. Używam danych Spring z danymi wiosennymi mybatis: github.com/hatunet/spring-data-mybatis
Dimitri Kopriwa
@DimitriKopriwa: EntityManagerJest częścią specyfikacji JPA, a MyBatis nie implementuje JPA (spójrz na Czy MyBatis podąża za JPA? ). Więc myślę, że powinieneś skonfigurować wszystkie encje jeden po drugim, używając config.exposeIdsFor(...)metody jak w zaakceptowanej odpowiedzi .
lcnicolau
24

Odpowiedź od @ eric-peladan nie zadziałała po wyjęciu z pudełka, ale była dość bliska, może to zadziałało w poprzednich wersjach Spring Boot. Teraz tak powinno być skonfigurowane zamiast tego, popraw mnie, jeśli się mylę:

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);
    }
}
Salvatorelab
źródło
3
Potwierdzono, że to rozwiązanie działa dobrze pod Spring Boot v1.3.3.RELEASE w przeciwieństwie do rozwiązania proponowanego przez @ eric-peladan.
Poliakoff
4

W przypadku Spring Boot musisz rozszerzyć SpringBootRepositoryRestMvcConfiguration,
jeśli używasz RepositoryRestMvcConfiguration, konfiguracja zdefiniowana w application.properties może nie działać

@Configuration
public class MyConfiguration extends SpringBootRepositoryRestMvcConfiguration  {

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

Ale dla tymczasowej potrzeby możesz użyć projekcji, aby dołączyć identyfikator do serializacji, na przykład:

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

}

eric peladan
źródło
W butach wiosennych 2 to nie działa. Klasa RepositoryRestMvcConfiguration nie ma configureRepositoryRestConfiguration do przesłonięcia.
pitchblack408
4

Klasa RepositoryRestConfigurerAdapterzostała wycofana od wersji 3.1, zaimplementuj RepositoryRestConfigurerbezpośrednio.

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

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

Gisomar Junior
źródło
2

Prosty sposób: zmień nazwę zmiennej private Long id;naprivate Long Id;

Pracuje dla mnie. Więcej na ten temat przeczytasz tutaj

Андрей Миронов
źródło
13
o, stary ... to taki zapach kodu ... naprawdę, nie rób tego
Igor Donin
@IgorDonin powiedz to społeczności Java, która uwielbia węszyć i przeszukiwać semantykę z nazw zmiennych / klas / funkcji ... przez cały czas i wszędzie.
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;
        }
    }
}
David B.
źródło
3
Witamy w SO! Umieszczając kod w swoich odpowiedziach, pomocne jest wyjaśnienie, w jaki sposób Twój kod rozwiązuje problem OP :)
Joel
1

Hm, ok wygląda na to, że znalazłem rozwiązanie. Usunięcie spring-boot-starter-data-rest z pliku pom i dodanie @JsonManagedReference do phoneNumbers i @JsonBackReference do osoby daje pożądane wyniki. Json w odpowiedzi nie jest już ładnie wydrukowany, ale teraz ma Id. Nie wiem, jakie magiczne buty sprężynowe działają pod maską z taką zależnością, ale mi się to nie podoba :)

Konstantin
źródło
Służy do ujawniania repozytoriów Spring Data jako punktów końcowych REST. Jeśli nie chcesz tej funkcji, możesz ją pominąć. Chodzi o to, Moduleże Jackson jest zarejestrowany przez SDR.
Dave Syer,
2
RESTful API nie powinny ujawniać zastępczych identyfikatorów kluczy, ponieważ nic nie znaczą dla systemów zewnętrznych. W architekturze RESTful identyfikator jest kanonicznym adresem URL tego zasobu.
Chris DaMour
4
Czytałem to już wcześniej, ale szczerze mówiąc, nie rozumiem, jak mogę połączyć front-end i back-end bez Id. Muszę przekazać identyfikator orm w celu usunięcia / aktualizacji. Może masz jakiś link, jak można to wykonać w prawdziwy RESTful sposób :)
Konstantin