001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.mvc.method.annotation; 018 019import java.io.IOException; 020import java.lang.annotation.Annotation; 021import java.lang.reflect.Method; 022import java.lang.reflect.Type; 023import java.util.concurrent.Callable; 024 025import javax.servlet.http.HttpServletRequest; 026import javax.servlet.http.HttpServletResponse; 027 028import org.springframework.core.MethodParameter; 029import org.springframework.core.ResolvableType; 030import org.springframework.http.HttpHeaders; 031import org.springframework.http.HttpStatus; 032import org.springframework.lang.Nullable; 033import org.springframework.util.Assert; 034import org.springframework.util.ClassUtils; 035import org.springframework.util.StringUtils; 036import org.springframework.web.bind.annotation.ResponseBody; 037import org.springframework.web.bind.annotation.ResponseStatus; 038import org.springframework.web.context.request.ServletWebRequest; 039import org.springframework.web.method.HandlerMethod; 040import org.springframework.web.method.support.HandlerMethodReturnValueHandler; 041import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite; 042import org.springframework.web.method.support.InvocableHandlerMethod; 043import org.springframework.web.method.support.ModelAndViewContainer; 044import org.springframework.web.servlet.View; 045import org.springframework.web.util.NestedServletException; 046 047/** 048 * Extends {@link InvocableHandlerMethod} with the ability to handle return 049 * values through a registered {@link HandlerMethodReturnValueHandler} and 050 * also supports setting the response status based on a method-level 051 * {@code @ResponseStatus} annotation. 052 * 053 * <p>A {@code null} return value (including void) may be interpreted as the 054 * end of request processing in combination with a {@code @ResponseStatus} 055 * annotation, a not-modified check condition 056 * (see {@link ServletWebRequest#checkNotModified(long)}), or 057 * a method argument that provides access to the response stream. 058 * 059 * @author Rossen Stoyanchev 060 * @author Juergen Hoeller 061 * @since 3.1 062 */ 063public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { 064 065 private static final Method CALLABLE_METHOD = ClassUtils.getMethod(Callable.class, "call"); 066 067 @Nullable 068 private HandlerMethodReturnValueHandlerComposite returnValueHandlers; 069 070 071 /** 072 * Creates an instance from the given handler and method. 073 */ 074 public ServletInvocableHandlerMethod(Object handler, Method method) { 075 super(handler, method); 076 } 077 078 /** 079 * Create an instance from a {@code HandlerMethod}. 080 */ 081 public ServletInvocableHandlerMethod(HandlerMethod handlerMethod) { 082 super(handlerMethod); 083 } 084 085 086 /** 087 * Register {@link HandlerMethodReturnValueHandler} instances to use to 088 * handle return values. 089 */ 090 public void setHandlerMethodReturnValueHandlers(HandlerMethodReturnValueHandlerComposite returnValueHandlers) { 091 this.returnValueHandlers = returnValueHandlers; 092 } 093 094 095 /** 096 * Invoke the method and handle the return value through one of the 097 * configured {@link HandlerMethodReturnValueHandler HandlerMethodReturnValueHandlers}. 098 * @param webRequest the current request 099 * @param mavContainer the ModelAndViewContainer for this request 100 * @param providedArgs "given" arguments matched by type (not resolved) 101 */ 102 public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, 103 Object... providedArgs) throws Exception { 104 105 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); 106 setResponseStatus(webRequest); 107 108 if (returnValue == null) { 109 if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { 110 disableContentCachingIfNecessary(webRequest); 111 mavContainer.setRequestHandled(true); 112 return; 113 } 114 } 115 else if (StringUtils.hasText(getResponseStatusReason())) { 116 mavContainer.setRequestHandled(true); 117 return; 118 } 119 120 mavContainer.setRequestHandled(false); 121 Assert.state(this.returnValueHandlers != null, "No return value handlers"); 122 try { 123 this.returnValueHandlers.handleReturnValue( 124 returnValue, getReturnValueType(returnValue), mavContainer, webRequest); 125 } 126 catch (Exception ex) { 127 if (logger.isTraceEnabled()) { 128 logger.trace(formatErrorForReturnValue(returnValue), ex); 129 } 130 throw ex; 131 } 132 } 133 134 /** 135 * Set the response status according to the {@link ResponseStatus} annotation. 136 */ 137 private void setResponseStatus(ServletWebRequest webRequest) throws IOException { 138 HttpStatus status = getResponseStatus(); 139 if (status == null) { 140 return; 141 } 142 143 HttpServletResponse response = webRequest.getResponse(); 144 if (response != null) { 145 String reason = getResponseStatusReason(); 146 if (StringUtils.hasText(reason)) { 147 response.sendError(status.value(), reason); 148 } 149 else { 150 response.setStatus(status.value()); 151 } 152 } 153 154 // To be picked up by RedirectView 155 webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, status); 156 } 157 158 /** 159 * Does the given request qualify as "not modified"? 160 * @see ServletWebRequest#checkNotModified(long) 161 * @see ServletWebRequest#checkNotModified(String) 162 */ 163 private boolean isRequestNotModified(ServletWebRequest webRequest) { 164 return webRequest.isNotModified(); 165 } 166 167 private void disableContentCachingIfNecessary(ServletWebRequest webRequest) { 168 if (isRequestNotModified(webRequest)) { 169 HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); 170 Assert.notNull(response, "Expected HttpServletResponse"); 171 if (StringUtils.hasText(response.getHeader(HttpHeaders.ETAG))) { 172 HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); 173 Assert.notNull(request, "Expected HttpServletRequest"); 174 } 175 } 176 } 177 178 private String formatErrorForReturnValue(@Nullable Object returnValue) { 179 return "Error handling return value=[" + returnValue + "]" + 180 (returnValue != null ? ", type=" + returnValue.getClass().getName() : "") + 181 " in " + toString(); 182 } 183 184 /** 185 * Create a nested ServletInvocableHandlerMethod subclass that returns the 186 * the given value (or raises an Exception if the value is one) rather than 187 * actually invoking the controller method. This is useful when processing 188 * async return values (e.g. Callable, DeferredResult, ListenableFuture). 189 */ 190 ServletInvocableHandlerMethod wrapConcurrentResult(Object result) { 191 return new ConcurrentResultHandlerMethod(result, new ConcurrentResultMethodParameter(result)); 192 } 193 194 195 /** 196 * A nested subclass of {@code ServletInvocableHandlerMethod} that uses a 197 * simple {@link Callable} instead of the original controller as the handler in 198 * order to return the fixed (concurrent) result value given to it. Effectively 199 * "resumes" processing with the asynchronously produced return value. 200 */ 201 private class ConcurrentResultHandlerMethod extends ServletInvocableHandlerMethod { 202 203 private final MethodParameter returnType; 204 205 public ConcurrentResultHandlerMethod(final Object result, ConcurrentResultMethodParameter returnType) { 206 super((Callable<Object>) () -> { 207 if (result instanceof Exception) { 208 throw (Exception) result; 209 } 210 else if (result instanceof Throwable) { 211 throw new NestedServletException("Async processing failed", (Throwable) result); 212 } 213 return result; 214 }, CALLABLE_METHOD); 215 216 if (ServletInvocableHandlerMethod.this.returnValueHandlers != null) { 217 setHandlerMethodReturnValueHandlers(ServletInvocableHandlerMethod.this.returnValueHandlers); 218 } 219 this.returnType = returnType; 220 } 221 222 /** 223 * Bridge to actual controller type-level annotations. 224 */ 225 @Override 226 public Class<?> getBeanType() { 227 return ServletInvocableHandlerMethod.this.getBeanType(); 228 } 229 230 /** 231 * Bridge to actual return value or generic type within the declared 232 * async return type, e.g. Foo instead of {@code DeferredResult<Foo>}. 233 */ 234 @Override 235 public MethodParameter getReturnValueType(@Nullable Object returnValue) { 236 return this.returnType; 237 } 238 239 /** 240 * Bridge to controller method-level annotations. 241 */ 242 @Override 243 public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) { 244 return ServletInvocableHandlerMethod.this.getMethodAnnotation(annotationType); 245 } 246 247 /** 248 * Bridge to controller method-level annotations. 249 */ 250 @Override 251 public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) { 252 return ServletInvocableHandlerMethod.this.hasMethodAnnotation(annotationType); 253 } 254 } 255 256 257 /** 258 * MethodParameter subclass based on the actual return value type or if 259 * that's null falling back on the generic type within the declared async 260 * return type, e.g. Foo instead of {@code DeferredResult<Foo>}. 261 */ 262 private class ConcurrentResultMethodParameter extends HandlerMethodParameter { 263 264 @Nullable 265 private final Object returnValue; 266 267 private final ResolvableType returnType; 268 269 public ConcurrentResultMethodParameter(Object returnValue) { 270 super(-1); 271 this.returnValue = returnValue; 272 this.returnType = (returnValue instanceof ReactiveTypeHandler.CollectedValuesList ? 273 ((ReactiveTypeHandler.CollectedValuesList) returnValue).getReturnType() : 274 ResolvableType.forType(super.getGenericParameterType()).getGeneric()); 275 } 276 277 public ConcurrentResultMethodParameter(ConcurrentResultMethodParameter original) { 278 super(original); 279 this.returnValue = original.returnValue; 280 this.returnType = original.returnType; 281 } 282 283 @Override 284 public Class<?> getParameterType() { 285 if (this.returnValue != null) { 286 return this.returnValue.getClass(); 287 } 288 if (!ResolvableType.NONE.equals(this.returnType)) { 289 return this.returnType.toClass(); 290 } 291 return super.getParameterType(); 292 } 293 294 @Override 295 public Type getGenericParameterType() { 296 return this.returnType.getType(); 297 } 298 299 @Override 300 public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationType) { 301 // Ensure @ResponseBody-style handling for values collected from a reactive type 302 // even if actual return type is ResponseEntity<Flux<T>> 303 return (super.hasMethodAnnotation(annotationType) || 304 (annotationType == ResponseBody.class && 305 this.returnValue instanceof ReactiveTypeHandler.CollectedValuesList)); 306 } 307 308 @Override 309 public ConcurrentResultMethodParameter clone() { 310 return new ConcurrentResultMethodParameter(this); 311 } 312 } 313 314}