SpringBoot: 解决Jpa双向1对多造成的死循环.以及 fastjson解析json对象出现$ref: "$"

本文探讨了在JPA中实现双向1对多关系时遇到的循环引用问题,详细介绍了如何通过使用FastJSON替代Jackson-databind来避免序列化时的死循环,并展示了配置FastJSON和使用@JsonBackReference注解的具体方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

这里的业务场景是一个用户User对应多条动态,且多条动态属于1个用户。即常见的双向1对多或者双向多对1.

看到jackson就应该知道应该是 JPA 中的实体类在处理映射关系,例如一对多的关系时,打印本类时会打印对方类,然后打印对方类又会调用本类,就出现相互调用,进入无限循环的情况,那么必然是序列化的问题了。

 

解决办法:

  • 破坏某一方的 toString()方法即可,最好是破坏多的一方的 toString()方法。
  • 在多的一方对应的实体类属性上加上 @JsonIgnore,进而就可以忽略该字段,跳出循环。但会造成的后果是,输出的结果会缺少该字段的信息,结果就会取不到该字段的相关数据。
  • 可以用 alibaba 旗下的高性能 JSON 框架:FastJSON ,该开源框架速度快,无论序列化和反序列化,都是当之无愧的,并且功能强大,支持普通JDK类包括任意Java Bean Class、Collection、Map、Date、enum,用 FastJSON 可以完美解决互相调用的问题。

上面的方法中,在关联的实体上面设置@JsonIgnore,这个注解的意思是表示在序列化的时候,忽略这个属性。当然这个方法不是很好。所以用fastjson.


演示,通过FastJson:[springboot自定义转换器]

json处理器除了jackson-databind之外。还有GSON、fastjson。

使用fastjson不同于Gson,fastjson继承完之后不能立马使用,需要自己提供响应的HttpMessageConverter才能使用。

步骤:

方式1:

1.首先去除jackson-databind依赖,然后引入fastjson依赖:“

 

2.配置fastjson的HttpMessageConverter:

package com.yinlei.vue.config;

import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.nio.charset.Charset;

/**
 * 配置fastjson的httpMessageConverter:
 * 这是为了解决springboot自带的jackson-databind造成的jpa1对多关联时候的死循环
 */
@Configuration
public class MyFastConfig {

    @Bean
    FastJsonHttpMessageConverter fastJsonHttpMessageConverter(){
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        FastJsonConfig config = new FastJsonConfig();

        //配置json解析过程的一些细节:日期格式、数据编码、是否再在生成JSON中输出类名、是否输出value为null的数据、生成json格式化、空集合输出而非null、空字符串输出而非null
        config.setDateFormat("yyyy-MM-dd HH:mm:ss");
        config.setCharset(Charset.forName("UTF-8"));
        config.setSerializerFeatures(
                SerializerFeature.WriteClassName,
                SerializerFeature.WriteMapNullValue,
                SerializerFeature.PrettyFormat,
                SerializerFeature.WriteNullListAsEmpty,
                SerializerFeature.WriteNullStringAsEmpty
        );
        converter.setFastJsonConfig(config);
        return converter;
    }
}

3.设置一下响应编码,否则返回的json中文会乱码。

这样就不会死循环了

 

方式2:

package com.yinlei.vue.config;

import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.nio.charset.Charset;

/**
 * 配置fastjson的httpMessageConverter:
 * 这是为了解决springboot自带的jackson-databind造成的jpa1对多关联时候的死循环
 */
@Configuration
public class MyWebConfig implements WebMvcConfigurer{

    @Override
    public void configureMessageConverters(List<HttoMessageConverter<?>> converters){
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        FastJsonConfig config = new FastJsonConfig();

        //配置json解析过程的一些细节:日期格式、数据编码、是否再在生成JSON中输出类名、是否输出value为null的数据、生成json格式化、空集合输出而非null、空字符串输出而非null
        config.setDateFormat("yyyy-MM-dd HH:mm:ss");
        config.setCharset(Charset.forName("UTF-8"));
        config.setSerializerFeatures(
                SerializerFeature.WriteClassName,
                SerializerFeature.WriteMapNullValue,
                SerializerFeature.PrettyFormat,
                SerializerFeature.WriteNullListAsEmpty,
                SerializerFeature.WriteNullStringAsEmpty
        );
        converter.setFastJsonConfig(config);
        converters.add(converter);
    }
}

JPA中的双向1对多配置:

