如何使用 Spring Boot 为嵌套实体配置 Jackson 反序列化器

Posted

技术标签:

【中文标题】如何使用 Spring Boot 为嵌套实体配置 Jackson 反序列化器【英文标题】:How to configure Jackson deserializer for nested entites with Spring Boot 【发布时间】:2015-06-07 17:20:36 【问题描述】:

考虑以下实体:

package br.com.investors.domain.endereco;

import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
import org.hibernate.validator.constraints.NotBlank;

import javax.persistence.*;
import java.io.Serializable;

import static com.google.common.base.Preconditions.checkArgument;
import static javax.persistence.GenerationType.SEQUENCE;

@Entity
public class Regiao implements Serializable, Comparable<Regiao> 

    @Id
    @GeneratedValue(strategy = SEQUENCE)
    private Long id;

    @Version
    private Long version;

    @NotBlank
    @Column(length = 100, unique = true)
    private String nome = "";

    Regiao() 

    public Regiao(String nome) 
        checkArgument(!Strings.isNullOrEmpty(nome), "Nome não pode ser vazio");
        this.nome = nome;
    

    @Override
    public boolean equals(Object obj) 
        if (obj instanceof Regiao) 
            Regiao o = (Regiao) obj;
            return Objects.equal(this.nome, o.nome);
        
        return false;
    

    @Override
    public int hashCode() 
        return Objects.hashCode(nome);
    

    @Override
    public int compareTo(Regiao o) 
        return ComparisonChain.start()
                .compare(this.nome, o.nome)
                .result();
    

    @Override
    public String toString() 
        return Objects.toStringHelper(getClass()).add("nome", nome).toString();
    

    public Long getId() 
        return id;
    

    public Long getVersion() 
        return version;
    

    public String getNome() 
        return nome;
    

package br.com.investors.domain.endereco;

import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
import org.hibernate.validator.constraints.NotBlank;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.io.Serializable;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static javax.persistence.GenerationType.SEQUENCE;

@Entity
public class Cidade implements Serializable, Comparable<Cidade> 

    @Id
    @GeneratedValue(strategy = SEQUENCE)
    private Long id;

    @Version
    private Long version;

    @NotBlank
    @Column(length = 100, unique = true)
    private String nome = "";

    @NotNull
    @ManyToOne
    private Regiao regiao;

    @NotNull
    @ManyToOne
    private Estado estado;

    Cidade() 

    public Cidade(String nome, Regiao regiao, Estado estado) 
        checkArgument(!Strings.isNullOrEmpty(nome), "Nome não pode ser vazio");
        checkNotNull(regiao, "Região não pode ser nulo");
        checkNotNull(estado, "Estado não pode ser nulo");

        this.nome = nome;
        this.regiao = regiao;
        this.estado = estado;
    

    @Override
    public boolean equals(Object obj) 
        if (obj instanceof Cidade) 
            Cidade o = (Cidade) obj;
            return Objects.equal(this.nome, o.nome) &&
                    Objects.equal(this.estado, o.estado) &&
                    Objects.equal(this.regiao, o.regiao);
        
        return false;
    

    @Override
    public int hashCode() 
        return Objects.hashCode(nome, regiao, estado);
    

    @Override
    public int compareTo(Cidade o) 
        return ComparisonChain.start()
                .compare(this.estado, o.estado)
                .compare(this.regiao, o.regiao)
                .compare(this.nome, o.nome)
                .result();
    

    @Override
    public String toString() 
        return Objects.toStringHelper(getClass()).add("nome", nome).add("regiao", regiao).add("estado", estado).toString();
    

    public Long getId() 
        return id;
    

    public Long getVersion() 
        return version;
    

    public String getNome() 
        return nome;
    

    public Regiao getRegiao() 
        return regiao;
    

    public Estado getEstado() 
        return estado;
    

我正在尝试将 JSON 发布到 RestController

@RequestMapping(value = "/cidades", method = POST, consumes = APPLICATION_JSON_VALUE)
void inserir(@RequestBody Cidade cidade) 
    repository.save(cidade);

我正在使用 Spring Boot 的默认配置来序列化和反序列化对象。

如果我发布这样的 JSON,它可以正常工作:


    "nome": "Cidade",
    "regiao": "/10"

但我需要像这样发布 JSON:


    "nome": "Cidade",
    "regiao": 
        "id": 10,
        "version": 0,
        "nome": "regiao"
    

如果我这样做,我会收到错误


    "timestamp": "2015-04-02",
    "status": 400,
    "error": "Bad Request",
    "exception": "org.springframework.http.converter.HttpMessageNotReadableException",
    "message": "Could not read JSON: Template must not be null or empty! (through reference chain: br.com.investors.domain.endereco.Cidade[\"regiao\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Template must not be null or empty! (through reference chain: br.com.investors.domain.endereco.Cidade[\"regiao\"])",
    "path": "/cidades/"

做一些调试,我发现杰克逊试图从发布对象的“regiao”属性创建一个 URI,等待像“/id”这样的字符串模板。我正在谷歌搜索,但找不到正确的答案。

我在 *** 上看到了一些相关问题,但没有一个对我有用。

你们能说说这是怎么回事吗?

我认为这只是一个配置,但不知道如何或在哪里。

我也在尝试避免使用自定义序列化器和反序列化器。

编辑:

如果我发布一个仅包含嵌套实体 ID 的 JSON,如下所示:


  "nome": "Cidade",
  "estado": "10",
  "regiao": "10"

