Skip to content

Commit fafc0a9

Browse files
wilkinsonaphilwebb
authored andcommitted
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 spring-projectsgh-18303
1 parent 1a3f810 commit fafc0a9

File tree

14 files changed

+289
-32
lines changed

14 files changed

+289
-32
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
package org.springframework.boot.autoconfigure.web.servlet;
1818

1919
import java.util.function.Supplier;
20+
import java.util.stream.Collectors;
2021

2122
import javax.servlet.DispatcherType;
2223
import javax.servlet.ServletRequest;
2324

2425
import org.springframework.beans.BeansException;
2526
import org.springframework.beans.factory.BeanFactory;
2627
import org.springframework.beans.factory.BeanFactoryAware;
28+
import org.springframework.beans.factory.ObjectProvider;
2729
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
2830
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
2931
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -38,6 +40,7 @@
3840
import org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor;
3941
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
4042
import org.springframework.boot.web.servlet.FilterRegistrationBean;
43+
import org.springframework.boot.web.servlet.WebListenerRegistrar;
4144
import org.springframework.context.annotation.Bean;
4245
import org.springframework.context.annotation.Configuration;
4346
import org.springframework.context.annotation.Import;
@@ -69,8 +72,10 @@
6972
public class ServletWebServerFactoryAutoConfiguration {
7073

7174
@Bean
72-
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) {
73-
return new ServletWebServerFactoryCustomizer(serverProperties);
75+
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties,
76+
ObjectProvider<WebListenerRegistrar> webListenerRegistrars) {
77+
return new ServletWebServerFactoryCustomizer(serverProperties,
78+
webListenerRegistrars.orderedStream().collect(Collectors.toList()));
7479
}
7580

7681
@Bean

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package org.springframework.boot.autoconfigure.web.servlet;
1818

19+
import java.util.Collections;
20+
import java.util.List;
21+
1922
import org.springframework.boot.autoconfigure.web.ServerProperties;
2023
import org.springframework.boot.context.properties.PropertyMapper;
2124
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
25+
import org.springframework.boot.web.servlet.WebListenerRegistrar;
2226
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
2327
import org.springframework.core.Ordered;
2428

@@ -37,8 +41,16 @@ public class ServletWebServerFactoryCustomizer
3741

3842
private final ServerProperties serverProperties;
3943

44+
private final Iterable<WebListenerRegistrar> webListenerRegistrars;
45+
4046
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) {
47+
this(serverProperties, Collections.emptyList());
48+
}
49+
50+
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties,
51+
List<WebListenerRegistrar> webListenerRegistrars) {
4152
this.serverProperties = serverProperties;
53+
this.webListenerRegistrars = webListenerRegistrars;
4254
}
4355

4456
@Override
@@ -62,6 +74,9 @@ public void customize(ConfigurableServletWebServerFactory factory) {
6274
map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
6375
map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters);
6476
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
77+
for (WebListenerRegistrar registrar : this.webListenerRegistrars) {
78+
registrar.register(factory);
79+
}
6580
}
6681

6782
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.ArrayList;
2828
import java.util.Arrays;
2929
import java.util.Collection;
30+
import java.util.EventListener;
3031
import java.util.LinkedHashSet;
3132
import java.util.List;
3233
import java.util.Set;
@@ -46,8 +47,11 @@
4647
import org.eclipse.jetty.server.session.FileSessionDataStore;
4748
import org.eclipse.jetty.server.session.SessionHandler;
4849
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
50+
import org.eclipse.jetty.servlet.ListenerHolder;
51+
import org.eclipse.jetty.servlet.ServletHandler;
4952
import org.eclipse.jetty.servlet.ServletHolder;
5053
import org.eclipse.jetty.servlet.ServletMapping;
54+
import org.eclipse.jetty.servlet.Source;
5155
import org.eclipse.jetty.util.resource.JarResource;
5256
import org.eclipse.jetty.util.resource.Resource;
5357
import org.eclipse.jetty.util.resource.ResourceCollection;
@@ -336,6 +340,7 @@ protected Configuration[] getWebAppContextConfigurations(WebAppContext webAppCon
336340
configurations.add(getServletContextInitializerConfiguration(webAppContext, initializers));
337341
configurations.add(getErrorPageConfiguration());
338342
configurations.add(getMimeTypeConfiguration());
343+
configurations.add(new WebListenersConfiguration(getWebListenerClassNames()));
339344
configurations.addAll(getConfigurations());
340345
return configurations.toArray(new Configuration[0]);
341346
}
@@ -604,4 +609,40 @@ public String[] list() {
604609

605610
}
606611

