Skip to content

Commit 7a1980d

Browse files
committed
Add boot-like ac bootstrap to spring-cloud-function-serverless-web
1 parent 649f008 commit 7a1980d

File tree

7 files changed

+624
-27
lines changed

7 files changed

+624
-27
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright 2023-2023 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.cloud.function.serverless.web;
18+
19+
import java.io.IOException;
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import jakarta.servlet.AsyncContext;
24+
import jakarta.servlet.AsyncEvent;
25+
import jakarta.servlet.AsyncListener;
26+
import jakarta.servlet.ServletContext;
27+
import jakarta.servlet.ServletException;
28+
import jakarta.servlet.ServletRequest;
29+
import jakarta.servlet.ServletResponse;
30+
import jakarta.servlet.http.HttpServletRequest;
31+
import jakarta.servlet.http.HttpServletResponse;
32+
33+
import org.springframework.beans.BeanUtils;
34+
import org.springframework.lang.Nullable;
35+
import org.springframework.util.Assert;
36+
import org.springframework.web.util.WebUtils;
37+
38+
/**
39+
* Implementation of Async context for {@link ProxyMvc}.
40+
*
41+
* @author Oleg Zhurakousky
42+
*/
43+
public class ProxyAsyncContext implements AsyncContext {
44+
private final HttpServletRequest request;
45+
46+
@Nullable
47+
private final HttpServletResponse response;
48+
49+
private final List<AsyncListener> listeners = new ArrayList<>();
50+
51+
@Nullable
52+
private String dispatchedPath;
53+
54+
private long timeout = 10 * 1000L;
55+
56+
private final List<Runnable> dispatchHandlers = new ArrayList<>();
57+
58+
59+
public ProxyAsyncContext(ServletRequest request, @Nullable ServletResponse response) {
60+
this.request = (HttpServletRequest) request;
61+
this.response = (HttpServletResponse) response;
62+
}
63+
64+
65+
public void addDispatchHandler(Runnable handler) {
66+
Assert.notNull(handler, "Dispatch handler must not be null");
67+
synchronized (this) {
68+
if (this.dispatchedPath == null) {
69+
this.dispatchHandlers.add(handler);
70+
}
71+
else {
72+
handler.run();
73+
}
74+
}
75+
}
76+
77+
@Override
78+
public ServletRequest getRequest() {
79+
return this.request;
80+
}
81+
82+
@Override
83+
@Nullable
84+
public ServletResponse getResponse() {
85+
return this.response;
86+
}
87+
88+
@Override
89+
public boolean hasOriginalRequestAndResponse() {
90+
return (this.request instanceof ProxyHttpServletRequest && this.response instanceof ProxyHttpServletResponse);
91+
}
92+
93+
@Override
94+
public void dispatch() {
95+
dispatch(this.request.getRequestURI());
96+
}
97+
98+
@Override
99+
public void dispatch(String path) {
100+
dispatch(null, path);
101+
}
102+
103+
@Override
104+
public void dispatch(@Nullable ServletContext context, String path) {
105+
synchronized (this) {
106+
this.dispatchedPath = path;
107+
this.dispatchHandlers.forEach(Runnable::run);
108+
}
109+
}
110+
111+
@Nullable
112+
public String getDispatchedPath() {
113+
return this.dispatchedPath;
114+
}
115+
116+
@Override
117+
public void complete() {
118+
ProxyHttpServletRequest mockRequest = WebUtils.getNativeRequest(this.request, ProxyHttpServletRequest.class);
119+
if (mockRequest != null) {
120+
mockRequest.setAsyncStarted(false);
121+
}
122+
for (AsyncListener listener : this.listeners) {
123+
try {
124+
listener.onComplete(new AsyncEvent(this, this.request, this.response));
125+
}
126+
catch (IOException ex) {
127+
throw new IllegalStateException("AsyncListener failure", ex);
128+
}
129+
}
130+
}
131+
132+
@Override
133+
public void start(Runnable runnable) {
134+
runnable.run();
135+
}
136+
137+
@Override
138+
public void addListener(AsyncListener listener) {
139+
this.listeners.add(listener);
140+
}
141+
142+
@Override
143+
public void addListener(AsyncListener listener, ServletRequest request, ServletResponse response) {
144+
this.listeners.add(listener);
145+
}
146+
147+
public List<AsyncListener> getListeners() {
148+
return this.listeners;
149+
}
150+
151+
@Override
152+
public <T extends AsyncListener> T createListener(Class<T> clazz) throws ServletException {
153+
return BeanUtils.instantiateClass(clazz);
154+
}
155+
156+
/**
157+
* By default this is set to 10000 (10 seconds) even though the Servlet API
158+
* specifies a default async request timeout of 30 seconds. Keep in mind the
159+
* timeout could further be impacted by global configuration through the MVC
160+
* Java config or the XML namespace, as well as be overridden per request on
161+
* {@link org.springframework.web.context.request.async.DeferredResult DeferredResult}
162+
* or on
163+
* {@link org.springframework.web.servlet.mvc.method.annotation.SseEmitter SseEmitter}.
164+
* @param timeout the timeout value to use.
165+
* @see AsyncContext#setTimeout(long)
166+
*/
167+
@Override
168+
public void setTimeout(long timeout) {
169+
this.timeout = timeout;
170+
}
171+
172+
@Override
173+
public long getTimeout() {
174+
return this.timeout;
175+
}
176+
}

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
113113

