Register classes that implement multiple Servlet interfaces

Closes gh-39056
This commit is contained in:
Moritz Halbritter
2024-01-17 14:30:15 +01:00
parent 489921b063
commit 7d4d8e955e
3 changed files with 137 additions and 8 deletions

View File

@@ -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<ServletCo
/**
* Seen bean instances or bean names.
*/
private final Set<Object> seen = new HashSet<>();
private final Seen seen = new Seen();
private final MultiValueMap<Class<?>, ServletContextInitializer> initializers;
@@ -129,7 +130,7 @@ public class ServletContextInitializerBeans extends AbstractCollection<ServletCo
this.initializers.add(type, initializer);
if (source != null) {
// Mark the underlying source as seen in case it wraps an existing bean
this.seen.add(source);
this.seen.add(type, source);
}
if (logger.isTraceEnabled()) {
String resourceDescription = getResourceDescription(beanName, beanFactory);
@@ -174,7 +175,7 @@ public class ServletContextInitializerBeans extends AbstractCollection<ServletCo
for (Entry<String, B> 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<ServletCo
}
private <T> List<Entry<String, T>> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class<T> type) {
return getOrderedBeansOfType(beanFactory, type, Collections.emptySet());
return getOrderedBeansOfType(beanFactory, type, Seen.empty());
}
private <T> List<Entry<String, T>> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class<T> type,
Set<?> excludes) {
Seen seen) {
String[] names = beanFactory.getBeanNamesForType(type, true, false);
Map<String, T> 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<ServletCo
}
private static final class Seen {
private final Map<Class<?>, Set<Object>> 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();
}
}
}

View File

@@ -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 {

View File

@@ -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() {
}
}