You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by me...@apache.org on 2026/05/29 19:31:54 UTC
(myfaces) branch main updated: MYFACES-4751 5.0 Destroy @ViewScoped beans of views that are never saved (#995) (#997)
This is an automated email from the ASF dual-hosted git repository.
melloware pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/myfaces.git
The following commit(s) were added to refs/heads/main by this push:
new b7300d6af MYFACES-4751 5.0 Destroy @ViewScoped beans of views that are never saved (#995) (#997)
b7300d6af is described below
commit b7300d6af0e2ab34f3fbe81113528c062ade8996
Author: Melloware <me...@gmail.com>
AuthorDate: Fri May 29 15:31:48 2026 -0400
MYFACES-4751 5.0 Destroy @ViewScoped beans of views that are never saved (#995) (#997)
A view rendered without writing its state - e.g. a page with no UIForm, so no
view state token is emitted - can never be restored, and unlike a saved view it
is never registered in the session SerializedViewCollection. Consequently the
normal "evict view -> destroy its view scope" path never ran for such a view,
and any @ViewScoped beans created while rendering it lingered in the session
(ViewScopeContextualStorageHolder) until the session expired. Requesting such a
page repeatedly within one session therefore leaked one view-scope storage per
request, unbounded.
FaceletViewDeclarationLanguage.renderView now destroys the view scope of a
non-transient view whose state was not written, at the end of the request,
publishing PreDestroyViewMapEvent and destroying its (CDI or non-CDI)
@ViewScoped storage.
Add ViewScopeFormlessLeakTestCase: rendering a formless page bound to a
@ViewScoped bean N times now destroys the bean N times instead of accumulating
it in the session.
Co-authored-by: Claude Opus 4.8 (1M context) <no...@anthropic.com>
---
.../facelets/FaceletViewDeclarationLanguage.java | 18 ++++++
.../viewscope/ViewScopeFormlessLeakTestCase.java | 74 ++++++++++++++++++++++
.../facelets/viewscope/ViewScopeLeakProbeBean.java | 62 ++++++++++++++++++
.../facelets/viewscope/formlessViewScoped.xhtml | 29 +++++++++
4 files changed, 183 insertions(+)
diff --git a/impl/src/main/java/org/apache/myfaces/view/facelets/FaceletViewDeclarationLanguage.java b/impl/src/main/java/org/apache/myfaces/view/facelets/FaceletViewDeclarationLanguage.java
index 022a09c5a..107107c91 100644
--- a/impl/src/main/java/org/apache/myfaces/view/facelets/FaceletViewDeclarationLanguage.java
+++ b/impl/src/main/java/org/apache/myfaces/view/facelets/FaceletViewDeclarationLanguage.java
@@ -99,6 +99,7 @@
import org.apache.myfaces.core.api.shared.lang.Assert;
import org.apache.myfaces.view.ViewDeclarationLanguageStrategy;
import org.apache.myfaces.view.ViewMetadataBase;
+import org.apache.myfaces.view.ViewScopeProxyMap;
import org.apache.myfaces.view.facelets.compiler.Compiler;
import org.apache.myfaces.view.facelets.compiler.SAXCompiler;
import org.apache.myfaces.view.facelets.el.CompositeComponentELUtils;
@@ -1897,6 +1898,23 @@ else if (stateWriter.isStateWrittenWithoutWrapper())
}
}
}
+
+ // This view was rendered without writing its state (no UIForm, hence no
+ // view state token), so it can never be restored. Any @ViewScoped beans
+ // created while building it are unreachable once this request ends, but -
+ // unlike views whose state is saved - it is never registered in the session
+ // SerializedViewCollection, so the normal "evict view -> destroy its view
+ // scope" path never runs and the beans would linger until the session
+ // expires. Destroy the view scope now (publishes PreDestroyViewMapEvent).
+ if (!view.isTransient())
+ {
+ Map<String, Object> viewMap = view.getViewMap(false);
+ if (viewMap instanceof ViewScopeProxyMap
+ && ((ViewScopeProxyMap) viewMap).getViewScopeId() != null)
+ {
+ viewMap.clear();
+ }
+ }
}
}
}
diff --git a/impl/src/test/java/org/apache/myfaces/view/facelets/viewscope/ViewScopeFormlessLeakTestCase.java b/impl/src/test/java/org/apache/myfaces/view/facelets/viewscope/ViewScopeFormlessLeakTestCase.java
new file mode 100644
index 000000000..49dcbf374
--- /dev/null
+++ b/impl/src/test/java/org/apache/myfaces/view/facelets/viewscope/ViewScopeFormlessLeakTestCase.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.view.facelets.viewscope;
+
+import jakarta.el.ExpressionFactory;
+import jakarta.faces.application.ProjectStage;
+
+import org.apache.myfaces.test.core.AbstractMyFacesCDIRequestTestCase;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Regression test for the @ViewScoped leak on formless views.
+ *
+ * <p>A view without any {@code UIForm} never has its state saved, so it can never be restored and
+ * is never registered in the session {@code SerializedViewCollection}. Repeatedly requesting such a
+ * view that references a {@code @ViewScoped} bean must not accumulate view scopes in the session:
+ * the scope has to be destroyed at the end of each request. Before the fix the beans were never
+ * destroyed and piled up in the session (a memory leak), so {@code DESTROYED} stayed at 0.</p>
+ */
+public class ViewScopeFormlessLeakTestCase extends AbstractMyFacesCDIRequestTestCase
+{
+
+ protected ExpressionFactory createExpressionFactory()
+ {
+ return new org.apache.el.ExpressionFactoryImpl();
+ }
+
+ @Override
+ protected void setUpWebConfigParams() throws Exception
+ {
+ super.setUpWebConfigParams();
+ servletContext.addInitParameter("org.apache.myfaces.annotation.SCAN_PACKAGES",
+ "org.apache.myfaces.view.facelets.viewscope");
+ servletContext.addInitParameter(ProjectStage.PROJECT_STAGE_PARAM_NAME, "Production");
+ }
+
+ @Test
+ public void testFormlessViewDoesNotLeakViewScopedBeans() throws Exception
+ {
+ ViewScopeLeakProbeBean.reset();
+
+ int requests = 5;
+ for (int i = 0; i < requests; i++)
+ {
+ startViewRequest("/formlessViewScoped.xhtml");
+ processLifecycleExecute();
+ renderResponse();
+ endRequest();
+ }
+
+ Assertions.assertEquals(requests, ViewScopeLeakProbeBean.CREATED.get(),
+ "the @ViewScoped bean must be created once per request");
+ Assertions.assertEquals(requests, ViewScopeLeakProbeBean.DESTROYED.get(),
+ "the @ViewScoped beans of a formless (never-saved) view must be destroyed each "
+ + "request, not leaked into the session");
+ }
+}
diff --git a/impl/src/test/java/org/apache/myfaces/view/facelets/viewscope/ViewScopeLeakProbeBean.java b/impl/src/test/java/org/apache/myfaces/view/facelets/viewscope/ViewScopeLeakProbeBean.java
new file mode 100644
index 000000000..0250c1079
--- /dev/null
+++ b/impl/src/test/java/org/apache/myfaces/view/facelets/viewscope/ViewScopeLeakProbeBean.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.view.facelets.viewscope;
+
+import java.io.Serializable;
+import java.util.concurrent.atomic.AtomicInteger;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.faces.view.ViewScoped;
+import jakarta.inject.Named;
+
+/**
+ * A {@code @ViewScoped} CDI bean that counts its creations and destructions, used to assert that
+ * the view scope of a formless (never-saved) view is destroyed at the end of the request instead
+ * of lingering in the session.
+ */
+@Named("viewScopeLeakProbeBean")
+@ViewScoped
+public class ViewScopeLeakProbeBean implements Serializable
+{
+ public static final AtomicInteger CREATED = new AtomicInteger();
+ public static final AtomicInteger DESTROYED = new AtomicInteger();
+
+ public static void reset()
+ {
+ CREATED.set(0);
+ DESTROYED.set(0);
+ }
+
+ @PostConstruct
+ public void created()
+ {
+ CREATED.incrementAndGet();
+ }
+
+ @PreDestroy
+ public void destroyed()
+ {
+ DESTROYED.incrementAndGet();
+ }
+
+ public String getValue()
+ {
+ return "view-scoped";
+ }
+}
diff --git a/impl/src/test/resources/org/apache/myfaces/view/facelets/viewscope/formlessViewScoped.xhtml b/impl/src/test/resources/org/apache/myfaces/view/facelets/viewscope/formlessViewScoped.xhtml
new file mode 100644
index 000000000..1aa0bf192
--- /dev/null
+++ b/impl/src/test/resources/org/apache/myfaces/view/facelets/viewscope/formlessViewScoped.xhtml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<!DOCTYPE html>
+<html xmlns:h="jakarta.faces.html">
+ <h:head>
+ <title>Formless page referencing a @ViewScoped bean</title>
+ </h:head>
+ <h:body>
+ <!-- No h:form: this view's state is never saved, so it can never be restored. -->
+ <h:outputText value="#{viewScopeLeakProbeBean.value}"/>
+ </h:body>
+</html>