Skip to content

Commit

Permalink
fix: prevent exception getting DevModeHandlerManager on application s…
Browse files Browse the repository at this point in the history
…hutdown (#19300) (#19307)

During the destruction of the web applicationi context, the
DevModeStartupListener's context destroy listener is invoked.
This listener attempts to locate the DevModeHandlerManager to
halt the Vaadin dev-server. However, this process could trigger
an exception if the dependency injection container behind the
Lookup mechanism has already been stopped.
This commit addresses the issue by capturing a reference to the
handler during web context start, eliminating the need for a
lookup during destruction, thus preventing potential exceptions.

Fixes #19183

Co-authored-by: Marco Collovati <marco@vaadin.com>
  • Loading branch information
vaadin-bot and mcollovati committed May 6, 2024
1 parent 668e0c8 commit 4bf5b0f
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@
*/
package com.vaadin.base.devserver.startup;

import java.io.Serializable;
import java.util.Set;

import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.HandlesTypes;
import jakarta.servlet.annotation.WebListener;

import java.io.Serializable;
import java.util.Set;

import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.WebComponentExporter;
import com.vaadin.flow.component.WebComponentExporterFactory;
import com.vaadin.flow.component.dependency.CssImport;
Expand Down Expand Up @@ -69,31 +71,55 @@ public class DevModeStartupListener
implements VaadinServletContextStartupInitializer, Serializable,
ServletContextListener {

private DevModeHandlerManager devModeHandlerManager;

@Override
public void initialize(Set<Class<?>> classes, VaadinContext context)
throws VaadinInitializerException {
Lookup lookup = context.getAttribute(Lookup.class);
DevModeHandlerManager devModeHandlerManager = lookup
.lookup(DevModeHandlerManager.class);
devModeHandlerManager.initDevModeHandler(classes, context);

lookupDevModeHandlerManager(context).initDevModeHandler(classes,
context);
}

@Override
public void contextInitialized(ServletContextEvent ctx) {
// No need to do anything on init
// Keep a reference to the dev mode handler manager to stop it on
// context destroy event, since lookup in that phase could fail, for
// example if the DI container behind lookup has been already disposed
devModeHandlerManager = lookupDevModeHandlerManager(
new VaadinServletContext(ctx.getServletContext()));
}

@Override
public void contextDestroyed(ServletContextEvent ctx) {
VaadinServletContext context = new VaadinServletContext(
ctx.getServletContext());
Lookup lookup = context.getAttribute(Lookup.class);
DevModeHandlerManager devModeHandlerManager = lookup
.lookup(DevModeHandlerManager.class);
if (devModeHandlerManager == null) {
// devModeHandlerManager should never be null here.
// if it happens try to lookup to ensure dev server is stopped
// but do not propagate potential failures to the container, since
// the situation error cannot be handled in any way.
try {
devModeHandlerManager = lookupDevModeHandlerManager(
new VaadinServletContext(ctx.getServletContext()));
} catch (Exception exception) {
LoggerFactory.getLogger(DevModeStartupListener.class).debug(
"Cannot obtain DevModeHandlerManager instance during ServletContext destroy event. "
+ "Potential cause could be DI container behind Lookup being already disposed.",
exception);
}
}
if (devModeHandlerManager != null) {
devModeHandlerManager.stopDevModeHandler();
}
devModeHandlerManager = null;
}

private DevModeHandlerManager lookupDevModeHandlerManager(
VaadinContext context) {
Lookup lookup = context.getAttribute(Lookup.class);
if (lookup == null) {
LoggerFactory.getLogger(DevModeStartupListener.class).debug(
"Cannot obtain a Lookup instance from VaadinContext.");
return null;
}
return lookup.lookup(DevModeHandlerManager.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2000-2024 Vaadin Ltd.
*
* Licensed 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 com.vaadin.flow.spring;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;

import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;

import com.vaadin.base.devserver.startup.DevModeStartupListener;
import com.vaadin.flow.di.LookupInitializer;
import com.vaadin.flow.internal.DevModeHandler;
import com.vaadin.flow.internal.DevModeHandlerManager;
import com.vaadin.flow.server.VaadinContext;
import com.vaadin.flow.server.startup.LookupServletContainerInitializer;

public class DevModeHandlerStopTest {

private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(Config.class,
SpringBootAutoConfiguration.class));

@Test
void devModeStartupListener_contextDestroyAfterSpringContextClosed_shouldNotThrow() {
// DevModeStartupListener is both ServletContextListener and
// ServletContainerInitializer so the servlet container will create two
// instances. Let's try to simulate the same behavior in the test
DevModeStartupListener startupListenerAsContainerInitializer = new DevModeStartupListener();
DevModeStartupListener startupListenerAsContextListener = new DevModeStartupListener();
AtomicReference<ServletContext> contextRef = new AtomicReference<>();
AtomicReference<MockDevModeHandlerManager> handlerManagerRef = new AtomicReference<>();
this.contextRunner.run((context) -> {
handlerManagerRef
.set(context.getBean(MockDevModeHandlerManager.class));
ServletContext servletContext = context.getServletContext();
contextRef.set(servletContext);
new LookupServletContainerInitializer()
.onStartup(
Set.of(LookupInitializer.class,
SpringLookupInitializer.class),
servletContext);
startupListenerAsContainerInitializer.onStartup(Set.of(),
servletContext);
startupListenerAsContextListener.contextInitialized(
new ServletContextEvent(servletContext));
});
Assertions.assertTrue(handlerManagerRef.get().initialized,
"Expecting DevModeHandlerManager initialization to be invoked, but it was not");
Assertions.assertFalse(handlerManagerRef.get().stopped,
"Expecting DevModeHandler not yet to be yet stopped, but it was");

// Stop the DevModeHandler after Spring Context has been closed
startupListenerAsContextListener
.contextDestroyed(new ServletContextEvent(contextRef.get()));
Assertions.assertTrue(handlerManagerRef.get().stopped,
"Expecting DevModeHandler to be stopped by DevModeHandlerManager, but it was not");
}

private static class MockDevModeHandlerManager
implements DevModeHandlerManager {

private boolean initialized;
private boolean stopped;

@Override
public Class<?>[] getHandlesTypes() {
return new Class[0];
}

@Override
public void initDevModeHandler(Set<Class<?>> classes,
VaadinContext context) {
initialized = true;
}

@Override
public void stopDevModeHandler() {
stopped = true;
}

@Override
public void setDevModeHandler(DevModeHandler devModeHandler) {
}

@Override
public DevModeHandler getDevModeHandler() {
return null;
}

@Override
public void launchBrowserInDevelopmentMode(String url) {

}
}

@TestConfiguration
static class Config {

@Bean
DevModeHandlerManager devModeHandlerManager() {
return new MockDevModeHandlerManager();
}
}
}

0 comments on commit 4bf5b0f

Please sign in to comment.