Move response status processing in InvocableHandlerMethod
Prior to this commit, WebFlux would look at the handler method annotations (`@ResponseStatus`) for each handler execution, even calling the expensive `synthesizeAnnotation`. This commit moves this logic to the InvocableHandlerMethod so that this executed once at instantiation time and for all result handlers. Issue: SPR-15227
This commit is contained in:
		
							parent
							
								
									ab50f7b0d5
								
							
						
					
					
						commit
						6f029392c7
					
				| 
						 | 
				
			
			@ -24,13 +24,10 @@ import java.util.Optional;
 | 
			
		|||
import java.util.Set;
 | 
			
		||||
import java.util.function.Supplier;
 | 
			
		||||
 | 
			
		||||
import org.springframework.core.MethodParameter;
 | 
			
		||||
import org.springframework.core.Ordered;
 | 
			
		||||
import org.springframework.core.ReactiveAdapterRegistry;
 | 
			
		||||
import org.springframework.core.annotation.AnnotationUtils;
 | 
			
		||||
import org.springframework.http.MediaType;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
import org.springframework.web.bind.annotation.ResponseStatus;
 | 
			
		||||
import org.springframework.web.reactive.HandlerMapping;
 | 
			
		||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
 | 
			
		||||
import org.springframework.web.server.ServerWebExchange;
 | 
			
		||||
