文章目录
-
- 一、问题背景
- 二、问题定位
- 三、疑问与解决
-
- 3.1 如何通过IDEA查找某个类的所有子类?
- 3.2 SpringApplication类是干什么的?
- 3.3 为什么启动时发生Error没有打印错误信息到控制台?
- 四、总结
一、问题背景
今天打算将原来一个非Spring Boot项目改造为Spring Boot项目,改造完成后启动项目,但是控制台报了如下错误:
但是仅凭这一点信息,是无法定位到问题原因的。
不过在继续寻找答案之前,有必要介绍下本地的相关环境:
jdk版本:1.8.0_91
spring boot版本:2.1.2.RELEASE
maven版本: 3.1
因为是项目改造,所以项目中包含很多已有的maven包依赖。
二、问题定位
首先,从控制台的输出可以看出并没有像一般的Spring Boot应用那样启动时打印banner信息。这一点确实有些可疑,但显然这不是我们的答案。
那么,究竟是什么原因导致了启动失败,并且只在控制台输出了那么少的信息呢?
此时我们还没有什么头绪,但是能否从启动类入手呢?
启动类的代码如下:
/**代码2-1**/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// 启动方法
SpringApplication.run(Application.class, args);
}
}
我们在SpringApplication中设置断点:
断点是可以命中的,这说明启动代码是没有问题的。但是当我放开断点后,依然报前面的错误,而且没有其他新的信息,所以问题就出在代码运行的过程中。
那么有没有可能在此过程中发生了异常呢?但是异常为什么没有打印出来呢?
为了证实我的猜想,对启动方法进行异常捕获:
/**代码2-2**/
public static void main(String[] args) {
try {
SpringApplication.run(Application.class, args);
} catch (Exception e) {
// 为了简化问题,不引入log
e.printStackTrace();
}
}
重启服务,控制台输出如下:
看结果貌似还是没有定位到问题的原因。代码没有抛异常,那为什么exit code不是0呢?
突然想起Java中异常的层次结构:
注:如果有同学不知道如何查找类的子类,可以参考:3.1节
或许代码发生了Error。所以将代码调整为:
/**代码2-3**/
public static void main(String[] args) {
try {
SpringApplication.run(Application.class, args);
} catch (Throwable th) {
th.printStackTrace();
}
}
重新启动应用,控制台终于出现错误信息了:
具体的错误信息如下:
java.lang.NoClassDefFoundError: ch/qos/logback/core/joran/spi/ElementSelector
at org.springframework.boot.logging.logback.SpringBootJoranConfigurator.addInstanceRules(SpringBootJoranConfigurator.java:45)
at ch.qos.logback.core.joran.GenericConfigurator.buildInterpreter(GenericConfigurator.java:123)
…
从错误信息可以看出,的确发生了Error错误:java.lang.NoClassDefFoundError
这个错误表示,找不到某个类。进入到org.springframework.boot.logging.logback.SpringBootJoranConfigurator#addInstanceRules,发现了如下情况:
从导入信息可知该类位于包:ch.qos.logback.core.joran.spi
import ch.qos.logback.core.joran.spi.ElementSelector;
该包的位置如下
看来是1.0.11版本的logback中没有这个类。是否升级一下包的版本就可以了呢?
我们尝试将版本进行如下升级:
1.0.11 -> 1.1.3
如果不知道logback的具体版本,可以查询如下网站:
https://mvnrepository.com/
升级maven,并重新导入maven project后重启服务。得到如下结果:
至此,问题的原因定位到了:
logback的版本过低,导致java.lang.NoClassDefFoundError(类定义未找到)的发生。
从以下地址:
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot/2.1.2.RELEASE
可以查看spring-boot的依赖关系:
建议的logback版本是:1.2.3
将pom文件中的logback的版本调整为:
logback.version>1.2.3logback.version>
...
dependency>
groupId>ch.qos.logbackgroupId>
artifactId>logback-classicartifactId>
version>${logback.version}version>
dependency>
三、疑问与解决
3.1 如何通过IDEA查找某个类的所有子类?
很多时候我们需要知道某个类有哪些子类,以及它和这些子类的层级关系。那么能否通过某种方式查找到这些类,并展示它们和该类的层级关系呢?
如果你平时使用IDEA进行Java开发,不妨尝试下其提供的图形功能:
在某个类定义中执行 Show Diagram… 后,会在图形窗口中绘制该类及其父类的类图。
那么如何添加Throwable的子类呢?
貌似首先需要找到Throwable有哪些子类,这个好像有点困难😂
幸运的IDEA给我们提供了方法:
在类图中选中待查询子类的类(如图中①),然后选择命令:Navigate -> 【Type Hierarchy 】(也可以使用热键:Ctrl + H),打开Hierarchy窗口:
我们可以看到左边窗口给出了Throwable的所有子类,但是其中也有一些噪音:比如,org.apache.tomcat.util下的MultiThrowable类,com.sun.xml.internal.bind.v2.util下的StackRecorder类 等等,都是我们不希望出现的类。
那么,有什么办法 消除这些噪音 呢?
IDEA其实已经提供了解决办法,那就是范围(Scope)设定:
其中有5个选项:
- Production:搜索范围为本应用的源码目录(一般是srcmainjava)
- Test:搜索范围为测试源码目录(一般是srctestjava)
- All:所有范围
- This Class:本类中
- Configure…:将打开自定义范围弹框
我们的解决方法就是设置 自定义范围(第5个选项)。
可按如下步骤设置自定义范围:
- 给本设置起一个名字,然后填入Name(我起的名字是jdk,表明将从jdk中搜索Throwable的子类)
- 设置搜索范围,这里有两种方式:
(1)直接设置Pattern的值。
(2)选择【Library Classes】,再点击Include按钮,一一指定搜索路径。这两种方式是等价的。
自定义范围的使用
再次回到类图中选择Throwable类,使用热键【Ctrl+H】打开Hierarchy窗口。选择Scope中的 jdk(前面定义的Scope),最终结果如下:
如果想要绘制具有层次结构的类图,可以在Diagram中选择【Add Class to Diagram…】(或是点击空格键),然后搜索Hierarchy中的相关类进行添加。
注:学会绘制类的层次结构图,对于我们源码阅读也是很有帮助的,请务必掌握。
3.2 SpringApplication类是干什么的?
下面是类的源码说明:
Class that can be used to bootstrap and launch a Spring application from a Java main method. By default class will perform the following steps to bootstrap your application:
- Create an appropriate ApplicationContext instance (depending on your classpath)
- Register a CommandLinePropertySource to expose command line arguments as Spring properties
- Refresh the application context, loading all singleton beans
- Trigger any CommandLineRunner beans
In most circumstances the static run(Class, String[]) method can be called directly from your main method to bootstrap your application:
译文:
该类可用于从Java的main方法去加载和启动一个Spring应用。默认将执行如下步骤去启动应用:
- 创建适合的 ApplicationContext 实例(依赖于 classpath–即类路径)。
- 注册 CommandLinePropertySource ,暴露命令行参数作为Spring属性(properties)。
- 刷新应用上下文,加载所有的单例bean。
- 触发任意的 CommandLineRunner bean。
在大部分情况下,可以从你自己应用的main方法,直接调用该类的静态run(Class, String[]) 方法来启动你的应用:
@Configuration
@EnableAutoConfiguration
public class MyApplication {
// ... Bean definitions
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
For more advanced configuration a SpringApplication instance can be created and customized before being run:
译文:
对于更高级的配置,可以先创建一个 SpringApplication 实例,对其自定义设置后再运行。
public static void main(String[] args) {
SpringApplication application = new SpringApplication(MyApplication.class);
// ... customize application settings here
application.run(args)
}
SpringApplications can read beans from a variety of different sources. It is generally recommended that a single @Configuration class is used to bootstrap your application, however, you may also set sources from:
- The fully qualified class name to be loaded by AnnotatedBeanDefinitionReader
- The location of an XML resource to be loaded by XmlBeanDefinitionReader, or a groovy script to be loaded by GroovyBeanDefinitionReader
- The name of a package to be scanned by ClassPathBeanDefinitionScanner
Configuration properties are also bound to the SpringApplication. This makes it possible to set SpringApplication properties dynamically, like additional sources (“spring.main.sources” – a CSV list) the flag to indicate a web environment (“spring.main.web-application-type=none”) or the flag to switch off the banner (“spring.main.banner-mode=off”).
译文:
Spring应用可以通过不同的来源读取bean。通常建议使用单个的 @Configuration 类去启动应用。然而,你也可以设置以下来源用于读取bean:
- 通过 AnnotatedBeanDefinitionReader 加载全限定类型名称。
- 通过 XmlBeanDefinitionReader 加载一个XML资源路径,或者通过 GroovyBeanDefinitionReader 加载一个groovy脚本。
- 通过 ClassPathBeanDefinitionScanner 扫描某个包名。
配置属性也被绑定到 SpringApplication 。这使得动态设置 SpringApplication 属性成为可能。例如, “spring.main.sources” (值为一个CSV列表)可用于附加资源;“spring.main.web-application-type=none” 是一个指示web环境的标志; 而”spring.main.banner-mode=off” 则是关闭banner的标志。
小结
文档说明了Spring应用的启动步骤。如果需要对Spring应用进行更高级的配置,如何去做。
还举例说明了有哪些读取 bean 的方式:注解+全限定名方式,XML配置方式,类路径+包名方式
3.3 为什么启动时发生Error没有打印错误信息到控制台?
通过debug进入org.springframework.boot.SpringApplication#run(java.lang.String…)方法,代码如下:
public ConfigurableApplicationContext run(String... args) {
//省略...
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
//省略...
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
//省略...
return context;
}
从代码看似乎会对异常进行捕获,并抛出给调用方。
在handleRunFailure处设置了断点,单步运行后没有走到下一行代码。那我们进入该方法看一下它的实现:
private void handleRunFailure(ConfigurableApplicationContext context,
Throwable exception,
CollectionSpringBootExceptionReporter> exceptionReporters,
SpringApplicationRunListeners listeners) {
try {
try {
handleExitCode(context, exception);
if (listeners != null) {
listeners.failed(context, exception);
}
}
finally {
reportFailure(exceptionReporters, exception);
if (context != null) {
context.close();
}
}
}
catch (Exception ex) {
logger.warn("Unable to close ApplicationContext", ex);
}
ReflectionUtils.rethrowRuntimeException(exception);
}
其中的静态方法ReflectionUtils.rethrowRuntimeException的名称比较敏感。看意思似乎是重新抛出运行时异常。那么它里面的实现是怎样的呢?看代码:
public static void rethrowRuntimeException(Throwable ex) {
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
if (ex instanceof Error) {
throw (Error) ex;
}
throw new UndeclaredThrowableException(ex);
}
其功能为:
如果异常为RuntimeException的实例,抛出RuntimeException异常;如果是Error的实例,抛出Error;否则抛出UndeclaredThrowableException异常。
对于它的应用,文档是这么讲的:
Rethrow the given exception, which is presumably the target exception of an InvocationTargetException. Should only be called if no checked exception is expected to be thrown by the target method.
Rethrows the underlying exception cast to a RuntimeException or Error if appropriate; otherwise, throws an UndeclaredThrowableException.
译文:
当某个异常可能是 InvocationTargetException 的目标异常时,才重新抛出该异常。只有当目标方法不打算抛出checked异常时,才调用该方法(ReflectionUtils.rethrowRuntimeException)。
该方法所做的是:将基础异常抛出为 RuntimeException 或 Error,否则抛出 UndeclaredThrowableException 异常。
结合我们文章中的情况:
应用启动触发了java.lang.NoClassDefFoundError
所以对外抛出的是Error,这里是从catch方法块中再次抛出的异常,而所有的上层调用链上都没有对该方法抛出的异常进行捕获并进行日志打印。所以,在控制台中才不会打印相关的错误信息。
得到的启示
该方法来自于 spring-core包,是在反射时调用的。
由于 UndeclaredThrowableException 继承自 RuntimeException ,所以该方法当 ex 是 Error 时,会抛出 Error,其余所有的 Exception,都被统一转成了 RuntimeException。
四、总结
本文记录了一次 Spring Boot 应用的启动异常。并通过异常捕获的方式最终定位了问题的原因。本文给出了Spring Boot应用启动报错(Process finished with exit code 1)的一般定位思路。
当不确定是发生了异常还是Error时,建议捕获它们的父类Throwable(如代码2-3所示)。
另外本文也给出了java.lang.NoClassDefFoundError的一般定位思路:通过发生异常的代码定位依赖的哪一个maven包没有相关的类,然后通过mvnrepository网站查询相关依赖的版本,最后调整maven依赖为建议版本。
本文还告诉了读者一些补充知识:比如如何通过一个类查询到其所有的子类;如何绘制类的层次架构图;Java的异常层次结构是什么样的…