612+
/**
613+
* {@link AbstractConfiguration} to apply {@code @WebListener} classes.
614+
*/
615+
private static class WebListenersConfiguration extends AbstractConfiguration {
616+
617+
private final Set<String> classNames;
618+
619+
WebListenersConfiguration(Set<String> webListenerClassNames) {
620+
this.classNames = webListenerClassNames;
621+
}
622+
623+
@Override
624+
public void configure(WebAppContext context) throws Exception {
625+
ServletHandler servletHandler = context.getServletHandler();
626+
for (String className : this.classNames) {
627+
configure(context, servletHandler, className);
628+
}
629+
}
630+
631+
private void configure(WebAppContext context, ServletHandler servletHandler, String className)
632+
throws ClassNotFoundException {
633+
ListenerHolder holder = servletHandler.newListenerHolder(new Source(Source.Origin.ANNOTATION, className));
634+
holder.setHeldClass(loadClass(context, className));
635+
servletHandler.addListener(holder);
636+
}
637+
638+
@SuppressWarnings("unchecked")
639+
private Class<? extends EventListener> loadClass(WebAppContext context, String className)
640+
throws ClassNotFoundException {
641+
ClassLoader classLoader = context.getClassLoader();
642+
classLoader = (classLoader != null) ? classLoader : getClass().getClassLoader();
643+
return (Class<? extends EventListener>) classLoader.loadClass(className);
644+
}
645+
646+
}
647+
607648
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,9 @@ protected void configureContext(Context context, ServletContextInitializer[] ini
383383
}
384384
configureSession(context);
385385
new DisableReferenceClearingContextCustomizer().customize(context);
386+
for (String webListenerClassName : getWebListenerClassNames()) {
387+
context.addApplicationListener(webListenerClassName);
388+
}
386389
for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
387390
customizer.customize(context);
388391
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Arrays;
2626
import java.util.Collection;
2727
import java.util.Collections;
28+
import java.util.EventListener;
2829
import java.util.HashMap;
2930
import java.util.LinkedHashSet;
3031
import java.util.List;
@@ -47,6 +48,7 @@
4748
import io.undertow.servlet.Servlets;
4849
import io.undertow.servlet.api.DeploymentInfo;
4950
import io.undertow.servlet.api.DeploymentManager;
51+
import io.undertow.servlet.api.ListenerInfo;
5052
import io.undertow.servlet.api.MimeMapping;
5153
import io.undertow.servlet.api.ServletContainerInitializerInfo;
5254
import io.undertow.servlet.api.ServletStackTraces;
@@ -330,6 +332,7 @@ private DeploymentManager createManager(ServletContextInitializer... initializer
330332
deployment.setEagerFilterInit(this.eagerFilterInit);
331333
deployment.setPreservePathOnForward(this.preservePathOnForward);
332334
configureMimeMappings(deployment);
335+
configureWebListeners(deployment);
333336
for (UndertowDeploymentInfoCustomizer customizer : this.deploymentInfoCustomizers) {
334337
customizer.customize(deployment);
335338
}
@@ -350,6 +353,22 @@ private DeploymentManager createManager(ServletContextInitializer... initializer
350353
return manager;
351354
}
352355

356+
private void configureWebListeners(DeploymentInfo deployment) {
357+
for (String className : getWebListenerClassNames()) {
358+
try {
359+
deployment.addListener(new ListenerInfo(loadWebListenerClass(className)));
360+
}
361+
catch (ClassNotFoundException ex) {
362+
throw new IllegalStateException("Failed to load web listener class '" + className + "'", ex);
363+
}
364+
}
365+
}
366+
367+
@SuppressWarnings("unchecked")
368+
private Class<? extends EventListener> loadWebListenerClass(String className) throws ClassNotFoundException {
369+
return (Class<? extends EventListener>) getServletClassLoader().loadClass(className);
370+
}
371+
353372
private boolean isZeroOrLess(Duration timeoutDuration) {
354373
return timeoutDuration == null || timeoutDuration.isZero() || timeoutDuration.isNegative();
355374
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/WebListenerHandler.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,9 +38,25 @@ class WebListenerHandler extends ServletComponentHandler {
3838
@Override
3939
protected void doHandle(Map<String, Object> attributes, AnnotatedBeanDefinition beanDefinition,
4040
BeanDefinitionRegistry registry) {
41-
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ServletListenerRegistrationBean.class);
42-
builder.addPropertyValue("listener", beanDefinition);
43-
registry.registerBeanDefinition(beanDefinition.getBeanClassName(), builder.getBeanDefinition());
41+
BeanDefinitionBuilder builder = BeanDefinitionBuilder
42+
.rootBeanDefinition(ServletComponentWebListenerRegistrar.class);
43+
builder.addConstructorArgValue(beanDefinition.getBeanClassName());
44+
registry.registerBeanDefinition(beanDefinition.getBeanClassName() + "Registrar", builder.getBeanDefinition());
45+
}
46+
47+
static class ServletComponentWebListenerRegistrar implements WebListenerRegistrar {
48+
49+
private final String listenerClassName;
50+
51+
ServletComponentWebListenerRegistrar(String listenerClassName) {
52+
this.listenerClassName = listenerClassName;
53+
}
54+
55+
@Override
56+
public void register(WebListenerRegistry registry) {
57+
registry.addWebListeners(this.listenerClassName);
58+
}
59+
4460
}
4561

4662
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.servlet;
18+
19+
import javax.servlet.annotation.WebListener;
20+
21+
/**
22+
* Interface to be implemented by types that register {@link WebListener @WebListeners}.
23+
*
24+
* @author Andy Wilkinson
25+
* @since 2.4.0
26+
*/
27+
public interface WebListenerRegistrar {
28+
29+
/**
30+
* Register web listeners with the given registry.
31+
* @param registry the web listener registry
32+
*/
33+
void register(WebListenerRegistry registry);
34+
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.servlet;
18+
19+
import javax.servlet.annotation.WebListener;
20+
21+
/**
22+
* A registry that holds {@link WebListener @WebListeners}.
23+
*
24+
* @author Andy Wilkinson
25+
* @since 2.4.0
26+
*/
27+
public interface WebListenerRegistry {
28+
29+
/**
30+
* Adds web listeners that will be registered with the servlet container.
31+
* @param webListenerClassNames the class names of the web listeners
32+
*/
33+
void addWebListeners(String... webListenerClassNames);
34+
35+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Arrays;
2424
import java.util.Collections;
2525
import java.util.HashMap;
26+
import java.util.HashSet;
2627
import java.util.LinkedHashSet;
2728
import java.util.List;
2829
import java.util.Locale;
@@ -81,6 +82,8 @@ public abstract class AbstractServletWebServerFactory extends AbstractConfigurab
8182

8283
private final StaticResourceJars staticResourceJars = new StaticResourceJars();
8384

85+
private final Set<String> webListenerClassNames = new HashSet<String>();
86+
8487
/**
8588
* Create a new {@link AbstractServletWebServerFactory} instance.
8689
*/
@@ -283,6 +286,15 @@ protected final File getValidSessionStoreDir(boolean mkdirs) {
283286
return this.session.getSessionStoreDirectory().getValidDirectory(mkdirs);
284287
}
285288

289+
@Override
290+
public void addWebListeners(String... webListenerClassNames) {
291+
this.webListenerClassNames.addAll(Arrays.asList(webListenerClassNames));
292+
}
293+
294+
protected final Set<String> getWebListenerClassNames() {
295+
return this.webListenerClassNames;
296+
}
297+
286298
/**
287299
* {@link ServletContextInitializer} to apply appropriate parts of the {@link Session}
288300
* configuration.

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/ConfigurableServletWebServerFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.boot.web.server.MimeMappings;
2929
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
3030
import org.springframework.boot.web.servlet.ServletContextInitializer;
31+
import org.springframework.boot.web.servlet.WebListenerRegistry;
3132

3233
/**
3334
* A configurable {@link ServletWebServerFactory}.
@@ -41,7 +42,8 @@
4142
* @see ServletWebServerFactory
4243
* @see WebServerFactoryCustomizer
4344
*/
44-
public interface ConfigurableServletWebServerFactory extends ConfigurableWebServerFactory, ServletWebServerFactory {
45+
public interface ConfigurableServletWebServerFactory
46+
extends ConfigurableWebServerFactory, ServletWebServerFactory, WebListenerRegistry {
4547

4648
/**
4749
* Sets the context path for the web server. The context should start with a "/"

0 commit comments

Comments
 (0)