提交 ccb3c44d 编写于 作者: B Brian Clozel

Add ResolvedResource in resource handling chain

Prior to this commit, the resource handling chain and its
`ResourceResolvers` would use specific `Resource` implementations in
order to add resource metadata to the HTTP response. For example,
`VersionedResource` and `EncodedResource` are both adding specific HTTP
response headers.

This commit aims at making this mechanism more stable and reusable,
since the previous implementation would fail in case a resolved resource
would be both a `VersionedResource` wrapping a `EncodedResource` (or the
other way arount). Only one of the specific implementations would
contribute its metadata since the code supporting that in
`ResourceHttpRequestHandler` would only check for `instanceof` tests,
whereas those implementations are acutally delegating calls to
the wrapped resource.

Now both `VersionedResource` and `EncodedResource` have been replaced by
specific implementations of `ResolvedResource`, which directly provides
those HTTP response headers as part of `getResponseHeaders()`.

This commit applies the same changes for the web reactive
implementations and its `ResourceWebHandler`.

Issue: SPR-14264
上级 42f101fb
/*
* 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.web.reactive.resource;
import org.springframework.core.io.Resource;
/**
* Interface for a resource descriptor that describes the encoding
* applied to the entire resource content.
*
* <p>This information is required if the client consuming that resource
* needs additional decoding capabilities to retrieve the resource's content.
*
* @author Rossen Stoyanchev
* @since 5.0
* @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.2.2">
* HTTP/1.1: Semantics and Content, section 3.1.2.2</a>
*/
public interface EncodedResource extends Resource {
/**
* The content coding value, as defined in the IANA registry
* @return the content encoding
* @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.2.1">HTTP/1.1: Semantics
* and Content, section 3.1.2.1</a>
*/
String getContentEncoding();
}
......@@ -25,6 +25,7 @@ import java.util.List;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.web.server.ServerWebExchange;
/**
......@@ -74,7 +75,7 @@ public class GzipResourceResolver extends AbstractResourceResolver {
}
private static final class GzippedResource extends AbstractResource implements EncodedResource {
private static final class GzippedResource extends AbstractResource implements ResolvedResource {
private final Resource original;
......@@ -138,8 +139,17 @@ public class GzipResourceResolver extends AbstractResourceResolver {
return this.gzipped.getDescription();
}
public String getContentEncoding() {
return "gzip";
@Override
public HttpHeaders getResponseHeaders() {
HttpHeaders headers;
if(this.original instanceof ResolvedResource) {
headers = ((ResolvedResource) this.original).getResponseHeaders();
}
else {
headers = new HttpHeaders();
}
headers.add(HttpHeaders.CONTENT_ENCODING, "gzip");
return headers;
}
}
......
package org.springframework.web.reactive.resource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
/**
* Interface for resources resolved through the
* {@link org.springframework.web.reactive.resource.ResourceResolverChain}
* that may contribute HTTP response headers as they're served to HTTP clients.
*
* <p>Some resource implementations, while served by the
* {@link org.springframework.web.reactive.resource.ResourceResolverChain} need
* to contribute resource metadata as HTTP response headers so that HTTP clients
* can interpret them properly.
*
* @author Brian Clozel
* @since 5.0
*/
public interface ResolvedResource extends Resource {
/**
* The HTTP headers to be contributed to the HTTP response
* that serves the current resource.
* @return the HTTP response headers
*/
HttpHeaders getResponseHeaders();
}
......@@ -482,11 +482,9 @@ public class ResourceWebHandler
if (mediaType != null) {
headers.setContentType(mediaType);
}
if (resource instanceof EncodedResource) {
headers.set(HttpHeaders.CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding());
}
if (resource instanceof VersionedResource) {
headers.setETag("\"" + ((VersionedResource) resource).getVersion() + "\"");
if (resource instanceof ResolvedResource) {
HttpHeaders resourceHeaders = ((ResolvedResource) resource).getResponseHeaders();
exchange.getResponse().getHeaders().putAll(resourceHeaders);
}
headers.set(HttpHeaders.ACCEPT_RANGES, "bytes");
}
......
......@@ -31,6 +31,7 @@ import java.util.Map;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
......@@ -241,7 +242,7 @@ public class VersionResourceResolver extends AbstractResourceResolver {
}
private class FileNameVersionedResource extends AbstractResource implements VersionedResource {
private class FileNameVersionedResource extends AbstractResource implements ResolvedResource {
private final Resource original;
......@@ -318,8 +319,16 @@ public class VersionResourceResolver extends AbstractResourceResolver {
}
@Override
public String getVersion() {
return this.version;
public HttpHeaders getResponseHeaders() {
HttpHeaders headers;
if(this.original instanceof ResolvedResource) {
headers = ((ResolvedResource) this.original).getResponseHeaders();
}
else {
headers = new HttpHeaders();
}
headers.setETag("\"" + this.version + "\"");
return headers;
}
}
......
/*
* Copyright 2002-2016 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.web.reactive.resource;
import org.springframework.core.io.Resource;
/**
* Interface for a resource descriptor that describes its version with a
* version string that can be derived from its content and/or metadata.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
* @see VersionResourceResolver
*/
public interface VersionedResource extends Resource {
String getVersion();
}
......@@ -123,8 +123,8 @@ public class GzipResourceResolverTests {
Resource resource = new ClassPathResource("test/" + gzFile, getClass());
assertEquals(resource.getDescription(), resolved.getDescription());
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
resolved instanceof EncodedResource);
assertTrue("Expected " + resolved + " to be of type " + ResolvedResource.class,
resolved instanceof ResolvedResource);
}
@Test
......@@ -137,8 +137,8 @@ public class GzipResourceResolverTests {
Resource resource = new ClassPathResource("test/" + gzFile, getClass());
assertEquals(resource.getDescription(), resolved.getDescription());
assertEquals(new ClassPathResource("test/"+file).getFilename(), resolved.getFilename());
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
resolved instanceof EncodedResource);
assertTrue("Expected " + resolved + " to be of type " + ResolvedResource.class,
resolved instanceof ResolvedResource);
}
@Test
......@@ -151,8 +151,8 @@ public class GzipResourceResolverTests {
Resource gzResource = new ClassPathResource("test/"+gzFile, getClass());
assertEquals(gzResource.getDescription(), resolved.getDescription());
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
resolved instanceof EncodedResource);
assertTrue("Expected " + resolved + " to be of type " + ResolvedResource.class,
resolved instanceof ResolvedResource);
// resolved resource is now cached in CachingResourceResolver
......@@ -165,8 +165,8 @@ public class GzipResourceResolverTests {
Resource resource = new ClassPathResource("test/"+file, getClass());
assertEquals(resource.getDescription(), resolved.getDescription());
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
assertFalse("Expected " + resolved + " to *not* be of type " + EncodedResource.class,
resolved instanceof EncodedResource);
assertFalse("Expected " + resolved + " to *not* be of type " + ResolvedResource.class,
resolved instanceof ResolvedResource);
}
@Test // SPR-13149
......@@ -178,7 +178,7 @@ public class GzipResourceResolverTests {
Resource gzResource = new ClassPathResource("test/" + gzFile, getClass());
assertEquals(gzResource.getDescription(), resolved.getDescription());
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
resolved instanceof EncodedResource);
assertTrue("Expected " + resolved + " to be of type " + ResolvedResource.class,
resolved instanceof ResolvedResource);
}
}
......@@ -163,8 +163,8 @@ public class VersionResourceResolverTests {
Resource actual = this.resolver.resolveResourceInternal(exchange, versionFile, this.locations, this.chain);
assertEquals(expected.getFilename(), actual.getFilename());
verify(this.versionStrategy, times(1)).getResourceVersion(expected);
assertThat(actual, instanceOf(VersionedResource.class));
assertEquals(version, ((VersionedResource)actual).getVersion());
assertThat(actual, instanceOf(ResolvedResource.class));
assertEquals("\"" + version + "\"", ((ResolvedResource)actual).getResponseHeaders().getETag());
}
@Test
......
/*
* 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.web.servlet.resource;
import org.springframework.core.io.Resource;
/**
* Interface for a resource descriptor that describes the encoding
* applied to the entire resource content.
*
* <p>This information is required if the client consuming that resource
* needs additional decoding capabilities to retrieve the resource's content.
*
* @author Jeremy Grelle
* @since 4.1
* @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.2.2">HTTP/1.1: Semantics
* and Content, section 3.1.2.2</a>
*/
public interface EncodedResource extends Resource {
/**
* The content coding value, as defined in the IANA registry
* @return the content encoding
* @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.2.1">HTTP/1.1: Semantics
* and Content, section 3.1.2.1</a>
*/
String getContentEncoding();
}
......@@ -22,10 +22,12 @@ import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
/**
* A {@code ResourceResolver} that delegates to the chain to locate a resource
......@@ -76,7 +78,7 @@ public class GzipResourceResolver extends AbstractResourceResolver {
}
private static final class GzippedResource extends AbstractResource implements EncodedResource {
private static final class GzippedResource extends AbstractResource implements ResolvedResource {
private final Resource original;
......@@ -140,9 +142,19 @@ public class GzipResourceResolver extends AbstractResourceResolver {
return this.gzipped.getDescription();
}
public String getContentEncoding() {
return "gzip";
@Override
public HttpHeaders getResponseHeaders() {
HttpHeaders headers;
if(this.original instanceof ResolvedResource) {
headers = ((ResolvedResource) this.original).getResponseHeaders();
}
else {
headers = new HttpHeaders();
}
headers.add(HttpHeaders.CONTENT_ENCODING, "gzip");
return headers;
}
}
}
package org.springframework.web.servlet.resource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
/**
* Interface for resources resolved through the
* {@link org.springframework.web.servlet.resource.ResourceResolverChain}
* that may contribute HTTP response headers as they're served to HTTP clients.
*
* <p>Some resource implementations, while served by the
* {@link org.springframework.web.servlet.resource.ResourceResolverChain} need
* to contribute resource metadata as HTTP response headers so that HTTP clients
* can interpret them properly.
*
* @author Brian Clozel
* @since 5.0
*/
public interface ResolvedResource extends Resource {
/**
* The HTTP headers to be contributed to the HTTP response
* that serves the current resource.
* @return the HTTP response headers
*/
HttpHeaders getResponseHeaders();
}
......@@ -542,11 +542,10 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
if (mediaType != null) {
response.setContentType(mediaType.toString());
}
if (resource instanceof EncodedResource) {
response.setHeader(HttpHeaders.CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding());
}
if (resource instanceof VersionedResource) {
response.setHeader(HttpHeaders.ETAG, "\"" + ((VersionedResource) resource).getVersion() + "\"");
if (resource instanceof ResolvedResource) {
HttpHeaders resourceHeaders = ((ResolvedResource) resource).getResponseHeaders();
resourceHeaders.toSingleValueMap().entrySet()
.stream().forEach(entry -> response.setHeader(entry.getKey(), entry.getValue()));
}
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
}
......
......@@ -32,6 +32,7 @@ import javax.servlet.http.HttpServletRequest;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
......@@ -238,7 +239,7 @@ public class VersionResourceResolver extends AbstractResourceResolver {
}
private class FileNameVersionedResource extends AbstractResource implements VersionedResource {
private class FileNameVersionedResource extends AbstractResource implements ResolvedResource {
private final Resource original;
......@@ -315,9 +316,18 @@ public class VersionResourceResolver extends AbstractResourceResolver {
}
@Override
public String getVersion() {
return this.version;
public HttpHeaders getResponseHeaders() {
HttpHeaders headers;
if(this.original instanceof ResolvedResource) {
headers = ((ResolvedResource) this.original).getResponseHeaders();
}
else {
headers = new HttpHeaders();
}
headers.setETag("\"" + this.version + "\"");
return headers;
}
}
}
/*
* Copyright 2002-2016 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.web.servlet.resource;
import org.springframework.core.io.Resource;
/**
* Interface for a resource descriptor that describes its version with a
* version string that can be derived from its content and/or metadata.
*
* @author Brian Clozel
* @since 4.2.5
* @see VersionResourceResolver
*/
public interface VersionedResource extends Resource {
String getVersion();
}
......@@ -111,8 +111,8 @@ public class GzipResourceResolverTests {
Resource resource = new ClassPathResource("test/"+gzFile, getClass());
assertEquals(resource.getDescription(), resolved.getDescription());
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
resolved instanceof EncodedResource);
assertTrue("Expected " + resolved + " to be of type " + ResolvedResource.class,
resolved instanceof ResolvedResource);
}
@Test
......@@ -126,8 +126,8 @@ public class GzipResourceResolverTests {
Resource resource = new ClassPathResource("test/"+gzFile, getClass());
assertEquals(resource.getDescription(), resolved.getDescription());
assertEquals(new ClassPathResource("test/"+file).getFilename(), resolved.getFilename());
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
resolved instanceof EncodedResource);
assertTrue("Expected " + resolved + " to be of type " + ResolvedResource.class,
resolved instanceof ResolvedResource);
}
@Test
......@@ -141,8 +141,8 @@ public class GzipResourceResolverTests {
Resource gzResource = new ClassPathResource("test/"+gzFile, getClass());
assertEquals(gzResource.getDescription(), resolved.getDescription());
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
resolved instanceof EncodedResource);
assertTrue("Expected " + resolved + " to be of type " + ResolvedResource.class,
resolved instanceof ResolvedResource);
// resolved resource is now cached in CachingResourceResolver
......@@ -152,8 +152,8 @@ public class GzipResourceResolverTests {
Resource resource = new ClassPathResource("test/"+file, getClass());
assertEquals(resource.getDescription(), resolved.getDescription());
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
assertFalse("Expected " + resolved + " to *not* be of type " + EncodedResource.class,
resolved instanceof EncodedResource);
assertFalse("Expected " + resolved + " to *not* be of type " + ResolvedResource.class,
resolved instanceof ResolvedResource);
}
@Test // SPR-13149
......@@ -165,8 +165,8 @@ public class GzipResourceResolverTests {
Resource gzResource = new ClassPathResource("test/"+gzFile, getClass());
assertEquals(gzResource.getDescription(), resolved.getDescription());
assertEquals(new ClassPathResource("test/" + file).getFilename(), resolved.getFilename());
assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class,
resolved instanceof EncodedResource);
assertTrue("Expected " + resolved + " to be of type " + ResolvedResource.class,
resolved instanceof ResolvedResource);
}
}
......@@ -150,8 +150,8 @@ public class VersionResourceResolverTests {
Resource actual = this.resolver.resolveResourceInternal(request, versionFile, this.locations, this.chain);
assertEquals(expected.getFilename(), actual.getFilename());
verify(this.versionStrategy, times(1)).getResourceVersion(expected);
assertThat(actual, instanceOf(VersionedResource.class));
assertEquals(version, ((VersionedResource)actual).getVersion());
assertThat(actual, instanceOf(ResolvedResource.class));
assertEquals("\"" + version + "\"", ((ResolvedResource)actual).getResponseHeaders().getETag());
}
@Test
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册