spring boot使用jsp的一个大坑

现象

最近有个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里面,我的天!看来我只好修改一下大神的代码了。
修改点包括:

  1. 增加applicationClass参数,确保StaticResourceConfigurer这个类放在其他jar中获取的路径也不会出错。
  2. // when run as exploded directory那一段几乎很少用的上,所以我注释掉了。
  3. 把/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

你可能感兴趣的:(spring boot使用jsp的一个大坑)