Introduce support for webjars-locator-lite
This commit introduces support for org.webjars:webjars-locator-lite via a new LiteWebJarsResourceResolver in Spring MVC and WebFlux, and deprecates WebJarsResourceResolver which is performing a classpath scanning that slows down application startup. Closes gh-27619
This commit is contained in:
parent
81bc586e15
commit
67edcde0a2
|
@ -143,6 +143,7 @@ dependencies {
|
|||
api("org.testng:testng:7.9.0")
|
||||
api("org.webjars:underscorejs:1.8.3")
|
||||
api("org.webjars:webjars-locator-core:0.55")
|
||||
api("org.webjars:webjars-locator-lite:0.0.2")
|
||||
api("org.xmlunit:xmlunit-assertj:2.9.1")
|
||||
api("org.xmlunit:xmlunit-matchers:2.9.1")
|
||||
api("org.yaml:snakeyaml:2.2")
|
||||
|
|
|
@ -32,6 +32,7 @@ dependencies {
|
|||
optional("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||
optional("org.webjars:webjars-locator-core")
|
||||
optional("org.webjars:webjars-locator-lite")
|
||||
testImplementation(testFixtures(project(":spring-beans")))
|
||||
testImplementation(testFixtures(project(":spring-core")))
|
||||
testImplementation(testFixtures(project(":spring-web")))
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.springframework.util.ClassUtils;
|
|||
import org.springframework.web.reactive.resource.CachingResourceResolver;
|
||||
import org.springframework.web.reactive.resource.CachingResourceTransformer;
|
||||
import org.springframework.web.reactive.resource.CssLinkResourceTransformer;
|
||||
import org.springframework.web.reactive.resource.LiteWebJarsResourceResolver;
|
||||
import org.springframework.web.reactive.resource.PathResourceResolver;
|
||||
import org.springframework.web.reactive.resource.ResourceResolver;
|
||||
import org.springframework.web.reactive.resource.ResourceTransformer;
|
||||
|
@ -43,9 +44,12 @@ public class ResourceChainRegistration {
|
|||
|
||||
private static final String DEFAULT_CACHE_NAME = "spring-resource-chain-cache";
|
||||
|
||||
private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
|
||||
private static final boolean isWebJarAssetLocatorPresent = ClassUtils.isPresent(
|
||||
"org.webjars.WebJarAssetLocator", ResourceChainRegistration.class.getClassLoader());
|
||||
|
||||
private static final boolean isWebJarVersionLocatorPresent = ClassUtils.isPresent(
|
||||
"org.webjars.WebJarVersionLocator", ResourceChainRegistration.class.getClassLoader());
|
||||
|
||||
|
||||
private final List<ResourceResolver> resolvers = new ArrayList<>(4);
|
||||
|
||||
|
@ -79,6 +83,7 @@ public class ResourceChainRegistration {
|
|||
* @param resolver the resolver to add
|
||||
* @return the current instance for chained method invocation
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
public ResourceChainRegistration addResolver(ResourceResolver resolver) {
|
||||
Assert.notNull(resolver, "The provided ResourceResolver should not be null");
|
||||
this.resolvers.add(resolver);
|
||||
|
@ -88,7 +93,7 @@ public class ResourceChainRegistration {
|
|||
else if (resolver instanceof PathResourceResolver) {
|
||||
this.hasPathResolver = true;
|
||||
}
|
||||
else if (resolver instanceof WebJarsResourceResolver) {
|
||||
else if (resolver instanceof WebJarsResourceResolver || resolver instanceof LiteWebJarsResourceResolver) {
|
||||
this.hasWebjarsResolver = true;
|
||||
}
|
||||
return this;
|
||||
|
@ -108,10 +113,14 @@ public class ResourceChainRegistration {
|
|||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
protected List<ResourceResolver> getResourceResolvers() {
|
||||
if (!this.hasPathResolver) {
|
||||
List<ResourceResolver> result = new ArrayList<>(this.resolvers);
|
||||
if (isWebJarsAssetLocatorPresent && !this.hasWebjarsResolver) {
|
||||
if (isWebJarVersionLocatorPresent && !this.hasWebjarsResolver) {
|
||||
result.add(new LiteWebJarsResourceResolver());
|
||||
}
|
||||
else if (isWebJarAssetLocatorPresent && !this.hasWebjarsResolver) {
|
||||
result.add(new WebJarsResourceResolver());
|
||||
}
|
||||
result.add(new PathResourceResolver());
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright 2002-2022 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
|
||||
*
|
||||
* https://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 java.util.List;
|
||||
|
||||
import org.webjars.WebJarVersionLocator;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@code ResourceResolver} that delegates to the chain to locate a resource and then
|
||||
* attempts to find a matching versioned resource contained in a WebJar JAR file.
|
||||
*
|
||||
* <p>This allows WebJars.org users to write version agnostic paths in their templates,
|
||||
* like {@code <script src="/webjars/jquery/jquery.min.js"/>}.
|
||||
* This path will be resolved to the unique version {@code <script src="/webjars/jquery/1.2.0/jquery.min.js"/>},
|
||||
* which is a better fit for HTTP caching and version management in applications.
|
||||
*
|
||||
* <p>This also resolves resources for version agnostic HTTP requests {@code "GET /jquery/jquery.min.js"}.
|
||||
*
|
||||
* <p>This resolver requires the {@code org.webjars:webjars-locator-lite} library
|
||||
* on the classpath and is automatically registered if that library is present.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.2
|
||||
* @see <a href="https://www.webjars.org">webjars.org</a>
|
||||
*/
|
||||
public class LiteWebJarsResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private static final int WEBJARS_LOCATION_LENGTH = WebJarVersionLocator.WEBJARS_PATH_PREFIX.length() + 1;
|
||||
|
||||
private final WebJarVersionLocator webJarAssetLocator;
|
||||
|
||||
/**
|
||||
* Create a {@code LiteWebJarsResourceResolver} with a default {@code WebJarVersionLocator} instance.
|
||||
*/
|
||||
public LiteWebJarsResourceResolver() {
|
||||
this.webJarAssetLocator = new WebJarVersionLocator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code LiteWebJarsResourceResolver} with a custom {@code WebJarVersionLocator} instance,
|
||||
* e.g. with a custom cache implementation.
|
||||
*/
|
||||
public LiteWebJarsResourceResolver(WebJarVersionLocator webJarAssetLocator) {
|
||||
this.webJarAssetLocator = webJarAssetLocator;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Resource> resolveResourceInternal(@Nullable ServerWebExchange exchange,
|
||||
String requestPath, List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
return chain.resolveResource(exchange, requestPath, locations)
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
String webJarsResourcePath = findWebJarResourcePath(requestPath);
|
||||
if (webJarsResourcePath != null) {
|
||||
return chain.resolveResource(exchange, webJarsResourcePath, locations);
|
||||
}
|
||||
else {
|
||||
return Mono.empty();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<String> resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
return chain.resolveUrlPath(resourceUrlPath, locations)
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
String webJarResourcePath = findWebJarResourcePath(resourceUrlPath);
|
||||
if (webJarResourcePath != null) {
|
||||
return chain.resolveUrlPath(webJarResourcePath, locations);
|
||||
}
|
||||
else {
|
||||
return Mono.empty();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String findWebJarResourcePath(String path) {
|
||||
int startOffset = (path.startsWith("/") ? 1 : 0);
|
||||
int endOffset = path.indexOf('/', 1);
|
||||
if (endOffset != -1) {
|
||||
String webjar = path.substring(startOffset, endOffset);
|
||||
String partialPath = path.substring(endOffset + 1);
|
||||
String webJarPath = this.webJarAssetLocator.fullPath(webjar, partialPath);
|
||||
if (webJarPath != null) {
|
||||
return webJarPath.substring(WEBJARS_LOCATION_LENGTH);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -46,7 +46,9 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see <a href="https://www.webjars.org">webjars.org</a>
|
||||
* @deprecated as of Spring Framework 6.2 in favor of {@link LiteWebJarsResourceResolver}
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public class WebJarsResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private static final String WEBJARS_LOCATION = "META-INF/resources/webjars/";
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
|
|||
import org.springframework.web.reactive.resource.CachingResourceResolver;
|
||||
import org.springframework.web.reactive.resource.CachingResourceTransformer;
|
||||
import org.springframework.web.reactive.resource.CssLinkResourceTransformer;
|
||||
import org.springframework.web.reactive.resource.LiteWebJarsResourceResolver;
|
||||
import org.springframework.web.reactive.resource.PathResourceResolver;
|
||||
import org.springframework.web.reactive.resource.ResourceResolver;
|
||||
import org.springframework.web.reactive.resource.ResourceTransformer;
|
||||
|
@ -142,7 +143,7 @@ class ResourceHandlerRegistryTests {
|
|||
zero -> assertThat(zero).isInstanceOfSatisfying(CachingResourceResolver.class,
|
||||
cachingResolver -> assertThat(cachingResolver.getCache()).isInstanceOf(ConcurrentMapCache.class)),
|
||||
one -> assertThat(one).isEqualTo(mockResolver),
|
||||
two -> assertThat(two).isInstanceOf(WebJarsResourceResolver.class),
|
||||
two -> assertThat(two).isInstanceOf(LiteWebJarsResourceResolver.class),
|
||||
three -> assertThat(three).isInstanceOf(PathResourceResolver.class));
|
||||
assertThat(handler.getResourceTransformers()).satisfiesExactly(
|
||||
zero -> assertThat(zero).isInstanceOf(CachingResourceTransformer.class),
|
||||
|
@ -156,7 +157,7 @@ class ResourceHandlerRegistryTests {
|
|||
|
||||
ResourceWebHandler handler = getHandler("/resources/**");
|
||||
assertThat(handler.getResourceResolvers()).hasExactlyElementsOfTypes(
|
||||
WebJarsResourceResolver.class, PathResourceResolver.class);
|
||||
LiteWebJarsResourceResolver.class, PathResourceResolver.class);
|
||||
assertThat(handler.getResourceTransformers()).isEmpty();
|
||||
}
|
||||
|
||||
|
@ -172,7 +173,7 @@ class ResourceHandlerRegistryTests {
|
|||
assertThat(handler.getResourceResolvers()).satisfiesExactly(
|
||||
zero -> assertThat(zero).isInstanceOf(CachingResourceResolver.class),
|
||||
one -> assertThat(one).isSameAs(versionResolver),
|
||||
two -> assertThat(two).isInstanceOf(WebJarsResourceResolver.class),
|
||||
two -> assertThat(two).isInstanceOf(LiteWebJarsResourceResolver.class),
|
||||
three -> assertThat(three).isInstanceOf(PathResourceResolver.class));
|
||||
assertThat(handler.getResourceTransformers()).hasExactlyElementsOfTypes(
|
||||
CachingResourceTransformer.class, CssLinkResourceTransformer.class);
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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
|
||||
*
|
||||
* https://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 java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebJarsResourceResolver}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class LiteWebJarsResourceResolverTests {
|
||||
|
||||
private static final Duration TIMEOUT = Duration.ofSeconds(1);
|
||||
|
||||
|
||||
private List<Resource> locations = List.of(new ClassPathResource("/META-INF/resources/webjars"));
|
||||
|
||||
// for this to work, an actual WebJar must be on the test classpath
|
||||
private LiteWebJarsResourceResolver resolver = new LiteWebJarsResourceResolver();
|
||||
|
||||
private ResourceResolverChain chain = mock();
|
||||
|
||||
private ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(""));
|
||||
|
||||
|
||||
@Test
|
||||
void resolveUrlExisting() {
|
||||
String file = "/foo/2.3/foo.txt";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.just(file));
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain).block(TIMEOUT);
|
||||
|
||||
assertThat(actual).isEqualTo(file);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveUrlExistingNotInJarFile() {
|
||||
String file = "foo/foo.txt";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty());
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain).block(TIMEOUT);
|
||||
|
||||
assertThat(actual).isNull();
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
verify(this.chain, never()).resolveUrlPath("foo/2.3/foo.txt", this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveUrlWebJarResource() {
|
||||
String file = "underscorejs/underscore.js";
|
||||
String expected = "underscorejs/1.8.3/underscore.js";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty());
|
||||
given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(Mono.just(expected));
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain).block(TIMEOUT);
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
verify(this.chain, times(1)).resolveUrlPath(expected, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveUrlWebJarResourceNotFound() {
|
||||
String file = "something/something.js";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty());
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain).block(TIMEOUT);
|
||||
|
||||
assertThat(actual).isNull();
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
verify(this.chain, never()).resolveUrlPath(null, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveResourceExisting() {
|
||||
Resource expected = mock();
|
||||
String file = "foo/2.3/foo.txt";
|
||||
given(this.chain.resolveResource(this.exchange, file, this.locations)).willReturn(Mono.just(expected));
|
||||
|
||||
Resource actual = this.resolver
|
||||
.resolveResource(this.exchange, file, this.locations, this.chain)
|
||||
.block(TIMEOUT);
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
verify(this.chain, times(1)).resolveResource(this.exchange, file, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveResourceNotFound() {
|
||||
String file = "something/something.js";
|
||||
given(this.chain.resolveResource(this.exchange, file, this.locations)).willReturn(Mono.empty());
|
||||
|
||||
Resource actual = this.resolver
|
||||
.resolveResource(this.exchange, file, this.locations, this.chain)
|
||||
.block(TIMEOUT);
|
||||
|
||||
assertThat(actual).isNull();
|
||||
verify(this.chain, times(1)).resolveResource(this.exchange, file, this.locations);
|
||||
verify(this.chain, never()).resolveResource(this.exchange, null, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveResourceWebJar() {
|
||||
String file = "underscorejs/underscore.js";
|
||||
given(this.chain.resolveResource(this.exchange, file, this.locations)).willReturn(Mono.empty());
|
||||
|
||||
Resource expected = mock();
|
||||
String expectedPath = "underscorejs/1.8.3/underscore.js";
|
||||
given(this.chain.resolveResource(this.exchange, expectedPath, this.locations))
|
||||
.willReturn(Mono.just(expected));
|
||||
|
||||
Resource actual = this.resolver
|
||||
.resolveResource(this.exchange, file, this.locations, this.chain)
|
||||
.block(TIMEOUT);
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
verify(this.chain, times(1)).resolveResource(this.exchange, file, this.locations);
|
||||
}
|
||||
|
||||
}
|
|
@ -39,6 +39,7 @@ dependencies {
|
|||
optional("org.jetbrains.kotlinx:kotlinx-serialization-protobuf")
|
||||
optional("org.reactivestreams:reactive-streams")
|
||||
optional("org.webjars:webjars-locator-core")
|
||||
optional("org.webjars:webjars-locator-lite")
|
||||
testImplementation(testFixtures(project(":spring-beans")))
|
||||
testImplementation(testFixtures(project(":spring-context")))
|
||||
testImplementation(testFixtures(project(":spring-core")))
|
||||
|
|
|
@ -306,6 +306,7 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private void parseResourceResolversTransformers(boolean isAutoRegistration,
|
||||
ManagedList<Object> resourceResolvers, ManagedList<Object> resourceTransformers,
|
||||
ParserContext context, Element element, @Nullable Object source) {
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.springframework.util.ClassUtils;
|
|||
import org.springframework.web.servlet.resource.CachingResourceResolver;
|
||||
import org.springframework.web.servlet.resource.CachingResourceTransformer;
|
||||
import org.springframework.web.servlet.resource.CssLinkResourceTransformer;
|
||||
import org.springframework.web.servlet.resource.LiteWebJarsResourceResolver;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
import org.springframework.web.servlet.resource.ResourceResolver;
|
||||
import org.springframework.web.servlet.resource.ResourceTransformer;
|
||||
|
@ -43,9 +44,12 @@ public class ResourceChainRegistration {
|
|||
|
||||
private static final String DEFAULT_CACHE_NAME = "spring-resource-chain-cache";
|
||||
|
||||
private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
|
||||
private static final boolean isWebJarAssetLocatorPresent = ClassUtils.isPresent(
|
||||
"org.webjars.WebJarAssetLocator", ResourceChainRegistration.class.getClassLoader());
|
||||
|
||||
private static final boolean isWebJarVersionLocatorPresent = ClassUtils.isPresent(
|
||||
"org.webjars.WebJarVersionLocator", ResourceChainRegistration.class.getClassLoader());
|
||||
|
||||
|
||||
private final List<ResourceResolver> resolvers = new ArrayList<>(4);
|
||||
|
||||
|
@ -79,6 +83,7 @@ public class ResourceChainRegistration {
|
|||
* @param resolver the resolver to add
|
||||
* @return the current instance for chained method invocation
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
public ResourceChainRegistration addResolver(ResourceResolver resolver) {
|
||||
Assert.notNull(resolver, "The provided ResourceResolver should not be null");
|
||||
this.resolvers.add(resolver);
|
||||
|
@ -88,7 +93,7 @@ public class ResourceChainRegistration {
|
|||
else if (resolver instanceof PathResourceResolver) {
|
||||
this.hasPathResolver = true;
|
||||
}
|
||||
else if (resolver instanceof WebJarsResourceResolver) {
|
||||
else if (resolver instanceof WebJarsResourceResolver || resolver instanceof LiteWebJarsResourceResolver) {
|
||||
this.hasWebjarsResolver = true;
|
||||
}
|
||||
return this;
|
||||
|
@ -108,10 +113,14 @@ public class ResourceChainRegistration {
|
|||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
protected List<ResourceResolver> getResourceResolvers() {
|
||||
if (!this.hasPathResolver) {
|
||||
List<ResourceResolver> result = new ArrayList<>(this.resolvers);
|
||||
if (isWebJarsAssetLocatorPresent && !this.hasWebjarsResolver) {
|
||||
if (isWebJarVersionLocatorPresent && !this.hasWebjarsResolver) {
|
||||
result.add(new LiteWebJarsResourceResolver());
|
||||
}
|
||||
else if (isWebJarAssetLocatorPresent && !this.hasWebjarsResolver) {
|
||||
result.add(new WebJarsResourceResolver());
|
||||
}
|
||||
result.add(new PathResourceResolver());
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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
|
||||
*
|
||||
* https://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 java.util.List;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.webjars.WebJarVersionLocator;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@code ResourceResolver} that delegates to the chain to locate a resource and then
|
||||
* attempts to find a matching versioned resource contained in a WebJar JAR file.
|
||||
*
|
||||
* <p>This allows WebJars.org users to write version agnostic paths in their templates,
|
||||
* like {@code <script src="/webjars/jquery/jquery.min.js"/>}.
|
||||
* This path will be resolved to the unique version {@code <script src="/webjars/jquery/1.2.0/jquery.min.js"/>},
|
||||
* which is a better fit for HTTP caching and version management in applications.
|
||||
*
|
||||
* <p>This also resolves resources for version agnostic HTTP requests {@code "GET /jquery/jquery.min.js"}.
|
||||
*
|
||||
* <p>This resolver requires the {@code org.webjars:webjars-locator-lite} library
|
||||
* on the classpath and is automatically registered if that library is present.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.2
|
||||
* @see org.springframework.web.servlet.config.annotation.ResourceChainRegistration
|
||||
* @see <a href="https://www.webjars.org">webjars.org</a>
|
||||
*/
|
||||
public class LiteWebJarsResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private static final int WEBJARS_LOCATION_LENGTH = WebJarVersionLocator.WEBJARS_PATH_PREFIX.length() + 1;
|
||||
|
||||
private final WebJarVersionLocator webJarAssetLocator;
|
||||
|
||||
/**
|
||||
* Create a {@code LiteWebJarsResourceResolver} with a default {@code WebJarVersionLocator} instance.
|
||||
*/
|
||||
public LiteWebJarsResourceResolver() {
|
||||
this.webJarAssetLocator = new WebJarVersionLocator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code LiteWebJarsResourceResolver} with a custom {@code WebJarVersionLocator} instance,
|
||||
* e.g. with a custom cache implementation.
|
||||
*/
|
||||
public LiteWebJarsResourceResolver(WebJarVersionLocator webJarAssetLocator) {
|
||||
this.webJarAssetLocator = webJarAssetLocator;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Resource resolveResourceInternal(@Nullable HttpServletRequest request, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
Resource resolved = chain.resolveResource(request, requestPath, locations);
|
||||
if (resolved == null) {
|
||||
String webJarResourcePath = findWebJarResourcePath(requestPath);
|
||||
if (webJarResourcePath != null) {
|
||||
return chain.resolveResource(request, webJarResourcePath, locations);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected String resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
String path = chain.resolveUrlPath(resourceUrlPath, locations);
|
||||
if (path == null) {
|
||||
String webJarResourcePath = findWebJarResourcePath(resourceUrlPath);
|
||||
if (webJarResourcePath != null) {
|
||||
return chain.resolveUrlPath(webJarResourcePath, locations);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String findWebJarResourcePath(String path) {
|
||||
int startOffset = (path.startsWith("/") ? 1 : 0);
|
||||
int endOffset = path.indexOf('/', 1);
|
||||
if (endOffset != -1) {
|
||||
String webjar = path.substring(startOffset, endOffset);
|
||||
String partialPath = path.substring(endOffset + 1);
|
||||
String webJarPath = this.webJarAssetLocator.fullPath(webjar, partialPath);
|
||||
if (webJarPath != null) {
|
||||
return webJarPath.substring(WEBJARS_LOCATION_LENGTH);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
|
@ -42,10 +42,14 @@ import org.springframework.lang.Nullable;
|
|||
* could slow down application startup.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Sebastien Deleuze
|
||||
* @since 4.2
|
||||
* @see org.springframework.web.servlet.config.annotation.ResourceChainRegistration
|
||||
* @see <a href="https://www.webjars.org">webjars.org</a>
|
||||
* @see LiteWebJarsResourceResolver
|
||||
* @deprecated as of Spring Framework 6.2 in favor of {@link LiteWebJarsResourceResolver}
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public class WebJarsResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private static final String WEBJARS_LOCATION = "META-INF/resources/webjars/";
|
||||
|
|
|
@ -439,6 +439,7 @@ public class MvcNamespaceTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
void testResourcesWithResolversTransformers() {
|
||||
loadBeanDefinitions("mvc-config-resources-chain.xml");
|
||||
|
||||
|
|
|
@ -31,12 +31,12 @@ import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
|
|||
import org.springframework.web.servlet.resource.CachingResourceResolver;
|
||||
import org.springframework.web.servlet.resource.CachingResourceTransformer;
|
||||
import org.springframework.web.servlet.resource.CssLinkResourceTransformer;
|
||||
import org.springframework.web.servlet.resource.LiteWebJarsResourceResolver;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
|
||||
import org.springframework.web.servlet.resource.ResourceResolver;
|
||||
import org.springframework.web.servlet.resource.ResourceTransformer;
|
||||
import org.springframework.web.servlet.resource.VersionResourceResolver;
|
||||
import org.springframework.web.servlet.resource.WebJarsResourceResolver;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
import org.springframework.web.testfixture.servlet.MockServletContext;
|
||||
|
@ -49,6 +49,7 @@ import static org.mockito.Mockito.mock;
|
|||
* Tests for {@link ResourceHandlerRegistry}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class ResourceHandlerRegistryTests {
|
||||
|
||||
|
@ -128,6 +129,7 @@ class ResourceHandlerRegistryTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
void resourceChain() {
|
||||
ResourceResolver mockResolver = mock();
|
||||
ResourceTransformer mockTransformer = mock();
|
||||
|
@ -138,7 +140,7 @@ class ResourceHandlerRegistryTests {
|
|||
zero -> assertThat(zero).isInstanceOfSatisfying(CachingResourceResolver.class,
|
||||
cachingResolver -> assertThat(cachingResolver.getCache()).isInstanceOf(ConcurrentMapCache.class)),
|
||||
one -> assertThat(one).isEqualTo(mockResolver),
|
||||
two -> assertThat(two).isInstanceOf(WebJarsResourceResolver.class),
|
||||
two -> assertThat(two).isInstanceOf(LiteWebJarsResourceResolver.class),
|
||||
three -> assertThat(three).isInstanceOf(PathResourceResolver.class));
|
||||
assertThat(handler.getResourceTransformers()).satisfiesExactly(
|
||||
zero -> assertThat(zero).isInstanceOf(CachingResourceTransformer.class),
|
||||
|
@ -151,7 +153,7 @@ class ResourceHandlerRegistryTests {
|
|||
|
||||
ResourceHttpRequestHandler handler = getHandler("/resources/**");
|
||||
assertThat(handler.getResourceResolvers()).hasExactlyElementsOfTypes(
|
||||
WebJarsResourceResolver.class, PathResourceResolver.class);
|
||||
LiteWebJarsResourceResolver.class, PathResourceResolver.class);
|
||||
assertThat(handler.getResourceTransformers()).isEmpty();
|
||||
}
|
||||
|
||||
|
@ -167,7 +169,7 @@ class ResourceHandlerRegistryTests {
|
|||
assertThat(handler.getResourceResolvers()).satisfiesExactly(
|
||||
zero -> assertThat(zero).isInstanceOf(CachingResourceResolver.class),
|
||||
one -> assertThat(one).isSameAs(versionResolver),
|
||||
two -> assertThat(two).isInstanceOf(WebJarsResourceResolver.class),
|
||||
two -> assertThat(two).isInstanceOf(LiteWebJarsResourceResolver.class),
|
||||
three -> assertThat(three).isInstanceOf(PathResourceResolver.class));
|
||||
assertThat(handler.getResourceTransformers()).hasExactlyElementsOfTypes(
|
||||
CachingResourceTransformer.class, CssLinkResourceTransformer.class);
|
||||
|
@ -177,7 +179,7 @@ class ResourceHandlerRegistryTests {
|
|||
void resourceChainWithOverrides() {
|
||||
CachingResourceResolver cachingResolver = mock();
|
||||
VersionResourceResolver versionResolver = mock();
|
||||
WebJarsResourceResolver webjarsResolver = mock();
|
||||
LiteWebJarsResourceResolver webjarsResolver = mock();
|
||||
PathResourceResolver pathResourceResolver = new PathResourceResolver();
|
||||
CachingResourceTransformer cachingTransformer = mock();
|
||||
CssLinkResourceTransformer cssLinkTransformer = new CssLinkResourceTransformer();
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright 2002-2024 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
|
||||
*
|
||||
* https://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 java.util.List;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link LiteWebJarsResourceResolver}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class LiteWebJarsResourceResolverTests {
|
||||
|
||||
private List<Resource> locations = List.of(new ClassPathResource("/META-INF/resources/webjars"));
|
||||
|
||||
// for this to work, an actual WebJar must be on the test classpath
|
||||
private LiteWebJarsResourceResolver resolver = new LiteWebJarsResourceResolver();
|
||||
|
||||
private ResourceResolverChain chain = mock();
|
||||
|
||||
private HttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
|
||||
@Test
|
||||
void resolveUrlExisting() {
|
||||
String file = "/foo/2.3/foo.txt";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(file);
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
|
||||
|
||||
assertThat(actual).isEqualTo(file);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveUrlExistingNotInJarFile() {
|
||||
String file = "foo/foo.txt";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
|
||||
|
||||
assertThat(actual).isNull();
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
verify(this.chain, never()).resolveUrlPath("foo/2.3/foo.txt", this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveUrlWebJarResource() {
|
||||
String file = "underscorejs/underscore.js";
|
||||
String expected = "underscorejs/1.8.3/underscore.js";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
|
||||
given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(expected);
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
verify(this.chain, times(1)).resolveUrlPath(expected, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveUrlWebJarResourceNotFound() {
|
||||
String file = "something/something.js";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
|
||||
|
||||
assertThat(actual).isNull();
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
verify(this.chain, never()).resolveUrlPath(null, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveResourceExisting() {
|
||||
Resource expected = mock();
|
||||
String file = "foo/2.3/foo.txt";
|
||||
given(this.chain.resolveResource(this.request, file, this.locations)).willReturn(expected);
|
||||
|
||||
Resource actual = this.resolver.resolveResource(this.request, file, this.locations, this.chain);
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
verify(this.chain, times(1)).resolveResource(this.request, file, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveResourceNotFound() {
|
||||
String file = "something/something.js";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
|
||||
|
||||
Resource actual = this.resolver.resolveResource(this.request, file, this.locations, this.chain);
|
||||
|
||||
assertThat(actual).isNull();
|
||||
verify(this.chain, times(1)).resolveResource(this.request, file, this.locations);
|
||||
verify(this.chain, never()).resolveResource(this.request, null, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveResourceWebJar() {
|
||||
Resource expected = mock();
|
||||
String file = "underscorejs/underscore.js";
|
||||
String expectedPath = "underscorejs/1.8.3/underscore.js";
|
||||
given(this.chain.resolveResource(this.request, expectedPath, this.locations)).willReturn(expected);
|
||||
|
||||
Resource actual = this.resolver.resolveResource(this.request, file, this.locations, this.chain);
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
verify(this.chain, times(1)).resolveResource(this.request, file, this.locations);
|
||||
}
|
||||
|
||||
}
|
|
@ -38,6 +38,7 @@ import static org.mockito.Mockito.verify;
|
|||
* @author Brian Clozel
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
class WebJarsResourceResolverTests {
|
||||
|
||||
private List<Resource> locations = List.of(new ClassPathResource("/META-INF/resources/webjars"));
|
||||
|
|
Loading…
Reference in New Issue