user类是1端,Dongtai类是多端。

下面的配置是双向1对多。

package com.yinlei.vue.entity;

import lombok.*;

import javax.persistence.*;
import javax.validation.constraints.Size;
import java.util.List;

/**
 * 实体类:
 * 用户(登录或者注册或者修改信息或者注销账户)
 * 外键关联:动态表应该隶属于用户表
 * 属于1对多:1个用户对应了多个动态
 * PA使用@OneToMany和@ManyToOne来标识一对多的双向关联。一端(Author)使用@OneToMany,多端(Article)使用@ManyToOne。
 * 在JPA规范中,一对多的双向关系由多端(Article)来维护。就是说多端(Article)为关系维护端,负责关系的增删改查。一端(Author)则为关系被维护端,不能维护关系。
 * 一端(Author)使用@OneToMany注释的mappedBy="author"属性表明Author是关系被维护端。
 *
 * 多端(Article)使用@ManyToOne和@JoinColumn来注释属性 author,@ManyToOne表明Article是多端,@JoinColumn设置在article表中的关联字段(外键)。
 */
@Table(name = "user")
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    @Id
    @Column(name = "user_id", nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer userId; //用户id

    @Column(name = "user_name", nullable = false)
    private String userName;//用户名

    @Column(name = "user_sex", nullable = true)
    private String userSex;//性别

    @Size(min=1, max=100)
    @Column(name = "user_age", nullable = true)
    private int userAge;//年龄

    @Column(name = "user_pwd", nullable = false)
    private String userPassword;//密码

    @Column(name = "user_tel", nullable = false)
    private String userTelephone;//手机号

    @Column(name = "user_email", nullable = false)
    private String userEmail;//电子邮件

//    1对多这里的1是User,多是动态。所以User是被维护端.不保持关系。
    @OneToMany(mappedBy = "belongOfUser", cascade = CascadeType.ALL,fetch = FetchType.LAZY)
    private List<DongTai> dongTais;//动态列表
}
package com.yinlei.vue.entity;

import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import javax.validation.constraints.Size;
import java.util.Date;
import java.util.List;

/**
 * 实体类:动态
 * 这个是重要的要与用户交互的类。
 * TODO 用户的登录注册
 */
@Table(name = "dongtai")
@Entity
@EntityListeners(AuditingEntityListener.class)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DongTai {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
    @Column(name = "dongtai_id", nullable = false)
    private Integer dongtaiId ;//动态id

    @Column(name = "dongtai_title", nullable = false)
    private String dongtaiTitle;//标题

    @Column(name = "select_location", nullable = false)
    private String dongtaiSelectLoca;//用户选择的地址

    @Column(name = "all_location", nullable = false)
    private String dongtaiAllLoca;//后端拥有提供给前端的全部地址

    @Column(name = "avatar", nullable = false)
    private String dongtaiAvatar;//上传图片的url

    @Column(name = "score", nullable = false)
    private String dongtaiScore;//心情等级

    @Lob  // 大对象,映射 MySQL 的 Long Text 类型
    @Basic(fetch = FetchType.LAZY) // 懒加载
    @Size(min = 2)
    @Column(name = "editcontent",nullable = false) // 映射为字段,值不能为空
    private String dongtaiEditContent;//用户编辑的内容

    /**
     * 创建时间
     */
    @CreatedDate
    @Column(name = "create_time")
    private Date createTime;

    /**
     * 修改时间
     */
    @LastModifiedDate
    @Column(name = "modify_time")
    private Date modifyTime;

    //1对多: 这里的动态是多端。维护关系端
    @ManyToOne(cascade = {CascadeType.MERGE,CascadeType.REFRESH},optional = false)//可选属性optional=false,表示user不能为空。删除动态,不影响用户
    @JoinColumn(name = "user_id")//设置在动态表中的关联字段(外键)
    private User belongOfUser;//动态所属用户
}
package com.yinlei.vue;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class VueApplication {

    public static void main(String[] args) {
        SpringApplication.run(VueApplication.class, args);
    }

}

 

此外,用非原生jackson,还可以解决jpa实体类上添加Date类型的错误。

提一下,jpa中,设置时间:

可以在实体类中添加

并在启动类上添加:

 


fastjson解析json对象出现$ref: "$":

问题是循环引用造成的:

fastjson提供了多种json转换方案,有兴趣的同学可以自己看看源码,这里我们可以采用禁止循环引用的方案:

SerializerFeature.DisableCircularReferenceDetect就是禁止循环引用的方案

循环引用:当一个对象包含另一个对象时,fastjson就会把该对象解析成引用。引用是通过$ref标示的,下面介绍一些引用的描述
"$ref":".." 上一级
"$ref":"@" 当前对象,也就是自引用
"$ref":"$" 根对象
"$ref":"$.children.0" 基于路径的引用,相当于 root.getChildren().get(0)
 

使用SpringBoot+FastJson的时候,如果json里面的list,包含相同内容,会显示为$.ref[x]或者$.row[x].xxx[x],所以需要在FastJson里面设置一下。

1。FastJson的.java配置增加以下项

//禁用循环引用$ref.xxx[x]
fastConverter.setFeatures(SerializerFeature.DisableCircularReferenceDetect);

 2。如果是代码显式转换,需要

//传入对象进行转换
JSON.toJSONString(object, SerializerFeature.DisableCircularReferenceDetect);

查阅了网上的资料,大多是https://www.cnblogs.com/zhujiabin/p/6132951.html

比较好的是用@JsonBackReference。

使用方法:在"多端"的getter 和setter方法上加上@JsonBackReference。如果是用的lombock。就加载属性字段上。

亲测:

不要打开注释这条

“1端”:

不要用@Data,即不用@toString,并且单端需要用@JsonBackReference

相当于破坏了单端的tostring方法

这样就不会内存溢出了。

 

 

设计:

user.java:

package com.yinlei.vue.entity;

import com.fasterxml.jackson.annotation.JsonManagedReference;
import lombok.*;

import javax.persistence.*;
import javax.validation.constraints.Size;
import java.util.List;

/**
 * 实体类:
 * 用户(登录或者注册或者修改信息或者注销账户)
 * 外键关联:动态表应该隶属于用户表
 * 属于1对多:1个用户对应了多个动态
 * PA使用@OneToMany和@ManyToOne来标识一对多的双向关联。一端(Author)使用@OneToMany,多端(Article)使用@ManyToOne。
 * 在JPA规范中,一对多的双向关系由多端(Article)来维护。就是说多端(Article)为关系维护端,负责关系的增删改查。一端(Author)则为关系被维护端,不能维护关系。
 * 一端(Author)使用@OneToMany注释的mappedBy="author"属性表明Author是关系被维护端。
 *
 * 多端(Article)使用@ManyToOne和@JoinColumn来注释属性 author,@ManyToOne表明Article是多端,@JoinColumn设置在article表中的关联字段(外键)。
 */
@Table(name = "user")
@Entity
@Getter
@Setter
//@ToString
@AllArgsConstructor
@NoArgsConstructor
public class User {

