From fafc0a91e3943fdea386a3af92deb00d3150aeba Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 31 Jul 2020 11:31:50 +0100 Subject: [PATCH] Register @WebListeners in a way that allows them to register components Previously, @WebListeners were discovered via custom component scanning and then registered programmatically via the ServletContext. The servlet spec requires any ServletContextListener registered in this manner to be prohibited from programatically configuring servlets, filters, and listeners. This left us not strictly complying with the servlet spec as a ServletContextListener registered via a @WebListener annotation should be able to programatically configure other components. This commit updates WebListenerHandler to register each @WebListener component directly with Jetty, Tomcat, or Undertow rather than via the ServletContext API. This ensure that any @WebListener-annoated ServletContextListener registered via servlet component scanning is able to programatically register servlets, filters, and listeners. There is a small chance that this will be a breaking change for some users: 1. The ServletListenerRegistrationBeans that were previously defined for each @WebListener will now be WebListenerHandler.WebListenerRegistrars 2. Each @WebListener-annotated class will now be instantiated by Jetty, Tomcat, or Undertow. Jetty and Tomcat both require the class to be public and have a public default constructor. Previously, a package-private class or default constructor could be used as the instantiation was performed by Spring Framework. Undertow is not affected as it can instantiate a package-private type. Fixes gh-18303 --- ...vletWebServerFactoryAutoConfiguration.java | 9 +- .../ServletWebServerFactoryCustomizer.java | 15 ++++ .../jetty/JettyServletWebServerFactory.java | 41 +++++++++ .../tomcat/TomcatServletWebServerFactory.java | 3 + .../UndertowServletWebServerFactory.java | 19 ++++ .../boot/web/servlet/WebListenerHandler.java | 24 ++++- .../web/servlet/WebListenerRegistrar.java | 35 ++++++++ .../boot/web/servlet/WebListenerRegistry.java | 35 ++++++++ .../AbstractServletWebServerFactory.java | 12 +++ .../ConfigurableServletWebServerFactory.java | 4 +- .../ServletComponentScanIntegrationTests.java | 89 +++++++++++++++---- .../web/servlet/WebListenerHandlerTests.java | 4 +- .../servlet/testcomponents/TestListener.java | 27 +++++- .../servlet/testcomponents/TestServlet.java | 4 +- 14 files changed, 289 insertions(+), 32 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerRegistrar.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerRegistry.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java index 91ef4daf5a..fc3d9c1380 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.web.servlet; import java.util.function.Supplier; +import java.util.stream.Collectors; import javax.servlet.DispatcherType; import javax.servlet.ServletRequest; @@ -24,6 +25,7 @@ import javax.servlet.ServletRequest; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -38,6 +40,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor; import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor; import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.boot.web.servlet.WebListenerRegistrar; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -69,8 +72,10 @@ import org.springframework.web.filter.ForwardedHeaderFilter; public class ServletWebServerFactoryAutoConfiguration { @Bean - public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) { - return new ServletWebServerFactoryCustomizer(serverProperties); + public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties, + ObjectProvider webListenerRegistrars) { + return new ServletWebServerFactoryCustomizer(serverProperties, + webListenerRegistrars.orderedStream().collect(Collectors.toList())); } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java index 6ce83bcc7b..ae259d1a17 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java @@ -16,9 +16,13 @@ package org.springframework.boot.autoconfigure.web.servlet; +import java.util.Collections; +import java.util.List; + import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.WebListenerRegistrar; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.core.Ordered; @@ -37,8 +41,16 @@ public class ServletWebServerFactoryCustomizer private final ServerProperties serverProperties; + private final Iterable webListenerRegistrars; + public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) { + this(serverProperties, Collections.emptyList()); + } + + public ServletWebServerFactoryCustomizer(ServerProperties serverProperties, + List webListenerRegistrars) { this.serverProperties = serverProperties; + this.webListenerRegistrars = webListenerRegistrars; } @Override @@ -62,6 +74,9 @@ public class ServletWebServerFactoryCustomizer map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader); map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters); map.from(this.serverProperties.getShutdown()).to(factory::setShutdown); + for (WebListenerRegistrar registrar : this.webListenerRegistrars) { + registrar.register(factory); + } } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index fde5218367..d1fe7c98cd 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -27,6 +27,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.EventListener; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -46,8 +47,11 @@ import org.eclipse.jetty.server.session.DefaultSessionCache; import org.eclipse.jetty.server.session.FileSessionDataStore; import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.servlet.ErrorPageErrorHandler; +import org.eclipse.jetty.servlet.ListenerHolder; +import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletMapping; +import org.eclipse.jetty.servlet.Source; import org.eclipse.jetty.util.resource.JarResource; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; @@ -336,6 +340,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor configurations.add(getServletContextInitializerConfiguration(webAppContext, initializers)); configurations.add(getErrorPageConfiguration()); configurations.add(getMimeTypeConfiguration()); + configurations.add(new WebListenersConfiguration(getWebListenerClassNames())); configurations.addAll(getConfigurations()); return configurations.toArray(new Configuration[0]); } @@ -604,4 +609,40 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor } + /** + * {@link AbstractConfiguration} to apply {@code @WebListener} classes. + */ + private static class WebListenersConfiguration extends AbstractConfiguration { + + private final Set classNames; + + WebListenersConfiguration(Set webListenerClassNames) { + this.classNames = webListenerClassNames; + } + + @Override + public void configure(WebAppContext context) throws Exception { + ServletHandler servletHandler = context.getServletHandler(); + for (String className : this.classNames) { + configure(context, servletHandler, className); + } + } + + private void configure(WebAppContext context, ServletHandler servletHandler, String className) + throws ClassNotFoundException { + ListenerHolder holder = servletHandler.newListenerHolder(new Source(Source.Origin.ANNOTATION, className)); + holder.setHeldClass(loadClass(context, className)); + servletHandler.addListener(holder); + } + + @SuppressWarnings("unchecked") + private Class loadClass(WebAppContext context, String className) + throws ClassNotFoundException { + ClassLoader classLoader = context.getClassLoader(); + classLoader = (classLoader != null) ? classLoader : getClass().getClassLoader(); + return (Class) classLoader.loadClass(className); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index d8b8eb2a09..1dfd70ff98 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -383,6 +383,9 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto } configureSession(context); new DisableReferenceClearingContextCustomizer().customize(context); + for (String webListenerClassName : getWebListenerClassNames()) { + context.addApplicationListener(webListenerClassName); + } for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) { customizer.customize(context); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java index b5bba89c7d..2181233303 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EventListener; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -47,6 +48,7 @@ import io.undertow.server.session.SessionManager; import io.undertow.servlet.Servlets; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.DeploymentManager; +import io.undertow.servlet.api.ListenerInfo; import io.undertow.servlet.api.MimeMapping; import io.undertow.servlet.api.ServletContainerInitializerInfo; import io.undertow.servlet.api.ServletStackTraces; @@ -330,6 +332,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac deployment.setEagerFilterInit(this.eagerFilterInit); deployment.setPreservePathOnForward(this.preservePathOnForward); configureMimeMappings(deployment); + configureWebListeners(deployment); for (UndertowDeploymentInfoCustomizer customizer : this.deploymentInfoCustomizers) { customizer.customize(deployment); } @@ -350,6 +353,22 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac return manager; } + private void configureWebListeners(DeploymentInfo deployment) { + for (String className : getWebListenerClassNames()) { + try { + deployment.addListener(new ListenerInfo(loadWebListenerClass(className))); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Failed to load web listener class '" + className + "'", ex); + } + } + } + + @SuppressWarnings("unchecked") + private Class loadWebListenerClass(String className) throws ClassNotFoundException { + return (Class) getServletClassLoader().loadClass(className); + } + private boolean isZeroOrLess(Duration timeoutDuration) { return timeoutDuration == null || timeoutDuration.isZero() || timeoutDuration.isNegative(); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerHandler.java index 5d107c0057..00463e4401 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerHandler.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,9 +38,25 @@ class WebListenerHandler extends ServletComponentHandler { @Override protected void doHandle(Map attributes, AnnotatedBeanDefinition beanDefinition, BeanDefinitionRegistry registry) { - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ServletListenerRegistrationBean.class); - builder.addPropertyValue("listener", beanDefinition); - registry.registerBeanDefinition(beanDefinition.getBeanClassName(), builder.getBeanDefinition()); + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .rootBeanDefinition(ServletComponentWebListenerRegistrar.class); + builder.addConstructorArgValue(beanDefinition.getBeanClassName()); + registry.registerBeanDefinition(beanDefinition.getBeanClassName() + "Registrar", builder.getBeanDefinition()); + } + + static class ServletComponentWebListenerRegistrar implements WebListenerRegistrar { + + private final String listenerClassName; + + ServletComponentWebListenerRegistrar(String listenerClassName) { + this.listenerClassName = listenerClassName; + } + + @Override + public void register(WebListenerRegistry registry) { + registry.addWebListeners(this.listenerClassName); + } + } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerRegistrar.java new file mode 100644 index 0000000000..e964010300 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerRegistrar.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * 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 + * + * https://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.springframework.boot.web.servlet; + +import javax.servlet.annotation.WebListener; + +/** + * Interface to be implemented by types that register {@link WebListener @WebListeners}. + * + * @author Andy Wilkinson + * @since 2.4.0 + */ +public interface WebListenerRegistrar { + + /** + * Register web listeners with the given registry. + * @param registry the web listener registry + */ + void register(WebListenerRegistry registry); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerRegistry.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerRegistry.java new file mode 100644 index 0000000000..31ccf76d24 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerRegistry.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * 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 + * + * https://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.springframework.boot.web.servlet; + +import javax.servlet.annotation.WebListener; + +/** + * A registry that holds {@link WebListener @WebListeners}. + * + * @author Andy Wilkinson + * @since 2.4.0 + */ +public interface WebListenerRegistry { + + /** + * Adds web listeners that will be registered with the servlet container. + * @param webListenerClassNames the class names of the web listeners + */ + void addWebListeners(String... webListenerClassNames); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java index 38e6e29fca..335e3654af 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -81,6 +82,8 @@ public abstract class AbstractServletWebServerFactory extends AbstractConfigurab private final StaticResourceJars staticResourceJars = new StaticResourceJars(); + private final Set webListenerClassNames = new HashSet(); + /** * Create a new {@link AbstractServletWebServerFactory} instance. */ @@ -283,6 +286,15 @@ public abstract class AbstractServletWebServerFactory extends AbstractConfigurab return this.session.getSessionStoreDirectory().getValidDirectory(mkdirs); } + @Override + public void addWebListeners(String... webListenerClassNames) { + this.webListenerClassNames.addAll(Arrays.asList(webListenerClassNames)); + } + + protected final Set getWebListenerClassNames() { + return this.webListenerClassNames; + } + /** * {@link ServletContextInitializer} to apply appropriate parts of the {@link Session} * configuration. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/ConfigurableServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/ConfigurableServletWebServerFactory.java index a2cdd5264a..d58d86ec28 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/ConfigurableServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/ConfigurableServletWebServerFactory.java @@ -28,6 +28,7 @@ import org.springframework.boot.web.server.ConfigurableWebServerFactory; import org.springframework.boot.web.server.MimeMappings; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.ServletContextInitializer; +import org.springframework.boot.web.servlet.WebListenerRegistry; /** * A configurable {@link ServletWebServerFactory}. @@ -41,7 +42,8 @@ import org.springframework.boot.web.servlet.ServletContextInitializer; * @see ServletWebServerFactory * @see WebServerFactoryCustomizer */ -public interface ConfigurableServletWebServerFactory extends ConfigurableWebServerFactory, ServletWebServerFactory { +public interface ConfigurableServletWebServerFactory + extends ConfigurableWebServerFactory, ServletWebServerFactory, WebListenerRegistry { /** * Sets the context path for the web server. The context should start with a "/" diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletComponentScanIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletComponentScanIntegrationTests.java index bf9ffcf7b2..5792bd96e7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletComponentScanIntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletComponentScanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.Map; import java.util.Properties; +import java.util.stream.Stream; import javax.servlet.MultipartConfigElement; import javax.servlet.annotation.WebFilter; @@ -30,12 +31,19 @@ import javax.servlet.annotation.WebListener; import javax.servlet.annotation.WebServlet; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer; +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.boot.web.servlet.testcomponents.TestMultipartServlet; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -52,6 +60,9 @@ class ServletComponentScanIntegrationTests { private AnnotationConfigServletWebServerApplicationContext context; + @TempDir + File temp; + @AfterEach void cleanUp() { if (this.context != null) { @@ -59,37 +70,40 @@ class ServletComponentScanIntegrationTests { } } - @Test - void componentsAreRegistered() { + @ParameterizedTest(name = "{0}") + @MethodSource("testConfiguration") + void componentsAreRegistered(String serverName, Class configuration) { this.context = new AnnotationConfigServletWebServerApplicationContext(); - this.context.register(TestConfiguration.class); + this.context.register(configuration); new ServerPortInfoApplicationContextInitializer().initialize(this.context); this.context.refresh(); String port = this.context.getEnvironment().getProperty("local.server.port"); String response = new RestTemplate().getForObject("http://localhost:" + port + "/test", String.class); - assertThat(response).isEqualTo("alpha bravo"); + assertThat(response).isEqualTo("alpha bravo charlie"); } - @Test - void indexedComponentsAreRegistered(@TempDir File temp) throws IOException { - writeIndex(temp); + @ParameterizedTest(name = "{0}") + @MethodSource("testConfiguration") + void indexedComponentsAreRegistered(String serverName, Class configuration) throws IOException { + writeIndex(this.temp); this.context = new AnnotationConfigServletWebServerApplicationContext(); - try (URLClassLoader classLoader = new URLClassLoader(new URL[] { temp.toURI().toURL() }, + try (URLClassLoader classLoader = new URLClassLoader(new URL[] { this.temp.toURI().toURL() }, getClass().getClassLoader())) { this.context.setClassLoader(classLoader); - this.context.register(TestConfiguration.class); + this.context.register(configuration); new ServerPortInfoApplicationContextInitializer().initialize(this.context); this.context.refresh(); String port = this.context.getEnvironment().getProperty("local.server.port"); String response = new RestTemplate().getForObject("http://localhost:" + port + "/test", String.class); - assertThat(response).isEqualTo("alpha bravo"); + assertThat(response).isEqualTo("alpha bravo charlie"); } } - @Test - void multipartConfigIsHonoured() { + @ParameterizedTest(name = "{0}") + @MethodSource("testConfiguration") + void multipartConfigIsHonoured(String serverName, Class configuration) { this.context = new AnnotationConfigServletWebServerApplicationContext(); - this.context.register(TestConfiguration.class); + this.context.register(configuration); new ServerPortInfoApplicationContextInitializer().initialize(this.context); this.context.refresh(); @SuppressWarnings("rawtypes") @@ -118,15 +132,54 @@ class ServletComponentScanIntegrationTests { } } - @Configuration(proxyBeanMethods = false) + static Stream testConfiguration() { + return Stream.of(Arguments.of("Jetty", JettyTestConfiguration.class), + Arguments.of("Tomcat", TomcatTestConfiguration.class), + Arguments.of("Undertow", UndertowTestConfiguration.class)); + } + @ServletComponentScan(basePackages = "org.springframework.boot.web.servlet.testcomponents") - static class TestConfiguration { + abstract static class AbstractTestConfiguration { @Bean - TomcatServletWebServerFactory webServerFactory() { + protected ServletWebServerFactory webServerFactory(ObjectProvider webListenerRegistrars) { + ConfigurableServletWebServerFactory factory = createWebServerFactory(); + webListenerRegistrars.orderedStream().forEach((registrar) -> registrar.register(factory)); + return factory; + } + + abstract ConfigurableServletWebServerFactory createWebServerFactory(); + + } + + @Configuration(proxyBeanMethods = false) + static class JettyTestConfiguration extends AbstractTestConfiguration { + + @Override + ConfigurableServletWebServerFactory createWebServerFactory() { + return new JettyServletWebServerFactory(0); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TomcatTestConfiguration extends AbstractTestConfiguration { + + @Override + ConfigurableServletWebServerFactory createWebServerFactory() { return new TomcatServletWebServerFactory(0); } } + @Configuration(proxyBeanMethods = false) + static class UndertowTestConfiguration extends AbstractTestConfiguration { + + @Override + ConfigurableServletWebServerFactory createWebServerFactory() { + return new UndertowServletWebServerFactory(0); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/WebListenerHandlerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/WebListenerHandlerTests.java index 3fa5b1e515..f787927469 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/WebListenerHandlerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/WebListenerHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,7 +49,7 @@ class WebListenerHandlerTests { given(definition.getMetadata()).willReturn(new SimpleMetadataReaderFactory() .getMetadataReader(TestListener.class.getName()).getAnnotationMetadata()); this.handler.handle(definition, this.registry); - this.registry.getBeanDefinition(TestListener.class.getName()); + this.registry.getBeanDefinition(TestListener.class.getName() + "Registrar"); } @WebListener diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/testcomponents/TestListener.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/testcomponents/TestListener.java index 3eb45556ef..0a8b0caeb8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/testcomponents/TestListener.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/testcomponents/TestListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,27 @@ package org.springframework.boot.web.servlet.testcomponents; +import java.io.IOException; +import java.util.EnumSet; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import javax.servlet.annotation.WebListener; @WebListener -class TestListener implements ServletContextListener { +public class TestListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { + sce.getServletContext().addFilter("listenerAddedFilter", new ListenerAddedFilter()) + .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); sce.getServletContext().setAttribute("listenerAttribute", "alpha"); - } @Override @@ -34,4 +44,15 @@ class TestListener implements ServletContextListener { } + static class ListenerAddedFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + request.setAttribute("listenerAddedFilterAttribute", "charlie"); + chain.doFilter(request, response); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/testcomponents/TestServlet.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/testcomponents/TestServlet.java index a7cc03cabe..0887548827 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/testcomponents/TestServlet.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/testcomponents/TestServlet.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ public class TestServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().print(((String) req.getServletContext().getAttribute("listenerAttribute")) + " " - + req.getAttribute("filterAttribute")); + + req.getAttribute("filterAttribute") + " " + req.getAttribute("listenerAddedFilterAttribute")); resp.getWriter().flush(); }