七的博客

SpringBoot+JSP打包后访问异常问题

Java

SpringBoot+JSP打包后访问异常问题

环境:

  • JDK17、8
  • SpringBoot 2.3
  • Tomcat Embed 9.0.39
  • JSP

启动命令:

java -jar xxx.jar

1. 背景

公司有个超级老的项目,以前是个 JSP 应用,很多逻辑都写在 JSP 页面里面 。现在为了完成某些指标任务,需要改造到 SpringBoot 2.3 + JSP 的形式。改造过程也是各种坑,不过一般顺着报错就给处理了。

改造完成后,在 IDEA 中启动项目都是正常的。 不过打成 JAR 包后,启动的时候会报找不到 JAR 包的错误。

2. 启动报错排查

启动错误如下:

java.io.IOException: Unable to open root Jar file 'war:file:/E:/xxx-web/target/xxx-web-1.0.0.jar*/BOOT-INF/lib/spring-webmvc-5.2.10.RELEASE.jar'
        at org.springframework.boot.loader.jar.Handler.getRootJarFile(Handler.java:251)
        at org.springframework.boot.loader.jar.Handler.getRootJarFileFromUrl(Handler.java:232)
        at org.springframework.boot.loader.jar.Handler.openConnection(Handler.java:94)
        at java.base/java.net.URL.openConnection(URL.java:1094)
        at java.base/java.net.URL.openStream(URL.java:1161)
        at org.apache.tomcat.util.descriptor.tld.TldResourcePath.openStream(TldResourcePath.java:127)
        at org.apache.tomcat.util.descriptor.tld.TldParser.parse(TldParser.java:61)
        at org.apache.jasper.servlet.TldScanner.parseTld(TldScanner.java:275)
        at org.apache.jasper.servlet.TldScanner$TldScannerCallback.scan(TldScanner.java:315)
        at org.apache.tomcat.util.scan.StandardJarScanner.process(StandardJarScanner.java:387)
        at org.apache.tomcat.util.scan.StandardJarScanner.processURLs(StandardJarScanner.java:318)
        at org.apache.tomcat.util.scan.StandardJarScanner.doScanClassPath(StandardJarScanner.java:270)
        at org.apache.tomcat.util.scan.StandardJarScanner.scan(StandardJarScanner.java:233)
        at org.apache.jasper.servlet.TldScanner.scanJars(TldScanner.java:262)
        at org.apache.jasper.servlet.TldScanner.scan(TldScanner.java:104)
        at org.apache.jasper.servlet.JasperInitializer.onStartup(JasperInitializer.java:83)
        at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5166)
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
        at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1384)
        at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1374)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
        at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
        at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:909)
        at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:843)
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
        at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1384)
        at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1374)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
        at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
        at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:909)
        at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:262)
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
        at org.apache.catalina.core.StandardService.startInternal(StandardService.java:421)
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
        at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:930)
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
        at org.apache.catalina.startup.Tomcat.start(Tomcat.java:486)
        at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:123)
        at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.<init>(TomcatWebServer.java:104)
        at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:440)
        at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:193)
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:178)
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:158)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:545)
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:405)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
        at com.linyang.xxx.EnergyWebApplication.main(EnergyWebApplication.java:24)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:87)
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:50)
        at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:58)
Caused by: java.lang.IllegalStateException: Not a file URL
        at org.springframework.boot.loader.jar.Handler.getRootJarFile(Handler.java:238)
        ... 61 common frames omitted
[main] WARN  org.apache.tomcat.util.scan.StandardJarScanner:175 - Failed to scan [jar:file:/E:/xxx-web/target/xxx-web-1.0.0.jar!/BOOT-INF/lib/jstl-1.2.jar!/] from classloader hierarchy
java.io.IOException: Unable to open root Jar file 'war:file:/E:/xxx-web/target/xxx-web-1.0.0.jar*/BOOT-INF/lib/jstl-1.2.jar'
        at org.springframework.boot.loader.jar.Handler.getRootJarFile(Handler.java:251)
        at org.springframework.boot.loader.jar.Handler.getRootJarFileFromUrl(Handler.java:232)
        at org.springframework.boot.loader.jar.Handler.openConnection(Handler.java:94)
        at java.base/java.net.URL.openConnection(URL.java:1094)
        at java.base/java.net.URL.openStream(URL.java:1161)
        ... 省略
Caused by: java.lang.IllegalStateException: Not a file URL
        at org.springframework.boot.loader.jar.Handler.getRootJarFile(Handler.java:238)
        ... 61 common frames omitted

