从日志看,使用fastjson库来序列化需要打印到日志的Java对象时,触发了StackOverflowError。这个错误表明需要序列化的对象很可能存在递归引用的问题,即对象直接或间接地引用了自己。
交待下背景:报错项目的SpringBoot版本:2.5.2。
引发报错的变更: 只是新增一个服务类,这个类干了两个事:
import com.zkh360.gbb.user.advertisement.job.FetchAdvertisementReportDailyJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@Slf4j
public class FetchReportCommon implements ApplicationListener<ApplicationReadyEvent> {
private static StringRedisTemplate stringRedisTemplate;
private Map<String, FetchAdvertisementReportDailyJob> mapper;
@Autowired
private ApplicationContext applicationContext;
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
this.mapper = this.applicationContext.getBeansOfType(FetchAdvertisementReportDailyJob.class);
stringRedisTemplate = this.applicationContext.getBean(StringRedisTemplate.class);
}
@PostMapping("/fetch/report/refreshToken/trigger")
public String refreshAccessToken() {
log.info("[refreshAccessToken] start.");
try {
for (FetchAdvertisementReportDailyJob value : mapper.values()) {
value.refreshAccessToken();
}
log.info("[refreshAccessToken] finished");
return "success";
} catch (Exception e) {
log.info("[refreshAccessToken] failed,errorMessage:{}", e.getMessage());
return "failed,errorMessage:" + e.getMessage();
}
}
}
报错原因:log了一个包含存在循环依赖的对象。
net.ai-as.ad.job.FetchReportCommon#onApplicationEvent中的ApplicationReadyEvent是个超级对象,包含了Spring容器的上下文对象AnnotationConfigServletWebServerApplicationContext,同时也依赖了FetchReportCommon。
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext 是Spring Boot提供的特定实现类,继承自AnnotationConfigApplicationContext并扩展了ServletWebServerApplicationContext。 这个类是专门为基于Servlet API的Web应用程序设计的,它结合了基于注解的配置和Servlet Web服务器的功能。
在Spring Boot中,当你创建一个Web应用程序时,AnnotationConfigServletWebServerApplicationContext或其子类通常会被用来初始化和配置应用程序上下文。例如,Spring Boot自动配置类会根据你的应用程序需求自动创建一个这样的上下文实例,并配置嵌入式Web服务器。
这个类是Spring Boot中实现Web应用程序快速开发和部署的关键组件之一,它结合了Spring的依赖注入和Web应用程序的Servlet API,提供了一个简单而强大的运行时环境。
不敢相信,Spring应用容器对象 居然是一个事件对象的构建参数!!
这很明显没有遵守SOLID中的LoD法则。 按照最少知识原则(Least Knowledge Principle),一个对象应该对其他对象有最少的了解,即“Talk only to your immediate friends and not to strangers”。
是不是场景比较特殊? 来看看SpringBoot是如何实例化ApplicationReadyEvent对象的:
org.springframework.boot.context.event. EventPublishingRunListener#running
好吧,事情已经发生了,我们就没啥不好意思讲的。
再画类图,来更直观的观摩下这个循环引用:
这样看循环是不是清晰了
想基于这个循环依赖引发fastjson中的Stack0verflowError还需要一个打印applicationReadyEvent。
问题已经搞清了: SpringBoot2.5.2在初始化ApplicationListener实现类FetchReportCommon,在执行onApplicationEvent方法后,会把入参对象applicationReadyEvent打印到日志。 applicationReadyEvent对象中包含了一个循环依赖。 打印applicationReadyEvent时,fastjson就StackOverflowError了。
那么如何改?
新加的类虽然增强了应用程序的功能,但也带来了一系列挑战。 项目中对调用RestController的方法以及传递的参数都进行了日志记录。这是一种好的实践,因为它可以帮助我们追踪请求和响应的流程,以及调试潜在的问题。然而,这种全面的日志策略可能会引入性能问题, 从日志策略到事件处理,再到第三方库的使用,每一个环节都需要我们仔细设计和审查。特别是对于循环依赖和递归引用这种潜在的问题,我们需要提前识别并采取措施避免。通过这些经验,我们可以继续优化我们的代码,确保应用程序的稳定性和可靠性。