现象
最近有个spring boot的应用总感觉卡卡的,前前后后花了三四天的时间才定位到问题所在。现象是程序刚跑起来的时候,做任何数据库查询都是正常的毫秒内完成,一旦涉及到更新的事务提交就会慢到3秒以上,而且只有第一次事务提交这个现象是百分百重现,之后的事务提交则是间歇性出现。花了很长时间,把cpu、内存、网络、数据库、连接池的问题全都排除了,还是找不到问题的原因。只好使出最无奈的一招,把全局日志级别调到TRACE,每一行代码之间打日志看具体哪一行慢,慢的过程中有什么特别的TRACE日志。上天眷顾,果真出现了端倪:
2019-11-14 00:20:37.251 DEBUG 7035 --- [http-nio-8081-exec-1] o.a.c.loader.WebappClassLoaderBase : --> Returning ClassNotFoundException
2019-11-14 00:20:37.251 DEBUG 7035 --- [http-nio-8081-exec-1] o.a.c.loader.WebappClassLoaderBase : --> Passing on ClassNotFoundException
2019-11-14 00:20:37.252 DEBUG 7035 --- [http-nio-8081-exec-1] o.a.c.loader.WebappClassLoaderBase : findClass(com.renaissance.core.entity.BaseEntityCustomizer)
2019-11-14 00:20:37.253 DEBUG 7035 --- [http-nio-8081-exec-1] o.a.c.loader.WebappClassLoaderBase : findClassInternal(com.renaissance.core.entity.BaseEntityCustomizer)
2019-11-14 00:20:39.695 DEBUG 7035 --- [http-nio-8081-exec-1] o.a.c.loader.WebappClassLoaderBase : --> Returning ClassNotFoundException
2019-11-14 00:20:39.695 DEBUG 7035 --- [http-nio-8081-exec-1] o.a.c.loader.WebappClassLoaderBase : --> Passing on ClassNotFoundException
这种时候,花2秒的时间加做类加载?而且还没加载到!!!WTF?
经过了一顿的google操作,终于找到了问题的根源。
为什么慢?
https://github.com/spring-projects/spring-boot/issues/16471
根据github里面的讨论,这种情况只会出现在spring boot使用jsp的时候,因为使用jsp要求你将项目打成war包,通过java -jar xxx.war的方式运行的时候,类加载需要经历两层压缩嵌套,war包和war包内WEB-INF/lib内的jar包,tomcat的WebappClassLoaderBase没有对这种加载做过优化,所以会异常慢。
为什么会时不时地就加载类?
https://stackoverflow.com/questions/39234159/springboot-embedded-tomcat-classloader-slowness
根据stackoverflow上的讨论,因为embeded tomcat默认会开启reloadable热加载,默认每超过15秒都会尝试重新加载。这也大概解释了为什么日志显示加载不到还是能正常跑,估计首次启动的时候是通过spring boot的类加载成功了,后面通过tomcat的热加载不支持的两层嵌套就会一直失败,不过这也纯粹是我的猜测,但因为懒惰,我也没有深入去验证这个猜测!
解决方案
既然是war包导致的,换到jar包就好了。spring boot大部分人应该本来就打的是jar包,所以不会遇到这个问题,这大概也是spring boot那些作者认为这个bug是个low priority的原因吧(这些人也太不负责任了)。
但是如果你的项目里面用了jsp,按照官方文档,只有打war包才能让jsp生效!幸好有个阿里大神给出了让jar包也能支持jsp的代码方案。http://hengyunabc.github.io/spring-boot-fat-jar-jsp-sample/
按照大神的说法,servlet 3规范里的应用jar包的META-INF/resources就是一个ResourceSet,所以只要在项目的resources目录里新建META-INF/resources,把jsp放在里面就ok了。但是后来spring boot 1.4调整了fat jar里面的结构,将项目代码都放在fat jar的BOOT-INF/classes目录里面,导致tomcat扫不到META-INF/resources了,所以需要手动把fatjar根目录下的META-INF/ classes加入resourceSet。
对于大部分人来说这个大神的这个解决方案是可行的,然而,放到我的项目里面却还是不行!!!又是几个小时的时间,我发现,尽管是相同的spring boot版本,用maven打出来的jar包和用gradle打出来的jar包是不一样的。maven出来的jar包确实如大神所说jsp会被放在在fatjar的META-INF/resources目录里面,但是gradle出来的jar包jsp却是在BOOT-INF/classes/META-INF/resources里面,我的天!看来我只好修改一下大神的代码了。
修改点包括:
- 增加applicationClass参数,确保StaticResourceConfigurer这个类放在其他jar中获取的路径也不会出错。
-
// when run as exploded directory
那一段几乎很少用的上,所以我注释掉了。 - 把/BOOT-INF/classes/META-INF/resources也加入resourceSet。
public class StaticResourceConfigurer implements LifecycleListener {
private final Context context;
private final Class applicationClass;
public StaticResourceConfigurer(Context context, Class applicationClass) {
this.context = context;
this.applicationClass = applicationClass;
}
@Override
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
URL location = applicationClass.getProtectionDomain().getCodeSource().getLocation();
// if (ResourceUtils.isFileURL(location)) {
// // when run as exploded directory
// String rootFile = location.getFile();
// if (rootFile.endsWith("/BOOT-INF/classes/")) {
// rootFile = rootFile.substring(0, rootFile.length() - "/BOOT-INF/classes/".length() + 1);
// }
// if (!new File(rootFile, "META-INF" + File.separator + "resources").isDirectory()) {
// return;
// }
//
// try {
// location = new File(rootFile).toURI().toURL();
// } catch (MalformedURLException e) {
// throw new IllegalStateException("Can not add tomcat resources", e);
// }
// }
String locationStr = location.toString();
// when run as fat jar
if (locationStr.endsWith("/BOOT-INF/classes!/")) {
locationStr = locationStr.substring(0, locationStr.length() - "/BOOT-INF/classes!/".length() + 1);
try {
location = new URL(locationStr);
} catch (MalformedURLException e) {
throw new IllegalStateException("Can not add tomcat resources", e);
}
// maven jar
this.context.getResources().createWebResourceSet(WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", location, "/META-INF/resources");
// gradle jar
this.context.getResources().createWebResourceSet(WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", location, "/BOOT-INF/classes/META-INF/resources");
}
}
}
}
也附上我的demo地址,使用的是gradle+spring boot 2.1.0。
https://github.com/shougaoshougao/spring-boot-fat-jar-jsp-sample