    @Id
    @Column(name = "user_id", nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer userId; //用户id

    @Column(name = "user_name", nullable = false)
    private String userName;//用户名

    @Column(name = "user_sex", nullable = true)
    private String userSex;//性别

    @Size(min=1, max=100)
    @Column(name = "user_age", nullable = true)
    private int userAge;//年龄

    @Column(name = "user_pwd", nullable = false)
    private String userPassword;//密码

    @Column(name = "user_tel", nullable = false)
    private String userTelephone;//手机号

    @Column(name = "user_email", nullable = false)
    private String userEmail;//电子邮件

//    1对多这里的1是User,多是动态。所以User是被维护端.不保持关系。
    @OneToMany(mappedBy = "belongOfUser", cascade = CascadeType.ALL,fetch = FetchType.LAZY)
    @JsonManagedReference
    private List<DongTai> dongTais;//动态列表
}

Dongtai.java: 

package com.yinlei.vue.entity;

import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import javax.validation.constraints.Size;
import java.util.Date;
import java.util.List;

/**
 * 实体类:动态
 * 这个是重要的要与用户交互的类。
 * TODO 用户的登录注册
 */
@Table(name = "dongtai")
@Entity
@EntityListeners(AuditingEntityListener.class)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DongTai {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
    @Column(name = "dongtai_id", nullable = false)
    private Integer dongtaiId ;//动态id

    @Column(name = "dongtai_title", nullable = false)
    private String dongtaiTitle;//标题

    @Column(name = "select_location", nullable = false)
    private String dongtaiSelectLoca;//用户选择的地址

    @Column(name = "all_location", nullable = false)
    private String dongtaiAllLoca;//后端拥有提供给前端的全部地址

    @Column(name = "avatar", nullable = false)
    private String dongtaiAvatar;//上传图片的url

    @Column(name = "score", nullable = false)
    private String dongtaiScore;//心情等级

    @Lob  // 大对象,映射 MySQL 的 Long Text 类型
    @Basic(fetch = FetchType.LAZY) // 懒加载
    @Size(min = 2)
    @Column(name = "editcontent",nullable = false) // 映射为字段,值不能为空
    private String dongtaiEditContent;//用户编辑的内容

    /**
     * 创建时间
     */
    @CreatedDate
    @Column(name = "create_time")
    private Date createTime;

    /**
     * 修改时间
     */
    @LastModifiedDate
    @Column(name = "modify_time")
    private Date modifyTime;

    //1对多: 这里的动态是多端。维护关系端
    @JsonBackReference
    @ManyToOne(cascade = {CascadeType.MERGE,CascadeType.REFRESH},optional = false)//可选属性optional=false,表示user不能为空。删除动态,不影响用户
    @JoinColumn(name = "user_id")//设置在动态表中的关联字段(外键)
    private User belongOfUser;//动态所属用户
}

 

下面的代码逐步解释 @Override @Transactional(rollbackFor = {Exception.class, Error.class}) public void importEquipCodeIccmFailureState(String subClassCode) { //插入大小 int batchSize = 20; ObjectMapper objectMapper = new ObjectMapper(); List<ClassStandardFailureState> batchList = new ArrayList<>(); //构造设备类字典的远程接口 String realServiceUrl = "/iccmrml/eqData"; String url = CgnHeader.getUrl("test") + realServiceUrl; CgnRequestHeader header = null; ApiResult result = null; try { header = CgnHeader.getHeader(realServiceUrl, aepProperties); } catch (Exception e) { throw new BusinessException("获取cgn对象头失败"); } RemoteParamDTO remoteParamDTO = new RemoteParamDTO(); remoteParamDTO.setPageSize(20); int pageNum = 1; int totalPages; //获取本数据库中的标准失效状态集合 用来去重 List<ClassStandardFailureState> standardFailureStates = lambdaQuery() .eq(ClassStandardFailureState::getSubClassCode, subClassCode) .eq(ClassStandardFailureState::getDeleted, 0).list(); remoteParamDTO.setSubclassCode(subClassCode); //失效状态集合 Set<String> failureStateList = standardFailureStates.stream().map(ClassStandardFailureState::getFailureState).collect(Collectors.toSet()); do { remoteParamDTO.setPageNumber(pageNum); //调用远程接口获取数据 result = restClient.postCgnVoForRest(url, header, remoteParamDTO); String code = result.getCode(); if (!"200".equals(result.getCode())) { throw new BusinessException(code + ":cud.查询设备类字典.error"); } //数据转换 Object rawData = result.getData(); String json = null; List<IccmEquipmentFailureStateVO> dataList; try { json = objectMapper.writeValueAsString(rawData); TypeReference<IccmDataVO<IccmEquipmentFailureStateVO>> typeRef = new TypeReference<IccmDataVO<IccmEquipmentFailureStateVO>>() { }; IccmDataVO<IccmEquipmentFailureStateVO> iccmEquipmentSmallClassVO = objectMapper.readValue(json, typeRef); //拿到设备小类包含的失效状态 totalPages = iccmEquipmentSmallClassVO.getPages(); dataList = iccmEquipmentSmallClassVO.getList(); } catch (JsonProcessingException e) { logger.error("JSON 转换异常,原始数据: {}", json, e); throw new BusinessException("同步iccm标准失效状态数据类型转换异常"); } if (dataList.isEmpty()) { break; } else { for (IccmEquipmentFailureStateVO item : dataList) { //用失效状态字段去重 if (!failureStateList.contains(item.getScFmodeDesc())) { //将iccm的数据同步过来 ClassStandardFailureState target = new ClassStandardFailureState(); target.setSource(0); target.setSubClassCode(subClassCode); target.setFailureState(item.getScFmodeDesc()); target.setFailureCategory(item.getFmdPrLevel()); batchList.add(target); } // 达到批量大小,进行入库 if (batchList.size() >= batchSize) { saveBatch(batchList); batchList.clear(); } } } pageNum++; } while (pageNum <= totalPages); //插入剩余数据 if (!batchList.isEmpty()) { saveBatch(batchList); batchList.clear(); } }
最新发布
07-18
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

醒不了的星期八

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值