From 21cf377f7e6ba7b0c85a8a7675ab85219f36db82 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 5 Jan 2026 16:33:32 -0800 Subject: [PATCH] Add 'WAR_SOURCE_DIRECTORY' environment variable support Add an escape hatch for users that deviate from the standard `src/main/webbapp` directory structure. Fixes gh-23829 --- .../modules/reference/pages/web/servlet.adoc | 2 + .../boot/web/server/servlet/DocumentRoot.java | 24 ++++++++++-- .../web/server/servlet/DocumentRootTests.java | 26 ++++++++++++- .../JspTemplateAvailabilityProvider.java | 22 ++++++++++- .../JspTemplateAvailabilityProviderTests.java | 38 ++++++++++++++++++- 5 files changed, 104 insertions(+), 8 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc index 35f8e9a372e..5a53f1d36fc 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc @@ -745,3 +745,5 @@ JSPs are not supported when using an executable jar. * Creating a custom `error.jsp` page does not override the default view for xref:web/servlet.adoc#web.servlet.spring-mvc.error-handling[error handling]. xref:web/servlet.adoc#web.servlet.spring-mvc.error-handling.error-pages[Custom error pages] should be used instead. + +* If you run your application using `mvn spring-boot:run` or `gradle bootRun` and you deviate from the standard `src/main/webbapp` directory structure you may need to set a `WAR_SOURCE_DIRECTORY` environment variable so that Spring Boot can find your JSPs. diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/DocumentRoot.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/DocumentRoot.java index 18ba6be49c4..69ed87eae5b 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/DocumentRoot.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/DocumentRoot.java @@ -23,6 +23,7 @@ import java.net.URLConnection; import java.security.CodeSource; import java.util.Arrays; import java.util.Locale; +import java.util.function.Function; import org.apache.commons.logging.Log; import org.jspecify.annotations.Nullable; @@ -35,14 +36,29 @@ import org.jspecify.annotations.Nullable; */ public class DocumentRoot { - private static final String[] COMMON_DOC_ROOTS = { "src/main/webapp", "public", "static" }; + private static final String WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE = "WAR_SOURCE_DIRECTORY"; private final Log logger; + private final File rootDirectory; + + private final String[] commonDocRoots; + private @Nullable File directory; public DocumentRoot(Log logger) { + this(logger, new File("."), System::getenv); + } + + DocumentRoot(Log logger, File rootDirectory, Function systemEnvironment) { this.logger = logger; + this.rootDirectory = rootDirectory; + this.commonDocRoots = new String[] { getWarSourceDirectory(systemEnvironment), "public", "static" }; + } + + private static String getWarSourceDirectory(Function systemEnvironment) { + String name = systemEnvironment.apply(WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE); + return (name != null) ? name : "src/main/webapp"; } @Nullable File getDirectory() { @@ -137,8 +153,8 @@ public class DocumentRoot { } private @Nullable File getCommonDocumentRoot() { - for (String commonDocRoot : COMMON_DOC_ROOTS) { - File root = new File(commonDocRoot); + for (String commonDocRoot : this.commonDocRoots) { + File root = new File(this.rootDirectory, commonDocRoot); if (root.exists() && root.isDirectory()) { return root.getAbsoluteFile(); } @@ -147,7 +163,7 @@ public class DocumentRoot { } private void logNoDocumentRoots() { - this.logger.debug("None of the document roots " + Arrays.asList(COMMON_DOC_ROOTS) + this.logger.debug("None of the document roots " + Arrays.asList(this.commonDocRoots) + " point to a directory and will be ignored."); } diff --git a/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/servlet/DocumentRootTests.java b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/servlet/DocumentRootTests.java index 86a61ec515c..8712fe6ba82 100644 --- a/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/servlet/DocumentRootTests.java +++ b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/servlet/DocumentRootTests.java @@ -20,7 +20,10 @@ import java.io.File; import java.net.URL; import java.security.CodeSource; import java.security.cert.Certificate; +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -34,11 +37,13 @@ import static org.assertj.core.api.Assertions.assertThat; */ class DocumentRootTests { + private static final Log logger = LogFactory.getLog(DocumentRootTests.class); + @TempDir @SuppressWarnings("NullAway.Init") File tempDir; - private final DocumentRoot documentRoot = new DocumentRoot(LogFactory.getLog(getClass())); + private final DocumentRoot documentRoot = new DocumentRoot(logger); @Test void explodedWarFileDocumentRootWhenRunningFromExplodedWar() throws Exception { @@ -70,4 +75,23 @@ class DocumentRootTests { assertThat(codeSourceArchive).isEqualTo(new File("/test/path/with space/")); } + @Test + void getValidDirectoryWhenHasSrcMainWebApp() { + Map systemEnvironment = new HashMap<>(); + File directory = new File(this.tempDir, "src/main/webapp"); + directory.mkdirs(); + DocumentRoot documentRoot = new DocumentRoot(logger, this.tempDir, systemEnvironment::get); + assertThat(documentRoot.getValidDirectory()).isEqualTo(directory); + } + + @Test + void getValidDirectoryWhenHasCustomSrcMainWebApp() { + Map systemEnvironment = new HashMap<>(); + systemEnvironment.put("WAR_SOURCE_DIRECTORY", "src/main/unusual"); + File directory = new File(this.tempDir, "src/main/unusual"); + directory.mkdirs(); + DocumentRoot documentRoot = new DocumentRoot(logger, this.tempDir, systemEnvironment::get); + assertThat(documentRoot.getValidDirectory()).isEqualTo(directory); + } + } diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProvider.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProvider.java index 6857277c605..4e5d8f0bcde 100755 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProvider.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProvider.java @@ -17,6 +17,9 @@ package org.springframework.boot.webmvc.autoconfigure; import java.io.File; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; import org.springframework.core.env.Environment; @@ -34,6 +37,23 @@ import org.springframework.util.ClassUtils; */ public class JspTemplateAvailabilityProvider implements TemplateAvailabilityProvider { + private static final String WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE = "WAR_SOURCE_DIRECTORY"; + + private final File warSourceDirectory; + + public JspTemplateAvailabilityProvider() { + this(new File("."), System::getenv); + } + + JspTemplateAvailabilityProvider(File rootDirectory, Function systemEnvironment) { + this.warSourceDirectory = new File(rootDirectory, getWarSourceDirectory(systemEnvironment)); + } + + private static String getWarSourceDirectory(Function systemEnvironment) { + String name = systemEnvironment.apply(WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE); + return (name != null) ? name : "src/main/webapp"; + } + @Override public boolean isTemplateAvailable(String view, Environment environment, ClassLoader classLoader, ResourceLoader resourceLoader) { @@ -42,7 +62,7 @@ public class JspTemplateAvailabilityProvider implements TemplateAvailabilityProv if (resourceLoader.getResource(resourceName).exists()) { return true; } - return new File("src/main/webapp", resourceName).exists(); + return new File(this.warSourceDirectory, resourceName).exists(); } return false; } diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProviderTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProviderTests.java index 767d2c1aa96..595e09d5613 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProviderTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProviderTests.java @@ -16,12 +16,18 @@ package org.springframework.boot.webmvc.autoconfigure; +import java.io.File; +import java.util.HashMap; +import java.util.Map; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.testsupport.classpath.resources.WithResource; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -57,9 +63,37 @@ class JspTemplateAvailabilityProviderTests { assertThat(isTemplateAvailable("suffixed")).isTrue(); } + @Test + void availabilityOfTemplateInSrcMainWebapp(@TempDir File rootDirectory) throws Exception { + File jsp = new File(rootDirectory, "src/main/webapp/test.jsp"); + jsp.getParentFile().mkdirs(); + FileCopyUtils.copy(new byte[0], jsp); + Map systemEnvironment = new HashMap<>(); + JspTemplateAvailabilityProvider provider = new JspTemplateAvailabilityProvider(rootDirectory, + systemEnvironment::get); + assertThat(isTemplateAvailable(provider, "test.jsp")).isTrue(); + assertThat(isTemplateAvailable(provider, "missing.jsp")).isFalse(); + } + + @Test + void availabilityOfTemplateInCustomSrcMainWebapp(@TempDir File rootDirectory) throws Exception { + File jsp = new File(rootDirectory, "src/main/unusual/test.jsp"); + jsp.getParentFile().mkdirs(); + FileCopyUtils.copy(new byte[0], jsp); + Map systemEnvironment = new HashMap<>(); + systemEnvironment.put("WAR_SOURCE_DIRECTORY", "src/main/unusual"); + JspTemplateAvailabilityProvider provider = new JspTemplateAvailabilityProvider(rootDirectory, + systemEnvironment::get); + assertThat(isTemplateAvailable(provider, "test.jsp")).isTrue(); + assertThat(isTemplateAvailable(provider, "missing.jsp")).isFalse(); + } + private boolean isTemplateAvailable(String view) { - return this.provider.isTemplateAvailable(view, this.environment, getClass().getClassLoader(), - this.resourceLoader); + return isTemplateAvailable(this.provider, view); + } + + private boolean isTemplateAvailable(JspTemplateAvailabilityProvider provider, String view) { + return provider.isTemplateAvailable(view, this.environment, getClass().getClassLoader(), this.resourceLoader); } }