The behavior of javax.servlet.ServletContext#getResourcePaths
and (its sibling methods) is inconstent, depending on how the application is packaged and which servlet container is used.
I've created the following example application to demonstrate the problem: https://github.com/larsgrefer/servlet-context-test All example applications in this repository return a list of all found resources under localhost:8080/resources
Jar based applications
Expected behaviour
All applications should always return the following items (in any order)
/library-classpath-resource.txt
/classpath-resource.txt
Actual behaviour:
Servlet Container | gradle bootRun |
java -jar |
docker container build with jib |
---|---|---|---|
Tomcat | Shows both items, but also a third one(*) | /classpath-resource.txt is missing |
both items are shown |
Undertow | Both items are missing | Both items are missing | Both items are missing |
Jetty | Shows both items, but also a third one(*) | /classpath-resource.txt is missing |
both items are shown |
War based applications
Expected behaviour
All applications should always return the following items (in any order)
/library-classpath-resource.txt
/war-resource.txt
Actual behaviour:
Servlet Container | gradle bootRun |
java -jar |
docker container build with jib (**) |
---|---|---|---|
Tomcat | /classpath-resource.txt is shown in addition to the expected ones |
All items are shown as expected | All items are shown as expected |
Undertow | Only /war-resource.txt is shown |
All items are missing | All items are shown as expected |
Jetty | /classpath-resource.txt is shown in addition to the expected ones |
All items are shown as expected | All items are shown as expected |
(**) jib uses a distroless jetty for war based project, so this is a traditional deployment to an external servlet container from the spring-boot point of view
Conclusion
- (*)
src/main/webapp
seems to be used as resource root, even if the project is not war-based. classpath*:META-INF/resources
is only used as resource root by Tomcat and Jetty, but not by undertow.classpath*:META-INF/resources
is not used as resource root when its theBOOT-INF/classes
orWEB-INF/classes
folder of a repacked jar or war.
Comment From: wilkinsona
Thanks for sharing your detailed findings. I've yet to digest everything, but the first thing I've noticed is that this is a partial duplicate of https://github.com/spring-projects/spring-boot/issues/8324. That /classpath-resource.txt
is missing is things working as designed. Please see this comment for details.
Comment From: larsgrefer
Thanks for sharing the links. This makes some things clearer to me.
I now understand the rationale of not exposing WEB-INF/classes/META-INF/resources
as resource root for war files, but I think BOOT-INF/classes/META-INF/resources
should be supported.
For war files it might be unnecessary to expose WEB-INF/classes/META-INF/resources
because I just could have put my resources in the root of the war file (src/main/webapp
), but for jar packaged applications I have no other choice than putting my resources to src/main/resources/META-INF/resources
).
Some additional thoughts about this:
- The very same application should behave the same, regardless of how its run (IDE/main-method, gradle/maven bootRun,
java -jar
) - It (still) seems a bit strange to me that
classpath*:META-INF/resources
is handled differently, depending on where on the classpath it is.
Comment From: wilkinsona
but I think BOOT-INF/classes/META-INF/resources should be supported
This is what was considered in #8324 and we decided not to do it. I don't think we're going to change that decision I'm afraid.
I have no other choice than putting my resources to src/main/resources/META-INF/resources
In keeping with the servlet spec loading resources from WEB-INF/lib/*.jar/META-INF/resources
, we support the same with jar packaging where resources can be loaded from BOOT-INF/lib/*.jar/META-INF/resources
. In other words, if you want to use jar packaging, you should package your static resources in a separate module and then depend upon it.
The very same application should behave the same, regardless of how its run (IDE/main-method, gradle/maven bootRun, java -jar)
Agreed. We thought that things were consistent and we have tests the are intended to cover this so it would appear that we're missing something. We can use this issue to investigate and straighten things out.
Comment From: wilkinsona
The expectations for a war file are incorrect. The servlet contains requires static resources to be served from the root of the war file or from META-INF/resources
in files in WEB-INF/lib
. They should not be loaded from WEB-INF/classes/META-INF/resources
. This means that classpath-resource.txt
should not be loaded and that Tomcat and Jetty are behaving correctly when running a packaged war with java -jar
.
Comment From: larsgrefer
I agree that the servlet specification doesn't require /classpath-resource.txt
to be shown for war files. I've updated the table above to reflect that.
Comment From: wilkinsona
There's a bug in how we set up Undertow's ResourceManager
which breaks getResourcePaths(String)
(our integration tests only cover getResource(String)
at the moment). I've opened https://github.com/spring-projects/spring-boot/issues/17243 to fix it.
Comment From: larsgrefer
After this is resolved, a few sentences should be added to the documentation about which locations are used as servlet context resources (and which locations are not used) and how this can be changed if necessary.
Some other thoughts I had in the meantime:
- The servlet spec (section 4.6) does not specify whether the locations mentioned there (war root and
/WEB-INF/lib/*.jar!/META-INF/resources
) are the only locations allowed to be used as resource roots or if additional locations are permitted, too. - Im not sure if section 4.6 of the servlet spec can or should be applied to jar-based spring boot applications, because its not clear what "root of the context" should be. Also the directory structure in section 10.5 can't be applied to spring boot applications that aren't packaged as war file.
if you want to use jar packaging, you should package your static resources in a separate module and then depend upon it.
I'd rather modify the underlying servlet container to include a custom location as resource root than creating a separate module.
Comment From: larsgrefer
I've updated the tables above in order to include docker containers built with jib as third packaging option.
Comment From: larsgrefer
There seems to be an additional inconsistency for repackaged wars running using java -jar
:
When using an embedded Tomcat, ServletContext.getResourcePaths()
can traverse through /org/springframework/boot/loader/
, while an embedded Jetty (successfully) hides /org/springframework/boot/loader/
Comment From: wilkinsona
The servlet spec (section 4.6) does not specify whether the locations mentioned there are the only locations allowed to be used
To follow the principle of least surprise, I think it's important that we align with the Servlet spec. While it doesn't prohibit additional locations, I think would be surprising for additional locations to be used by default.
I'm not sure if section 4.6 of the servlet spec can or should be applied to jar-based spring boot applications
We have decided that it can and should be applied, with some translations to Spring Boot's layout. Where the spec talks about WEB-INF/lib
we apply the same rules to BOOT-INF/lib
.
because its not clear what "root of the context" should be
The is no root of the context in a jar-based application. If we followed the war model, we'd have the same problem with accidentally serving content from the root of the jar.
Also the directory structure in section 10.5 can't be applied to spring boot applications that aren't packaged as war file.
It's almost entirely applicable, with WEB-INF
translated to BOOT-INF
. Only the document root is not applicable.
When using an embedded Tomcat, ServletContext.getResourcePaths() can traverse through
/org/springframework/boot/loader/
, while an embedded Jetty (successfully) hides/org/springframework/boot/loader/
Thanks. Our main concern with hiding the loader is preventing it from being served over HTTP which we currently do. That said, we'll see if it's possible to hide it from getResourcePaths()
with Tomcat in the interests of consistency if nothing else. I've opened https://github.com/spring-projects/spring-boot/issues/17262.
Comment From: larsgrefer
We have decided that it can and should be applied, with some translations to Spring Boot's layout.
Does that mean my initial expectation to see /classpath-resource.txt
for non-war applications is wrong?
It's almost entirely applicable, with WEB-INF translated to BOOT-INF. Only the document root is not applicable.
I see, that the servlet spec, in fact, could be applied to spring-boot-repackaged jar files. But a non-war spring boot application can be run in a variety of ways: - repackaged jar - IDE's - gradle bootRun, mvn boot:run - jib-based docker images - gradle application distribution - just calling the main method
I think for most of these ways of running the application it would be difficult, error-prone or even improssible to distinguish between /classpath-resource.txt
and /library-classpath-resource.txt
. (Which would be neccessary to exclude the first but include the latter).
Comment From: wilkinsona
I think we've done all that we can here for the time being at least. The Undertow issue I opened remains open. If it's fixed we can revisit that part of things in the future.