114114
private boolean asyncStarted = false;
115115

116-
private boolean asyncSupported = false;
116+
private boolean asyncSupported = true;
117117

118118
private DispatcherType dispatcherType = DispatcherType.REQUEST;
119119

@@ -163,6 +163,8 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
163163

164164
private final MultiValueMap<String, Part> parts = new LinkedMultiValueMap<>();
165165

166+
private AsyncContext asyncContext;
167+
166168
public ProxyHttpServletRequest(ServletContext servletContext, String method, String requestURI) {
167169
this.servletContext = servletContext;
168170
this.method = method;
@@ -246,8 +248,6 @@ public byte[] getContentAsByteArray() {
246248
*/
247249
@Nullable
248250
public String getContentAsString() throws IllegalStateException, UnsupportedEncodingException {
249-
// Assert.state(this.characterEncoding != null, "Cannot get content as a String for a null character encoding. "
250-
// + "Consider setting the characterEncoding in the request.");
251251

252252
if (this.content == null) {
253253
return null;
@@ -633,7 +633,10 @@ public AsyncContext startAsync() {
633633

634634
@Override
635635
public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) {
636-
throw new UnsupportedOperationException();
636+
Assert.state(this.asyncSupported, "Async not supported");
637+
this.asyncStarted = true;
638+
this.asyncContext = this.asyncContext == null ? new ProxyAsyncContext(request, response) : this.asyncContext;
639+
return this.asyncContext;
637640
}
638641

639642
public void setAsyncStarted(boolean asyncStarted) {
@@ -647,6 +650,7 @@ public boolean isAsyncStarted() {
647650

648651
public void setAsyncSupported(boolean asyncSupported) {
649652
this.asyncSupported = asyncSupported;
653+
this.dispatcherType = DispatcherType.ASYNC;
650654
}
651655

652656
@Override
@@ -655,15 +659,16 @@ public boolean isAsyncSupported() {
655659
}
656660

657661
public void setAsyncContext(@Nullable AsyncContext asyncContext) {
658-
throw new UnsupportedOperationException();
662+
this.asyncContext = asyncContext;
659663
}
660664

661665
@Override
662666
@Nullable
663667
public AsyncContext getAsyncContext() {
664-
return null;
668+
return this.asyncContext;
665669
}
666670

671+
667672
public void setDispatcherType(DispatcherType dispatcherType) {
668673
this.dispatcherType = dispatcherType;
669674
}
@@ -692,7 +697,7 @@ public Cookie[] getCookies() {
692697
@Override
693698
@Nullable
694699
public String getHeader(String name) {
695-
return this.headers.containsKey(name) ? this.headers.get(name).toString() : null;
700+
return this.headers.containsKey(name) ? this.headers.get(name).get(0) : null;
696701
}
697702

698703
@Override

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.util.Assert;
4242
import org.springframework.web.util.WebUtils;
4343

44+
4445
/**
4546
*
4647
* @author Oleg Zhurakousky
@@ -160,7 +161,6 @@ public int getBufferSize() {
160161

161162
@Override
162163
public void flushBuffer() {
163-
164164
}
165165

166166
@Override
@@ -248,7 +248,7 @@ public Collection<String> getHeaderNames() {
248248
@Override
249249
@Nullable
250250
public String getHeader(String name) {
251-
return this.headers.containsKey(name) ? this.headers.get(name).toString() : null;
251+
return this.headers.containsKey(name) ? this.headers.get(name).get(0) : null;
252252
}
253253

254254
/**

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

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,16 @@
4141
import org.apache.commons.logging.Log;
4242
import org.apache.commons.logging.LogFactory;
4343

44-
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext;
44+
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
45+
import org.springframework.context.support.GenericApplicationContext;
4546
import org.springframework.http.HttpStatus;
4647
import org.springframework.lang.Nullable;
4748
import org.springframework.util.Assert;
4849
import org.springframework.util.ObjectUtils;
4950
import org.springframework.util.StringUtils;
5051
import org.springframework.web.context.ConfigurableWebApplicationContext;
52+
import org.springframework.web.context.request.async.WebAsyncManager;
53+
import org.springframework.web.context.request.async.WebAsyncUtils;
5154
import org.springframework.web.servlet.DispatcherServlet;
5255

5356
/**
@@ -59,7 +62,7 @@
5962
* @author Oleg Zhurakousky
6063
*
6164
*/
62-
public class ProxyMvc {
65+
public final class ProxyMvc {
6366

6467
private static Log LOG = LogFactory.getLog(ProxyMvc.class);
6568

@@ -84,8 +87,7 @@ public static ProxyMvc INSTANCE(ConfigurableWebApplicationContext applpicationCo
8487
}
8588

8689
public static ProxyMvc INSTANCE(Class<?>... componentClasses) {
87-
AnnotationConfigServletWebApplicationContext applpicationContext = new AnnotationConfigServletWebApplicationContext();
88-
applpicationContext.scan(componentClasses[0].getPackageName());
90+
ConfigurableWebApplicationContext applpicationContext = ServerlessWebApplication.run(componentClasses, new String[] {});
8991
return INSTANCE(applpicationContext);
9092
}
9193

@@ -97,21 +99,23 @@ public static ProxyMvc INSTANCE(Class<?>... componentClasses) {
9799
this.applicationContext = applicationContext;
98100
ProxyServletContext servletContext = new ProxyServletContext();
99101
this.applicationContext.setServletContext(servletContext);
100-
this.dispatcher = new DispatcherServlet(this.applicationContext);
101-
this.dispatcher.setDetectAllHandlerMappings(false);
102+
this.applicationContext.refresh();
102103

103-
ServletRegistration.Dynamic reg = servletContext.addServlet("dispatcherServlet", dispatcher);
104+
if (this.applicationContext.containsBean(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) {
105+
this.dispatcher = this.applicationContext.getBean(DispatcherServlet.class);
106+
}
107+
else {
108+
this.dispatcher = new DispatcherServlet(this.applicationContext);
109+
this.dispatcher.setDetectAllHandlerMappings(false);
110+
((GenericApplicationContext) this.applicationContext).registerBean(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME,
111+
DispatcherServlet.class, () -> this.dispatcher);
112+
}
113+
114+
ServletRegistration.Dynamic reg = servletContext.addServlet(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME, dispatcher);
104115
reg.setLoadOnStartup(1);
105116
this.servletContext = applicationContext.getServletContext();
106117
try {
107-
108118
this.dispatcher.init(new ProxyServletConfig(this.servletContext));
109-
try {
110-
this.service(new ProxyHttpServletRequest(servletContext, "INFO", "/"), new ProxyHttpServletResponse());
111-
}
112-
catch (Exception e) {
113-
//ignore as this is just a pre-warming attempt
114-
}
115119
}
116120
catch (Exception e) {
117121
throw new IllegalStateException("Faild to create Spring MVC DispatcherServlet proxy", e);
@@ -137,10 +141,17 @@ public void service(HttpServletRequest request, HttpServletResponse response) th
137141
this.service(request, response, (CountDownLatch) null);
138142
}
139143

144+
140145
public void service(HttpServletRequest request, HttpServletResponse response, CountDownLatch latch) throws Exception {
141146
ProxyFilterChain filterChain = new ProxyFilterChain(this.dispatcher);
142147
filterChain.doFilter(request, response);
143148

149+
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
150+
if (asyncManager.isConcurrentHandlingStarted()) {
151+
this.dispatcher.service(request, response);
152+
}
153+
154+
144155
if (latch != null) {
145156
latch.countDown();
146157
}
@@ -170,7 +181,7 @@ private static class ProxyFilterChain implements FilterChain {
170181
ProxyFilterChain(DispatcherServlet servlet) {
171182
List<Filter> filters = new ArrayList<>();
172183
servlet.getServletContext().getFilterRegistrations().values().forEach(fr -> filters.add(((ProxyFilterRegistration) fr).getFilter()));
173-
servlet.getWebApplicationContext().getBeansOfType(Filter.class).values().forEach(f -> filters.add(f));
184+
//servlet.getWebApplicationContext().getBeansOfType(Filter.class).values().forEach(f -> filters.add(f));
174185
Assert.notNull(filters, "filters cannot be null");
175186
Assert.noNullElements(filters, "filters cannot contain null values");
176187
this.filters = initFilterList(servlet, filters.toArray(new Filter[] {}));
@@ -310,7 +321,7 @@ private static class ProxyServletConfig implements ServletConfig {
310321

311322
@Override
312323
public String getServletName() {
313-
return "spring-serverless-proxy";
324+
return DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME;
314325
}
315326

316327
@Override

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,9 @@ public FilterRegistration.Dynamic addFilter(String filterName, String className)
227227

228228
@Override
229229
public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) {
230-
throw new UnsupportedOperationException("This ServletContext does not represent a running web container");
230+
ProxyFilterRegistration registration = new ProxyFilterRegistration(filterName, filter);
231+
filterRegistrations.put(filterName, registration);
232+
return registration;
231233
}
232234

233235
Map<String, FilterRegistration> filterRegistrations = new HashMap<>();

0 commit comments

Comments
 (0)