Skip to content

Commit cba1fbc

Browse files
Oblioolegz
authored andcommitted
Fix serverless dispatcher init ordering and harden proxy filter chain; add regression coverage and override caution docs
Signed-off-by: Oblio <oblio.leitch@vermont.gov>
1 parent bb0fb26 commit cba1fbc

5 files changed

Lines changed: 67 additions & 1 deletion

File tree

spring-cloud-function-adapters/spring-cloud-function-serverless-web/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ A sample is provided in [sample](https://github.com/spring-cloud/spring-cloud-fu
88

99
_NOTE: Although this module is AWS specific, this dependency is protocol only (not binary), therefore there is no AWS dependnecies._
1010

11+
_NOTE: The serverless `ServletWebServerFactory` is declared with `@ConditionalOnMissingBean`. If your
12+
application defines its own `ServletWebServerFactory` bean (for example Tomcat/Jetty/Undertow customization),
13+
that custom bean will take precedence and can disable the serverless adapter path. For serverless-web usage,
14+
do not provide a competing `ServletWebServerFactory` bean unless it delegates to
15+
`ServerlessAutoConfiguration.ServerlessServletWebServerFactory`._
16+
1117
The aformentioned proxy is identified as AWS Lambda [handler](https://github.com/spring-cloud/spring-cloud-function/blob/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml#L14)
1218

1319
The main Spring Boot configuration file is identified as [MAIN_CLASS](https://github.com/spring-cloud/spring-cloud-function/blob/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml#L22)

spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.springframework.beans.BeansException;
2323
import org.springframework.beans.factory.InitializingBean;
24+
import org.springframework.boot.autoconfigure.AutoConfiguration;
2425
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2526
import org.springframework.boot.web.server.WebServer;
2627
import org.springframework.boot.web.server.WebServerException;
@@ -39,13 +40,15 @@
3940
* @author Oleg Zhurakousky
4041
* @since 4.x
4142
*/
43+
@AutoConfiguration(beforeName = "org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration")
4244
@Configuration(proxyBeanMethods = false)
4345
public class ServerlessAutoConfiguration {
4446
private static Log logger = LogFactory.getLog(ServerlessAutoConfiguration.class);
4547

4648
@Bean
4749
@ConditionalOnMissingBean
4850
public ServletWebServerFactory servletWebServerFactory() {
51+
// A user-defined ServletWebServerFactory bean will override this and may bypass serverless initialization.
4952
return new ServerlessServletWebServerFactory();
5053
}
5154

spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessMVC.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Enumeration;
2424
import java.util.Iterator;
2525
import java.util.List;
26+
import java.util.Map;
2627
import java.util.concurrent.CountDownLatch;
2728
import java.util.concurrent.TimeUnit;
2829
import java.util.stream.Stream;
@@ -33,6 +34,7 @@
3334
import jakarta.servlet.Filter;
3435
import jakarta.servlet.FilterChain;
3536
import jakarta.servlet.FilterConfig;
37+
import jakarta.servlet.FilterRegistration;
3638
import jakarta.servlet.RequestDispatcher;
3739
import jakarta.servlet.Servlet;
3840
import jakarta.servlet.ServletConfig;
@@ -126,6 +128,8 @@ private void initContext(Class<?>... componentClasses) {
126128
if (this.applicationContext.containsBean(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) {
127129
this.dispatcher = this.applicationContext.getBean(DispatcherServlet.class);
128130
}
131+
Assert.state(this.dispatcher != null, "DispatcherServlet bean was not initialized. "
132+
+ "Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory.");
129133
}
130134

131135
public ConfigurableWebApplicationContext getApplicationContext() {
@@ -161,6 +165,8 @@ public void service(HttpServletRequest request, HttpServletResponse response) th
161165
}
162166

163167
public void service(HttpServletRequest request, HttpServletResponse response, CountDownLatch latch) throws Exception {
168+
Assert.state(this.dispatcher != null, "DispatcherServlet is not initialized. "
169+
+ "Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory.");
164170
ProxyFilterChain filterChain = new ProxyFilterChain(this.dispatcher);
165171
filterChain.doFilter(request, response);
166172

@@ -217,7 +223,17 @@ private static class ProxyFilterChain implements FilterChain {
217223
*/
218224
ProxyFilterChain(DispatcherServlet servlet) {
219225
List<Filter> filters = new ArrayList<>();
220-
servlet.getServletContext().getFilterRegistrations().values().forEach(fr -> filters.add(((ServerlessFilterRegistration) fr).getFilter()));
226+
for (Map.Entry<String, ? extends FilterRegistration> entry : servlet.getServletContext().getFilterRegistrations()
227+
.entrySet()) {
228+
FilterRegistration registration = entry.getValue();
229+
if (registration instanceof ServerlessFilterRegistration serverlessFilterRegistration) {
230+
filters.add(serverlessFilterRegistration.getFilter());
231+
}
232+
else {
233+
LOG.debug("Skipping unsupported filter registration type '" + registration.getClass().getName()
234+
+ "' for filter '" + entry.getKey() + "'");
235+
}
236+
}
221237
Assert.notNull(filters, "filters cannot be null");
222238
Assert.noNullElements(filters, "filters cannot contain null values");
223239
this.filters = initFilterList(servlet, filters.toArray(new Filter[] {}));

spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616

1717
package org.springframework.cloud.function.serverless.web;
1818

19+
import java.lang.reflect.Field;
20+
import java.lang.reflect.Proxy;
1921
import java.util.List;
22+
import java.util.Map;
2023

24+
import jakarta.servlet.FilterRegistration;
2125
import jakarta.servlet.http.HttpServletRequest;
2226
import org.junit.jupiter.api.AfterEach;
2327
import org.junit.jupiter.api.BeforeEach;
@@ -195,4 +199,38 @@ public void validatePostAsyncWithBody() throws Exception {
195199
assertThat(pet.getName()).isNotEmpty();
196200
}
197201

202+
@Test
203+
public void validateNonServerlessFilterRegistrationIsSkipped() throws Exception {
204+
ServerlessServletContext servletContext = (ServerlessServletContext) this.mvc.getServletContext();
205+
Field registrationsField = ServerlessServletContext.class.getDeclaredField("filterRegistrations");
206+
registrationsField.setAccessible(true);
207+
@SuppressWarnings("unchecked")
208+
Map<String, FilterRegistration> registrations =
209+
(Map<String, FilterRegistration>) registrationsField.get(servletContext);
210+
211+
FilterRegistration nonServerlessRegistration = (FilterRegistration) Proxy.newProxyInstance(
212+
FilterRegistration.class.getClassLoader(),
213+
new Class[]{FilterRegistration.class},
214+
(proxy, method, args) -> {
215+
if ("getName".equals(method.getName())) {
216+
return "nonServerless";
217+
}
218+
if (method.getReturnType().isPrimitive()) {
219+
if (method.getReturnType() == boolean.class) {
220+
return false;
221+
}
222+
return 0;
223+
}
224+
return null;
225+
});
226+
227+
registrations.put("nonServerless", nonServerlessRegistration);
228+
229+
HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/pets");
230+
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
231+
this.mvc.service(request, response);
232+
233+
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
234+
}
235+
198236
}

spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/ServerlessWebServerFactoryTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import org.springframework.boot.autoconfigure.SpringBootApplication;
2222

23+
import static org.assertj.core.api.Assertions.assertThat;
24+
2325
/**
2426
* @author Oleg Zhurakousky
2527
*/
@@ -29,6 +31,7 @@ public class ServerlessWebServerFactoryTests {
2931
public void testServerFactoryExists() {
3032
ServerlessMVC mvc = ServerlessMVC.INSTANCE(TestApplication.class);
3133
mvc.getApplicationContext();
34+
assertThat(mvc.getServletContext()).isNotNull();
3235
}
3336

3437
@SpringBootApplication

0 commit comments

Comments
 (0)