| 
						 | 
				
			
			@ -158,17 +155,4 @@ public abstract class AbstractHandlerResultHandler implements Ordered {
 | 
			
		|||
		return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Optionally set the response status using the information provided by {@code @ResponseStatus}.
 | 
			
		||||
	 * @param methodParameter the controller method return parameter
 | 
			
		||||
	 * @param exchange the server exchange being handled
 | 
			
		||||
	 */
 | 
			
		||||
	protected void updateResponseStatus(MethodParameter methodParameter, ServerWebExchange exchange) {
 | 
			
		||||
		ResponseStatus annotation = methodParameter.getMethodAnnotation(ResponseStatus.class);
 | 
			
		||||
		if (annotation != null) {
 | 
			
		||||
			annotation = AnnotationUtils.synthesizeAnnotation(annotation, methodParameter.getMethod());
 | 
			
		||||
			exchange.getResponse().setStatusCode(annotation.code());
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,9 +32,12 @@ import org.springframework.core.DefaultParameterNameDiscoverer;
 | 
			
		|||
import org.springframework.core.GenericTypeResolver;
 | 
			
		||||
import org.springframework.core.MethodParameter;
 | 
			
		||||
import org.springframework.core.ParameterNameDiscoverer;
 | 
			
		||||
import org.springframework.core.annotation.AnnotatedElementUtils;
 | 
			
		||||
import org.springframework.http.HttpStatus;
 | 
			
		||||
import org.springframework.util.ClassUtils;
 | 
			
		||||
import org.springframework.util.ObjectUtils;
 | 
			
		||||
import org.springframework.util.ReflectionUtils;
 | 
			
		||||
import org.springframework.web.bind.annotation.ResponseStatus;
 | 
			
		||||
import org.springframework.web.method.HandlerMethod;
 | 
			
		||||
import org.springframework.web.reactive.BindingContext;
 | 
			
		||||
import org.springframework.web.reactive.HandlerResult;
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +57,8 @@ public class InvocableHandlerMethod extends HandlerMethod {
 | 
			
		|||
 | 
			
		||||
	private static final Object NO_ARG_VALUE = new Object();
 | 
			
		||||
 | 
			
		||||
	private HttpStatus responseStatus;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	private List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -62,12 +67,23 @@ public class InvocableHandlerMethod extends HandlerMethod {
 | 
			
		|||
 | 
			
		||||
	public InvocableHandlerMethod(HandlerMethod handlerMethod) {
 | 
			
		||||
		super(handlerMethod);
 | 
			
		||||
		initResponseStatus();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public InvocableHandlerMethod(Object bean, Method method) {
 | 
			
		||||
		super(bean, method);
 | 
			
		||||
		initResponseStatus();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void initResponseStatus() {
 | 
			
		||||
		ResponseStatus annotation = getMethodAnnotation(ResponseStatus.class);
 | 
			
		||||
		if (annotation == null) {
 | 
			
		||||
			annotation = AnnotatedElementUtils.findMergedAnnotation(getBeanType(), ResponseStatus.class);
 | 
			
		||||
		}
 | 
			
		||||
		if (annotation != null) {
 | 
			
		||||
			this.responseStatus = annotation.code();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Configure the argument resolvers to use to use for resolving method
 | 
			
		||||
| 
						 | 
				
			
			@ -102,6 +118,9 @@ public class InvocableHandlerMethod extends HandlerMethod {
 | 
			
		|||
			try {
 | 
			
		||||
				Object value = doInvoke(args);
 | 
			
		||||
				HandlerResult result = new HandlerResult(this, value, getReturnType(), bindingContext);
 | 
			
		||||
				if (this.responseStatus != null) {
 | 
			
		||||
					exchange.getResponse().setStatusCode(this.responseStatus);
 | 
			
		||||
				}
 | 
			
		||||
				return Mono.just(result);
 | 
			
		||||
			}
 | 
			
		||||
			catch (InvocationTargetException ex) {
 | 
			
		||||
| 
						 | 
				
			
			@ -165,7 +184,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
 | 
			
		|||
			return resolver.resolveArgument(parameter, bindingContext, exchange)
 | 
			
		||||
					.defaultIfEmpty(NO_ARG_VALUE)
 | 
			
		||||
					.doOnError(cause -> {
 | 
			
		||||
						if(logger.isDebugEnabled()) {
 | 
			
		||||
						if (logger.isDebugEnabled()) {
 | 
			
		||||
							logger.debug(getDetailedErrorMessage("Failed to resolve", parameter), cause);
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -111,8 +111,7 @@ public abstract class AbstractMessageWriterResultHandler extends AbstractHandler
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		if (void.class == elementType.getRawClass() || Void.class == elementType.getRawClass()) {
 | 
			
		||||
			return Mono.from((Publisher<Void>) publisher)
 | 
			
		||||
					.doOnSubscribe(sub -> updateResponseStatus(bodyParameter, exchange));
 | 
			
		||||
			return Mono.from((Publisher<Void>) publisher);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ServerHttpRequest request = exchange.getRequest();
 | 
			
		||||
| 
						 | 
				
			
			@ -121,12 +120,11 @@ public abstract class AbstractMessageWriterResultHandler extends AbstractHandler
 | 
			
		|||
		if (bestMediaType != null) {
 | 
			
		||||
			for (HttpMessageWriter<?> messageWriter : getMessageWriters()) {
 | 
			
		||||
				if (messageWriter.canWrite(elementType, bestMediaType)) {
 | 
			
		||||
					Mono<Void> bodyWriter = (messageWriter instanceof ServerHttpMessageWriter ?
 | 
			
		||||
					return (messageWriter instanceof ServerHttpMessageWriter ?
 | 
			
		||||
							((ServerHttpMessageWriter<?>) messageWriter).write((Publisher) publisher,
 | 
			
		||||
									valueType, elementType, bestMediaType, request, response, Collections.emptyMap()) :
 | 
			
		||||
							messageWriter.write((Publisher) publisher, elementType,
 | 
			
		||||
									bestMediaType, response, Collections.emptyMap()));
 | 
			
		||||
					return bodyWriter.doOnSubscribe(sub -> updateResponseStatus(bodyParameter, exchange));
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -208,8 +208,6 @@ public class ViewResolutionResultHandler extends AbstractHandlerResultHandler
 | 
			
		|||
				.otherwiseIfEmpty(exchange.isNotModified() ? Mono.empty() : NO_VALUE_MONO)
 | 
			
		||||
				.then(returnValue -> {
 | 
			
		||||
 | 
			
		||||
					updateResponseStatus(result.getReturnTypeSource(), exchange);
 | 
			
		||||
 | 
			
		||||
					Mono<List<View>> viewsMono;
 | 
			
		||||
					Model model = result.getModel();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,8 +24,10 @@ import org.junit.Test;
 | 
			
		|||
import reactor.core.publisher.Mono;
 | 
			
		||||
import reactor.test.StepVerifier;
 | 
			
		||||
 | 
			
		||||
import org.springframework.http.HttpStatus;
 | 
			
		||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
 | 
			
		||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
 | 
			
		||||
import org.springframework.web.bind.annotation.ResponseStatus;
 | 
			
		||||
import org.springframework.web.reactive.BindingContext;
 | 
			
		||||
import org.springframework.web.reactive.HandlerResult;
 | 
			
		||||
import org.springframework.web.reactive.result.ResolvableMethod;
 | 
			
		||||
| 
						 | 
				
			
			@ -147,6 +149,15 @@ public class InvocableHandlerMethodTests {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void invokeMethodWithResponseStatus() throws Exception {
 | 
			
		||||
		InvocableHandlerMethod hm = handlerMethod("responseStatus");
 | 
			
		||||
		Mono<HandlerResult> mono = hm.invoke(this.exchange, new BindingContext());
 | 
			
		||||
 | 
			
		||||
		assertHandlerResultValue(mono, "created");
 | 
			
		||||
		assertThat(this.exchange.getResponse().getStatusCode(), is(HttpStatus.CREATED));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	private InvocableHandlerMethod handlerMethod(String name) throws Exception {
 | 
			
		||||
		TestController controller = new TestController();
 | 
			
		||||
| 
						 | 
				
			
			@ -186,6 +197,11 @@ public class InvocableHandlerMethodTests {
 | 
			
		|||
		public void exceptionMethod() {
 | 
			
		||||
			throw new IllegalStateException("boo");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@ResponseStatus(HttpStatus.CREATED)
 | 
			
		||||
		public String responseStatus() {
 | 
			
		||||
			return "created";
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,13 +23,11 @@ import java.util.List;
 | 
			
		|||
import org.junit.Before;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import reactor.core.publisher.Mono;
 | 
			
		||||
import reactor.test.StepVerifier;
 | 
			
		||||
import rx.Completable;
 | 
			
		||||
import rx.Single;
 | 
			
		||||
 | 
			
		||||
import org.springframework.core.codec.ByteBufferEncoder;
 | 
			
		||||
import org.springframework.core.codec.CharSequenceEncoder;
 | 
			
		||||
import org.springframework.http.HttpStatus;
 | 
			
		||||
import org.springframework.http.ResponseEntity;
 | 
			
		||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
 | 
			
		||||
import org.springframework.http.codec.HttpMessageWriter;
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +40,6 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse
 | 
			
		|||
import org.springframework.stereotype.Controller;
 | 
			
		||||
import org.springframework.util.ObjectUtils;
 | 
			
		||||
import org.springframework.web.bind.annotation.ResponseBody;
 | 
			
		||||
import org.springframework.web.bind.annotation.ResponseStatus;
 | 
			
		||||
import org.springframework.web.bind.annotation.RestController;
 | 
			
		||||
import org.springframework.web.method.HandlerMethod;
 | 
			
		||||
import org.springframework.web.reactive.HandlerResult;
 | 
			
		||||
| 
						 | 
				
			
			@ -119,24 +116,6 @@ public class ResponseBodyResultHandlerTests {
 | 
			
		|||
		testSupports(controller, "handleToMonoResponseEntity", false);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void writeResponseStatus() throws NoSuchMethodException {
 | 
			
		||||
		Object controller = new TestRestController();
 | 
			
		||||
		HandlerMethod hm = handlerMethod(controller, "handleToString");
 | 
			
		||||
		HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType());
 | 
			
		||||
 | 
			
		||||
		initExchange();
 | 
			
		||||
		StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify();
 | 
			
		||||
		assertEquals(HttpStatus.NO_CONTENT, this.response.getStatusCode());
 | 
			
		||||
 | 
			
		||||
		hm = handlerMethod(controller, "handleToMonoVoid");
 | 
			
		||||
		handlerResult = new HandlerResult(hm, null, hm.getReturnType());
 | 
			
		||||
 | 
			
		||||
		initExchange();
 | 
			
		||||
		StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify();
 | 
			
		||||
		assertEquals(HttpStatus.CREATED, this.response.getStatusCode());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException {
 | 
			
		||||
		HandlerMethod hm = handlerMethod(controller, method);
 | 
			
		||||
		HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType());
 | 
			
		||||
| 
						 | 
				
			
			@ -157,10 +136,8 @@ public class ResponseBodyResultHandlerTests {
 | 
			
		|||
	@RestController @SuppressWarnings("unused")
 | 
			
		||||
	private static class TestRestController {
 | 
			
		||||
 | 
			
		||||
		@ResponseStatus(code = HttpStatus.CREATED)
 | 
			
		||||
		public Mono<Void> handleToMonoVoid() { return null;}
 | 
			
		||||
 | 
			
		||||
		@ResponseStatus(code = HttpStatus.NO_CONTENT)
 | 
			
		||||
		public String handleToString() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,7 +41,6 @@ import org.springframework.core.Ordered;
 | 
			
		|||
import org.springframework.core.ResolvableType;
 | 
			
		||||
import org.springframework.core.io.buffer.DataBuffer;
 | 
			
		||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
 | 
			
		||||
import org.springframework.http.HttpStatus;
 | 
			
		||||
import org.springframework.http.MediaType;
 | 
			
		||||
import org.springframework.http.server.reactive.ServerHttpResponse;
 | 
			
		||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +48,6 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse
 | 
			
		|||
import org.springframework.ui.ConcurrentModel;
 | 
			
		||||
import org.springframework.ui.Model;
 | 
			
		||||
import org.springframework.web.bind.annotation.ModelAttribute;
 | 
			
		||||
import org.springframework.web.bind.annotation.ResponseStatus;
 | 
			
		||||
import org.springframework.web.reactive.BindingContext;
 | 
			
		||||
import org.springframework.web.reactive.HandlerResult;
 | 
			
		||||
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
 | 
			
		||||
| 
						 | 
				
			
			@ -131,23 +129,19 @@ public class ViewResolutionResultHandlerTests {
 | 
			
		|||
 | 
			
		||||
		returnType = forClass(View.class);
 | 
			
		||||
		returnValue = new TestView("account");
 | 
			
		||||
		ServerWebExchange exchange = testHandle("/path", returnType, returnValue, "account: {id=123}");
 | 
			
		||||
		assertEquals(HttpStatus.NO_CONTENT, exchange.getResponse().getStatusCode());
 | 
			
		||||
		testHandle("/path", returnType, returnValue, "account: {id=123}");
 | 
			
		||||
 | 
			
		||||
		returnType = forClassWithGenerics(Mono.class, View.class);
 | 
			
		||||
		returnValue = Mono.just(new TestView("account"));
 | 
			
		||||
		exchange = testHandle("/path", returnType, returnValue, "account: {id=123}");
 | 
			
		||||
		assertEquals(HttpStatus.SEE_OTHER, exchange.getResponse().getStatusCode());
 | 
			
		||||
		testHandle("/path", returnType, returnValue, "account: {id=123}");
 | 
			
		||||
 | 
			
		||||
		returnType = forClass(String.class);
 | 
			
		||||
		returnValue = "account";
 | 
			
		||||
		exchange = testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
 | 
			
		||||
		assertEquals(HttpStatus.CREATED, exchange.getResponse().getStatusCode());
 | 
			
		||||
		testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
 | 
			
		||||
 | 
			
		||||
		returnType = forClassWithGenerics(Mono.class, String.class);
 | 
			
		||||
		returnValue = Mono.just("account");
 | 
			
		||||
		exchange = testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
 | 
			
		||||
		assertEquals(HttpStatus.PARTIAL_CONTENT, exchange.getResponse().getStatusCode());
 | 
			
		||||
		testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
 | 
			
		||||
 | 
			
		||||
		returnType = forClass(Model.class);
 | 
			
		||||
		returnValue = new ConcurrentModel().addAttribute("name", "Joe");
 | 
			
		||||
| 
						 | 
				
			
			@ -438,38 +432,61 @@ public class ViewResolutionResultHandlerTests {
 | 
			
		|||
	@SuppressWarnings("unused")
 | 
			
		||||
	private static class TestController {
 | 
			
		||||
 | 
			
		||||
		@ResponseStatus(code = HttpStatus.CREATED)
 | 
			
		||||
		String string() { return null; }
 | 
			
		||||
		String string() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@ResponseStatus(HttpStatus.NO_CONTENT)
 | 
			
		||||
		View view() { return null; }
 | 
			
		||||
		View view() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@ResponseStatus(HttpStatus.PARTIAL_CONTENT)
 | 
			
		||||
		Mono<String> monoString() { return null; }
 | 
			
		||||
		Mono<String> monoString() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@ResponseStatus(code = HttpStatus.SEE_OTHER)
 | 
			
		||||
		Mono<View> monoView() { return null; }
 | 
			
		||||
		Mono<View> monoView() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Mono<Void> monoVoid() { return null; }
 | 
			
		||||
		Mono<Void> monoVoid() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		void voidMethod() {}
 | 
			
		||||
		void voidMethod() {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Single<String> singleString() { return null; }
 | 
			
		||||
		Single<String> singleString() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Single<View> singleView() { return null; }
 | 
			
		||||
		Single<View> singleView() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Completable completable() { return null; }
 | 
			
		||||
		Completable completable() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Model model() { return null; }
 | 
			
		||||
		Model model() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Map map() { return null; }
 | 
			
		||||
		Map map() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		TestBean testBean() { return null; }
 | 
			
		||||
		TestBean testBean() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Integer integer() { return null; }
 | 
			
		||||
		Integer integer() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@ModelAttribute("num")
 | 
			
		||||
		Long longAttribute() { return null; }
 | 
			
		||||
		Long longAttribute() {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue