开门见山
我们系统中使用到了Springboot的devtools,然后再启动类中设置了环境变量:
public static void main(String[] args) {
SpringApplication.run(Application.class, ArrayUtils.add(args, "--SYS_ENV=t3"));
}
然后,系统中有一个service方法去获取这个环境变量,并进行相关判断:
@Component
public class EnvService {
/** 系统环境标识 */
private static final String T3_ENV = "t3";
/** 系统环境标识 */
private static final String SYS_ENV = "SYS_ENV";
@Autowired
private Environment environment;
/**
* 判断是否是T3环境
*
* @return 是否
*/
public boolean isT3() {
return StringUtils.equals(T3_ENV, environment.getProperty(SYS_ENV));
}
}
上面代码看起来很正常,在测试环境和稳定环境中均表现正常。但是诡异的事情发生了,在研发环境中返回总是false
。经排查,研发环境中environment.getProperty(SYS_ENV)
返回为t3,t3
。
寻龙点穴
全文搜索了一下"t3c"
这个字符串,发现只有main
方法中有。于是乎,我开始怀疑run
方法中设置环境变量有问题,然后我就一步一步的进入到使用args
参数的地方:
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
// 这儿有使用
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
// 这儿也有使用
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, listeners, exceptionReporters, ex);
throw new IllegalStateException(ex);
}
listeners.running(context);
return context;
}
好吧,代码太多了。干啃源码有点啃不动了,索性来个断点一步一步跟着走吧。然后我就在SpringApplicationRunListeners listeners = getRunListeners(args);
处放了一个断点:
重启一下。这个时候懒惰起到了作用,第一次断点时候我直接放了。
本想看看程序问题是否有改变,结果有一次进断点了。呵呵哒,无心插柳柳成荫啊。
看到了点有意思的东西,有个叫做RestartLauncher
的东西,在devtools
包里面。干脆果断点验证一下是不是devtools
在捣鬼,找到pom
中里面的依赖,直接注释掉。再重启,正常了。
那么线上环境为啥为没有问题呢?
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
...
<configuration>
...
<excludeDevtools>true</excludeDevtools>
</configuration>
...
</plugin>
加了<excludeDevtools>true</excludeDevtools>
这个配置,在打包的时候直接排除了devtools
。这波操作非常优秀!!!
考虑到我们在VM options
中设置的参数没有被设置两次,因此如果仅仅是要解决目前遇到的问题,有两种方法:
- 直接去除项目中的
devtools
依赖以及相关配置,但是这种方案显然太过于粗暴了。 - 不在
main
方法中设置参数,在启动参数中设置-DSYS_ENV=t3
,可以相对优雅的解决问题。
到此问题算是有了解决方案,但是事情就这么结束了吗?我还是有些不甘心,想着万一找着一个spring
的bug
也有了一些谈资,那就继续吧!
没有标题
本章确实想不到名字了,都怪平时读书太少啊!
就两个问题:
- 为什么为执行执行两次?
- 为什么会重复设置参数?
为什么会执行两次?
关键代码:
/** org.springframework.boot.SpringApplication#run(java.lang.String...) */
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
此处会启动所有监视器,在devtools中有个叫做RestartApplicationListener
的监视器:
/** org.springframework.boot.devtools.restart.RestartApplicationListener */
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
// It's too early to use the Spring environment but we should still allow
// users to disable restart using a System property.
String enabled = System.getProperty(ENABLED_PROPERTY);
if (enabled == null || Boolean.parseBoolean(enabled)) {
String[] args = event.getArgs();
DefaultRestartInitializer initializer = new DefaultRestartInitializer();
boolean restartOnInitialize = !AgentReloader.isActive();
Restarter.initialize(args, false, initializer, restartOnInitialize);
}
else {
Restarter.disable();
}
}
此处使用到了spring.devtools.restart.enabled
配置,可以使用该配置屏蔽该监视器的使用。但是如果将该配置文件中写在配置文件中的话是无效的,只能写在启动启动脚本中。
在这个监视器中初始化了一个叫做Restarter
的实例,该实例时单例的。关键代码:Restarter.initialize(args, false, initializer, restartOnInitialize);
, 继续跟踪Restarter中的源码,查看该方法实现:
/** org.springframework.boot.devtools.restart.Restarter */
public static void initialize(String[] args, boolean forceReferenceCleanup,
RestartInitializer initializer, boolean restartOnInitialize) {
...
if (localInstance != null) {
// 关键代码
localInstance.initialize(restartOnInitialize);
}
}
protected void initialize(boolean restartOnInitialize) {
preInitializeLeakyClasses();
if (this.initialUrls != null) {
this.urls.addAll(Arrays.asList(this.initialUrls));
if (restartOnInitialize) {
this.logger.debug("Immediately restarting application");
// 关键代码
immediateRestart();
}
}
}
private void immediateRestart() {
try {
getLeakSafeThread().callAndWait(() -> {
// 关键代码
start(FailureHandler.NONE);
cleanupCaches();
return null;
});
}
...
}
protected void start(FailureHandler failureHandler) throws Exception {
do {
// 关键代码
Throwable error = doStart();
...
}
while (true);
}
private Throwable doStart() throws Exception {
...
// 关键代码
return relaunch(classLoader);
}
protected Throwable relaunch(ClassLoader classLoader) throws Exception {
// 最关键代码来了
RestartLauncher launcher = new RestartLauncher(classLoader, this.mainClassName,
this.args, this.exceptionHandler);
...
}
此时将会开启一个新的线程:RestartLauncher
,线程中将会再次执行main
方法。
/** org.springframework.boot.devtools.restart.RestartLauncher#run */
@Override
public void run() {
try {
Class<?> mainClass = getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
catch (Throwable ex) {
this.error = ex;
getUncaughtExceptionHandler().uncaughtException(this, ex);
}
}
到此,已经明了为什么为执行两次run
方法了。
为什么会重复设置参数?
这两行代码:
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
在重启的时候会将上次启动的参数作为本次执行的参数,然后在main
中会再加一次参数:
SpringApplication.run(ZnbqApplication.class, ArrayUtils.add(args, "--SYS_ENV=t3"));
相当于:
SpringApplication.run(ZnbqApplication.class, ArrayUtils.add("--SYS_ENV=t3", "--SYS_ENV=t3"));
嗯,这个问题算是真相大白了,重启相当于递归调用了一次main
方法,参数也就多设置了一次。
尘埃落地
综上所述,解决这个问题目前有四种方法:
- 直接去除
devtools
。 - 将
main
中的--SYS_ENV=t3
改为-DSYS_ENV=t3
设置到启动参数中。 - 在启动参数中增加
-Dspring.devtools.restart.enabled=false
配置,屏蔽RestartApplicationListener
监视器。 - 还有一种比较骚的操作,将
main
中的--SYS_ENV=t3
改为--t3
,然后把EnvService
中的判断改为:environment.containsProperty("t3")
。
这个问题虽然影响不大,也不常见,但遇到了还是挺麻烦的。折磨了我们团队的几个兄弟伙很久,真实汗颜啊。