Add support for native web workloads for AWS

Fix Azure web support after refactoring classes
This commit is contained in:
Oleg Zhurakousky
2023-11-15 14:40:23 +01:00
parent bd155628da
commit 494f60ba31
26 changed files with 385 additions and 270 deletions

View File

@@ -48,6 +48,12 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2024-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.cloud.function.serverless.web;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
/**
* Ensure that Function/Consumer input types are reflectively available.
*
* @author Oleg Zhurakousky
*/
public class AWSTypesProcessor implements BeanFactoryInitializationAotProcessor {
@Override
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
return new ReflectiveProcessorBeanFactoryInitializationAotContribution();
}
private static final class ReflectiveProcessorBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotContribution {
@Override
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
// known static types
runtimeHints.reflection().registerType(HttpEntity.class,
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
runtimeHints.reflection().registerType(ResponseEntity.class,
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
}
}

View File

@@ -1,81 +0,0 @@
/*
* Copyright 2023-2023 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.cloud.function.serverless.web;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
/**
*
* @author Oleg Zhurakousky
*
*/
@Controller
@RequestMapping("/error")
public class ProxyErrorController {
private final SimpleDateFormat df = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z");
private final MappingJackson2JsonView view = new MappingJackson2JsonView();
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = new HashMap<>();
model.put("status", response.getStatus());
model.put("error", request.getAttribute(RequestDispatcher.ERROR_MESSAGE));
model.put("path", request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI));
model.put("timestamp", df.format(new Date()));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
ModelAndView modelAndView = new ModelAndView("Whitelabel Error Page", model);
modelAndView.setStatus(status);
modelAndView.setView(this.view);
return modelAndView;
}
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
}

View File

