Spring Boot 3.0.1 + JDK 17 + packaing war Startup Error by java -jar demo-springboot-jsp.war
I create a simple spring boot project , and config to support very simple jsp, and packing to war.
then run java -jar demo-springboot-jsp.war
if config spring boot 2.7.6 will run and startup success.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
if config spring boot 3.0.1 will run and startup fail.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
Error Description first
$ java -jar demo-springboot-jsp.war
...
2022-12-26T13:43:01.062+08:00 INFO 21566 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.4]
2022-12-26T13:43:01.293+08:00 ERROR 21566 --- [ main] org.apache.catalina.core.ContainerBase : A child container failed during start
java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbeddedContext[]]
at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122) ~[na:na]
...
Compare two version war
Spring boot 2.7.6 , built war , has org/springframework/boot/loader/data/RandomAccessDataFile$1.class
Spring boot 3.0.1, built war , does not have this file.
My Project
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── example
│ └── demospringbootjsp
│ ├── DemoSpringbootJspApplication.java
│ └── HelloJSPController.java
├── resources
│ └── application.properties
└── webapp
├── index.jsp
└── WEB-INF
└── jsp
└── hello.jsp
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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!--<version>3.0.1</version>-->
<version>2.7.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo-springboot-jsp</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>demo-springboot-jsp</name>
<description>Demo Spring Boot JSP</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
DemoSpringbootJspApplication.java
package com.example.demospringbootjsp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class DemoSpringbootJspApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(DemoSpringbootJspApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(DemoSpringbootJspApplication.class, args);
}
}
HelloJSPController.java
package com.example.demospringbootjsp;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import java.util.Map;
@Controller
public class HelloJSPController {
@RequestMapping(value="/hellojsp", method = RequestMethod.GET)
public ModelAndView hello(Map<String, Object> model) {
System.out.println("HelloJSP Controller hello");
model.put("message", "HelloJSP Controller hello");
// /WEB-INF/jsp/hello.jsp
return new ModelAndView("hello");
}
}
hello.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello</title>
</head>
<%
// debug
System.out.println("webapp / WEB-INF / jsp / hello.jsp ");
%>
<body>
<h1 class="color-red">SpringBoot hello.jsp!! ${message} </h1>
</body>
</html>
Build
mvn clean package
Run
cd target
java -jar demo-springboot-jsp.war
Spring boot 3.0.1
Spring boot 3.0.1 / 3.0.0 demo-springboot-jsp.war , can run ok in standalone apache-tomcat-10.1.4 (some dependency need add scope provided)
but can't use java -jar demo-springboot-jsp.war run and startup fail.
Same code, only change to 2.7.6
It can use java -jar demo-springboot-jsp.war run and startup OK.
Comment From: lico
I am facing exactly the same issue. It seems to be caused by tomcat-embed-jasper (JSP support for Tomcat).
The application was working just fine in Spring Boot 2.7. Then, I upgraded it to Spring Boot 3 and I was able to execute it without errors both in my Eclipse environment and from the command line by typing mvn -o spring-boot:run.
Then, I was able to generate the war file using a standard "mvn clean install". But when I run Spring Boot using "java -jar myFile.war" I got the following error message:
java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbeddedContext[/vault]]
...
Caused by: java.lang.IllegalStateException: zip file closed
at java.base/java.util.zip.ZipFile.ensureOpen(ZipFile.java:831)
at java.base/java.util.zip.ZipFile.getManifestName(ZipFile.java:1057)
at java.base/java.util.zip.ZipFile$1.getManifestName(ZipFile.java:1100)
at java.base/java.util.jar.JarFile.getManEntry(JarFile.java:937)
at java.base/java.util.jar.JarFile.checkForSpecialAttributes(JarFile.java:1000)
at java.base/java.util.jar.JarFile.isMultiRelease(JarFile.java:389)
at org.apache.tomcat.util.scan.JarFileUrlJar.<init>(JarFileUrlJar.java:68)
at org.apache.tomcat.util.scan.JarFactory.newInstance(JarFactory.java:41)
at org.apache.tomcat.util.scan.StandardJarScanner.process(StandardJarScanner.java:393)
at org.apache.tomcat.util.scan.StandardJarScanner.processURLs(StandardJarScanner.java:328)
at org.apache.tomcat.util.scan.StandardJarScanner.doScanClassPath(StandardJarScanner.java:271)
at org.apache.tomcat.util.scan.StandardJarScanner.scan(StandardJarScanner.java:234)
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:5153)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
... 43 common frames omitted
After many trials and errors, I found the problem comes from Jasper. I tried different combinations of versions (Tomcat, Jasper) but nothing was working effectively.
Some other people got the problem too, see for example: https://stackoverflow.com/questions/74913190/spring-boot-apps-jar-not-working-issue-with-tomcat-embed-jasper
This incident is important because otherwise it is impossible to make JSP work with Spring Boot 3.
Comment From: markvr
As a workaround you could unpack the jar and run it with something like:
java -classpath "app/WEB-INF/lib/*;app/WEB-INF/classes/" com.example.jsptest.Application
Comment From: wilkinsona
With both Spring Boot 2.7.x and 3.0.x, Tomcat's check to see if it's scanning a multi-release jar results in java.lang.IllegalStateException: zip file closed being thrown. This only causes a problem in Spring Boot 3.0.x due to a change in Tomcat.
In Tomcat 9, this multi-release check is performed reflectively and the failure is caught and mapped to false:
@Override
public boolean jarFileIsMultiRelease(JarFile jarFile) {
try {
return ((Boolean) isMultiReleaseMethod.invoke(jarFile)).booleanValue();
} catch (ReflectiveOperationException | IllegalArgumentException e) {
return false;
}
}
In Tomcat 10.1 it calls the isMultiRelease method directly with no error handling:
multiRelease = jarFile.isMultiRelease();
Despite the failure being due to a change in Tomcat, I think we need to fix this in Boot as ourJarUrlConnection shouldn't be returning a JarFile that has already been closed.
Comment From: wilkinsona
This is the same underlying problem as https://github.com/spring-projects/spring-boot/issues/32106. Closing as a duplicate.
Comment From: philwebb
Reopening because it might be possible to fix this in isolation without needing the full #32106 fix.
Comment From: wilkinsona
I thought we might be able to override isMultiRelease() and catch the failure. Unfortunately, it's final. An alternative would be to call isMultiRelease() in the constructor before we call super.close(). However, I suspect that will have a noticeable impact on performance as, presumably, the special attributes check that it triggers is deferred until first use for a reason.
Comment From: philwebb
I wonder if we can set the jdk.util.jar.enableMultiRelease system property to false for now?
Comment From: wilkinsona
That's a good idea. It fixes the sample above:
java -Djdk.util.jar.enableMultiRelease=false -jar target/demo-springboot-jsp.war
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.1)
2023-01-18T17:54:39.390Z INFO 65312 --- [ main] c.e.d.DemoSpringbootJspApplication : Starting DemoSpringbootJspApplication v0.0.1-SNAPSHOT using Java 17.0.5 with PID 65312 (/Users/awilkinson/dev/temp/gh-33633/target/demo-springboot-jsp.war started by awilkinson in /Users/awilkinson/dev/temp/gh-33633)
2023-01-18T17:54:39.393Z INFO 65312 --- [ main] c.e.d.DemoSpringbootJspApplication : No active profile set, falling back to 1 default profile: "default"
2023-01-18T17:54:40.138Z INFO 65312 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2023-01-18T17:54:40.147Z INFO 65312 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-01-18T17:54:40.147Z INFO 65312 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.4]
2023-01-18T17:54:40.425Z INFO 65312 --- [ main] org.apache.jasper.servlet.TldScanner : At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
2023-01-18T17:54:40.506Z INFO 65312 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-01-18T17:54:40.509Z INFO 65312 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1074 ms
2023-01-18T17:54:40.795Z INFO 65312 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-01-18T17:54:40.808Z INFO 65312 --- [ main] c.e.d.DemoSpringbootJspApplication : Started DemoSpringbootJspApplication in 1.749 seconds (process running for 2.081)
Perhaps we should just document it rather than disabling multi-release jars out of the box?
Comment From: philwebb
That might have to do until we can get to the JarFile rewrite.
Comment From: garretwilson
In Tomcat 10.1 it calls the isMultiRelease method directly with no error handling …
So if this is related to a Tomcat change, has anyone file a ticket with the Tomcat team? Do they even know about it?
Despite the failure being due to a change in Tomcat, I think we need to fix this in Boot as our
JarUrlConnectionshouldn't be returning aJarFilethat has already been closed.
That of course sounds like the correct approach, but later the ticket seems to imply "but we won't get around to fixing JarUrlConnection for a while, and instead we should just document that Spring Boot 3 executable JARs are broken".
Shouldn't we be coordinating with the Tomcat team to at least get some less disruptive workaround in the meantime? Should I reach out to them, or is someone already working on that end?
Comment From: garretwilson
I have started a Tomcat users mailing list thread to see if they can help on their end.
Specifying a system property on the command line defeats much of the purpose of an executable JAR and is not satisfactory. It seems a shame that the entire Spring Boot 3.x executable JAR system is broken; surely we can find a more transparent workaround until #32106 is fixed.
Comment From: wilkinsona
It seems a shame that the entire Spring Boot 3.x executable JAR system is broken
As far as we're aware, that's not the case. The problem only applies to applications that use JSPs. From what have seen, those are an increasingly small minority.
Comment From: garretwilson
The problem only applies to applications that use JSPs.
Ah, that is good news. Thank you for the clarification.
From what have seen, those are an increasingly small minority.
And for good reasons.
Still it doesn't help someone like me who is pulling out an old app I still need to work until it is migrated.
Someone above mentioned documentation. These details would be super-helpful in documentation. Things like:
- If you upgrade to Spring Boot 3, your executable JAR will break if you're using JSPs.
- The only workaround so far is to add some system property to the command line, partially defeating the purpose of the executable JAR.
- You could downgrade to Spring Boot 2, but this may break your unit tests because Spring Boot 2.x (still) explicitly includes an old version of the JUnit API which doesn't include some classes. (See JUnit 5 Issue 2991.)
- You could downgrade to Spring Boot 2, but this may break your logging, because Spring Boot 2.x (still) relies on an obsolete SLF4J 1.x static class no longer present in SLF4J 2.x, forcing you to use the workaround I published. (See comments in SLF4J-561).
Just that information would save someone hours of wasted time.
Comment From: wilkinsona
Someone above mentioned documentation. These details would be super-helpful in documentation.
That's the purpose of this issue. It wouldn't still be open if we didn't plan to improve the documentation.
You could downgrade to Spring Boot 2, but this may break your unit tests because Spring Boot 2.x (still) explicitly includes an old version of the JUnit API which doesn't include some classes. (See https://github.com/junit-team/junit5/issues/2991.)
Spring Boot 2.7 uses JUnit 5.9 which, at the time of writing, is the newest minor version available.
You could downgrade to Spring Boot 2, but this may break your logging, because Spring Boot 2.x (still) relies on an obsolete SLF4J 1.x static class no longer present in SLF4J 2.x, forcing you to use the workaround I published. (See comments in SLF4J-561).
SLF4J 1.x is not obsolete and is still actively maintained.
Comment From: garretwilson
Spring Boot 2.7 uses JUnit 5.9 which, at the time of writing, is the newest minor version available.
If I misinterpreted the dependency tree, please point out my mistake.
org.springframework.boot:spring-boot-starter-parent:2.7.8 extends org.springframework.boot:spring-boot-dependencies:2.7.8, which specifies:
<junit-jupiter.version>5.8.2</junit-jupiter.version>
Thus even though my parent POM specifies org.junit.jupiter:junit-jupiter-engine:jar:5.9.1, importing org.springframework.boot:spring-boot-starter-parent:2.7.8 explicitly downgrades one dependency to org.junit.jupiter:junit-jupiter-api:jar:5.8.2, which is an older version that doesn't contain the CleanupMode class. Please see JUnit 5 Issue 2991 for more details.
SLF4J 1.x is not obsolete and is still actively maintained.
We should distinguish between the SLF4J API and SL4JF logger implementations. The SLF4J 2.x API is backwards-compatible with the SLF4J 1.x API, both of when are still in use. A consumer of the API should never reference StaticLoggerBinder, which is an obsolete approach to binding service implementations (and really should have been abandoned a decade or more ago). If Spring Boot 2.x references StaticLoggerBinder, it breaks the SLF4J 2.x API, which no longer allows it. And unless Spring Boot includes a logging implementation, it shouldn't be using StaticLoggerBinder in the first place. Perhaps Spring Boot 2.x includes a logging implementation, I don't know, but the point is that by referencing StaticLoggerBindiner as an SLF4J 1.x implementation, it breaks applications that use the SLF4J 2.x API which would otherwise work fine.
In any case, as discussed in SLF4J-561 I have published a shim that brings back StaticLoggerBinder for applications using the SLF4J 2.x API so that they will still work when they are forced to work with libraries such as Spring Boot 2.x which reference StaticLoggerBinder. (This also included Spark until recently, until they removed references to StaticLoggerBinder.)
Comment From: garretwilson
That's the purpose of this issue. It wouldn't still be open if we didn't plan to improve the documentation.
Indeed the status of this ticket was not clear to me, so thank you for clarifying. The forthcoming documentation is good news. 🎉
Comment From: garretwilson
Another thing that was not clear is that this error only appears with Tomcat Jasper (more good news). Normally this translates to "is the application using JSP". But in my case, my application used to use JSP but had migrated to Thymeleaf. Even though it no longer used JSP, the project still had a reference to Tomcat Jasper:
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
Once this was removed from the Maven POM, the application ran normally with no need for a workaround.
That's the purpose of this issue. It wouldn't still be open if we didn't plan to improve the documentation.
It seems that the new documentation regarding this issue and the workaround is slow in forthcoming. Is that something that I could help with? Is this something I could just open a pull request for in the Spring documentation? Would it be useful and welcome?
Comment From: wilkinsona
Thanks for the offer, @garretwilson. We've added a small section to the migration guide but I think it may also be worth adding a bullet to the list of JSP limitations in the reference documentation. A pull request that does so would certainly be welcome. The source for that section of the documentation is https://github.com/spring-projects/spring-boot/blob/3.0.x/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc#jsp-limitations.
Comment From: garretwilson
We've added a small section to the migration guide …
I think that about covers it. The only thing I would add is to make clear that what is causing the problem is Tomcat Jasper (JSP is only indirectly the problem).
it may also be worth adding a bullet to the list of JSP limitations …
~~… I don't know why JSP limitations need to be duplicated. …~~ Oh, sorry, when I first read that the opposite way. You were referring to adding this limitation to the list of JSP limitations.
Comment From: wilkinsona
Jasper's the only game in town when it comes to JSPs with an embedded web server but it doesn't hurt to mention it explicitly. I've just updated the wiki to that effect.
The migration guide hopefully does a good job of picking up existing users how are upgrading. I think adding the same to the reference docs is also worthwhile as it will hopefully be of use to new users who start with 3.0 and for some reason need/want to use JSPs.
Comment From: garretwilson
I've just updated the wiki to that effect.
I think that is perfect, thanks—concise, direct, and complete. 👍
Comment From: mrbusche
Anyone gotten new relic to work while using this method?
Comment From: syphr42
It looks like the Tomcat project committed a fix for this back on Feb 2: https://github.com/apache/tomcat/commit/10733c4228daece83195bb553ed97cf5996714f0
Backported to 10.1.x here: https://github.com/apache/tomcat/commit/0fb9397ff90ee9ca8090f3da3da4c58ae353a442
Tag 10.1.6 appears to have this commit in its history.
Comment From: wilkinsona
Thanks very much, @syphr42. The change is indeed in 10.1.6 and later. Closing in favour of https://github.com/spring-projects/spring-boot/issues/34582 where we upgraded to 10.1.7. I have also updated the migration guide to remove the section on JSPs as this should now work out of the box with Spring Boot 3.0.5 and later or with earlier versions and a manual Tomcat upgrade.