spring-boot
spring-boot copied to clipboard
Document how to downgrade dependencies to use embedded Jetty 11
In addition to downgrading the Servlet API to 5.0, we also need to cover the impact that this will have on testing. Framework's Servlet-related mocks require Servlet 6.0 and will fail due to the absence of ServletConnection
when run against the Servlet 5.0 API. Similarly, Jetty requires Servlet 5.0 and will fail due to the absence of HttpSessionContext
when run against the Servlet 6.0 API. This leaves us with two arrangements that will work:
- Tests use the Servlet 6.0 API and avoid starting Jetty by only using a mock web environment
- Tests use the Servlet 5.0 API and avoid using Framework's Servlet mocks by only using a full-blown web environment
If a mixture of web environments is required by an application's tests, it can be done but it may require some structural changes to separate the two web environments. With Gradle this could be done with different source sets or separate modules. With Maven, I believe separate modules will always be required.
Hi @wilkinsona,
I'm facing this issue in a real application, not only in tests. Do you know how to fix it?
Starters added:
- spring-boot-starter-web
- exclude
- spring-boot-starter-tomcat
- exclude
- spring-boot-starter-jetty
- spring-boot-starter-amqp
- spring-boot-starter-data-redis
- spring-boot-starter-actuator
- spring-boot-starter-validation
- spring-boot-starter-json
- spring-boot-starter-webflux
- spring-boot-starter-data-jpa
Version: 3.0.0-RC2
Error trace:
java.lang.ClassNotFoundException: jakarta.servlet.http.HttpSessionContext
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
... 41 common frames omitted
Wrapped by: java.lang.NoClassDefFoundError: jakarta/servlet/http/HttpSessionContext
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012)
at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150)
at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862)
at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760)
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681)
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
at org.eclipse.jetty.server.session.SessionHandler.<clinit>(SessionHandler.java:136)
at org.eclipse.jetty.servlet.ServletContextHandler.newSessionHandler(ServletContextHandler.java:339)
at org.eclipse.jetty.servlet.ServletContextHandler.getSessionHandler(ServletContextHandler.java:432)
at org.eclipse.jetty.servlet.ServletContextHandler.relinkHandlers(ServletContextHandler.java:257)
at org.eclipse.jetty.servlet.ServletContextHandler.<init>(ServletContextHandler.java:180)
at org.eclipse.jetty.webapp.WebAppContext.<init>(WebAppContext.java:301)
at org.eclipse.jetty.webapp.WebAppContext.<init>(WebAppContext.java:228)
at org.springframework.boot.web.embedded.jetty.JettyEmbeddedWebAppContext.<init>(JettyEmbeddedWebAppContext.java:28)
at org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory.getWebServer(JettyServletWebServerFactory.java:158)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:183)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:161)
... 21 common frames omitted
Wrapped by: org.springframework.context.ApplicationContextException: Unable to start web server
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:164)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:578)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:432)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1302)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1291)
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0.0-RC2-Release-Notes#jetty
Would it be possible to let spring-boot-starter-jetty
have different dependencies?
Currently, both the Maven pom and the Gradle module explicitly request Servlet 6:
<dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>compile</scope> </dependency>
https://repo.spring.io/artifactory/milestone/org/springframework/boot/spring-boot-starter-jetty/3.0.0-RC2/spring-boot-starter-jetty-3.0.0-RC2.pom
{ "group": "jakarta.servlet", "module": "jakarta.servlet-api", "version": { "requires": "6.0.0" } },
https://repo.spring.io/artifactory/milestone/org/springframework/boot/spring-boot-starter-jetty/3.0.0-RC2/spring-boot-starter-jetty-3.0.0-RC2.module
It's possible, although it would require some surgery to Spring Boot's build. For the vast majority of users it would have no benefit as Spring Boot's dependency management would upgrade the dependency to 6.0.0 again anyway. When we were in a similar situation with Jetty in Spring Boot 2.x (when Jetty did not support Servlet 4.0 and Tomcat and Undertow did), we used the current arrangement and it seemed to work well.
For the runtime setup, this works easily enough indeed, but as you rightly point out in the first message this isn't nearly as easy to work with for the test side of things (at least right now), and splitting tests into different maven modules is a rather nuclear approach to the issue.
So, realistically, most affected users will have to just stall the upgrade till Jetty supports servlet-api 6. Which might be fine or not, depending on how long it will likely take to happen. Couldn't find a reference to the matter on their repo but I'm also not too familiar with how they manage their roadmap for the upcoming Jetty 12 (which I imagine is the one that will target servlet-api 6)
Actually, scratch that, found someone asking the same question for the same reasons here https://www.eclipse.org/lists/jetty-users/msg10348.html
Quoting:
Date: Wed, 9 Nov 2022 13:59:43 -0600 Jetty 12 already has Alpha quality releases out that you can use to start experimenting with. We should be going to Betas (or Release candidates) in the next few weeks. Joakim Erdfelt / joakim@xxxxxxxxxxx
So hopefully not too long of a weird inter-version timeframe all-in-all (barring the usual delays one should expect with software development, ofc)
Unfortunately, as I understand it, there are some fairly significant breaking changes in Jetty 12. I believe @rstoyanchev saw quite a big impact on Spring Framework's WebSocket integration, for example. As such, Jetty 12 support may take some time to fully materialise.
It's possible, although it would require some surgery to Spring Boot's build. For the vast majority of users it would have no benefit as Spring Boot's dependency management would upgrade the dependency to 6.0.0 again anyway. When we were in a similar situation with Jetty in Spring Boot 2.x (when Jetty did not support Servlet 4.0 and Tomcat and Undertow did), we used the current arrangement and it seemed to work well.
Something like this in spring-boot-project/spring-boot-starters/spring-boot-starter-jetty/build.gradle
should do the trick (at least for Gradle users):
api("jakarta.servlet:jakarta.servlet-api") {
version {
strictly "5.0.0"
because "Jetty 11 does not support Servlet 6 yet"
}
}
Unfortunately, it's not that simple. Internally, Boot's build is structured such our dependency management is enforced. This ensures that we know that we're compiling and testing against the versions in our dependency management. As I said, it would require some surgery to use a different version in spring-boot-starter-jetty
. We don't believe it's worth it.
FTR, this is how you get a working setup with gradle:
ext['jakarta-servlet.version'] = '5.0.0'
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation("org.springframework.boot:spring-boot-starter-jetty")
modules {
module("org.springframework.boot:spring-boot-starter-tomcat") {
replacedBy("org.springframework.boot:spring-boot-starter-jetty", "Use Jetty instead of Tomcat")
}
}
}
Unfortunately, it's not that simple. Internally, Boot's build is structured such our dependency management is enforced. This ensures that we know that we're compiling and testing against the versions in our dependency management. As I said, it would require some surgery to use a different version in
spring-boot-starter-jetty
. We don't believe it's worth it.
Ok, but for end users a fix would look like:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
?
We have a company wide BOM so my plan is to do something like this unless there's another concern not do so.
Yes, I believe that will work as your dependency management for jakarta.servlet-api
is nearer to the root so it will take precedence over what's in the imported spring-boot-dependencies
.
This leaves us with two arrangements that will work:
Tests use the Servlet 6.0 API and avoid starting Jetty by only using a mock web environment Tests use the Servlet 5.0 API and avoid using Framework's Servlet mocks by only using a full-blown web environment
The first option seems simple, that sounds like using @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
which is the default.
How would we go about using the second option which avoids using Framework's Servlet mocks
?
EDIT: The second option would let you use WebEnvironment.RANDOM_PORT
, DEFINED_PORT
and NONE
.
Hi @wilkinsona,
I'm facing this issue in a real application, not only in tests. Do you know how to fix it?
Starters added:
spring-boot-starter-web
exclude
- spring-boot-starter-tomcat
spring-boot-starter-jetty
spring-boot-starter-amqp
spring-boot-starter-data-redis
spring-boot-starter-actuator
spring-boot-starter-validation
spring-boot-starter-json
spring-boot-starter-webflux
spring-boot-starter-data-jpa
Version: 3.0.0-RC2
Error trace:
java.lang.ClassNotFoundException: jakarta.servlet.http.HttpSessionContext at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520) ... 41 common frames omitted Wrapped by: java.lang.NoClassDefFoundError: jakarta/servlet/http/HttpSessionContext at java.base/java.lang.ClassLoader.defineClass1(Native Method) at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012) at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150) at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862) at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760) at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681) at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520) at org.eclipse.jetty.server.session.SessionHandler.<clinit>(SessionHandler.java:136) at org.eclipse.jetty.servlet.ServletContextHandler.newSessionHandler(ServletContextHandler.java:339) at org.eclipse.jetty.servlet.ServletContextHandler.getSessionHandler(ServletContextHandler.java:432) at org.eclipse.jetty.servlet.ServletContextHandler.relinkHandlers(ServletContextHandler.java:257) at org.eclipse.jetty.servlet.ServletContextHandler.<init>(ServletContextHandler.java:180) at org.eclipse.jetty.webapp.WebAppContext.<init>(WebAppContext.java:301) at org.eclipse.jetty.webapp.WebAppContext.<init>(WebAppContext.java:228) at org.springframework.boot.web.embedded.jetty.JettyEmbeddedWebAppContext.<init>(JettyEmbeddedWebAppContext.java:28) at org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory.getWebServer(JettyServletWebServerFactory.java:158) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:183) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:161) ... 21 common frames omitted Wrapped by: org.springframework.context.ApplicationContextException: Unable to start web server at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:164) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:578) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:432) at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1302) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1291)
we are facing the same issue during startup but with 3.0.0 released version. Thought we can directly jump to Boot 3 with our new service but it looks like we have to wait...
There are workarounds for both Maven and Gradle:
Maven
<properties>
<jakarta-servlet.version>5.0.0</jakarta-servlet.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
</dependencies>
Gradle
ext['jakarta-servlet.version'] = '5.0.0'
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation("org.springframework.boot:spring-boot-starter-jetty")
modules {
module("org.springframework.boot:spring-boot-starter-tomcat") {
replacedBy("org.springframework.boot:spring-boot-starter-jetty", "Use Jetty instead of Tomcat")
}
}
}
Does that work for you?
Hi, just wanted to report that for me, the gradle-based version adjustment you suggest did not work, it kept pulling version 6 in. Digging through the code a little, I also did not find how that mechanism would work, the only references I found were related to maven.
What worked for me was the following snippet, based on this link: https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/#managing-dependencies.gradle-bom-support.customizing
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (details.requested.group == 'jakarta.servlet') {
details.useVersion '5.0.0'
}
}
}
FTR, this is how you get a working setup with gradle:
ext['jakarta-servlet.version'] = '5.0.0' dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation("org.springframework.boot:spring-boot-starter-jetty") modules { module("org.springframework.boot:spring-boot-starter-tomcat") { replacedBy("org.springframework.boot:spring-boot-starter-jetty", "Use Jetty instead of Tomcat") } } }
This seems to be working. Thanks.
Found a temporary fix for the problem. Move back to Jakarta 6.0.0 and add the following dependency:
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
</dependency>
With the latest version of jetty-server.
With this the service starts up and the unit tests with MockMvc works as expected.
I tried the maven approach above, I tried adding the <jakarta-servlet.version>5.0.0</jakarta-servlet.version>
in Maven properties, yet still no luck. Also tried the other approaches. But still getting that classes are not found.
I've had the same issue, and the previously suggested options were not working for everything. Either the service would start or all the tests (inc mockmvc) would succeed. But not everything. :-(
In the end, this setup has worked for all:
ext {
set("jakarta-servlet.version", '5.0.0')
}
plus:
testImplementation
'org.eclipse.jetty:jetty-server:11.0.14',
'jakarta.servlet:jakarta.servlet-api:6.0.0',
@tasosz @simenaj96 Did you try set only 'org.eclipse.jetty:jetty-server:11.0.14' in implementation section?
I also tried the maven workarounds but still no success. I also tried to ugprade to jetty 12 beta maven plugin, but that didn't help. It's still the test that fail
Update:
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
</dependency>
with
<properties>
<jetty.version>11.0.15</jetty.version>
<jakarta-servlet.version>5.0.0</jakarta-servlet.version>
</properties>
worked
In order to use jetty AND being able to run the unit test with spring boot 3.0.x, we use the following approach:
- downgrade jakarta-servlet version to 5.0.0 as explained in the release notes
<properties>
<jakarta-servlet.version>5.0.0</jakarta-servlet.version>
</properties>
- copy and paste in the
src/test/java
folder the interfacejakarta.servlet.ServerConnection.java
. It seems adding this interface in the test classpath is enough to fix one of the main issue to run unit tests. Source code for this interface can be found here.
Obviously, the second part is clearly a hack. I'm not sure to which extend we can trust the result of unit tests (but at least they can run)....
@Nowheresly yours is a partial fix and works because the mockhttprequest (iirc) explicitly mocks this interface. However if you play with cookies, you'll get another error, since the Cookie class has been heavily modified.
Our "solution" is to package a local servlet-api with the cookie and servletconnection copied back.
Of course this will break should spring tests end up deliberately or accidentally referring to servlet 6 only other stuff.
https://github.com/opentable/servlet/compare/5.0.0-RELEASE...opentable:servlet:5.9.0 basically makes the idea concrete.
Another limitation besides the inherent fragility is of course you CANNOT open source any thing built with this, since of course the patched version doesn't exist in maven central.
I hasten to add this is a no good fragile solution. If Spring changes or another third party relies on Servlet 6 and uses it's methods, one will live to regret this solution
Hi, is there any updates? Thanks!
@fzyzcjy No, I'm afraid not. We work in the open so any updates will appear in this issue. Anything that may be documented is already described in this issue.
Thanks @Tak1za.
In the end it looks like this for me with gradle.
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-jetty")
modules {
module("org.springframework.boot:spring-boot-starter-tomcat") {
replacedBy("org.springframework.boot:spring-boot-starter-jetty", "Use Jetty instead of Tomcat")
}
}
implementation("org.eclipse.jetty:jetty-server:11.0.15")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
Unfortunately, no of the suggestions above worked for me at all. I tried with jetty-server 12.0.1, servlet-api 5.0.0/6.0.0, spring-boot-dependencies 3.1.3 in the dependency-management, ...I always get: org.eclipse.jetty.server.handler.HandlerWrapper
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory]: Factory method 'jettyServletWebServerFactory' threw exception with message: org/eclipse/jetty/server/handler/HandlerWrapper
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:171)
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:655)
... 97 more
Caused by: java.lang.NoClassDefFoundError: org/eclipse/jetty/server/handler/HandlerWrapper
at org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedJetty.jettyServletWebServerFactory(ServletWebServerFactoryConfiguration.java:93)
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.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:139)
... 98 more
Caused by: java.lang.ClassNotFoundException: org.eclipse.jetty.server.handler.HandlerWrapper
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
@du-it
Did you try use version 11.0.14
instead 12.0.1
?