首页 > 其他分享 >Spring容器管理的配置Bean转换对象为json字符串时StackOverflowError问题

Spring容器管理的配置Bean转换对象为json字符串时StackOverflowError问题

时间:2023-03-20 19:33:07浏览次数:40  
标签:map Spring StackOverflowError xxx json XxxConfig com

背景

项目中某配置类XxxConfig定义了很多配置参数,通过Spring的@Value注解与配置中心的项目yml里的配置项关联。

@Slf4j
@Getter
@Setter
@RefreshScope
@Configuration
public class XxxConfig implements Serializable {

    @Value("${com.xxx.dynamic.serviceError.code:500}")
    private Integer serviceErrorCode;

    @Value("${com.xxx.dynamic.serviceError.msg:服务繁忙,请稍候再试}")
    private String serviceErrorMsg;

    @Value("#{'${com.xxx.dynamic.xxxCode:111,222}'.split(',')}")
    private List<String> xxxCodes;

    @Value("#{${com.xxx.dynamic.xxx}}")
    private Map<String, String> xxx;

    @Value("#{${com.xxx.dynamic.yyy}}")
    private Map<String, Map<String, Object>> yyy;
    ...
}

期望在项目日志中能将此配置类的所有参数以json格式打印出来,
1.在项目启动的时候
2.在配置中心修改了配置通过@RefreshScope刷新的时候
通过日志文件搜索关键字,能方便看到项目当前的参数配置。

问题

Spring的Bean生命周期,实例化->字段赋值。
因此在XxxConfig类新建init方法,@PostConstruct注解标识,然后打印json日志:

@PostConstruct
public String init() {
    log.info("XxxConfig init,json={}", JsonUtils.obj2Json(this));
}

本地启动项目,结果启动失败,报了StackOverflowError的错,

Caused by: com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError)
(through reference chain: org.springframework.beans.factory.support.DefaultListableBeanFactory["singletonMutex"]->
java.util.concurrent.ConcurrentHashMap["xxx"]->...
   at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:734)
   at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize
...

项目中JsonUtils使用了jackson库,报错信息显示有无限循环,导致了StackOverflowError。

分析

通过@Configuration标识的XxxConfig类,是由Spring容器里创建管理的Bean。
JsonUtils.obj2Json(this))这里的this对象的类型不是XxxConfig类,而是被Spring代理了,
项目基于Spring Boot,在默认基于CGLib进行代理,通过本地断点调试查看this对象的类型为com.xxx.config.XxxConfig$$EnhancerBySpringCGLIB$$97246a65
代理后的对象,增添了Spring增强的一些属性,如$$BeanFactory,导致jackson库序列时报错。

解决

不用json序列化后打印,通过apache commons-long库里的ToStringBuilder.reflectionToString(this)转换为String后打印,
但这种方式打印的String不是json格式,不能直接复制到json图形化工具里方便地查看。

想到一个折中的方式,新建一个Map,将所有参数字段名和字段值按顺序添加到里面,然后json序列化后打印。

Map<String, Object> map = new LinkedHashMap<>();
map.put("serviceErrorCode", serviceErrorCode)
...
log.info("XxxConfig init,json={}", JsonUtils.obj2Json(map));

注:这里使用LinkedHashMap,为了保证序列化的json的顺序,跟类的字段代码添加顺序一致,方便查看和核对。
这种方式能实现功能,但是map里字段是硬编码的,当配置类有很多字段时,编码比较繁琐,需要仔细检查保证没有遗漏和按顺序添加。

继续思考,XxxConfig被Spring通过CGLib代理了生成Bean,能否获取到原对象,在json序列化后打印呢?
参考Spring的AopUtils类里的getTargetClass方法,发现能获取到原对象的类型,即XxxConfig,然后再通过反射获取对象里各字段的值。
最终实现代码如下:

@PostConstruct
public String init() {
    LinkedHashMap<String, Object> map = Arrays.stream(this.getClass().getSuperclass().getDeclaredFields())
            .filter(f -> !Modifier.isStatic(f.getModifiers()))
            .collect(Collectors.toMap(f -> f.getName(), f -> {
                ReflectionUtils.makeAccessible(f);
                return ReflectionUtils.getField(f, this);
            }, (o, n) -> n, LinkedHashMap::new));
    log.info("XxxConfig init,json={}", JsonUtils.obj2Json(map));
    return JsonUtils.obj2Json(map);
}

注:
通过Modifier.isStatic方法过滤掉了类的static字段,包括@Slf4j生成的log字段和实现Serializable接口IDEA工具生成的serialVersionUID字段。
转换map指定了LinkedHashMap类型,为了保证最后json里字段的顺序。

后记

隔了几天,再次审视这段代码时发现:
this.getClass().getSuperclass().getDeclaredFields()这里想复杂了,
因为这里已确定是XxxConfig类,直接用DynamicConfig.class.getDeclaredFields()即可获取。
不过这种写法会更通用些,比如有多个配置类代码可复用。

标签:map,Spring,StackOverflowError,xxx,json,XxxConfig,com
From: https://www.cnblogs.com/cdfive2018/p/17235957.html

相关文章

  • Spring MVC3: Controller接受Form数据
    SpringMVC3:Controller接受Form数据<formaction="./saveIntoDatabase.do"method="post"name="saveIntoDatabase"><inputname="name"t......
  • spring-servlet.xml
    WEB-INF目录下面建一个ascweb-servlet.xml文件,其实这个文件的命名就是Web.xml中servlet-name的名字加-servlet.xml.其文件内容如下:<?xmlversion="......
  • Spring容器加载完之后执行特定任务
    有两个服务器类,需要SpringContext在InitAfterSpringContextService之前初始化:1、SpringContextspring容器的上线文环境packagecom.cdelabcare.pubservice;importorg.sp......
  • Spring MVC防重复提交
    如何在SpringMVC里面解决此问题(其它框架也一样,逻辑一样,思想一样,和具体框架没什么关系)。要解决重复提交,有很多办法,比如说在提交完成后redirect一下,也可以用本文提到的使用to......
  • Springboot项目密码加密器jasypt
    最新版依赖<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version></dependenc......
  • vue-json-viewer 展示json数据
    main.jsimportJsonViewerfrom'vue-json-viewer'Vue.use(JsonViewer);vue<el-dialogtitle="退款数据详情":visible.sync="dialogvisible2"v-if="dialogvisible......
  • spring工具类
    spring工具类获取bean编写packagecom.cloudiip.security.utils;importorg.springframework.stereotype.Component;importorg.springframework.context.Applicatio......
  • org.springframework.core.metrics.ApplicationStartup
    日志Exceptioninthread"main"java.lang.NoClassDefFoundError:org/springframework/core/metrics/ApplicationStartup atorg.springframework.boot.SpringApplicat......
  • Spring@Transactional事务失效的场景
    ①未启用事务管理功能②事务方法所在类未被加载成Bean③事务方法不是public类型④事务方法被final修饰⑤事务方法被同类的方法调用⑥多线程调用⑦手动trycatch了异......
  • 初始JSON和JSON的3种形式以及常用方法
    初始JSONJSON全称是JavaScriptObjectNotation为什么需要JSONJSON有3种形式,每种形式的写法都和JS中的数据类型很像,可以很轻松和JS中的数据类型互相转换JS->JSO......