diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java index 7a5c24cc1d..e771b5ce26 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EventListener; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -67,7 +68,7 @@ public class ServletContextInitializerBeans extends AbstractCollection seen = new HashSet<>(); + private final Seen seen = new Seen(); private final MultiValueMap, ServletContextInitializer> initializers; @@ -129,7 +130,7 @@ public class ServletContextInitializerBeans extends AbstractCollection entry : entries) { String beanName = entry.getKey(); B bean = entry.getValue(); - if (this.seen.add(bean)) { + if (this.seen.add(type, bean)) { // One that we haven't already seen RegistrationBean registration = adapter.createRegistrationBean(beanName, bean, entries.size()); int order = getOrder(bean); @@ -198,17 +199,17 @@ public class ServletContextInitializerBeans extends AbstractCollection List> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class type) { - return getOrderedBeansOfType(beanFactory, type, Collections.emptySet()); + return getOrderedBeansOfType(beanFactory, type, Seen.empty()); } private List> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class type, - Set excludes) { + Seen seen) { String[] names = beanFactory.getBeanNamesForType(type, true, false); Map map = new LinkedHashMap<>(); for (String name : names) { - if (!excludes.contains(name) && !ScopedProxyUtils.isScopedTarget(name)) { + if (!seen.contains(type, name) && !ScopedProxyUtils.isScopedTarget(name)) { T bean = beanFactory.getBean(name, type); - if (!excludes.contains(bean)) { + if (!seen.contains(type, bean)) { map.put(name, bean); } } @@ -310,4 +311,37 @@ public class ServletContextInitializerBeans extends AbstractCollection, Set> seen = new HashMap<>(); + + boolean add(Class type, Object object) { + if (contains(type, object)) { + return false; + } + return this.seen.computeIfAbsent(type, (ignore) -> new HashSet<>()).add(object); + } + + boolean contains(Class type, Object object) { + if (this.seen.isEmpty()) { + return false; + } + // If it has been directly seen, or the implemented ServletContextInitializer + // has been seen already + if (type != ServletContextInitializer.class + && this.seen.getOrDefault(type, Collections.emptySet()).contains(object)) { + return true; + } + if (this.seen.getOrDefault(ServletContextInitializer.class, Collections.emptySet()).contains(object)) { + return true; + } + return false; + } + + static Seen empty() { + return new Seen(); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java index 2f8272265c..62b0c1d59d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link ServletContextInitializerBeans}. * * @author Andy Wilkinson + * @author Moritz Halbritter */ class ServletContextInitializerBeansTests { @@ -82,6 +83,26 @@ class ServletContextInitializerBeansTests { .isInstanceOf(HttpSessionIdListener.class); } + @Test + void classesThatImplementMultipleInterfacesAreRegisteredForAllOfThem() { + load(MultipleInterfacesConfiguration.class); + ServletContextInitializerBeans initializerBeans = new ServletContextInitializerBeans( + this.context.getBeanFactory()); + assertThat(initializerBeans).hasSize(3); + assertThat(initializerBeans).element(0) + .isInstanceOf(ServletRegistrationBean.class) + .extracting((initializer) -> ((ServletRegistrationBean) initializer).getServlet()) + .isInstanceOf(TestServletAndFilterAndListener.class); + assertThat(initializerBeans).element(1) + .isInstanceOf(FilterRegistrationBean.class) + .extracting((initializer) -> ((FilterRegistrationBean) initializer).getFilter()) + .isInstanceOf(TestServletAndFilterAndListener.class); + assertThat(initializerBeans).element(2) + .isInstanceOf(ServletListenerRegistrationBean.class) + .extracting((initializer) -> ((ServletListenerRegistrationBean) initializer).getListener()) + .isInstanceOf(TestServletAndFilterAndListener.class); + } + private void load(Class... configuration) { this.context = new AnnotationConfigApplicationContext(configuration); } @@ -106,6 +127,16 @@ class ServletContextInitializerBeansTests { } + @Configuration(proxyBeanMethods = false) + static class MultipleInterfacesConfiguration { + + @Bean + TestServletAndFilterAndListener testServletAndFilterAndListener() { + return new TestServletAndFilterAndListener(); + } + + } + @Configuration(proxyBeanMethods = false) static class TestConfiguration { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/TestServletAndFilterAndListener.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/TestServletAndFilterAndListener.java new file mode 100644 index 0000000000..f3ed7aa2fa --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/TestServletAndFilterAndListener.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2024 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 java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.ServletResponse; + +class TestServletAndFilterAndListener implements Servlet, Filter, ServletRequestListener { + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + + } + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + + } + + @Override + public ServletConfig getServletConfig() { + return null; + } + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) + throws ServletException, IOException { + + } + + @Override + public String getServletInfo() { + return null; + } + + @Override + public void destroy() { + + } + +}