Support HTTP range requests in MVC Controllers

Prior to this commit, HTTP Range requests were only supported by the
ResourceHttpRequestHandler when serving static resources.

This commit improves the `HttpEntityMethodProcessor` and
the `RequestResponseBodyMethodProcessor`. They now extract
`ResourceRegion`s from the `Resource` instance returned by the
Controller and let the Resource-related message converters
handle the writing of the resource (including partial writes).

Controller methods can now handle Range requests for
return types that extend Resource or HttpEntity:

    @RequestMapping("/example/video.mp4")
    public Resource handler() { }

    @RequestMapping("/example/video.mp4")
    public HttpEntity<Resource> handler() { }

Issue: SPR-15789, SPR-13834
This commit is contained in:
Brian Clozel
2017-08-24 19:35:16 +02:00
parent d20b3cf86d
commit 582014e944
7 changed files with 118 additions and 17 deletions

View File

@@ -44,6 +44,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.ResourceRegionHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
@@ -579,6 +580,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
messageConverters.add(stringConverterDef);
messageConverters.add(createConverterDefinition(ResourceHttpMessageConverter.class, source));
messageConverters.add(createConverterDefinition(ResourceRegionHttpMessageConverter.class, source));
messageConverters.add(createConverterDefinition(SourceHttpMessageConverter.class, source));
messageConverters.add(createConverterDefinition(AllEncompassingFormHttpMessageConverter.class, source));

View File

@@ -41,6 +41,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.ResourceRegionHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
@@ -790,6 +791,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(stringConverter);
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new ResourceRegionHttpMessageConverter());
messageConverters.add(new SourceHttpMessageConverter<>());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());

View File

@@ -30,10 +30,15 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
@@ -75,6 +80,9 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application");
private static final Type RESOURCE_REGION_LIST_TYPE =
new ParameterizedTypeReference<List<ResourceRegion>>() { }.getType();
private static final UrlPathHelper decodingUrlPathHelper = new UrlPathHelper();
@@ -183,6 +191,24 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
valueType = getReturnValueType(outputValue, returnType);
declaredType = getGenericType(returnType);
}
if (isResourceType(value, returnType)) {
outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null) {
Resource resource = (Resource) value;
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
outputValue = HttpRange.toResourceRegions(httpRanges, resource);
valueType = outputValue.getClass();
declaredType = RESOURCE_REGION_LIST_TYPE;
}
catch (IllegalArgumentException ex) {
outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}
HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
@@ -266,6 +292,13 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
return (value != null ? value.getClass() : returnType.getParameterType());
}
/**
* Return whether the returned value or the declared return type extend {@link Resource}
*/
protected boolean isResourceType(@Nullable Object value, MethodParameter returnType) {
return Resource.class.isAssignableFrom(value != null ? value.getClass() : returnType.getParameterType());
}
/**
* Return the generic type of the {@code returnType} (or of the nested type
* if it is an {@link HttpEntity}).

View File

@@ -387,14 +387,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
if (httpRanges.size() == 1) {
ResourceRegion resourceRegion = httpRanges.get(0).toResourceRegion(resource);
this.resourceRegionHttpMessageConverter.write(resourceRegion, mediaType, outputMessage);
}
else {
this.resourceRegionHttpMessageConverter.write(
HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage);
}
}
catch (IllegalArgumentException ex) {
response.setHeader("Content-Range", "bytes */" + resource.contentLength());