背景

是的,标题没有错误,不是Spring Bean的循环依赖,而是静态变量之间的循环依赖。

近期的项目均是简单的Maven项目,通过K8S部署在阿里云上,其配置文件读取规则如下所示:

(1) 优先读取应用部署同层目录下的配置文件;

(2) 如果没有外部配置文件,则读取打包至jar包中的配置文件。

在部署的过程中,发现应用无法争取读取外部配置文件中的配置信息,坚持不懈的读取打包文件中的。

日志文件配置错误

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration scan="true" scanPeriod="60 seconds" debug="false">
  3. <contextName>external-portal-inspector</contextName>
  4. <property name="LOG_PATH" value="./logs"/>
  5. <property resource="application.properties"/>
  6. <define name="application.system" class="com.eqos.network.config.LogSystemParamDefiner"/>
  7. <define name="application.region" class="com.eqos.network.config.LogRegionParamDefiner"/>
  8. <define name="application.platform" class="com.eqos.network.config.LogPlatformParamDefiner"/>
  9. <property name="log.pattern" value='{
  10. "date":"%date{\\\"yyyy-MM-dd HH:mm:ss.SSS\\\",UTC}",
  11. "level":"%level",
  12. "system":"${application.system}",
  13. "platform":"${application.platform}",
  14. "region":"${application.region}",
  15. "filepath":"%class:%line",
  16. "msg":"%msg"
  17. }'/>
  18. <!--输出到控制台-->
  19. <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
  20. <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
  21. <level>
  22. DEBUG
  23. </level>
  24. </filter>
  25. <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
  26. <providers class="net.logstash.logback.composite.loggingevent.LoggingEventJsonProviders">
  27. <pattern>
  28. <pattern>
  29. ${log.pattern}
  30. </pattern>
  31. </pattern>
  32. </providers>
  33. <charset>UTF-8</charset>
  34. </encoder>
  35. </appender>
  36. </configuration>

截取部分日志配置,日志格式定义为json格式,其中system,level以及platform参数是在程序运行时进行获取的(通过logback.xml文件中定义的${application.system},${application.region},${application.platform},这几个参数后面定义了相关的类,应用在写日志时会调用相关类接口获取对应参数信息)。但是此时遇到问题,无论如何修改外部配置文件,日志均是读取Jar包中配置文件信息,在com.eqos.network.config.LogSystemParamDefiner类中打断点,发现程序未进入此类获取system信息。以下是application.properties文件中的配置信息。

  1. application.platform=chongqing
  2. application.region=HUADONG
  3. application.system=network

生成日志信息如下所示:

  1. {"date":"2019-11-14 01:48:42.695","level":"INFO","system":"network","platform":"chongqing","region":"HUADONG","filepath":"com.eqos.network.service.TcpProbeService$1$1:78","msg":"{\"tags\":{\"namespace\":\"oneNet.service.TCP.echo\",\"metric.time-delay\":24,\"metric.correct\":true}}"}

简单对照一下配置文件,就会发现日志中使用${application.system},这与配置文件中的配置key值重名,会不会是日志程序在无法通过接口获取应用配置信息后,直接从Jar包内的配置文件中读取同名配置属性。针对此,修改logback.xml中的相关配置,如下所示:

  1. <define name="system" class="com.eqos.network.config.LogSystemParamDefiner"/>
  2. <define name="region" class="com.eqos.network.config.LogRegionParamDefiner"/>
  3. <define name="platform" class="com.eqos.network.config.LogPlatformParamDefiner"/>
  4. <property name="log.pattern" value='{
  5. "date":"%date{\\\"yyyy-MM-dd HH:mm:ss.SSS\\\",UTC}",
  6. "level":"%level",
  7. "system":"${system}",
  8. "platform":"${platform}",
  9. "region":"${region}",
  10. "filepath":"%class:%line",
  11. "msg":"%msg"
  12. }'/>

修改如上所示,去除application前缀,重新启动程序:

  1. {"date":"2019-11-14 01:54:24.486","level":"INFO","system":"system_IS_UNDEFINED","platform":"platform_IS_UNDEFINED","region":"region_IS_UNDEFINED","filepath":"com.eqos.network.service.TcpProbeService$1$1:78","msg":"{\"tags\":{\"namespace\":\"oneNet.service.TCP.echo\",\"metric.time-delay\":33,\"metric.correct\":true}}"}
  2. {"date":"2019-11-14 01:54:24.546","level":"INFO","system":"system_IS_UNDEFINED","platform":"platform_IS_UNDEFINED","region":"region_IS_UNDEFINED","filepath":"com.eqos.network.service.UdpProbeService$1:87","msg":"{\"tags\":{\"namespace\":\"oneNet.service.UDP.echo\",\"metric.time-delay\":5,\"metric.correct\":true}}"}

美妙的事情发生了,去除了前缀后,日志应用就无法从Jar包内的配置文件读取同名的属性信息了,日志中的system显示为system_IS_UNDEFINED,此处带上了_IS_UNDEFINED后缀,表明日志应用无法获取此属性对应的具体数值。

进一步检查日志配置文件,发现从配置文件读取属性是因为配置了以下属性:<property resource="application.properties"/>

后续的问题就集中于,为什么日志应用无法读取com.eqos.network.config.LogSystemParamDefiner通过接口提供的属性信息呢?

静态变量循环依赖

抽丝剥茧

以下为com.eqos.network.config.LogSystemParamDefiner的相关代码,其继承于logabck提供的PropertyDefinerBase抽象类,当应用启动时,logback就会调用实现此抽象类的对象,获取属性的具体数值。

  1. public class LogSystemParamDefiner extends PropertyDefinerBase {
  2. @Override
  3. public String getPropertyValue() {
  4. return AppConfig.INSTANCE.getConfigInfoMap().getOrDefault("application.system", "UNKNOWN");
  5. }
  6. }
  7. @Slf4j
  8. public class AppConfig {
  9. public static final AppConfig INSTANCE = new AppConfig();
  10. private final ConcurrentHashMap<String, String> configInfoMap = new ConcurrentHashMap<>(16);
  11. private AppConfig() {
  12. // 读取配置文件中属性,相关代码非重点,因此省略
  13. initApplicationConfig();
  14. }
  15. public ConcurrentHashMap<String, String> getConfigInfoMap() {
  16. return configInfoMap;
  17. }
  18. }

由以上代码可以看出,LogSystemParamDefiner类较为简单,其依赖于AppConfig类的单例对象。AppConfig类也很简单,通过静态变量实现单例(当前应用程序就不考虑反射或者序列化破坏单例了)。那就只能打断点跟进,看看究竟是什么原因 导致无法读取system属性,按理说此处就算无法正确读取到system属性值,也会使用UNKNOWN值进行替代。

打断点进入程序,发现在启动阶段进入LogSystemParamDefiner.getPropertyValue方法,尴尬的事情发生了,此时AppConfig.INSTANCE为null(是的,java中的绝大多数问题都是NPE,此处就不计较为什么NPE被吞了)。这是为什么呢,按理说静态单例里应该在AppConfig类进行加载的时候,就创建相应的静态变量对象,不应该为空。

探寻究竟

现在问题很简单,就是为什么AppConfig的单例类初始化失败呢?通过有限的编程经验来说,应该是发生循环依赖了,而且这个循环依赖必然是发生在日志单例和AppConfig单例之间(logback的实现肯定是单例,我想不到它不是单例的理由)。

检查AppConfig代码,其实主要就是检查import引入的依赖项,代码如下所示:

  1. import lombok.extern.slf4j.Slf4j;
  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.io.IOException;
  5. import java.io.InputStream;
  6. import java.util.Properties;
  7. import java.util.concurrent.ConcurrentHashMap;

问题已很明显了,只有Slf4j与日志相关,应该就是它引入了对logback单例的依赖。检查AppConfig代码,发现虽然未使用到日志类,但是在类上存在@Slf4j注解。现在去除@Slf4j注解,再次进行试验发现,代码再次进入LogSystemParamDefiner.getPropertyValue方法时,AppConfig.INSTANCE已经完成了初始化操作,输出日志也正常加载外部配置文件的相关信息。

举个例子

  1. class A
  2. {
  3. public static int X;
  4. static { X = B.Y + 1;}
  5. }
  6. public class B
  7. {
  8. public static int Y = A.X + 1;
  9. static {}
  10. public static void main(String[] args) {
  11. System.out.println("X = "+A.X+", Y = "+B.Y);
  12. }
  13. }

讲原理大家可能不太容易理解,此处就通过stackoverflow上的例子进行讲解。这个代码如果写在实际的工程中,可能会被打的很惨(是的,前面的问题确实很愚蠢),不过很容易看出来类A与类B中的静态变量存在循环依赖。

以下讲解此程序的执行顺序:

(1) 主线程执行main函数吗,class loader加载类B;

(2) 加载类B时,初始化静态变量B.Y;

(3) B.Y依赖于A.X,因此class loader加载类A;

(4) 加载类B时,初始化静态变量A.X,A.X依赖于B.Y;

(5) 此时B尚未加载完全,因此B.Y数值为0(如果B.Y为Object,则对应数值为null),所以A.X数值为1;

(6) 返回类B继续执行B.Y的初始化操作,因为A.X=1,因此B.Y数值为2;

(7) 类A与类B的初始化完成,A.X=1,B.Y=2。

以上代码能够得到具体数值,这是因为java对于基本类型以及对象类型均会赋予初始化数值。如果换成C++,在UB的指引下,会出现什么那就真的不得而知了。

