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:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user