我收到这条消息:


    "timestamp": "2015-04-07",
    "status": 400,
    "error": "Bad Request",
    "exception": "org.springframework.http.converter.HttpMessageNotReadableException",
    "message": "Could not read JSON: Failed to convert from type java.net.URI to type br.com.investors.domain.endereco.Estado for value '10'; nested exception is java.lang.IllegalArgumentException: Cannot resolve URI 10. Is it local or remote? Only local URIs are resolvable. (through reference chain: br.com.investors.domain.endereco.Cidade[\"estado\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Failed to convert from type java.net.URI to type br.com.investors.domain.endereco.Estado for value '10'; nested exception is java.lang.IllegalArgumentException: Cannot resolve URI 10. Is it local or remote? Only local URIs are resolvable. (through reference chain: br.com.investors.domain.endereco.Cidade[\"estado\"])",
    "path": "/cidades"

我看到发送嵌套实体的正确方法类似于“regiao”:“/10”,我在我的 javascript 中对其进行硬编码以解决问题:

function(item) 
    item.regiao = "/" + item.regiao.id; //OMG
    item.estado = "/" + item.estado.id; //OMG!!

    if (item.id) 
        return $http.put('/cidades/' + item.id, item);
     else 
        return $http.post('/cidades', item);
    

它有效,但很糟糕。 如何在 Javascript 或配置 Jackson 中解决此问题?

阅读了一些文档,与UriToEntityConverter有关,但仍然不知道配置它的正确方法。

谢谢。

【问题讨论】:

您能否发布相关字段的 getter 和 setter,即 Cidadae 中的 regiao 和您要发布的 Regiao 的属性? @ci_ 实体已更新。现在你可以看到完整的课程了。 如何设置version和id字段?它们没有构造函数或设置器。尝试为所有字段成员添加设置器。有一些方法可以让它在没有的情况下工作,但先看看添加 setter 是否有效。 @ci_ versionid 由 Hibernate 创建和更新。即使对所有类中的所有字段使用 getter 和 setter,我也会收到错误的请求错误。至少当我创建一个新的 Regiao 时,我发布了一个只有 nome 属性的 JSON。 在 Cidade 中,您将 Estado 定义为 NotNull,如果您查看错误,它会显示“无法从 java.net.URI 类型转换为 br.com.investors.domain.endereco 类型。 Estado”尝试在您的 Cidade 类中使用 JsonIgnoreProperties 或查看是否删除 NotNull 是否可以解决问题 【参考方案1】:

我在 EstadoRepository 和 RegiaoRepository 类上使用 @RestResource(exported = false) 注释解决了这个问题。

当它自动配置端点和东西时,它会“隐藏”来自 spring 的 de repo...

【讨论】:

【参考方案2】:

您可以像这样在您的实体类上使用@JsonIgnoreProperties(ignoreUnknown = true) 注释。

@Entity
@JsonIgnoreProperties(ignoreUnknown = true)
public class Area implements Serializable, CompanyAware, IdentifiableModel<Long> 
private static long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id = 0l;
@NotNull
@NotEmpty
@Column(nullable = false, unique = true)
private String name;
@NotNull
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(nullable = false)
private Region region;
private boolean active = true;

@ManyToOne
@JoinColumn(updatable = false)
private Company company;

public Long getId() 
    return id;


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


@Override
public int hashCode() 
    int hash = 0;
    hash += (getId() != null ? getId().hashCode() : 0);
    return hash;


@Override
public boolean equals(Object object) 
    // TODO: Warning - this method won't work in the case the id fields are not set
    if (!(object instanceof Area)) 
        return false;
    
    Area other = (Area) object;
    if ((this.getId() == null && other.getId() != null) || (this.getId() != null && !this.id.equals(other.id))) 
        return false;
    
    return true;


@Override
public String toString() 
    return "Area[ id=" + getId() + " ]";


/**
 * @return the name
 */
public String getName() 
    return name;


/**
 * @param name the name to set
 */
public void setName(String name) 
    this.name = name;


/**
 * @return the region
 */
public Region getRegion() 
    return region;


/**
 * @param region the region to set
 */
public void setRegion(Region region) 
    this.region = region;


/**
 * @return the active
 */
public boolean isActive() 
    return active;


/**
 * @param active the active to set
 */
public void setActive(boolean active) 
    this.active = active;


/**
 * @return the company
 */
public Company getCompany() 
    return company;


/**
 * @param company the company to set
 */
public void setCompany(Company company) 
    this.company = company;
 

它可能会解决您的问题。它将忽略 json 对象中未知的缺失字段。 它将仅使用 json 对象中的可用字段并忽略未知字段。

【讨论】:

这对问题不起作用。这与如何在 Spring Boot 上更改 Jackson 的 ConditionalGenericConverter 有关。我正在阅读它,看看一些代码,但还没有结果:(

以上是关于如何使用 Spring Boot 为嵌套实体配置 Jackson 反序列化器的主要内容,如果未能解决你的问题,请参考以下文章

Spring boot 参数校验

Spring boot 参数校验

如何使用 Spring boot 和 MYSQL 为多级菜单列表创建嵌套 JSON?

spring boot 日志/页面处理实体类构建后台管理

如何在 Spring Boot 中将嵌套的 JSON 对象映射为 SQL 表行

Spring Boot:如何使用复合键创建实体