Spring容器管理的配置Bean转换对象为json字符串时StackOverflowError问题
背景
项目中某配置类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()
即可获取。
不过这种写法会更通用些,比如有多个配置类代码可复用。
后续2
当有配置项值为null时:
@Value("${search.shopproduct.dynamic.xxx:#{null}}")
private String xxx;
toMap
方法转换map会产生NPE
,优化使用另一种方式转换map:
@PostConstruct
public String init() {
LinkedHashMap<String, Object> map = Arrays.stream(this.getClass().getSuperclass().getDeclaredFields())
.filter(f -> !Modifier.isStatic(f.getModifiers()))
.collect(LinkedHashMap::new, (m, f) -> {
ReflectionUtils.makeAccessible(f);
m.put(f.getName(), ReflectionUtils.getField(f, this));
}, LinkedHashMap::putAll);
String json = JsonUtils.obj2Json(map);
log.info("XxxConfig init,json={}", json);
return json;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Ollama——大语言模型本地部署的极速利器
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现