一看这个错误原因,看着像是找不到 JAR 包导致的报错。 解压程序包看看提示的 JAR 包是否真的缺失,先看看 spring-webmvc 的包:

$ ls -al | grep "spring-webmvc-5.2.10.RELEASE.jar"
-rw-r--r-- 1 dev 197121   956758 Oct 27  2020 spring-webmvc-5.2.10.RELEASE.jar

可以看到 spring-webmvc-5.2.10.RELEASE.jar 包是存在的,那么再试试 jstl-1.2.jar 这个包:

$ ls -al | grep "jstl-1.2.jar"
-rw-r--r-- 1 dev 197121   414240 Jul 20  2006 jstl-1.2.jar

可以看到这两个找不到的 JAR 包都是可以找到的,这就很奇怪了。不过虽然是报错,但是没有影响最终的报错。

3. 运行报错排查

抱着侥幸的心理还是试了下看看登录页面能不能正常显示,果不其然还是报错的:

[http-nio-18090-exec-9] ERROR o.a.c.c.C.[.[localhost].[/].[dispatcherServlet]:175 - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [/WEB-INF/page/xxx/framework/login.jsp (line: [2], column: [0]) The absolute uri: [http://java.sun.com/jsp/jstl/core] cannot be resolved in either web.xml or the jar files deployed with this application] with root cause
org.apache.jasper.JasperException: /WEB-INF/page/xxx/framework/login.jsp (line: [2], column: [0]) The absolute uri: [http://java.sun.com/jsp/jstl/core] cannot be resolved in either web.xml or the jar files deployed with this application
        at org.apache.jasper.compiler.DefaultErrorHandler.jspError(DefaultErrorHandler.java:42)
        at org.apache.jasper.compiler.ErrorDispatcher.dispatch(ErrorDispatcher.java:292)
        ... 省略
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.base/java.lang.Thread.run(Thread.java:842)


这个错误还是关于 JSTL 的,仔细看了下这个异常的信息,在末尾发现了这几行不太寻常的:

at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

这个错误堆栈信息一看就是 JDK9以上的。从 JDK 9 开始,Java 引入了模块系统,所以这个错误堆栈中包含了模块名 java.base

而在 JDK 8 及之前的版本中,类似的错误堆栈信息通常不会显示模块名称,通常长这个样子:

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)

突然想到现在运行这个 JAR 包使用的肯定不是 JDK8,而是更新的版本,看了下当前电脑默认的 JDK 版本:

java version "17.0.9" 2023-10-17 LTS
Java(TM) SE Runtime Environment (build 17.0.9+11-LTS-201)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.9+11-LTS-201, mixed mode, sharing)

果然是使用了 JDK17,切换成 JDK8 重新跑。 很诡异的是,这次控制台也是不报错,但是访问 JSP 页面直接就没响应了。

4. JDK跟Tomcat的兼容排查

一开始将问题指向了 JDK ,因为切换成低版本的 JDK 就可以运行了。

在 Tomcat 官网这篇文档里面 https://tomcat.apache.org/whichversion.html 找到了下面表格内容:

Servlet Spec JSP Spec EL Spec WebSocket Spec Authentication Spec (JASPIC) Apache Tomcat Version Latest Released Version Supported Java Versions
6.1 4.0 6.0 2.2 3.1 11.0.x 11.0.0-M21 (beta) 17 and later
6.0 3.1 5.0 2.1 3.0 10.1.x 10.1.25 11 and later
4.0 2.3 3.0 1.1 1.1 9.0.x 9.0.90 8 and later

从表格里面可以看到,Tomcat 9 是支持 JDK8 以及以后的版本,JDK 通常保持向前兼容的,问题应该不是出在 Tomcat 跟 JDK 本身。

再试试从网上搜搜 JDK9 跟 Tomcat 9的兼容性问题,在 Github 上找到了下面几个 issue :

这是一篇关于 Jetty 跟 JDK9 的兼容性问题,虽然主题不是 Tomcat ,但是 Tomcat 跟 Jetty 都是实现,问题应该差不太多。评论区有人提出这是 SpringBoot 的问题导致,于是直接去 SpringBoot 的社区看问题。

the issue was about Spring Boot. The discussion was over there. Here is the relavant issue. spring-projects/spring-boot#10456 (comment)

This is a limitation of Jasper’s StandardJarScanner. When running via the main method (rather than as a packaged war), TLDs are found as a result of StandardJarScanner scanning the class path. It does so by walking up the ClassLoader hierarchy and examine the URLs of any URLClassLoaders that it finds. When running via a main method using Java 9, the ClassLoader that has the TLD-containing jars on its class path is not a URLClassLoader so the TLDs are not found. It works fine when packaging the application as a war because the TLD-containing jars are found by virtue of being packaged in WEB-INF/lib.

这是 Jasper 的 StandardJarScanner 的一个限制。当通过主方法(而不是打包的 WAR 文件)运行时,StandardJarScanner 通过扫描类路径找到 TLD 文件。它通过遍历类加载器层次结构并检查 URLClassLoader 的 URL 实现这一点。在使用 Java 9 的主方法运行时,包含 TLD 文件的类加载器不是 URLClassLoader,因此无法找到 TLD 文件。而打包成 WAR 文件时,由于 TLD 文件在 WEB-INF/lib 中,所以可以正常找到。

那么问题就来到 SpringBoot 上了,还是得看看 SpringBoot 调整了些什么。

5. SpringBoot各版本对JSP的支持排查

先排查是不是 Springboot 高版本不支持访问 JSP 了,看看 Github 上有没有相关的 issue 。找到一个类似的 issue :

JSP files are not loaded starting with version 1.4.3 https://github.com/spring-projects/spring-boot/issues/13420

作者回应了这么一句话:

Thanks for the report but, as noted in the documentation, JSPs are only supported with war packaging. JSP 仅在 WAR 包中受支持。

然后作者给出来对应的 SpringBoot 关于 JSP 相关描述的文档地址:

https://docs.spring.io/spring-boot/docs/2.1.9.RELEASE/reference/html/boot-features-developing-web-applications.html#boot-features-jsp-limitations

29.4.5 JSP Limitations When running a Spring Boot application that uses an embedded servlet container (and is packaged as an executable archive), there are some limitations in the JSP support.

With Jetty and Tomcat, it should work if you use war packaging. An executable war will work when launched with java -jar, and will also be deployable to any standard container. JSPs are not supported when using an executable jar. Undertow does not support JSPs. Creating a custom error.jsp page does not override the default view for error handling. Custom error pages should be used instead.

上面的意思总结下就是:

  • 当你运行一个使用内嵌 servlet 容器的 Spring Boot 应用程序,并且这个程序是作为一个可执行的归档文件打包时,你会遇到一些关于 JSP 支持的限制。

  • 对于 Jetty 和 Tomcat,选择使用 war 格式打包,那么可以正常工作。通过 java -jar 命令启动的可执行 war 文件不仅可以运行,还可以部署到任何标准的 servlet 容器中。但是,如果你使用可执行 jar 文件的方式,则不支持 JSP。

  • 如果你使用 Undertow 作为服务器,那么 JSP 是不被支持的。

  • 如果你尝试创建一个自定义的 error.jsp 页面,它不会替代默认的错误处理视图。因此,建议使用自定义错误页面来处理错误。

同时作者更改了 Spring-boot 的文档,调整前:

With Tomcat it should work if you use war packaging, i.e. an executable war will work, and will also be deployable to a standard container (not limited to, but including Tomcat). An executable jar will not work because of a hard coded file pattern in Tomcat.

对于 Tomcat,如果你使用 war 格式打包,它应该能够正常工作,即一个可执行的 war 文件可以运行, 同时也可以部署到标准容器中(不限于但包括 Tomcat)。由于 Tomcat 中硬编码的文件模式问题,可执行的 jar 文件将无法工作。

With Jetty it should work if you use war packaging, i.e. an executable war will work, and will also be deployable to any standard container.

对于 Jetty,如果你使用 war 格式打包,它也应该能够正常工作,即一个可执行的 war 文件可以运行, 并且可以部署到任何标凾容器中。

调整后:

With Jetty and Tomcat it should work if you use war packaging. An executable war will work when launched with java -jar, and will also be deployable to any standard container. JSPs are not supported when using an executable jar.

对于 Jetty 和 Tomcat,如果你使用 war 格式打包,它应该能够正常工作。使用 java -jar 命令启动的可执行 war 文件将会运行, 并且也可以部署到任何标准容器中。使用可执行 jar 文件时,不支持 JSP。

从上面的结论来看,在 Springboot 任意版本,应该都是支持 WAR 包访问 JSP 页面的。 但是在高版本中 JAR 是不会正常工作的。

6. 问题解决方式

经过上面的排查可以得出结论:

  • SpringBoot 低版本打包成 JAR 或者 WAR 都可以正常运行。

  • SpringBoot 高版本打包成 JAR 不能正常运行,打成 WAR 可以正常运行。

同时又测了下 2种 SpringBoot 插件版本 + 2种打包方式的组合,得出来的测试结果如下:

  • spring-boot-maven-plugin 2.3.5.RELEASE + JAR 不能正常访问 JSP 页面
  • spring-boot-maven-plugin 1.4.2.RELEASE + JAR 可以正常访问 JSP 页面
  • spring-boot-maven-plugin 2.3.5.RELEASE + WAR 可以正常访问 JSP 页面
  • spring-boot-maven-plugin 1.4.2.RELEASE + WAR 可以正常访问 JSP 页面

7. 项目配置参考

7.1 JSP 文件目录配置

在项目 src/main 文件夹中新建一个 webapp 文件夹。 webapp 目录下新建文件夹 /WEB-INF/page/ 目录用来放 JSP 文件。

7.2 application.yml 配置

spring:
    mvc:
      view:
        prefix: /WEB-INF/page/
        suffix: .jsp

  • spring.mvc.view.prefix 用来指定视图的前缀路径,我们项目将视图文件存放在 /WEB-INF/page/ 目录下。
  • spring.mvc.view.suffix 用来指定视图的后缀,通常扩展名都配置为 .jsp。

上面的配置主要是告诉 Spring MVC 在解析视图时,去路径 /WEB-INF/page/ 下查找 JSP 文件。

7.3 pom.xml 配置

<?xml version="1.0" encoding="utf-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">  
  <parent> 
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-parent</artifactId>  
    <version>2.3.5.RELEASE</version>  
    <relativePath/> 
  </parent>
  <groupId>com.xxx.xxx</groupId>  
  <artifactId>xx</artifactId>  
  <version>1.0.0</version> 

  <!-- 注意这里更改为 war -->
  <packaging>war</packaging>  
  <dependencies> 
    <dependency> 
      <groupId>org.springframework.boot</groupId>  
      <artifactId>spring-boot-starter-web</artifactId> 
    </dependency>  
    <dependency> 
      <groupId>org.springframework.boot</groupId>  
      <artifactId>spring-boot-starter-tomcat</artifactId> 
    </dependency>  
    <dependency> 
      <groupId>javax.servlet</groupId>  
      <artifactId>javax.servlet-api</artifactId>  
      <scope>provided</scope> 
    </dependency>  
    <dependency> 
      <groupId>javax.servlet</groupId>  
      <artifactId>jstl</artifactId> 
    </dependency>  
    <dependency> 
      <groupId>org.apache.tomcat.embed</groupId>  
      <artifactId>tomcat-embed-jasper</artifactId>  
      <scope>provided</scope> 
    </dependency> 
  </dependencies>  
  <build> 
    <plugins> 
      <plugin> 
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-maven-plugin</artifactId>  
        <configuration> 
          <excludes> 
            <exclude> 
              <groupId>org.projectlombok</groupId>  
              <artifactId>lombok</artifactId> 
            </exclude> 
          </excludes>  
          <mainClass>com.xxx.xxx.xxxxApplication</mainClass> 
        </configuration> 
      </plugin>  
      <plugin> 
        <groupId>org.apache.maven.plugins</groupId>  
        <artifactId>maven-war-plugin</artifactId>  
        <version>3.3.1</version>  
        <configuration> 
          <webResources> 
            <resource> 
              <directory>${project.basedir}/libs</directory>  
              <targetPath>WEB-INF/lib</targetPath>  
              <includes> 
                <include>**/*.jar</include> 
              </includes> 
            </resource> 
          </webResources> 
        </configuration> 
      </plugin> 
    </plugins>  
    <resources> 
      <resource> 
        <directory>${project.basedir}/libs</directory>  
        <targetPath>BOOT-INF/lib/</targetPath>  
        <includes> 
          <include>**/*.jar</include> 
        </includes> 
      </resource>  
      <resource> 
        <directory>src/main/java</directory>  
        <includes> 
          <include>**/*.xml</include>  
          <include>**/*.json</include>  
          <include>**/*.ftl</include> 
        </includes> 
      </resource>  
      <resource> 
        <directory>src/main/webapp</directory>  
        <targetPath>META-INF/resources</targetPath>  
        <filtering>false</filtering>  
        <includes> 
          <include>**/**</include> 
        </includes> 
      </resource>  
      <resource> 
        <directory>src/main/resources</directory>  
        <filtering>true</filtering> 
      </resource> 
    </resources> 
  </build> 
</project>

在 pom.xml 中主要注意下面几点: - 打包方式切换成 war ,这样无论使用 SpringBoot 的高低版本都可以正常访问 JSP 页面。 - resource 标签中配置了一些打包相关的配置,比如将自定义的 JAR 一起打包到最终的压缩包中。

参考链接