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:
@@ -23,8 +23,7 @@ dependencies {
|
||||
compile "org.springframework:spring-web:4.2.0.RELEASE"
|
||||
compile "org.reactivestreams:reactive-streams:1.0.0"
|
||||
compile "io.projectreactor:reactor-stream:2.0.5.RELEASE"
|
||||
compile "org.slf4j:slf4j-api:1.7.6"
|
||||
compile "ch.qos.logback:logback-classic:1.1.2"
|
||||
compile "commons-logging:commons-logging:1.2"
|
||||
|
||||
optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT"
|
||||
optional "io.reactivex:rxjava-reactive-streams:1.0.1"
|
||||
@@ -40,7 +39,10 @@ dependencies {
|
||||
testCompile 'org.eclipse.jetty:jetty-server:9.3.2.v20150730'
|
||||
testCompile 'org.eclipse.jetty:jetty-servlet:9.3.2.v20150730'
|
||||
|
||||
testCompile "org.slf4j:slf4j-jcl:1.7.12"
|
||||
testCompile "org.slf4j:jul-to-slf4j:1.7.12"
|
||||
testCompile("log4j:log4j:1.2.16")
|
||||
|
||||
testCompile("org.mockito:mockito-core:1.10.19") {
|
||||
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
log4j.rootCategory=INFO, stdout
|
||||
log4j.logger.org.springframework.rx=DEBUG
|
||||
log4j.rootCategory=WARN, stdout
|
||||
|
||||
log4j.logger.org.springframework.reactive=DEBUG
|
||||
log4j.logger.org.springframework.web=DEBUG
|
||||
|
||||
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
|
||||
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.stdout.layout.ConversionPattern=%d %p [%25.25c{1}] <%t> - %m%n
|
||||
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] <%t> - %m%n
|
||||
@@ -26,7 +26,10 @@ import org.reactivestreams.Publisher;
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
|
||||
@@ -19,7 +19,9 @@ package org.springframework.reactive.util;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.reactivex.netty.protocol.http.server.HttpServer;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.rx.Streams;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.reactive.web.http.ServerHttpRequest;
|
||||
import org.springframework.reactive.web.http.ServerHttpResponse;
|
||||
import org.springframework.reactive.web.http.rxnetty.RequestHandlerAdapter;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class DispatcherApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
StaticWebApplicationContext wac = new StaticWebApplicationContext();
|
||||
wac.registerSingleton("handlerMapping", SimpleUrlHandlerMapping.class);
|
||||
wac.registerSingleton("handlerAdapter", PlainTextHandlerAdapter.class);
|
||||
wac.registerSingleton("resultHandler", PlainTextResultHandler.class);
|
||||
wac.refresh();
|
||||
|
||||
SimpleUrlHandlerMapping handlerMapping = wac.getBean(SimpleUrlHandlerMapping.class);
|
||||
handlerMapping.addHandler("/text", new HelloWorldTextHandler());
|
||||
|
||||
DispatcherHandler dispatcherHandler = new DispatcherHandler();
|
||||
dispatcherHandler.initStrategies(wac);
|
||||
|
||||
RequestHandlerAdapter requestHandler = new RequestHandlerAdapter(dispatcherHandler);
|
||||
HttpServer<ByteBuf, ByteBuf> server = HttpServer.newServer(8080);
|
||||
server.start(requestHandler::handle);
|
||||
server.awaitShutdown();
|
||||
}
|
||||
|
||||
|
||||
private static class SimpleUrlHandlerMapping implements HandlerMapping {
|
||||
|
||||
private final Map<String, Object> handlerMap = new HashMap<>();
|
||||
|
||||
|
||||
public void addHandler(String path, Object handler) {
|
||||
this.handlerMap.put(path, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getHandler(ServerHttpRequest request) {
|
||||
return this.handlerMap.get(request.getURI().getPath());
|
||||
}
|
||||
}
|
||||
|
||||
private interface PlainTextHandler {
|
||||
|
||||
Publisher<String> handle(ServerHttpRequest request, ServerHttpResponse response);
|
||||
|
||||
}
|
||||
|
||||
private static class HelloWorldTextHandler implements PlainTextHandler {
|
||||
|
||||
@Override
|
||||
public Publisher<String> handle(ServerHttpRequest request, ServerHttpResponse response) {
|
||||
return Streams.just("Hello world.");
|
||||
}
|
||||
}
|
||||
|
||||
private static class PlainTextHandlerAdapter implements HandlerAdapter {
|
||||
|
||||
@Override
|
||||
public boolean supports(Object handler) {
|
||||
return PlainTextHandler.class.isAssignableFrom(handler.getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<HandlerResult> handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) {
|
||||
Publisher<String> publisher = ((PlainTextHandler) handler).handle(request, response);
|
||||
return Streams.wrap(publisher).map(HandlerResult::new);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PlainTextResultHandler implements HandlerResultHandler {
|
||||
|
||||
@Override
|
||||
public boolean supports(HandlerResult result) {
|
||||
Object value = result.getReturnValue();
|
||||
return (value != null && String.class.equals(value.getClass()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<Void> handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) {
|
||||
response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
|
||||
byte[] bytes = ((String) result.getReturnValue()).getBytes(Charset.forName("UTF-8"));
|
||||
return response.writeWith(Streams.just(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.reactive.web.dispatch;
|
||||
package org.springframework.reactive.web.dispatch.handler;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
@@ -26,8 +26,7 @@ import reactor.rx.Streams;
|
||||
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.reactive.web.dispatch.handler.HttpHandlerAdapter;
|
||||
import org.springframework.reactive.web.dispatch.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.reactive.web.dispatch.DispatcherHandler;
|
||||
import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests;
|
||||
import org.springframework.reactive.web.http.HttpHandler;
|
||||
import org.springframework.reactive.web.http.ServerHttpRequest;
|
||||
@@ -43,7 +42,7 @@ import static org.junit.Assert.assertArrayEquals;
|
||||
*/
|
||||
public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests {
|
||||
|
||||
private static final Charset CHARSET = Charset.forName("UTF-8");
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
|
||||
@Override
|
||||
@@ -55,7 +54,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler
|
||||
wac.refresh();
|
||||
|
||||
DispatcherHandler dispatcherHandler = new DispatcherHandler();
|
||||
dispatcherHandler.initStrategies(wac);
|
||||
dispatcherHandler.setApplicationContext(wac);
|
||||
return dispatcherHandler;
|
||||
}
|
||||
|
||||
@@ -68,7 +67,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler
|
||||
RequestEntity<Void> request = RequestEntity.get(url).build();
|
||||
ResponseEntity<byte[]> response = restTemplate.exchange(request, byte[].class);
|
||||
|
||||
assertArrayEquals("foo".getBytes(CHARSET), response.getBody());
|
||||
assertArrayEquals("foo".getBytes(UTF_8), response.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -80,7 +79,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler
|
||||
RequestEntity<Void> request = RequestEntity.get(url).build();
|
||||
ResponseEntity<byte[]> response = restTemplate.exchange(request, byte[].class);
|
||||
|
||||
assertArrayEquals("bar".getBytes(CHARSET), response.getBody());
|
||||
assertArrayEquals("bar".getBytes(UTF_8), response.getBody());
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +97,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler
|
||||
|
||||
@Override
|
||||
public Publisher<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
|
||||
return response.writeWith(Streams.just("foo".getBytes(CHARSET)));
|
||||
return response.writeWith(Streams.just("foo".getBytes(UTF_8)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +105,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler
|
||||
|
||||
@Override
|
||||
public Publisher<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
|
||||
return response.writeWith(Streams.just("bar".getBytes(CHARSET)));
|
||||
return response.writeWith(Streams.just("bar".getBytes(UTF_8)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.rx.Streams;
|
||||
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.reactive.web.dispatch.DispatcherHandler;
|
||||
import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests;
|
||||
import org.springframework.reactive.web.http.HttpHandler;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests {
|
||||
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
|
||||
@Override
|
||||
protected HttpHandler createHttpHandler() {
|
||||
|
||||
StaticWebApplicationContext wac = new StaticWebApplicationContext();
|
||||
wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class);
|
||||
wac.registerSingleton("handlerAdapter", RequestMappingHandlerAdapter.class);
|
||||
wac.registerSingleton("responseBodyResultHandler", ResponseBodyResultHandler.class);
|
||||
wac.registerSingleton("controller", TestController.class);
|
||||
wac.refresh();
|
||||
|
||||
DispatcherHandler dispatcherHandler = new DispatcherHandler();
|
||||
dispatcherHandler.setApplicationContext(wac);
|
||||
return dispatcherHandler;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void helloWithQueryParam() throws Exception {
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
URI url = new URI("http://localhost:" + port + "/param?name=George");
|
||||
RequestEntity<Void> request = RequestEntity.get(url).build();
|
||||
ResponseEntity<byte[]> response = restTemplate.exchange(request, byte[].class);
|
||||
|
||||
assertArrayEquals("Hello George!".getBytes(UTF_8), response.getBody());
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
@SuppressWarnings("unused")
|
||||
private static class TestController {
|
||||
|
||||
@RequestMapping("/param")
|
||||
@ResponseBody
|
||||
public Publisher<String> handleWithParam(@RequestParam String name) {
|
||||
return Streams.just("Hello ", name, "!");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user