提交 9e5a33c1 编写于 作者: B Brian Clozel

Add a ResourceResolver implementation for WebJars

Prior to this commit, WebJars users needed to use versioned links within
templates for WebJars resources, such as `/jquery/1.2.0/jquery.js`.
This can be rather cumbersome when updating libraries - all references
in templates need to be updated.

One could use version-less links in templates, but needed to add a
specific MVC Handler that uses webjars.org's webjar-locator library.
While this approach makes maintaing templates easier, this makes HTTP
caching strategies less optimal.

This commit adds a new WebJarsResourceResolver that search for resources
located in WebJar locations. This ResourceResolver is automatically
registered if the "org.webjars:webjars-locator" dependency is present.

Registering WebJars resource handling can be done like this:

```java
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
  registry.addResourceHandler("/webjars/**")
          .addResourceLocations("classpath:META-INF/resources/webjars")
          .resourceChain(true)
              .addResolver(new WebJarsResourceResolver());
}
```

Issue: SPR-12323

polish
上级 876c9694
......@@ -895,6 +895,7 @@ project("spring-webmvc") {
exclude group: "org.slf4j", module: "jcl-over-slf4j"
exclude group: "org.springframework", module: "spring-web"
}
optional 'org.webjars:webjars-locator:0.22'
testCompile(project(":spring-aop"))
testCompile("rhino:js:1.7R1")
testCompile("xmlunit:xmlunit:${xmlunitVersion}")
......@@ -921,6 +922,7 @@ project("spring-webmvc") {
testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}")
testCompile("org.jruby:jruby:${jrubyVersion}")
testCompile("org.python:jython-standalone:2.5.3")
testCompile("org.webjars:underscorejs:1.8.2")
}
}
......
......@@ -33,6 +33,7 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.core.Ordered;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.springframework.http.CacheControl;
......@@ -51,6 +52,7 @@ import org.springframework.web.servlet.resource.ResourceTransformer;
import org.springframework.web.servlet.resource.ResourceUrlProvider;
import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import org.springframework.web.servlet.resource.WebJarsResourceResolver;
/**
* {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a
......@@ -78,6 +80,9 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
private static final String RESOURCE_URL_PROVIDER = "mvcResourceUrlProvider";
private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
"org.webjars.WebJarAssetLocator", ResourcesBeanDefinitionParser.class.getClassLoader());
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
......@@ -302,6 +307,12 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
}
if (isAutoRegistration) {
if(isWebJarsAssetLocatorPresent) {
RootBeanDefinition webJarsResolverDef = new RootBeanDefinition(WebJarsResourceResolver.class);
webJarsResolverDef.setSource(source);
webJarsResolverDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
resourceResolvers.add(webJarsResolverDef);
}
RootBeanDefinition pathResolverDef = new RootBeanDefinition(PathResourceResolver.class);
pathResolverDef.setSource(source);
pathResolverDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
......
/*
* Copyright 2002-2014 the original author or authors.
* 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.
......@@ -22,6 +22,7 @@ import java.util.List;
import org.springframework.cache.Cache;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.util.Assert;
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;
......@@ -29,6 +30,7 @@ import org.springframework.web.servlet.resource.PathResourceResolver;
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;
/**
* Assists with the registration of resource resolvers and transformers.
......@@ -40,6 +42,9 @@ public class ResourceChainRegistration {
private static final String DEFAULT_CACHE_NAME = "spring-resource-chain-cache";
private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
"org.webjars.WebJarAssetLocator", ResourceChainRegistration.class.getClassLoader());
private final List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>(4);
private final List<ResourceTransformer> transformers = new ArrayList<ResourceTransformer>(4);
......@@ -98,6 +103,9 @@ public class ResourceChainRegistration {
protected List<ResourceResolver> getResourceResolvers() {
if (!this.hasPathResolver) {
List<ResourceResolver> result = new ArrayList<ResourceResolver>(this.resolvers);
if(isWebJarsAssetLocatorPresent) {
result.add(new WebJarsResourceResolver());
}
result.add(new PathResourceResolver());
return result;
}
......
/*
* 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 java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.webjars.MultipleMatchesException;
import org.webjars.WebJarAssetLocator;
import org.springframework.core.io.Resource;
/**
* 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 WabJar users to use version-less paths in their templates, like {@code "/jquery/jquery.min.js"}
* while this path is resolved to the unique version {@code "/jquery/1.2.0/jquery.min.js"}, which is a better fit
* for HTTP caching and version management in applications.
*
* <p>This resolver requires the "org.webjars:webjars-locator" library on classpath, and is automatically
* registered if that library is present.
*
* @author Brian Clozel
* @since 4.2
* @see <a href="http://www.webjars.org">webjars.org</a>
*/
public class WebJarsResourceResolver extends AbstractResourceResolver {
private final static String WEBJARS_LOCATION = "META-INF/resources/webjars";
private final static int WEBJARS_LOCATION_LENGTH = WEBJARS_LOCATION.length();
private final WebJarAssetLocator webJarAssetLocator;
public WebJarsResourceResolver() {
this.webJarAssetLocator = new WebJarAssetLocator();
}
@Override
protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath,
List<? extends Resource> locations, ResourceResolverChain chain) {
return chain.resolveResource(request, requestPath, locations);
}
@Override
protected String resolveUrlPathInternal(String resourceUrlPath,
List<? extends Resource> locations, ResourceResolverChain chain) {
String path = chain.resolveUrlPath(resourceUrlPath, locations);
if (path == null) {
try {
int startOffset = resourceUrlPath.startsWith("/") ? 1 : 0;
int endOffset = resourceUrlPath.indexOf("/", 1);
if (endOffset != -1) {
String webjar = resourceUrlPath.substring(startOffset, endOffset);
String partialPath = resourceUrlPath.substring(endOffset);
String webJarPath = webJarAssetLocator.getFullPath(webjar, partialPath);
return chain.resolveUrlPath(webJarPath.substring(WEBJARS_LOCATION_LENGTH), locations);
}
}
catch (MultipleMatchesException ex) {
logger.warn("WebJar version conflict for \"" + resourceUrlPath + "\"", ex);
}
catch (IllegalArgumentException ex) {
if (logger.isTraceEnabled()) {
logger.trace("No WebJar resource found for \"" + resourceUrlPath + "\"");
}
}
}
return path;
}
}
......@@ -119,6 +119,7 @@ import org.springframework.web.servlet.resource.ResourceTransformer;
import org.springframework.web.servlet.resource.ResourceUrlProvider;
import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import org.springframework.web.servlet.resource.WebJarsResourceResolver;
import org.springframework.web.servlet.theme.ThemeChangeInterceptor;
import org.springframework.web.servlet.view.BeanNameViewResolver;
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
......@@ -409,10 +410,11 @@ public class MvcNamespaceTests {
assertNotNull(handler);
List<ResourceResolver> resolvers = handler.getResourceResolvers();
assertThat(resolvers, Matchers.hasSize(3));
assertThat(resolvers, Matchers.hasSize(4));
assertThat(resolvers.get(0), Matchers.instanceOf(CachingResourceResolver.class));
assertThat(resolvers.get(1), Matchers.instanceOf(VersionResourceResolver.class));
assertThat(resolvers.get(2), Matchers.instanceOf(PathResourceResolver.class));
assertThat(resolvers.get(2), Matchers.instanceOf(WebJarsResourceResolver.class));
assertThat(resolvers.get(3), Matchers.instanceOf(PathResourceResolver.class));
CachingResourceResolver cachingResolver = (CachingResourceResolver) resolvers.get(0);
assertThat(cachingResolver.getCache(), Matchers.instanceOf(ConcurrentMapCache.class));
......
......@@ -40,6 +40,7 @@ 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 static org.junit.Assert.*;
......@@ -125,12 +126,13 @@ public class ResourceHandlerRegistryTests {
ResourceHttpRequestHandler handler = getHandler("/resources/**");
List<ResourceResolver> resolvers = handler.getResourceResolvers();
assertThat(resolvers.toString(), resolvers, Matchers.hasSize(3));
assertThat(resolvers.toString(), resolvers, Matchers.hasSize(4));
assertThat(resolvers.get(0), Matchers.instanceOf(CachingResourceResolver.class));
CachingResourceResolver cachingResolver = (CachingResourceResolver) resolvers.get(0);
assertThat(cachingResolver.getCache(), Matchers.instanceOf(ConcurrentMapCache.class));
assertThat(resolvers.get(1), Matchers.equalTo(mockResolver));
assertThat(resolvers.get(2), Matchers.instanceOf(PathResourceResolver.class));
assertThat(resolvers.get(2), Matchers.instanceOf(WebJarsResourceResolver.class));
assertThat(resolvers.get(3), Matchers.instanceOf(PathResourceResolver.class));
List<ResourceTransformer> transformers = handler.getResourceTransformers();
assertThat(transformers, Matchers.hasSize(2));
......@@ -144,8 +146,9 @@ public class ResourceHandlerRegistryTests {
ResourceHttpRequestHandler handler = getHandler("/resources/**");
List<ResourceResolver> resolvers = handler.getResourceResolvers();
assertThat(resolvers, Matchers.hasSize(1));
assertThat(resolvers.get(0), Matchers.instanceOf(PathResourceResolver.class));
assertThat(resolvers, Matchers.hasSize(2));
assertThat(resolvers.get(0), Matchers.instanceOf(WebJarsResourceResolver.class));
assertThat(resolvers.get(1), Matchers.instanceOf(PathResourceResolver.class));
List<ResourceTransformer> transformers = handler.getResourceTransformers();
assertThat(transformers, Matchers.hasSize(0));
......@@ -162,10 +165,11 @@ public class ResourceHandlerRegistryTests {
ResourceHttpRequestHandler handler = getHandler("/resources/**");
List<ResourceResolver> resolvers = handler.getResourceResolvers();
assertThat(resolvers.toString(), resolvers, Matchers.hasSize(3));
assertThat(resolvers.toString(), resolvers, Matchers.hasSize(4));
assertThat(resolvers.get(0), Matchers.instanceOf(CachingResourceResolver.class));
assertThat(resolvers.get(1), Matchers.sameInstance(versionResolver));
assertThat(resolvers.get(2), Matchers.instanceOf(PathResourceResolver.class));
assertThat(resolvers.get(2), Matchers.instanceOf(WebJarsResourceResolver.class));
assertThat(resolvers.get(3), Matchers.instanceOf(PathResourceResolver.class));
List<ResourceTransformer> transformers = handler.getResourceTransformers();
assertThat(transformers, Matchers.hasSize(3));
......
/*
* 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 static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
/**
* Unit tests for
* {@link org.springframework.web.servlet.resource.WebJarsResourceResolver}.
*
* @author Brian Clozel
*/
public class WebJarsResourceResolverTests {
private List<Resource> locations;
private WebJarsResourceResolver resolver;
private ResourceResolverChain chain;
@Before
public void setup() {
// for this to work, an actual WebJar must be on the test classpath
this.locations = Collections.singletonList(new ClassPathResource("/META-INF/resources/webjars/"));
this.resolver = new WebJarsResourceResolver();
this.chain = mock(ResourceResolverChain.class);
}
@Test
public void resolveUrlExisting() {
this.locations = Collections.singletonList(new ClassPathResource("/META-INF/resources/webjars/", getClass()));
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);
assertEquals(file, actual);
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
}
@Test
public void resolveUrlExistingNotInJarFile() {
this.locations = Collections.singletonList(new ClassPathResource("/META-INF/resources/webjars/", getClass()));
String file = "/foo/foo.txt";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
assertNull(actual);
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
verify(this.chain, never()).resolveUrlPath("/foo/2.3/foo.txt", this.locations);
}
@Test
public void resolveUrlWebJarResource() {
String file = "/underscorejs/underscore.js";
String expected = "/underscorejs/1.8.2/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);
assertEquals(expected, actual);
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
verify(this.chain, times(1)).resolveUrlPath(expected, this.locations);
}
@Test
public void resolverUrlWebJarResourceNotFound() {
String file = "/something/something.js";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
assertNull(actual);
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册