From e638fef508cda935bfbac64d966d564592e4d6de Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 6 Mar 2019 16:00:29 +0100 Subject: [PATCH] Add Servlet.fn integration with DispatcherServlet This commit adds support for HandlerFunctions and RouterFunctions to DispatcherServlet, in the form of a HandlerAdapter and HandlerMapping. See gh-21490 --- .../WebMvcConfigurationSupport.java | 35 +++- .../support/HandlerFunctionAdapter.java | 108 +++++++++++ .../support/RouterFunctionMapping.java | 183 ++++++++++++++++++ .../function/support/package-info.java | 27 +++ .../web/servlet/DispatcherServlet.properties | 7 +- 5 files changed, 357 insertions(+), 3 deletions(-) create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/package-info.java diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 190384c442..ec220dbf2e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -77,6 +77,8 @@ import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.function.support.HandlerFunctionAdapter; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor; @@ -474,6 +476,27 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv return mapping; } + /** + * Return a {@link RouterFunctionMapping} ordered at 3 to map + * {@linkplain org.springframework.web.servlet.function.RouterFunction router functions}. + * Consider overriding one of these other more fine-grained methods: + * + * @since 5.2 + */ + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(3); + mapping.setInterceptors(getInterceptors()); + mapping.setCorsConfigurations(getCorsConfigurations()); + mapping.setMessageConverters(getMessageConverters()); + return mapping; + } + /** * Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped * resource handlers. To configure resource handling, override @@ -593,6 +616,16 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv return new RequestMappingHandlerAdapter(); } + /** + * Returns a {@link HandlerFunctionAdapter} for processing requests through + * {@linkplain org.springframework.web.servlet.function.HandlerFunction handler functions}. + * @since 5.2 + */ + @Bean + public HandlerFunctionAdapter handlerFunctionAdapter() { + return new HandlerFunctionAdapter(); + } + /** * Return the {@link ConfigurableWebBindingInitializer} to use for * initializing all {@link WebDataBinder} instances. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java new file mode 100644 index 0000000000..ad3358e165 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2019 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 + * + * http://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.web.servlet.function.support; + +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.Ordered; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.function.HandlerFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; + +/** + * {@code HandlerAdapter} implementation that supports {@link HandlerFunction}s. + * + * @author Arjen Poutsma + * @since 5.2 + */ +public class HandlerFunctionAdapter implements HandlerAdapter, Ordered { + + private int order = Ordered.LOWEST_PRECEDENCE; + + + /** + * Specify the order value for this HandlerAdapter bean. + *

The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered. + * @see org.springframework.core.Ordered#getOrder() + */ + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public boolean supports(Object handler) { + return handler instanceof HandlerFunction; + } + + @Nullable + @Override + public ModelAndView handle(HttpServletRequest servletRequest, + HttpServletResponse servletResponse, + Object handler) throws Exception { + + + HandlerFunction handlerFunction = (HandlerFunction) handler; + + ServerRequest serverRequest = getServerRequest(servletRequest); + ServerResponse serverResponse = handlerFunction.handle(serverRequest); + + return serverResponse.writeTo(servletRequest, servletResponse, + new ServerRequestContext(serverRequest)); + } + + private ServerRequest getServerRequest(HttpServletRequest servletRequest) { + ServerRequest serverRequest = + (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); + Assert.state(serverRequest != null, () -> "Required attribute '" + + RouterFunctions.REQUEST_ATTRIBUTE + "' is missing"); + return serverRequest; + } + + @Override + public long getLastModified(HttpServletRequest request, Object handler) { + return -1L; + } + + + private static class ServerRequestContext implements ServerResponse.Context { + + private final ServerRequest serverRequest; + + + public ServerRequestContext(ServerRequest serverRequest) { + this.serverRequest = serverRequest; + } + + @Override + public List> messageConverters() { + return this.serverRequest.messageConverters(); + } + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java new file mode 100644 index 0000000000..791368dd9c --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2019 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 + * + * http://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.web.servlet.function.support; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; + +import org.jetbrains.annotations.NotNull; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; +import org.springframework.http.converter.xml.SourceHttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.handler.AbstractHandlerMapping; + +/** + * {@code HandlerMapping} implementation that supports {@link RouterFunction}s. + *

If no {@link RouterFunction} is provided at + * {@linkplain #RouterFunctionMapping(RouterFunction) construction time}, this mapping will detect + * all router functions in the application context, and consult them in + * {@linkplain org.springframework.core.annotation.Order order}. + * + * @author Arjen Poutsma + * @since 5.2 + */ +public class RouterFunctionMapping extends AbstractHandlerMapping implements InitializingBean { + + @Nullable + private RouterFunction routerFunction; + + private List> messageConverters = Collections.emptyList(); + + private boolean detectHandlerFunctionsInAncestorContexts = false; + + + + /** + * Create an empty {@code RouterFunctionMapping}. + *

If this constructor is used, this mapping will detect all {@link RouterFunction} instances + * available in the application context. + */ + public RouterFunctionMapping() { + } + + /** + * Create a {@code RouterFunctionMapping} with the given {@link RouterFunction}. + *

If this constructor is used, no application context detection will occur. + * @param routerFunction the router function to use for mapping + */ + public RouterFunctionMapping(RouterFunction routerFunction) { + this.routerFunction = routerFunction; + } + + /** + * Set the router function to map to. + *

If this property is used, no application context detection will occur. + */ + public void setRouterFunction(@Nullable RouterFunction routerFunction) { + this.routerFunction = routerFunction; + } + + /** + * Return the configured {@link RouterFunction}. + *

Note: When router functions are detected from the + * ApplicationContext, this method may return {@code null} if invoked + * prior to {@link #afterPropertiesSet()}. + * @return the router function or {@code null} + */ + @Nullable + public RouterFunction getRouterFunction() { + return this.routerFunction; + } + + public void setMessageConverters(List> messageConverters) { + this.messageConverters = messageConverters; + } + + /** + * Set whether to detect handler functions in ancestor ApplicationContexts. + *

Default is "false": Only handler functions in the current ApplicationContext + * will be detected, i.e. only in the context that this HandlerMapping itself + * is defined in (typically the current DispatcherServlet's context). + *

Switch this flag on to detect handler beans in ancestor contexts + * (typically the Spring root WebApplicationContext) as well. + */ + public void setDetectHandlerFunctionsInAncestorContexts(boolean detectHandlerFunctionsInAncestorContexts) { + this.detectHandlerFunctionsInAncestorContexts = detectHandlerFunctionsInAncestorContexts; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (this.routerFunction == null) { + initRouterFunction(); + } + if (CollectionUtils.isEmpty(this.messageConverters)) { + initMessageConverters(); + } + } + + /** + * Detect a all {@linkplain RouterFunction router functions} in the + * current application context. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private void initRouterFunction() { + ApplicationContext applicationContext = obtainApplicationContext(); + Map beans = + (this.detectHandlerFunctionsInAncestorContexts ? + BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, RouterFunction.class) : + applicationContext.getBeansOfType(RouterFunction.class)); + + List routerFunctions = new ArrayList<>(beans.values()); + if (!CollectionUtils.isEmpty(routerFunctions) && logger.isInfoEnabled()) { + routerFunctions.forEach(routerFunction -> logger.info("Mapped " + routerFunction)); + } + this.routerFunction = routerFunctions.stream() + .reduce(RouterFunction::andOther) + .orElse(null); + } + + /** + * Initializes a default set of {@linkplain HttpMessageConverter message converters}. + */ + private void initMessageConverters() { + List> messageConverters = new ArrayList<>(4); + + messageConverters.add(new ByteArrayHttpMessageConverter()); + + StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); + stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316 + messageConverters.add(stringHttpMessageConverter); + + try { + messageConverters.add(new SourceHttpMessageConverter<>()); + } + catch (Error err) { + // Ignore when no TransformerFactory implementation is available + } + messageConverters.add(new AllEncompassingFormHttpMessageConverter()); + + this.messageConverters = messageConverters; + } + + @Nullable + @Override + protected Object getHandlerInternal(@NotNull HttpServletRequest servletRequest) throws Exception { + if (this.routerFunction != null) { + ServerRequest request = ServerRequest.create(servletRequest, this.messageConverters); + servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE, request); + return this.routerFunction.route(request).orElse(null); + } + else { + return null; + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/package-info.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/package-info.java new file mode 100644 index 0000000000..b5c58ed0ef --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2019 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 + * + * http://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. + */ + +/** + * Classes supporting the {@code org.springframework.web.servlet.function} package. + * Contains a {@code HandlerAdapter} that supports {@code HandlerFunction}s, + * and a {@code HandlerMapping} that supports {@code RouterFunction}s. + */ +@NonNullApi +@NonNullFields +package org.springframework.web.servlet.function.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties b/spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties index 52920de27d..c55b61864e 100644 --- a/spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties +++ b/spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties @@ -7,11 +7,14 @@ org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ - org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping + org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\ + org.springframework.web.servlet.function.support.RouterFunctionMapping org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ - org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter + org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\ + org.springframework.web.servlet.function.support.HandlerFunctionAdapter + org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\ org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\ -- GitLab