question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Efficient webjars version resolution natively in Spring

See original GitHub issue

Spring (MVC and Webflux) has a ResourceResolver abstraction that can be used to resolve the versions in webjars, avoiding the need to maintain the version explicitly in 2 or more places (build file and HTML source). E.g. (from Petclinic):

 <script src="/webjars/jquery/jquery.min.js"></script>

Resolves to classpath:/META-INF/resources/webjars/jquery/<version>/jquery.min.js at runtime.

Spring Boot carries the responsibility of configuring the resource resolver, and currently it uses the webjars-locator-core (https://github.com/webjars/webjars-locator-core) library to do that, so version resolution only works if that library is on the classpath. The WebJarsAssetLocator from that library has a very broad and powerful API for locating files inside webjars, but there are some issues, namely:

  1. It is fairly inefficient, since it scans the whole /META-INF/resources/webjars classpath on startup (in a constructor!).
  2. It has 2 awkward dependencies (github classpath scanner and jackson)
  3. It doesn’t work in a native image (https://github.com/spring-projects-experimental/spring-native/issues/157) because of the classpath scanning

But we don’t need webjars-locator-core to just do version resolution, which is all Spring Boot offers, because webjars have a very well-defined structure. They all have a pom.properties with the version in it, and they only use a handful of well-known group ids, so they are easy to locate. It might be a good idea to implement it in Framework, since it is so straightforward and only depends on reading resources from the classpath.

All of the issues above could be addressed just by providing a simpler version resolver natively (and configuring the resource config in a native image with a hint).

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:23 (19 by maintainers)

github_iconTop GitHub Comments

5reactions
dsyercommented, Oct 28, 2021

Here’s an implementation (with no caching or any optimizations):

public class WebJarsVersionResourceResolver  extends AbstractResourceResolver {

	private static final String PROPERTIES_ROOT = "META-INF/maven/";
	private static final String NPM = "org.webjars.npm/";
	private static final String PLAIN = "org.webjars/";
	private static final String POM_PROPERTIES = "/pom.properties";

	@Override
	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
	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) {
		String webjar = webjar(path);
		if (webjar.length() > 0) {
			String version = version(webjar);
			// A possible refinement here would be to check if the version is already in the path
			if (version != null) {
				String partialPath = path(webjar, version, path);
				if (partialPath != null) {
					String webJarPath = webjar + File.separator + version + File.separator + partialPath;
					return webJarPath;
				}
			}
		}
		return null;
	}

	private String webjar(String path) {
		int startOffset = (path.startsWith("/") ? 1 : 0);
		int endOffset = path.indexOf('/', 1);
		String webjar = endOffset != -1 ? path.substring(startOffset, endOffset) : path;
		return webjar;
	}


	private String version(String webjar) {
		Resource resource = new ClassPathResource(PROPERTIES_ROOT + NPM + webjar + POM_PROPERTIES);
		if (!resource.isReadable()) {
			resource = new ClassPathResource(PROPERTIES_ROOT + PLAIN + webjar + POM_PROPERTIES);
		}
		// Webjars also uses org.webjars.bower as a group id, so we could add that as a fallback (but not so many people use those)
		if (resource.isReadable()) {
			Properties properties;
			try {
				properties = PropertiesLoaderUtils.loadProperties(resource);
				return properties.getProperty("version");
			} catch (IOException e) {
			}
		}
		return null;
	}

	private String path(String webjar, String version, String path) {
		if (path.startsWith(webjar)) {
			path = path.substring(webjar.length()+1);
		}
		return path;
	}
}

and here’s how to install it in a Spring Boot application:

	@Bean
	public WebMvcConfigurer configurer() {
		return new WebMvcConfigurer() {
			@Override
			public void addResourceHandlers(ResourceHandlerRegistry registry) {
				registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:META-INF/resources/webjars/").resourceChain(true).addResolver(new WebJarsVersionResourceResolver());
			}
		};
	}

(See it work in the Petclinic here: https://github.com/dsyer/spring-petclinic/blob/webjars/src/main/java/org/springframework/samples/petclinic/system/WebJarsVersionResourceResolver.java.)

4reactions
dreis2211commented, Oct 13, 2022

@dsyer said “It is fairly inefficient”, which is very polite of him but I want to stretch on that a bit:

I was profiling a test-suite the other day whose allocations flame-graphs have ~63% of the frames only matched by classgraph scanning that is entirely caused by resolving/locating webjars. image

Now of course this doesn’t translate to CPU 1:1 where it’s only ~10%, but notice how much is spent in G1 garbage collection on top of that (unsurprisingly). image

A test-suite is obviously not a production environment where this isn’t as noticeable. But tests usually start several contexts and the general startup routine is executed more often usually. So working on improving that inside Spring directly might be a tremendous boost in developer productivity for certain projects, because it will directly impact test suites, startups etc…

Read more comments on GitHub >

github_iconTop Results From Across the Web

Spring Boot Reference Documentation
Try the How-to documents. They provide solutions to the most common questions. Learn the Spring basics. Spring Boot builds on many other Spring...
Read more >
Introduction to WebJars - Baeldung
A quick and practical guide to using WebJars with Spring. ... and use it to automatically resolve the version of any WebJars assets....
Read more >
Springfox Reference Documentation - GitHub Pages
Springfox works by examining an application, once, at runtime to infer API semantics based on spring configurations, class structure and various ...
Read more >
Juzu Reference Guide
Provided Spring; Provided CDI. Templating. The templating engines. The native template engine; The Mustache template engine. Using templates.
Read more >
Spring Boot Reference Guide
Although Spring Boot is compatible with Java 1.6, if possible, you should consider using the latest version of Java. 10.1 Installation instructions for...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found