diff --git a/flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java b/flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java index 93e1df23855..1424db3eab5 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java @@ -46,6 +46,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.slf4j.Logger; @@ -589,14 +590,21 @@ public Registration addUIInitListener(UIInitListener listener) { /** * Adds a listener that gets notified when a Vaadin service session that has * been initialized for this service is destroyed. + * *

* The session being destroyed is locked and its UIs have been removed when * the listeners are called. * + *

+ * This method delivers notifications for all associated sessions. To be + * notified for only one specific session, use + * {@link VaadinSession#addSessionDestroyListener}. + * * @param listener * the vaadin service session destroy listener * @return a handle that can be used for removing the listener * @see #addSessionInitListener(SessionInitListener) + * @see VaadinSession#addSessionDestroyListener */ public Registration addSessionDestroyListener( SessionDestroyListener listener) { @@ -652,18 +660,19 @@ public void fireSessionDestroy(VaadinSession vaadinSession) { } SessionDestroyEvent event = new SessionDestroyEvent( VaadinService.this, session); - for (SessionDestroyListener listener : sessionDestroyListeners) { - try { - listener.sessionDestroy(event); - } catch (Exception e) { - /* - * for now, use the session error handler; in the future, - * could have an API for using some other handler for - * session init and destroy listeners - */ - session.getErrorHandler().error(new ErrorEvent(e)); - } - } + Stream.concat(session.destroyListeners.stream(), + sessionDestroyListeners.stream()).forEach(listener -> { + try { + listener.sessionDestroy(event); + } catch (Exception e) { + /* + * for now, use the session error handler; in the + * future, could have an API for using some other + * handler for session init and destroy listeners + */ + session.getErrorHandler().error(new ErrorEvent(e)); + } + }); session.setState(VaadinSessionState.CLOSED); }); diff --git a/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java b/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java index d98586c189d..9d897229072 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -32,6 +33,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Future; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -47,6 +49,7 @@ import com.vaadin.flow.internal.CurrentInstance; import com.vaadin.flow.internal.StateNode; import com.vaadin.flow.server.startup.ApplicationConfiguration; +import com.vaadin.flow.shared.Registration; import com.vaadin.flow.shared.communication.PushMode; import jakarta.servlet.http.HttpSession; @@ -78,6 +81,8 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { volatile boolean sessionClosedExplicitly = false; + final List destroyListeners = new CopyOnWriteArrayList<>(); + /** * Configuration for the session. */ @@ -489,6 +494,31 @@ public Collection getRequestHandlers() { return Collections.unmodifiableCollection(requestHandlers); } + /** + * Adds a listener that gets notified when this session is destroyed. + * + *

+ * This session will be locked and its {@link UI}s will have been removed + * when the listener is called. + * + *

+ * If this session is already closed, no notification is delivered. + * + *

+ * This method only delivers notifications for this session. To also be + * notified about other sessions, use + * {@link VaadinService#addSessionDestroyListener}. + * + * @param listener + * the session destroy listener + * @return a handle that can be used for removing the listener + * @see VaadinService#addSessionDestroyListener + */ + public Registration addSessionDestroyListener( + SessionDestroyListener listener) { + return Registration.addAndRemove(destroyListeners, listener); + } + /** * Gets the currently used session. The current session is automatically * defined when processing requests to the server (see {@link ThreadLocal}) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringVaadinServletService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringVaadinServletService.java index b41c3ab0816..075b8d607a4 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringVaadinServletService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringVaadinServletService.java @@ -46,8 +46,6 @@ public class SpringVaadinServletService extends VaadinServletService { private final transient ApplicationContext context; - private final Registration serviceDestroyRegistration; - static final String SPRING_BOOT_WEBPROPERTIES_CLASS = "org.springframework.boot.autoconfigure.web.WebProperties"; /** @@ -66,11 +64,6 @@ public SpringVaadinServletService(VaadinServlet servlet, ApplicationContext context) { super(servlet, deploymentConfiguration); this.context = context; - SessionDestroyListener listener = event -> sessionDestroyed( - event.getSession()); - Registration registration = addSessionDestroyListener(listener); - serviceDestroyRegistration = addServiceDestroyListener( - event -> serviceDestroyed(registration)); } @Override @@ -105,21 +98,13 @@ public void init() throws ServiceException { uiInitListeners.values().forEach(this::addUIInitListener); } + // This method should be removed when the deprecated class + // SpringVaadinSession is removed @Override protected VaadinSession createVaadinSession(VaadinRequest request) { return new SpringVaadinSession(this); } - private void sessionDestroyed(VaadinSession session) { - assert session instanceof SpringVaadinSession; - ((SpringVaadinSession) session).fireSessionDestroy(); - } - - private void serviceDestroyed(Registration registration) { - registration.remove(); - serviceDestroyRegistration.remove(); - } - @Override public URL getStaticResource(String path) { URL resource = super.getStaticResource(path); diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringVaadinSession.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringVaadinSession.java index 33eba0e5ca8..d2dcf5ad5a9 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringVaadinSession.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringVaadinSession.java @@ -15,12 +15,7 @@ */ package com.vaadin.flow.spring; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import com.vaadin.flow.server.SessionDestroyEvent; import com.vaadin.flow.server.SessionDestroyListener; -import com.vaadin.flow.server.SessionInitListener; import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinSession; @@ -28,14 +23,13 @@ * Vaadin session implementation for Spring. * * @author Vaadin Ltd - * + * @deprecated No replacement planned */ +@Deprecated(forRemoval = true) public class SpringVaadinSession extends VaadinSession { - private final List destroyListeners = new CopyOnWriteArrayList<>(); - /** - * Creates a new VaadinSession tied to a VaadinService. + * Creates a new SpringVaadinSession tied to a VaadinService. * * @param service * the Vaadin service for the new session @@ -48,10 +42,7 @@ public SpringVaadinSession(VaadinService service) { * Handles destruction of the session. */ public void fireSessionDestroy() { - SessionDestroyEvent event = new SessionDestroyEvent(getService(), this); - destroyListeners.stream() - .forEach(listener -> listener.sessionDestroy(event)); - destroyListeners.clear(); + getService().fireSessionDestroy(this); } /** @@ -70,7 +61,7 @@ public void fireSessionDestroy() { * the vaadin service session destroy listener */ public void addDestroyListener(SessionDestroyListener listener) { - destroyListeners.add(listener); + this.addSessionDestroyListener(listener); } } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java index 16e654ef8e0..c179f339df8 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java @@ -50,7 +50,6 @@ import com.vaadin.flow.server.VaadinServletContext; import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.shared.Registration; -import com.vaadin.flow.spring.SpringVaadinSession; import com.vaadin.flow.spring.annotation.RouteScopeOwner; /** @@ -72,22 +71,13 @@ private static class RouteStoreWrapper implements Serializable { private final VaadinSession session; - private final Registration sessionDestroyListenerRegistration; - private final Map routeStores; private RouteStoreWrapper(VaadinSession session) { assert session.hasLock(); this.session = session; + session.addSessionDestroyListener(event -> destroy()); routeStores = new HashMap<>(); - if (session instanceof SpringVaadinSession) { - sessionDestroyListenerRegistration = null; - ((SpringVaadinSession) session) - .addDestroyListener(event -> destroy()); - } else { - sessionDestroyListenerRegistration = session.getService() - .addSessionDestroyListener(event -> destroy()); - } } private RouteBeanStore getBeanStore(UI ui) { @@ -146,9 +136,6 @@ private void destroy() { routeStores.clear(); } finally { session.unlock(); - if (sessionDestroyListenerRegistration != null) { - sessionDestroyListenerRegistration.remove(); - } } } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java index 73b48ed2d68..a4893aaa204 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java @@ -20,7 +20,6 @@ import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.shared.Registration; -import com.vaadin.flow.spring.SpringVaadinSession; /** * Implementation of Spring's @@ -40,31 +39,15 @@ public class VaadinSessionScope extends AbstractScope { private static class SessionBeanStore extends BeanStore { - private final Registration sessionDestroyListenerRegistration; - private SessionBeanStore(VaadinSession session) { super(session); - if (session instanceof SpringVaadinSession) { - sessionDestroyListenerRegistration = null; - ((SpringVaadinSession) session) - .addDestroyListener(event -> destroy()); - } else { - sessionDestroyListenerRegistration = session.getService() - .addSessionDestroyListener(event -> destroy()); - } + session.addSessionDestroyListener(event -> destroy()); } @Override Void doDestroy() { - try { - getVaadinSession().setAttribute(BeanStore.class, null); - super.doDestroy(); - } finally { - if (sessionDestroyListenerRegistration != null) { - sessionDestroyListenerRegistration.remove(); - } - } - return null; + getVaadinSession().setAttribute(BeanStore.class, null); + return super.doDestroy(); } } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java index cbb700cdbeb..bfc11b79203 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java @@ -26,7 +26,6 @@ import com.vaadin.flow.component.UI; import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.shared.Registration; -import com.vaadin.flow.spring.SpringVaadinSession; /** * Implementation of Spring's @@ -48,22 +47,13 @@ private static class UIStoreWrapper private final VaadinSession session; - private final Registration sessionDestroyListenerRegistration; - private final Map uiStores; private UIStoreWrapper(VaadinSession session) { assert session.hasLock(); uiStores = new HashMap<>(); this.session = session; - if (session instanceof SpringVaadinSession) { - sessionDestroyListenerRegistration = null; - ((SpringVaadinSession) session) - .addDestroyListener(event -> destroy()); - } else { - sessionDestroyListenerRegistration = session.getService() - .addSessionDestroyListener(event -> destroy()); - } + session.addSessionDestroyListener(event -> destroy()); } @Override @@ -96,9 +86,6 @@ private void destroy() { uiStores.clear(); } finally { session.unlock(); - if (sessionDestroyListenerRegistration != null) { - sessionDestroyListenerRegistration.remove(); - } } } diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java index 17b663f8a38..759454a5acd 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import org.junit.After; @@ -35,7 +36,6 @@ import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.server.VaadinSessionState; import com.vaadin.flow.server.startup.ApplicationConfiguration; -import com.vaadin.flow.spring.SpringVaadinSession; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.times; @@ -46,12 +46,18 @@ public abstract class AbstractScopeTest { private VaadinSession session; - public static class TestSession extends SpringVaadinSession { + public static class TestSession extends VaadinSession { + + private final ReentrantLock lock = new ReentrantLock(); public TestSession() { - super(Mockito.mock(VaadinService.class)); + super(Mockito.spy(VaadinService.class)); } + @Override + public ReentrantLock getLockInstance() { + return this.lock; + } } @After @@ -131,7 +137,7 @@ protected void registerDestructionCallback_currentScopeIsSet_objectIsStored( @SuppressWarnings("unchecked") protected VaadinSession mockSession() { - SpringVaadinSession session = Mockito.mock(TestSession.class, + VaadinSession session = Mockito.mock(TestSession.class, Mockito.withSettings().useConstructor()); doCallRealMethod().when(session).setAttribute(Mockito.any(Class.class), Mockito.any()); diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java index c5d84690e30..3ed0babf774 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java @@ -37,7 +37,6 @@ import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinServletContext; import com.vaadin.flow.server.VaadinSession; -import com.vaadin.flow.spring.SpringVaadinSession; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.times; @@ -89,13 +88,14 @@ public void destroySession_sessionAttributeIsCleanedAndDestructionCallbackIsCall mockServletContext(ui); - SpringVaadinSession springSession = (SpringVaadinSession) VaadinSession - .getCurrent(); + VaadinSession session = VaadinSession.getCurrent(); + VaadinService service = session.getService(); - doCallRealMethod().when(springSession) - .addDestroyListener(Mockito.any()); - - doCallRealMethod().when(springSession).fireSessionDestroy(); + doCallRealMethod().when(session) + .addSessionDestroyListener(Mockito.any()); + doCallRealMethod().when(session).getLockInstance(); + doCallRealMethod().when(session).getPendingAccessQueue(); + doCallRealMethod().when(session).access(Mockito.any()); VaadinRouteScope scope = initScope(ui); @@ -108,12 +108,13 @@ public void destroySession_sessionAttributeIsCleanedAndDestructionCallbackIsCall + "$RouteStoreWrapper"; // self control - the attribute name is used by the implementation - Assert.assertNotNull(springSession.getAttribute(attribute)); + Assert.assertNotNull(session.getAttribute(attribute)); - springSession.fireSessionDestroy(); + service.fireSessionDestroy(session); + service.runPendingAccessTasks(session); Assert.assertEquals(1, count.get()); - Assert.assertNull(springSession.getAttribute(attribute)); + Assert.assertNull(session.getAttribute(attribute)); // Destruction callbacks are not called anymore (they are removed) scope.getBeanStore().destroy(); diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinSessionScopeTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinSessionScopeTest.java index e857ea7fd45..7e95740b0ad 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinSessionScopeTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinSessionScopeTest.java @@ -29,8 +29,8 @@ import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.Scope; +import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinSession; -import com.vaadin.flow.spring.SpringVaadinSession; import net.jcip.annotations.NotThreadSafe; @@ -71,12 +71,13 @@ public void registerDestructionCallback_currentSessionIsSet_objectIsStored() { @Test public void destroySession_sessionAttributeIsCleanedAndDestructionCallbackIsCalled() { VaadinSession session = mockSession(); - SpringVaadinSession springSession = (SpringVaadinSession) session; + VaadinService service = session.getService(); - doCallRealMethod().when(springSession) - .addDestroyListener(Mockito.any()); - - doCallRealMethod().when(springSession).fireSessionDestroy(); + doCallRealMethod().when(session) + .addSessionDestroyListener(Mockito.any()); + doCallRealMethod().when(session).getLockInstance(); + doCallRealMethod().when(session).getPendingAccessQueue(); + doCallRealMethod().when(session).access(Mockito.any()); VaadinSessionScope scope = new VaadinSessionScope(); @@ -89,7 +90,8 @@ public void destroySession_sessionAttributeIsCleanedAndDestructionCallbackIsCall when(factory.getObject()).thenReturn(object); scope.get("foo", factory); - springSession.fireSessionDestroy(); + service.fireSessionDestroy(session); + service.runPendingAccessTasks(session); Assert.assertEquals(1, count.get()); Assert.assertNull(session.getAttribute(BeanStore.class)); diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinUIScopeTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinUIScopeTest.java index a8c7f7e25b9..54e495823e1 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinUIScopeTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinUIScopeTest.java @@ -25,8 +25,8 @@ import com.vaadin.flow.component.ComponentUtil; import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinSession; -import com.vaadin.flow.spring.SpringVaadinSession; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.times; @@ -89,13 +89,14 @@ public void remove_noCurrentUI_throwException() { public void destroySession_sessionAttributeIsCleanedAndDestructionCallbackIsCalled() { mockUI(); - SpringVaadinSession springSession = (SpringVaadinSession) VaadinSession - .getCurrent(); + VaadinSession session = VaadinSession.getCurrent(); + VaadinService service = session.getService(); - doCallRealMethod().when(springSession) - .addDestroyListener(Mockito.any()); - - doCallRealMethod().when(springSession).fireSessionDestroy(); + doCallRealMethod().when(session) + .addSessionDestroyListener(Mockito.any()); + doCallRealMethod().when(session).getLockInstance(); + doCallRealMethod().when(session).getPendingAccessQueue(); + doCallRealMethod().when(session).access(Mockito.any()); VaadinUIScope scope = new VaadinUIScope(); @@ -111,12 +112,13 @@ public void destroySession_sessionAttributeIsCleanedAndDestructionCallbackIsCall String attribute = VaadinUIScope.class.getName() + "$UIStoreWrapper"; // self control - the attribute name is used by the implementation - Assert.assertNotNull(springSession.getAttribute(attribute)); + Assert.assertNotNull(session.getAttribute(attribute)); - springSession.fireSessionDestroy(); + service.fireSessionDestroy(session); + service.runPendingAccessTasks(session); Assert.assertEquals(1, count.get()); - Assert.assertNull(springSession.getAttribute(attribute)); + Assert.assertNull(session.getAttribute(attribute)); // Destruction callbacks are not called anymore (they are removed) scope.getBeanStore().destroy();