PS:

如果您觉得我的文章对您有帮助,请关注我的微信公众号,谢谢!

有趣的bug——Java静态变量的循环依赖的更多相关文章

  1. java 静态变量初始化

    java 静态变量在编译阶段就已经明确位置, 所以静态变量的声明与初始化在编码顺序上可以颠倒.也就是说可以先编写初始化的代码,再编写声明代码.如: public class Test { // 静态变 ...

  2. Java静态变量的初始化(static块的本质)

    Java静态变量的初始化(static块的本质) 标签: javaclassstring编译器jdk工作 2010-02-06 07:23 33336人阅读 评论(16) 收藏 举报  分类: Jav ...

  3. java 静态变量生命周期(类生命周期)

    Static: 加载:java虚拟机在加载类的过程中为静态变量分配内存. 类变量:static变量在内存中只有一个,存放在方法区,属于类变量,被所有实例所共享 销毁:类被卸载时,静态变量被销毁,并释放 ...

  4. java静态变量、静态方法和静态代码段

    先上实例 public class TestStatic { public static String staticString = "this is a static String&quo ...

  5. php和java静态变量用途的思考

    静态变量有哪些用途? 比如创建单例对象. 统计访问次数.数量等等. 多线路和进程中可能会使用. 深入理解补充.... PHP 单例模式解析和实战 php设计模式——单例模式 php static 与 ...

  6. java 静态变量 静态代码块 加载顺序问题

    在网上看了一个这样的题目 public class StaticTest { public static void main(String[] args) { staticFunction(); } ...

  7. Java知多少(31)static关键字以及Java静态变量和静态方法

    static 修饰符能够与变量.方法一起使用,表示是“静态”的. 静态变量和静态方法能够通过类名来访问,不需要创建一个类的对象来访问该类的静态成员,所以static修饰的成员又称作类变量和类方法.静态 ...

  8. java 静态变量生命周期(类生命周期)(转)

    Static: 加载:java虚拟机在加载类的过程中为静态变量分配内存. 类变量:static变量在内存中只有一个,存放在方法区,属于类变量,被所有实例所共享 销毁:类被卸载时,静态变量被销毁,并释放 ...

  9. Java class对象说明 Java 静态变量声明和赋值说明

        先看下JDK中的说明: java.lang.Object java.lang.Class<T> Instances of the class Class represent cla ...

随机推荐

  1. 应届生offer指南

    通用技术 1.一般公司对应届生都要考察编程能力,所以应聘之前先刷刷题.我做面试官出的编程题两年没有变过.就是这道

  2. SQL Server通过定义函数返回字段数据列表模板-干货

    CREATE FUNCTION [dbo].[GetReportDWCustomerOrder] (      @YearDate DATETIME,    参数条件.....    @Categor ...

  3. 安装 openmpi 4.0 用于 horovod 编译

    最近编译 horovod框架过程中,需要使用openmpi 4.0但是环境中的openmpi版本比较低,所以在手动安装openmpi4.0 用于编译,下面对过程进行简要记录,进行备忘: curl -O ...

  4. bootrom/spl/uboot/linux逐级加载是如何实现的?

    关键词:bootrom.spl.uboot.linux.mksheader.sb_header.mkimage.image_header_t等等. 首先看一个典型的bootrom->spl-&g ...

  5. Java哲学家进餐问题|多线程

    Java实验三 多线程 哲学家进餐问题: 5个哲学家共用一张圆桌,分别坐在周围的5张椅子上, 在圆桌上有5个碗和5只筷子(注意是5只筷子,不是5双), 碗和筷子交替排列.他们的生活方式是交替地进行思考 ...

  6. vscode笔记

    一.修改操作栏字体 https://www.cnblogs.com/liuyangfirst/p/9759966.html 1.代码改写,进入默认安装的如下路径,搜索workbench 2.用Vs c ...

  7. Spring Cloud 如何搭建eureka

    Eureka Server 的搭建 eureka 是 Spring Cloud 的注册中心,提供服务注册和服务发现的功能. 利用idea 快速创建一个eureka应用File - NewProject ...

  8. 剑指Offer-28.数组中出现次数超过一半的数字(C++/Java)

    题目: 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字.例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}.由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2.如 ...

  9. 基于Docker的Consul服务发现集群搭建

    在去年的.NET Core微服务系列文章中,初步学习了一下Consul服务发现,总结了两篇文章.本次基于Docker部署的方式,以一个Demo示例来搭建一个Consul的示例集群,最后给出一个HA的架 ...

  10. React: React的组件状态机制

    一.简介 在React中,有两个核心的默认属性,分别是state和props.state会记录组件的状态,React根据状态的变化,会对界面做相应的调整或渲染.props则是数据流向属性,React通 ...