Add basic support for @RequestMapping

Just enough for a test with an @ResponseBody method that accepts an
@RequestParam String arg and returning Publisher<String> or String.

See RequestMappingIntegrationTests.
This commit is contained in:
Rossen Stoyanchev
2015-08-25 12:35:43 -04:00
parent 9df3dd4495
commit 202825554c
17 changed files with 703 additions and 150 deletions
@@ -17,12 +17,18 @@ package org.springframework.reactive.web.dispatch;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.rx.Streams;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.HttpStatus;
import org.springframework.reactive.web.http.HttpHandler;
import org.springframework.reactive.web.http.ServerHttpRequest;
@@ -31,7 +37,10 @@ import org.springframework.reactive.web.http.ServerHttpResponse;
/**
* @author Rossen Stoyanchev
*/
public class DispatcherHandler implements HttpHandler {
public class DispatcherHandler implements HttpHandler, ApplicationContextAware {
private static final Log logger = LogFactory.getLog(DispatcherHandler.class);
private List<HandlerMapping> handlerMappings;
@@ -40,22 +49,40 @@ public class DispatcherHandler implements HttpHandler {
private List<HandlerResultHandler> resultHandlers;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
initStrategies(applicationContext);
}
protected void initStrategies(ApplicationContext context) {
this.handlerMappings = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors(
context, HandlerMapping.class, true, false).values());
Map<String, HandlerMapping> mappingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
this.handlerAdapters = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors(
context, HandlerAdapter.class, true, false).values());
this.handlerMappings = new ArrayList<>(mappingBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerMappings);
this.resultHandlers = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors(
context, HandlerResultHandler.class, true, false).values());
Map<String, HandlerAdapter> adapterBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
this.handlerAdapters = new ArrayList<>(adapterBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerAdapters);
Map<String, HandlerResultHandler> beans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerResultHandler.class, true, false);
this.resultHandlers = new ArrayList<>(beans.values());
AnnotationAwareOrderComparator.sort(this.resultHandlers);
}
@Override
public Publisher<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
if (logger.isDebugEnabled()) {
logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]");
}
Object handler = getHandler(request);
if (handler == null) {
// No exception handling mechanism yet
@@ -73,7 +100,7 @@ public class DispatcherHandler implements HttpHandler {
}
}
return Streams.fail(new IllegalStateException(
"No HandlerResultHandler for " + result.getReturnValue()));
"No HandlerResultHandler for " + result.getValue()));
});
}
@@ -15,21 +15,29 @@
*/
package org.springframework.reactive.web.dispatch;
/**
* @author Rossen Stoyanchev
*/
public class HandlerResult {
private final Object returnValue;
private final Object handler;
private final Object value;
public HandlerResult(Object returnValue) {
this.returnValue = returnValue;
public HandlerResult(Object handler, Object value) {
this.handler = handler;
this.value = value;
}
public Object getReturnValue() {
return this.returnValue;
public Object getHandler() {
return this.handler;
}
public Object getValue() {
return this.value;
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2002-2015 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.reactive.web.dispatch.method;
import org.springframework.core.MethodParameter;
import org.springframework.reactive.web.http.ServerHttpRequest;
/**
* @author Rossen Stoyanchev
*/
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
Object resolveArgument(MethodParameter parameter, ServerHttpRequest request);
}
@@ -0,0 +1,184 @@
/*
* Copyright 2002-2015 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.reactive.web.dispatch.method;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.reactive.web.http.ServerHttpRequest;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.method.HandlerMethod;
/**
* 90% overlap with the existing one in spring-web except for the different
* HandlerMethodArgumentResolver contract.
*
* @author Rossen Stoyanchev
*/
public class InvocableHandlerMethod extends HandlerMethod {
private List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
public InvocableHandlerMethod(HandlerMethod handlerMethod) {
super(handlerMethod);
}
public void setHandlerMethodArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
this.argumentResolvers.clear();
this.argumentResolvers.addAll(resolvers);
}
public Object invokeForRequest(ServerHttpRequest request, Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Invoking [" + getBeanType().getSimpleName() + "." +
getMethod().getName() + "] method with arguments " + Arrays.asList(args));
}
Object returnValue = doInvoke(args);
if (logger.isTraceEnabled()) {
logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");
}
return returnValue;
}
private Object[] getMethodArgumentValues(ServerHttpRequest request, Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
args[i] = resolveProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
try {
args[i] = resolver.resolveArgument(parameter, request);
break;
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);
}
throw ex;
}
}
}
if (args[i] == null) {
String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);
throw new IllegalStateException(msg);
}
}
return args;
}
private String getArgumentResolutionErrorMessage(String message, int index) {
MethodParameter param = getMethodParameters()[index];
message += " [" + index + "] [type=" + param.getParameterType().getName() + "]";
return getDetailedErrorMessage(message);
}
protected String getDetailedErrorMessage(String message) {
return message + "\n" + "HandlerMethod details: \n" +
"Controller [" + getBeanType().getName() + "]\n" +
"Method [" + getBridgedMethod().toGenericString() + "]\n";
}
private Object resolveProvidedArgument(MethodParameter parameter, Object... providedArgs) {
if (providedArgs == null) {
return null;
}
for (Object providedArg : providedArgs) {
if (parameter.getParameterType().isInstance(providedArg)) {
return providedArg;
}
}
return null;
}
protected Object doInvoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(getBridgedMethod());
try {
return getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
throw new IllegalStateException(getInvocationErrorMessage(ex.getMessage(), args), ex);
}
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
else if (targetException instanceof Error) {
throw (Error) targetException;
}
else if (targetException instanceof Exception) {
throw (Exception) targetException;
}
else {
String msg = getInvocationErrorMessage("Failed to invoke controller method", args);
throw new IllegalStateException(msg, targetException);
}
}
}
private void assertTargetBean(Method method, Object targetBean, Object[] args) {
Class<?> methodDeclaringClass = method.getDeclaringClass();
Class<?> targetBeanClass = targetBean.getClass();
if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
String msg = "The mapped controller method class '" + methodDeclaringClass.getName() +
"' is not an instance of the actual controller bean instance '" +
targetBeanClass.getName() + "'. If the controller requires proxying " +
"(e.g. due to @Transactional), please use class-based proxying.";
throw new IllegalStateException(getInvocationErrorMessage(msg, args));
}
}
private String getInvocationErrorMessage(String message, Object[] resolvedArgs) {
StringBuilder sb = new StringBuilder(getDetailedErrorMessage(message));
sb.append("Resolved arguments: \n");
for (int i=0; i < resolvedArgs.length; i++) {
sb.append("[").append(i).append("] ");
if (resolvedArgs[i] == null) {
sb.append("[null] \n");
}
else {
sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] ");
sb.append("[value=").append(resolvedArgs[i]).append("]\n");
}
}
return sb.toString();
}
}
@@ -0,0 +1,79 @@
/*
* Copyright 2002-2015 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.reactive.web.dispatch.method.annotation;
import java.util.ArrayList;
import java.util.List;
import org.reactivestreams.Publisher;
import reactor.rx.Streams;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.reactive.web.dispatch.HandlerAdapter;
import org.springframework.reactive.web.dispatch.HandlerResult;
import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver;
import org.springframework.reactive.web.dispatch.method.InvocableHandlerMethod;
import org.springframework.reactive.web.http.ServerHttpRequest;
import org.springframework.reactive.web.http.ServerHttpResponse;
import org.springframework.web.method.HandlerMethod;
/**
* @author Rossen Stoyanchev
*/
public class RequestMappingHandlerAdapter implements HandlerAdapter, InitializingBean {
private List<HandlerMethodArgumentResolver> argumentResolvers;
public void setHandlerMethodArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
this.argumentResolvers.clear();
this.argumentResolvers.addAll(resolvers);
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.argumentResolvers == null) {
this.argumentResolvers = new ArrayList<>();
this.argumentResolvers.add(new RequestParamArgumentResolver());
}
}
@Override
public boolean supports(Object handler) {
return HandlerMethod.class.equals(handler.getClass());
}
@Override
public Publisher<HandlerResult> handle(ServerHttpRequest request, ServerHttpResponse response,
Object handler) {
final InvocableHandlerMethod invocable = new InvocableHandlerMethod((HandlerMethod) handler);
invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers);
Object result;
try {
result = invocable.invokeForRequest(request);
}
catch (Exception ex) {
return Streams.fail(ex);
}
return Streams.just(new HandlerResult(invocable, result));
}
}
@@ -0,0 +1,91 @@
/*
* Copyright 2002-2015 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.reactive.web.dispatch.method.annotation;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.reactive.web.dispatch.HandlerMapping;
import org.springframework.reactive.web.http.ServerHttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.HandlerMethodSelector;
/**
* @author Rossen Stoyanchev
*/
public class RequestMappingHandlerMapping implements HandlerMapping,
ApplicationContextAware, InitializingBean {
private static final Log logger = LogFactory.getLog(RequestMappingHandlerMapping.class);
private final Map<String, HandlerMethod> methodMap = new LinkedHashMap<>();
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
for (Object bean : this.applicationContext.getBeansOfType(Object.class).values()) {
detectHandlerMethods(bean);
}
}
protected void detectHandlerMethods(final Object bean) {
final Class<?> beanType = bean.getClass();
if (AnnotationUtils.findAnnotation(beanType, Controller.class) != null) {
HandlerMethodSelector.selectMethods(beanType, method -> {
RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
if (annotation != null && annotation.value().length > 0) {
String path = annotation.value()[0];
HandlerMethod handlerMethod = new HandlerMethod(bean, method);
if (logger.isInfoEnabled()) {
logger.info("Mapped \"" + path + "\" onto " + handlerMethod);
}
methodMap.put(path, handlerMethod);
}
return false;
});
}
}
@Override
public Object getHandler(ServerHttpRequest request) {
String path = request.getURI().getPath();
HandlerMethod handlerMethod = this.methodMap.get(path);
if (logger.isDebugEnabled()) {
logger.debug("Mapped " + path + " to [" + handlerMethod + "]");
}
return handlerMethod;
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2002-2015 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.reactive.web.dispatch.method.annotation;
import org.springframework.core.MethodParameter;
import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver;
import org.springframework.reactive.web.http.ServerHttpRequest;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Support {@code @RequestParam} but for query params only.
*
* @author Rossen Stoyanchev
*/
public class RequestParamArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestParam.class);
}
@Override
public Object resolveArgument(MethodParameter param, ServerHttpRequest request) {
RequestParam annotation = param.getParameterAnnotation(RequestParam.class);
String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName());
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build();
return uriComponents.getQueryParams().getFirst(name);
}
}
@@ -0,0 +1,99 @@
/*
* Copyright 2002-2015 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.reactive.web.dispatch.method.annotation;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import org.reactivestreams.Publisher;
import reactor.rx.Streams;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.MediaType;
import org.springframework.reactive.web.dispatch.HandlerResult;
import org.springframework.reactive.web.dispatch.HandlerResultHandler;
import org.springframework.reactive.web.http.ServerHttpRequest;
import org.springframework.reactive.web.http.ServerHttpResponse;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.HandlerMethod;
/**
* For now a simple {@code String} or {@code Publisher<String>} to
* "text/plain;charset=UTF-8" conversion.
*
* @author Rossen Stoyanchev
*/
public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private int order = Ordered.LOWEST_PRECEDENCE;
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
@Override
public boolean supports(HandlerResult result) {
Object handler = result.getHandler();
if (handler instanceof HandlerMethod) {
Method method = ((HandlerMethod) handler).getMethod();
return AnnotatedElementUtils.isAnnotated(method, ResponseBody.class.getName());
}
return false;
}
@Override
public Publisher<Void> handleResult(ServerHttpRequest request, ServerHttpResponse response,
HandlerResult result) {
Object value = result.getValue();
HandlerMethod handlerMethod = (HandlerMethod) result.getHandler();
MethodParameter returnType = handlerMethod.getReturnValueType(value);
if (value == null) {
return Streams.empty();
}
if (value instanceof String) {
response.getHeaders().setContentType(new MediaType("text", "plain", UTF_8));
return response.writeWith(Streams.just(((String) value).getBytes(UTF_8)));
}
else if (value instanceof Publisher) {
Class<?> type = ResolvableType.forMethodParameter(returnType).resolveGeneric(0);
if (String.class.equals(type)) {
@SuppressWarnings("unchecked")
Publisher<String> content = (Publisher<String>) value;
return response.writeWith(Streams.wrap(content).map(value1 -> value1.getBytes(UTF_8)));
}
}
return Streams.fail(new IllegalStateException("Return value type not supported: " + returnType));
}
}
@@ -65,7 +65,9 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse {
private void writeHeaders() {
if (!this.headersWritten) {
for (String name : this.headers.keySet()) {
this.response.setHeader(name, this.headers.get(name));
for (String value : this.headers.get(name)) {
this.response.addHeader(name, value);
}
}
}
}
@@ -29,6 +29,7 @@ import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.http.HttpStatus;
import org.springframework.reactive.web.http.HttpHandler;
/**
@@ -66,7 +67,7 @@ public class HttpHandlerServlet extends HttpServlet {
response.getOutputStream().setWriteListener(responseSubscriber);
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response, responseSubscriber);
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(contextSynchronizer);
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(contextSynchronizer, httpResponse);
this.handler.handle(httpRequest, httpResponse).subscribe(resultSubscriber);
}
@@ -75,9 +76,12 @@ public class HttpHandlerServlet extends HttpServlet {
private final AsyncContextSynchronizer synchronizer;
private final ServletServerHttpResponse response;
public HandlerResultSubscriber(AsyncContextSynchronizer synchronizer) {
public HandlerResultSubscriber(AsyncContextSynchronizer synchronizer, ServletServerHttpResponse response) {
this.synchronizer = synchronizer;
this.response = response;
}
@@ -94,6 +98,7 @@ public class HttpHandlerServlet extends HttpServlet {
@Override
public void onError(Throwable ex) {
logger.error("Error from request handling. Completing the request.", ex);
this.response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
this.synchronizer.complete();
}