This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 05ad78a06 WW-5690 perf(dispatcher): load the dev-mode error template 
on first use (#1864)
05ad78a06 is described below

commit 05ad78a0691d80d87f96f22e60a1616f5a9100cb
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Aug 24 14:30:36 2026 +0200

    WW-5690 perf(dispatcher): load the dev-mode error template on first use 
(#1864)
    
    * WW-5690 perf(dispatcher): load the dev-mode error template on first use
    
    DefaultDispatcherErrorHandler.init() built a FreeMarker configuration and
    loaded /org/apache/struts2/dispatcher/error.ftl on every startup, including
    in production where the problem report is never rendered - handleError()
    delegates to the container's error page unless devMode is on.
    
    Defer the load to the first dev-mode error instead. Two threads racing there
    may both load the template, which is harmless: FreeMarker caches templates 
in
    its own configuration, and that is cheaper than locking a path taken once 
per
    application.
    
    One behavioural change: a missing or unparsable error.ftl used to fail the
    application at boot. It now surfaces on the first dev-mode error, where the
    existing catch in handleErrorInDevMode() degrades to sendError() with the
    cause. A dev-only template should not stop a production application 
starting.
    
    This removes the startup cost only; the FreeMarker dependency itself stays.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    
    * WW-5690 refactor(dispatcher): synchronise the lazy template load
    
    Sonar flags the volatile Template field (java:S3077): volatile publishes the
    reference safely but guarantees nothing about the object's own state, and
    Template extends Configurable, so it is not strictly immutable.
    
    The lock-free version was not worth defending anyway. It was justified as
    avoiding a lock on a path taken once per application, but that path only 
runs
    when devMode is on and a request has already failed - it is never hot. And
    FreemarkerManager.getConfiguration is itself synchronized, so this path was
    already taking a lock.
    
    A plain field behind a synchronized getter is simpler, obviously correct, 
and
    costs nothing here.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../dispatcher/DefaultDispatcherErrorHandler.java  | 30 ++++++++---
 .../DefaultDispatcherErrorHandlerTest.java         | 59 ++++++++++++++++++++++
 2 files changed, 82 insertions(+), 7 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/DefaultDispatcherErrorHandler.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/DefaultDispatcherErrorHandler.java
index d64345854..779ca7cd1 100644
--- 
a/core/src/main/java/org/apache/struts2/dispatcher/DefaultDispatcherErrorHandler.java
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/DefaultDispatcherErrorHandler.java
@@ -30,7 +30,6 @@ import org.apache.commons.lang3.BooleanUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.struts2.StrutsConstants;
-import org.apache.struts2.StrutsException;
 import org.apache.struts2.views.freemarker.FreemarkerManager;
 
 import java.io.IOException;
@@ -48,8 +47,11 @@ public class DefaultDispatcherErrorHandler implements 
DispatcherErrorHandler {
 
     private static final Logger LOG = 
LogManager.getLogger(DefaultDispatcherErrorHandler.class);
 
+    private static final String ERROR_TEMPLATE = 
"/org/apache/struts2/dispatcher/error.ftl";
+
     private FreemarkerManager freemarkerManager;
     private boolean devMode;
+    private ServletContext servletContext;
     private Template template;
 
     @Inject
@@ -63,12 +65,26 @@ public class DefaultDispatcherErrorHandler implements 
DispatcherErrorHandler {
     }
 
     public void init(ServletContext ctx) {
-        try {
-            freemarker.template.Configuration config = 
freemarkerManager.getConfiguration(ctx);
-            template = 
config.getTemplate("/org/apache/struts2/dispatcher/error.ftl");
-        } catch (IOException e) {
-            throw new StrutsException(e);
+        this.servletContext = ctx;
+    }
+
+    /**
+     * Loads the problem report template on first use rather than at startup: 
it is only ever
+     * rendered in devMode, so a production application should never build a 
FreeMarker
+     * configuration on its behalf.
+     * <p>
+     * Synchronised rather than lock-free: this runs only when devMode is on 
and a request has
+     * already failed, and {@link 
FreemarkerManager#getConfiguration(ServletContext)} is itself
+     * synchronised, so the lock costs nothing that this path was not paying 
already.
+     *
+     * @return the problem report template
+     * @throws IOException if the template cannot be loaded
+     */
+    protected synchronized Template getTemplate() throws IOException {
+        if (template == null) {
+            template = 
freemarkerManager.getConfiguration(servletContext).getTemplate(ERROR_TEMPLATE);
         }
+        return template;
     }
 
     public void handleError(HttpServletRequest request, HttpServletResponse 
response, int code, Exception e) {
@@ -115,7 +131,7 @@ public class DefaultDispatcherErrorHandler implements 
DispatcherErrorHandler {
             } while ((cur = cur.getCause()) != null);
 
             Writer writer = new StringWriter();
-            template.process(createReportData(e, chain), writer);
+            getTemplate().process(createReportData(e, chain), writer);
 
             response.setContentType("text/html");
             response.getWriter().write(writer.toString());
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/DefaultDispatcherErrorHandlerTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/DefaultDispatcherErrorHandlerTest.java
index 6d44c7dac..f1f521047 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/DefaultDispatcherErrorHandlerTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/DefaultDispatcherErrorHandlerTest.java
@@ -18,10 +18,14 @@
  */
 package org.apache.struts2.dispatcher;
 
+import freemarker.template.Configuration;
 import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.Collections;
 import org.apache.struts2.StrutsInternalTestCase;
 
+import jakarta.servlet.ServletContext;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import org.apache.struts2.views.freemarker.FreemarkerManager;
@@ -130,6 +134,61 @@ public class DefaultDispatcherErrorHandlerTest extends 
StrutsInternalTestCase {
         defaultDispatcherErrorHandler.handleError(requestMock, responseMock, 
HttpServletResponse.SC_INTERNAL_SERVER_ERROR, fakeException);
     }
 
+    /**
+     * The dev-mode problem report is the only thing that needs FreeMarker 
here, so booting the
+     * application must not build a FreeMarker configuration just to have the 
template ready.
+     */
+    public void testInitDoesNotLoadErrorTemplate() {
+        RecordingFreemarkerManager freemarkerManager = 
createFreemarkerManager();
+        DefaultDispatcherErrorHandler defaultDispatcherErrorHandler = new 
DefaultDispatcherErrorHandler();
+        defaultDispatcherErrorHandler.setDevMode("true");
+        defaultDispatcherErrorHandler.setFreemarkerManager(freemarkerManager);
+
+        defaultDispatcherErrorHandler.init(dispatcher.servletContext);
+
+        assertFalse("init() must not touch FreeMarker", 
freemarkerManager.configurationRequested);
+    }
+
+    /**
+     * The deferred load must still happen, otherwise the problem report 
silently stops rendering.
+     */
+    public void testErrorTemplateLoadedOnFirstDevModeError() throws 
IOException {
+        RecordingFreemarkerManager freemarkerManager = 
createFreemarkerManager();
+        DefaultDispatcherErrorHandler defaultDispatcherErrorHandler = new 
DefaultDispatcherErrorHandler();
+        defaultDispatcherErrorHandler.setDevMode("true");
+        defaultDispatcherErrorHandler.setFreemarkerManager(freemarkerManager);
+        defaultDispatcherErrorHandler.init(dispatcher.servletContext);
+        Exception fakeException = new Exception("Fake Exception, devMode 
true");
+        responseMock.setContentType("text/html");
+        expectLastCall();
+        expect(responseMock.getWriter()).andStubReturn(new PrintWriter(new 
StringWriter()));
+        replay(responseMock);
+
+        defaultDispatcherErrorHandler.handleError(requestMock, responseMock, 
HttpServletResponse.SC_INTERNAL_SERVER_ERROR, fakeException);
+
+        assertTrue("problem report must load the template on first use", 
freemarkerManager.configurationRequested);
+    }
+
+    private RecordingFreemarkerManager createFreemarkerManager() {
+        RecordingFreemarkerManager freemarkerManager = new 
RecordingFreemarkerManager();
+        container.inject(freemarkerManager);
+        return freemarkerManager;
+    }
+
+    /**
+     * Records whether the FreeMarker configuration was ever asked for, while 
still delegating so the
+     * template really renders.
+     */
+    private static class RecordingFreemarkerManager extends FreemarkerManager {
+        private boolean configurationRequested;
+
+        @Override
+        public Configuration getConfiguration(ServletContext servletContext) {
+            configurationRequested = true;
+            return super.getConfiguration(servletContext);
+        }
+    }
+
     protected void setUp() {
         requestMock = (HttpServletRequest) 
createMock(HttpServletRequest.class);
         responseMock = (HttpServletResponse) 
createMock(HttpServletResponse.class);

Reply via email to