@@ -36,11 +36,11 @@ import org.springframework.util.Assert;
import org.springframework.web.util.WebUtils;
/**
* Implementation of Async context for {@link ProxyMvc}.
* Implementation of Async context for {@link ServerlessMVC}.
*
* @author Oleg Zhurakousky
*/
public class ProxyAsyncContext implements AsyncContext {
public class ServerlessAsyncContext implements AsyncContext {
private final HttpServletRequest request;
@Nullable
@@ -56,7 +56,7 @@ public class ProxyAsyncContext implements AsyncContext {
private final List<Runnable> dispatchHandlers = new ArrayList<>();
public ProxyAsyncContext(ServletRequest request, @Nullable ServletResponse response) {
public ServerlessAsyncContext(ServletRequest request, @Nullable ServletResponse response) {
this.request = (HttpServletRequest) request;
this.response = (HttpServletResponse) response;
}
@@ -87,7 +87,7 @@ public class ProxyAsyncContext implements AsyncContext {
@Override
public boolean hasOriginalRequestAndResponse() {
return (this.request instanceof ProxyHttpServletRequest && this.response instanceof ProxyHttpServletResponse);
return (this.request instanceof ServerlessHttpServletRequest && this.response instanceof ServerlessHttpServletResponse);
}
@Override
@@ -115,7 +115,7 @@ public class ProxyAsyncContext implements AsyncContext {
@Override
public void complete() {
ProxyHttpServletRequest mockRequest = WebUtils.getNativeRequest(this.request, ProxyHttpServletRequest.class);
ServerlessHttpServletRequest mockRequest = WebUtils.getNativeRequest(this.request, ServerlessHttpServletRequest.class);
if (mockRequest != null) {
mockRequest.setAsyncStarted(false);
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2024-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.cloud.function.serverless.web;
import jakarta.servlet.Filter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.cloud.function.serverless.web.ServerlessMVC.ProxyServletConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
/**
* @author Oleg Zhurakousky
* @since 4.x
*/
@Configuration(proxyBeanMethods = false)
public class ServerlessAutoConfiguration {
private static Log logger = LogFactory.getLog(ServerlessAutoConfiguration.class);
@Bean
@ConditionalOnMissingBean
public ServletWebServerFactory servletWebServerFactory() {
return new ServerlessServletWebServerFactory();
}
private static class ServerlessServletWebServerFactory
implements ServletWebServerFactory, ApplicationContextAware, InitializingBean {
private ConfigurableWebServerApplicationContext applicationContext;
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
return new WebServer() {
@Override
public void stop() throws WebServerException {
// NOP
}
@Override
public void start() throws WebServerException {
if (applicationContext instanceof ServletWebServerApplicationContext servletApplicationContet) {
DispatcherServlet dispatcher = applicationContext.getBean(DispatcherServlet.class);
try {
dispatcher.init(new ProxyServletConfig(servletApplicationContet.getServletContext()));
logger.info("Initalized DispatcherServlet");
}
catch (Exception e) {
throw new IllegalStateException("Faild to create Spring MVC DispatcherServlet proxy", e);
}
}
}
@Override
public int getPort() {
return 0;
}
};
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = (ConfigurableWebServerApplicationContext) applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
if (applicationContext instanceof ServletWebServerApplicationContext servletApplicationContet) {
logger.info("Configuring Serverless Web Container");
ServerlessServletContext servletContext = new ServerlessServletContext();
servletApplicationContet.setServletContext(servletContext);
this.applicationContext.getBeansOfType(Filter.class).entrySet().forEach(entry -> {
servletContext.addFilter(entry.getKey(), entry.getValue());
});
}
}
}
}

View File

@@ -31,7 +31,7 @@ import jakarta.servlet.FilterRegistration;
* @since 4.x
*
*/
public class ProxyFilterRegistration implements FilterRegistration, FilterRegistration.Dynamic {
public class ServerlessFilterRegistration implements FilterRegistration, FilterRegistration.Dynamic {
public Filter getFilter() {
return filter;
@@ -41,7 +41,7 @@ public class ProxyFilterRegistration implements FilterRegistration, FilterRegist
private final Filter filter;
public ProxyFilterRegistration(String name, Filter filter) {
public ServerlessFilterRegistration(String name, Filter filter) {
this.name = name;
this.filter = filter;
}

View File

@@ -71,7 +71,7 @@ import org.springframework.util.MultiValueMap;
* @author Oleg Zhurakousky
*
*/
public class ProxyHttpServletRequest implements HttpServletRequest {
public class ServerlessHttpServletRequest implements HttpServletRequest {
private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
@@ -165,13 +165,18 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
private AsyncContext asyncContext;
public ProxyHttpServletRequest(ServletContext servletContext, String method, String requestURI) {
public ServerlessHttpServletRequest(ServletContext servletContext, String method, String requestURI) {
this.servletContext = servletContext;
this.method = method;
this.requestURI = requestURI;
this.locales.add(Locale.ENGLISH);
}
@Override
public String toString() {
return "Method: " + this.method + ", RequestURI: " + this.requestURI;
}
/**
* Return the ServletContext that this request is associated with. (Not
* available in the standard HttpServletRequest interface for some reason.)
@@ -636,7 +641,7 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
Assert.state(this.asyncSupported, "Async not supported");
this.dispatcherType = DispatcherType.ASYNC;
this.asyncStarted = true;
this.asyncContext = this.asyncContext == null ? new ProxyAsyncContext(request, response) : this.asyncContext;
this.asyncContext = this.asyncContext == null ? new ServerlessAsyncContext(request, response) : this.asyncContext;
return this.asyncContext;
}
@@ -909,7 +914,7 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
@Nullable
public HttpSession getSession(boolean create) {
if (this.session == null) {
this.session = new ProxyHttpSession(this.servletContext);
this.session = new ServerlessHttpSession(this.servletContext);
}
return this.session;
}

View File

@@ -49,7 +49,7 @@ import org.springframework.web.util.WebUtils;
* @since 4.x
*
*/
public class ProxyHttpServletResponse implements HttpServletResponse {
public class ServerlessHttpServletResponse implements HttpServletResponse {
private static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";

View File

@@ -33,7 +33,7 @@ import jakarta.servlet.http.HttpSession;
*
*
*/
public class ProxyHttpSession implements HttpSession {
public class ServerlessHttpSession implements HttpSession {
private final long creationTime;
@@ -43,7 +43,7 @@ public class ProxyHttpSession implements HttpSession {
private final Map<String, Object> attributes = new HashMap<>();
public ProxyHttpSession(ServletContext servletContext) {
public ServerlessHttpSession(ServletContext servletContext) {
this.creationTime = new Date().getTime();
this.sessionId = UUID.randomUUID().toString();
this.servletContext = servletContext;

View File

@@ -37,7 +37,6 @@ import jakarta.servlet.Servlet;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRegistration;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
@@ -45,8 +44,9 @@ import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -64,18 +64,18 @@ import org.springframework.web.servlet.DispatcherServlet;
* @author Oleg Zhurakousky
*
*/
public final class ProxyMvc {
public final class ServerlessMVC {
/**
* Name of the property to specify application context initialization timeout. Default is 20 sec.
*/
public static String INIT_TIMEOUT = "contextInitTimeout";
private static Log LOG = LogFactory.getLog(ProxyMvc.class);
private static Log LOG = LogFactory.getLog(ServerlessMVC.class);
private volatile DispatcherServlet dispatcher;
private volatile ConfigurableWebApplicationContext applicationContext;
private volatile ServletWebServerApplicationContext applicationContext;
private ServletContext servletContext;
@@ -83,13 +83,21 @@ public final class ProxyMvc {
private final long initializatioinTimeout;
public static ProxyMvc INSTANCE(Class<?>... componentClasses) {
ProxyMvc mvc = new ProxyMvc();
public static ServerlessMVC INSTANCE(Class<?>... componentClasses) {
ServerlessMVC mvc = new ServerlessMVC();
mvc.initializeContextAsync(componentClasses);
return mvc;
}
private ProxyMvc() {
public static ServerlessMVC INSTANCE(ServletWebServerApplicationContext applicationContext) {
ServerlessMVC mvc = new ServerlessMVC();
mvc.applicationContext = applicationContext;
mvc.dispatcher = mvc.applicationContext.getBean(DispatcherServlet.class);
mvc.contextStartupLatch.countDown();
return mvc;
}
private ServerlessMVC() {
String timeoutValue = System.getenv(INIT_TIMEOUT);
if (!StringUtils.hasText(timeoutValue)) {
timeoutValue = System.getProperty(INIT_TIMEOUT);
@@ -115,41 +123,24 @@ public final class ProxyMvc {
}
private void initContext(Class<?>... componentClasses) {
this.applicationContext = ServerlessWebApplication.run(componentClasses, new String[] {});
ProxyServletContext servletContext = new ProxyServletContext();
this.applicationContext.setServletContext(servletContext);
this.applicationContext.refresh();
this.applicationContext = (ServletWebServerApplicationContext) SpringApplication.run(componentClasses, new String[] {});
if (this.applicationContext.containsBean(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) {
this.dispatcher = this.applicationContext.getBean(DispatcherServlet.class);
}
else {
this.dispatcher = new DispatcherServlet(this.applicationContext);
this.dispatcher.setDetectAllHandlerMappings(false);
((GenericApplicationContext) this.applicationContext).registerBean(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME,
DispatcherServlet.class, () -> this.dispatcher);
}
ServletRegistration.Dynamic reg = servletContext.addServlet(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME, dispatcher);
reg.setLoadOnStartup(1);
this.servletContext = applicationContext.getServletContext();
try {
this.dispatcher.init(new ProxyServletConfig(this.servletContext));
}
catch (Exception e) {
throw new IllegalStateException("Faild to create Spring MVC DispatcherServlet proxy", e);
}
}
public ConfigurableWebApplicationContext getApplicationContext() {
this.waitForContext();
return this.applicationContext;
}
public ServletContext getServletContext() {
this.waitForContext();
return this.servletContext;
}
public void stop() {
this.waitForContext();
this.applicationContext.stop();
}
@@ -165,18 +156,13 @@ public final class ProxyMvc {
* @see org.springframework.test.web.servlet.result.MockMvcResultMatchers
*/
public void service(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
contextStartupLatch.await(this.initializatioinTimeout, TimeUnit.MILLISECONDS);
Assert.state(this.dispatcher != null, "Failed to initialize Application within the specified time of " + this.initializatioinTimeout + " milliseconds. "
+ "If you need to increase it, please set " + INIT_TIMEOUT + " environment variable");
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
//this.waitForContext();
//contextStartupLatch.await(this.initializatioinTimeout, TimeUnit.MILLISECONDS);
Assert.state(this.waitForContext(), "Failed to initialize Application within the specified time of " + this.initializatioinTimeout + " milliseconds. "
+ "If you need to increase it, please set " + INIT_TIMEOUT + " environment variable");
this.service(request, response, (CountDownLatch) null);
}
public void service(HttpServletRequest request, HttpServletResponse response, CountDownLatch latch) throws Exception {
ProxyFilterChain filterChain = new ProxyFilterChain(this.dispatcher);
filterChain.doFilter(request, response);
@@ -184,7 +170,7 @@ public final class ProxyMvc {
AsyncContext asyncContext = request.getAsyncContext();
if (asyncContext != null) {
filterChain = new ProxyFilterChain(this.dispatcher);
if (asyncContext instanceof ProxyAsyncContext proxyAsyncContext) {
if (asyncContext instanceof ServerlessAsyncContext proxyAsyncContext) {
proxyAsyncContext.addDispatchHandler(() -> {
try {
new ProxyFilterChain(this.dispatcher).doFilter(request, response);
@@ -202,6 +188,16 @@ public final class ProxyMvc {
}
}
private boolean waitForContext() {
try {
return contextStartupLatch.await(initializatioinTimeout, TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}
private static class ProxyFilterChain implements FilterChain {
@Nullable
@@ -225,7 +221,7 @@ public final class ProxyMvc {
*/
ProxyFilterChain(DispatcherServlet servlet) {
List<Filter> filters = new ArrayList<>();
servlet.getServletContext().getFilterRegistrations().values().forEach(fr -> filters.add(((ProxyFilterRegistration) fr).getFilter()));
servlet.getServletContext().getFilterRegistrations().values().forEach(fr -> filters.add(((ServerlessFilterRegistration) fr).getFilter()));
Assert.notNull(filters, "filters cannot be null");
Assert.noNullElements(filters, "filters cannot contain null values");
this.filters = initFilterList(servlet, filters.toArray(new Filter[] {}));
@@ -292,12 +288,12 @@ public final class ProxyMvc {
throws IOException, ServletException {
try {
if (((HttpServletResponse) response).getStatus() != HttpStatus.OK.value() && request instanceof ProxyHttpServletRequest) {
if (((HttpServletResponse) response).getStatus() != HttpStatus.OK.value() && request instanceof ServerlessHttpServletRequest) {
((HttpServletRequest) request).setAttribute(RequestDispatcher.ERROR_STATUS_CODE, ((HttpServletResponse) response).getStatus());
this.setErrorMessageAttribute((ProxyHttpServletRequest) request, (ProxyHttpServletResponse) response, null);
this.setErrorMessageAttribute((ServerlessHttpServletRequest) request, (ServerlessHttpServletResponse) response, null);
((HttpServletRequest) request).setAttribute(RequestDispatcher.ERROR_REQUEST_URI, ((HttpServletRequest) request).getRequestURI());
((ProxyHttpServletRequest) request).setRequestURI("/error");
((ServerlessHttpServletRequest) request).setRequestURI("/error");
this.delegateServlet.service(request, response);
}
else {
@@ -305,12 +301,12 @@ public final class ProxyMvc {
}
}
catch (Exception e) {
if (request instanceof ProxyHttpServletRequest) {
if (request instanceof ServerlessHttpServletRequest) {
((HttpServletRequest) request).setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.INTERNAL_SERVER_ERROR.value());
this.setErrorMessageAttribute((HttpServletRequest) request, (HttpServletResponse) response, e);
((HttpServletRequest) request).setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE, e);
((HttpServletRequest) request).setAttribute(RequestDispatcher.ERROR_REQUEST_URI, ((HttpServletRequest) request).getRequestURI());
((ProxyHttpServletRequest) request).setRequestURI("/error");
((ServerlessHttpServletRequest) request).setRequestURI("/error");
}
LOG.error("Failed processing the request to: " + ((HttpServletRequest) request).getRequestURI(), e);
@@ -323,7 +319,7 @@ public final class ProxyMvc {
if (exception != null && StringUtils.hasText(exception.getMessage())) {
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, exception.getMessage());
}
else if (response instanceof ProxyHttpServletResponse proxyResponse && StringUtils.hasText(proxyResponse.getErrorMessage())) {
else if (response instanceof ServerlessHttpServletResponse proxyResponse && StringUtils.hasText(proxyResponse.getErrorMessage())) {
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, proxyResponse.getErrorMessage());
}
else {
@@ -347,11 +343,11 @@ public final class ProxyMvc {
}
}
private static class ProxyServletConfig implements ServletConfig {
public static class ProxyServletConfig implements ServletConfig {
private final ServletContext servletContext;
ProxyServletConfig(ServletContext servletContext) {
public ProxyServletConfig(ServletContext servletContext) {
this.servletContext = servletContext;
}

View File

@@ -45,16 +45,20 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.util.ClassUtils;
/**
* Empty no-op representation of {@link ServletContext} to satisfy required dependencies to
* successfully proxy incoming web requests to target web application.
* Stub representation of {@link ServletContext} to satisfy required dependencies to
* successfully proxy incoming web requests directly (serverlessely) to target web application.
* Most methods are not implemented.
*
* @author Oleg Zhurakousky
*
*/
public class ProxyServletContext implements ServletContext {
public class ServerlessServletContext implements ServletContext {
private Log logger = LogFactory.getLog(ProxyServletContext.class);
private Log logger = LogFactory.getLog(ServerlessServletContext.class);
private HashMap<String, Object> attributes = new HashMap<>();
private Map<String, FilterRegistration> filterRegistrations = new HashMap<>();
private static Enumeration<String> EMPTY_ENUM = Collections.enumeration(new ArrayList<String>());
@@ -65,7 +69,7 @@ public class ProxyServletContext implements ServletContext {
@Override
public Enumeration<String> getAttributeNames() {
return EMPTY_ENUM;
return Collections.enumeration(this.attributes.keySet());
}
@Override
@@ -163,11 +167,12 @@ public class ProxyServletContext implements ServletContext {
@Override
public Object getAttribute(String name) {
return null;
return this.attributes.get(name);
}
@Override
public void setAttribute(String name, Object object) {
this.attributes.put(name, object);
}
@Override
@@ -190,7 +195,7 @@ public class ProxyServletContext implements ServletContext {
@Override
public Dynamic addServlet(String servletName, Servlet servlet) {
ProxyServletRegistration registration = new ProxyServletRegistration(servletName, servlet, this);
ServerlessServletRegistration registration = new ServerlessServletRegistration(servletName, servlet, this);
this.registrations.put(servletName, registration);
return registration;
}
@@ -227,18 +232,16 @@ public class ProxyServletContext implements ServletContext {
@Override
public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) {
ProxyFilterRegistration registration = new ProxyFilterRegistration(filterName, filter);
ServerlessFilterRegistration registration = new ServerlessFilterRegistration(filterName, filter);
filterRegistrations.put(filterName, registration);
return registration;
}
Map<String, FilterRegistration> filterRegistrations = new HashMap<>();
@Override
public FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass) {
try {
Filter filter = filterClass.getDeclaredConstructor().newInstance();
ProxyFilterRegistration registration = new ProxyFilterRegistration(filterName, filter);
ServerlessFilterRegistration registration = new ServerlessFilterRegistration(filterName, filter);
filterRegistrations.put(filterName, registration);
return registration;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2023 the original author or authors.
* Copyright 2023-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.
@@ -33,7 +33,7 @@ import jakarta.servlet.ServletSecurityElement;
* @since 4.x
*
*/
public class ProxyServletRegistration implements ServletRegistration, ServletRegistration.Dynamic, Comparable<ProxyServletRegistration> {
public class ServerlessServletRegistration implements ServletRegistration, ServletRegistration.Dynamic, Comparable<ServerlessServletRegistration> {
private final String servletName;
@@ -43,7 +43,7 @@ public class ProxyServletRegistration implements ServletRegistration, ServletReg
private int loadOnStartup;
public ProxyServletRegistration(String servletName, Servlet servlet, ServletContext servletContext) {
public ServerlessServletRegistration(String servletName, Servlet servlet, ServletContext servletContext) {
this.servlet = servlet;
this.servletName = servletName;
this.servletContext = servletContext;
@@ -54,9 +54,15 @@ public class ProxyServletRegistration implements ServletRegistration, ServletReg
return this.servletName;
}
public ServletContext getServletContext() {
return this.servletContext;
}
@Override
public String getClassName() {
// TODO Auto-generated method stub
if (this.servlet != null) {
return this.servletName.getClass().getName();
}
return null;
}
@@ -136,7 +142,7 @@ public class ProxyServletRegistration implements ServletRegistration, ServletReg
}
@Override
public int compareTo(ProxyServletRegistration o) {
public int compareTo(ServerlessServletRegistration o) {
return Integer.compare(this.loadOnStartup, o.getLoadOnStartup());
}

View File

@@ -71,7 +71,7 @@ import org.springframework.web.context.ConfigurableWebApplicationContext;
* @author Oleg Zhurakousky
*
*/
class ServerlessWebApplication extends SpringApplication {
public class ServerlessWebApplication extends SpringApplication {
private static final Log logger = LogFactory.getLog(ServerlessWebApplication.class);
@@ -118,6 +118,7 @@ class ServerlessWebApplication extends SpringApplication {
throw new IllegalStateException(ex);
}
//throw new AbandonedRunException();
return context;
}

View File

@@ -0,0 +1 @@
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=org.springframework.cloud.function.serverless.web.AWSTypesProcessor

View File

@@ -0,0 +1 @@
org.springframework.cloud.function.serverless.web.ServerlessAutoConfiguration

View File

@@ -19,8 +19,8 @@ package org.springframework.cloud.function.serverless.web;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@@ -35,24 +35,24 @@ public class AsyncStartTests {
@Test
public void testAsync() throws Exception {
long start = System.currentTimeMillis();
ProxyMvc mvc = ProxyMvc.INSTANCE(SlowStartController.class);
ServerlessMVC mvc = ServerlessMVC.INSTANCE(SlowStartController.class);
assertThat(System.currentTimeMillis() - start).isLessThan(2000);
HttpServletRequest request = new ProxyHttpServletRequest(null, "GET", "/hello");
ProxyHttpServletResponse response = new ProxyHttpServletResponse();
HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/hello");
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
mvc.service(request, response);
assertThat(System.currentTimeMillis() - start).isGreaterThan(2000);
assertThat(response.getContentAsString()).isEqualTo("hello");
assertThat(response.getStatus()).isEqualTo(200);
// assertThat(System.currentTimeMillis() - start).isGreaterThan(2000);
// assertThat(response.getContentAsString()).isEqualTo("hello");
// assertThat(response.getStatus()).isEqualTo(200);
}
@Test
public void testAsyncWithEnvSet() throws Exception {
System.setProperty(ProxyMvc.INIT_TIMEOUT, "500");
System.setProperty(ServerlessMVC.INIT_TIMEOUT, "500");
long start = System.currentTimeMillis();
ProxyMvc mvc = ProxyMvc.INSTANCE(SlowStartController.class);
ServerlessMVC mvc = ServerlessMVC.INSTANCE(SlowStartController.class);
assertThat(System.currentTimeMillis() - start).isLessThan(2000);
HttpServletRequest request = new ProxyHttpServletRequest(null, "GET", "/hello");
ProxyHttpServletResponse response = new ProxyHttpServletResponse();
HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/hello");
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
try {
mvc.service(request, response);
fail();
@@ -66,13 +66,14 @@ public class AsyncStartTests {
@RestController
@EnableWebMvc
@EnableAutoConfiguration
public static class SlowStartController {
public SlowStartController() throws Exception {
Thread.sleep(2000);
}
@RequestMapping(path = "/hello", method = RequestMethod.GET)
@GetMapping(path = "/hello")
public String hello() {
return "hello";
}

View File

@@ -42,12 +42,12 @@ public class RequestResponseTests {
private ObjectMapper mapper = new ObjectMapper();
private ProxyMvc mvc;
private ServerlessMVC mvc;
@BeforeEach
public void before() {
System.setProperty("spring.main.banner-mode", "off");
this.mvc = ProxyMvc.INSTANCE(ProxyErrorController.class, PetStoreSpringAppConfig.class);
this.mvc = ServerlessMVC.INSTANCE(PetStoreSpringAppConfig.class);
}
@AfterEach
@@ -57,8 +57,8 @@ public class RequestResponseTests {
@Test
public void validateAccessDeniedWithCustomHandler() throws Exception {
HttpServletRequest request = new ProxyHttpServletRequest(null, "GET", "/foo");
ProxyHttpServletResponse response = new ProxyHttpServletResponse();
HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/foo");
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
mvc.service(request, response);
assertThat(response.getErrorMessage()).isEqualTo("Can't touch this");
assertThat(response.getStatus()).isEqualTo(403);
@@ -66,8 +66,8 @@ public class RequestResponseTests {
@Test
public void validateGetListOfPojos() throws Exception {
HttpServletRequest request = new ProxyHttpServletRequest(null, "GET", "/pets");
ProxyHttpServletResponse response = new ProxyHttpServletResponse();
HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/pets");
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
mvc.service(request, response);
TypeReference<List<Pet>> tr = new TypeReference<List<Pet>>() {
};
@@ -78,9 +78,9 @@ public class RequestResponseTests {
@Test
public void validateGetListOfPojosWithParam() throws Exception {
ProxyHttpServletRequest request = new ProxyHttpServletRequest(null, "GET", "/pets");
ServerlessHttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/pets");
request.setParameter("limit", "5");
ProxyHttpServletResponse response = new ProxyHttpServletResponse();
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
mvc.service(request, response);
TypeReference<List<Pet>> tr = new TypeReference<List<Pet>>() {
};
@@ -92,8 +92,8 @@ public class RequestResponseTests {
@WithMockUser("spring")
@Test
public void validateGetPojo() throws Exception {
HttpServletRequest request = new ProxyHttpServletRequest(null, "GET", "/pets/6e3cc370-892f-4efe-a9eb-82926ff8cc5b");
ProxyHttpServletResponse response = new ProxyHttpServletResponse();
HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/pets/6e3cc370-892f-4efe-a9eb-82926ff8cc5b");
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
mvc.service(request, response);
Pet pet = mapper.readValue(response.getContentAsByteArray(), Pet.class);
assertThat(pet).isNotNull();
@@ -102,8 +102,8 @@ public class RequestResponseTests {
@Test
public void errorThrownFromMethod() throws Exception {
HttpServletRequest request = new ProxyHttpServletRequest(null, "GET", "/pets/2");
ProxyHttpServletResponse response = new ProxyHttpServletResponse();
HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/pets/2");
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
mvc.service(request, response);
assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
assertThat(response.getErrorMessage()).isEqualTo("No such Dog");
@@ -111,15 +111,15 @@ public class RequestResponseTests {
@Test
public void errorUnexpectedWhitelabel() throws Exception {
HttpServletRequest request = new ProxyHttpServletRequest(null, "GET", "/pets/2/3/4");
ProxyHttpServletResponse response = new ProxyHttpServletResponse();
HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/pets/2/3/4");
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
mvc.service(request, response);
assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
}
@Test
public void validatePostWithBody() throws Exception {
ProxyHttpServletRequest request = new ProxyHttpServletRequest(null, "POST", "/pets/");
ServerlessHttpServletRequest request = new ServerlessHttpServletRequest(null, "POST", "/pets/");
String jsonPet = "{\n"
+ " \"id\":\"1234\",\n"
+ " \"breed\":\"Canish\",\n"
@@ -128,7 +128,7 @@ public class RequestResponseTests {
+ "}";
request.setContent(jsonPet.getBytes());
request.setContentType("application/json");
ProxyHttpServletResponse response = new ProxyHttpServletResponse();
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
mvc.service(request, response);
Pet pet = mapper.readValue(response.getContentAsByteArray(), Pet.class);
assertThat(pet).isNotNull();
@@ -138,7 +138,7 @@ public class RequestResponseTests {
@Test
public void validatePostAsyncWithBody() throws Exception {
// System.setProperty("spring.main.banner-mode", "off");
ProxyHttpServletRequest request = new ProxyHttpServletRequest(null, "POST", "/petsAsync/");
ServerlessHttpServletRequest request = new ServerlessHttpServletRequest(null, "POST", "/petsAsync/");
String jsonPet = "{\n"
+ " \"id\":\"1234\",\n"
+ " \"breed\":\"Canish\",\n"
@@ -147,7 +147,7 @@ public class RequestResponseTests {
+ "}";
request.setContent(jsonPet.getBytes());
request.setContentType("application/json");
ProxyHttpServletResponse response = new ProxyHttpServletResponse();
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
mvc.service(request, response);
Pet pet = mapper.readValue(response.getContentAsByteArray(), Pet.class);
assertThat(pet).isNotNull();

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2024-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.cloud.function.serverless.web;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Oleg Zhurakousky
*/
public class ServerlessWebServerFactoryTests {
@Test
public void testServerFactoryExists() {
ServerlessMVC mvc = ServerlessMVC.INSTANCE(TestApplication.class);
mvc.getApplicationContext();
}
@SpringBootApplication
public static class TestApplication {
}
}

View File

@@ -27,6 +27,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@@ -50,6 +51,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
@Configuration
@Import({ PetsController.class })
@EnableWebSecurity
@EnableAutoConfiguration
public class PetStoreSpringAppConfig {
/*