Spring Boot WebJar
When creating an end to end application
that includes client side code and server side code, we need a full application
solution that can be easily built and deployed.
The client side may be written in different
frameworks – angular, backbone. Spring Boot supports all of these solutions in
multiple ways.
Static Resources
The strait forward way is to copy all
resources from the client code (js, html, css...) to the static folder user the
resources folder (see https://spring.io/blog/2013/12/19/serving-static-web-content-with-spring-boot).
The problem with this solution is that we
are tightly couples between the client and server code. In order to compile the
server code we need to copy the client compiled code to our resources folder.
To use this solution we need to create a
Jenkins job that takes the client code, compiles it (using grunt or other
tools) and then takes the folder that was created and copies it to the
resources folder and then compile the server code.
This is definitely not the maven way. The
solution that we would want is for the client code to create an artifact for us
to add as a dependency in our pom file. The solution that has become a standard
is what is called a webjar (www.webjar.com).
WebJar
A webjar is a zip file with the following
folder structure:
META-INF\resources\webjars\artifactId\version\
When the client requests a resource from
the server, Spring Boot looks in all its resource locations for the resource.
The resource locations are: resource\static, resource\public, webjars.
So far the solution looks like it works
nicely. The next issue is that webjar technology is not just a server side
technology. Webjars come to solve another issue on the client side. When the
client needs global functionality the solution for creating modules is to use
webjars. What this means is that when the client needs a resource from the
server, it needs to specify the artifactId and the version so that the server
can retrieve it from the webjar.
For example:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="webjars/jquery/2.0.3/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$('p').animate({
fontSize: '48px'
}, "slow");
});
</script>
</head>
<body>
<p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>
As you can see the client code references a
version of the modules it wants to use: "webjars/jquery/2.0.3/jquery.min.js".
This might be good in most cases, but the
problem is that in a lot of cases our client side is a single page solution and
does not necessarily need the webjar solution (with the full artifcatId and
version). What then happens is that the client requests a resource but the
server cannot find it since it needs the artifactid and version to find it
within the webjar. We would like the client to request a js file or html file
without the prefix of the webjar and for the server to find it and return it to
the client.
ResourceHandlers
To do this we need to add our own bean that
will supply the lookup for resources for spring.
We are actually going to solve here two
other issues that arise with single page applications and Spring Boot.
In addition to the issue that we don’t want
the client code to need to reference its own webjar, we would like to have the
option to have all the client resources out of our jar, so that we can upgrade
the client code without recompiling the server code.
Another issue with single page applications
is that the client side navigation uses the url path to do this. So if the
client side moves to an admin page, it will usually add to the url “/admin”.
The problem with this is that if the use now refreshes his browser, the server
will get a url request with “/admin” that the server does not have.
To solve this, we need to create another
configuration bean that will inherit WebMvcConfigurerAdapter.
The default implementation of spring is to
search all webjars in the application (see https://spring.io/blog/2014/01/03/utilizing-webjars-in-spring-boot):
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!registry.hasMappingForPattern("/webjars/**")) {
registry.addResourceHandler("/webjars/**").addResourceLocations(
"classpath:/META-INF/resources/webjars/");
}
if (!registry.hasMappingForPattern("/**")) {
registry.addResourceHandler("/**").addResourceLocations(
RESOURCE_LOCATIONS);
}
}
We will change this and override the
addResourceHandler as follows:
override def addResourceHandlers(registry: ResourceHandlerRegistry): Unit = { if (!registry.hasMappingForPattern("/**")) { registry.addResourceHandler("/**").addResourceLocations( classpathResourceLocations: _*) } }
This will now allow us
to define a string array that will tell spring were to search for resources.
Our basic resource
location array will be as follows:
private var classpathResourceLocations = List(
"classpath:/META-INF/resources/", "classpath:/resources/",
"classpath:/static/", "classpath:/public/")
This means, for spring
it will first search in the resources folder of the classpath, and the in
static folder, and lastly in the public folder (standard locations for
resources).
But what we want is to
add is the option to have a resource folder out of our jar and also to add the location
of the webjar within our application without the client having to specific the
full path.
classpathResourceLocations = (externalWebsite :: classpathResourceLocations) :+ s"classpath:/META-INF/resources/webjars/my.web-jar/$infoBuildVersion/"
The order that spring
boot searches for resources is by the order of the string array for the
locations. So the first entry will be a location to an external folder that can
be specified in the application.properties in the format of a url (file:\...).
The second entry will
be my web-jar with the fullpath of the resources inside the webjar. By doing
this the client does not need to send the path, since we have added all the
internal resources without the prefix.
Client Side Paths
The
last issue I wish to address is client side paths. The client side is written
in angular so navigation in the java script is done by urls. So the client can
have a url of : www.server.com\domain\method.
The “method” is not a
endpoint in the server side but an endpoint in the client side. All this works
fine until the user clicks on the page refresh. What then happens is that the URL
is sent to the server, where the serve does not have the endpoint and returns a
page not found error.
To fix this error we
need to add View Controllers path in our WebMvcConfigurerAdapter
as follows:
override def addViewControllers(registry: ViewControllerRegistry): Unit = {
registry.addViewController("/").setViewName("forward:/index.html")
registry.addViewController("/administration").setViewName("forward:/index.html")
registry.addViewController("/license").setViewName("forward:/index.html")
registry.addViewController("/404").setViewName("forward:/index.html")
registry.addViewController("/download").setViewName("forward:/index.html")
}
In this method we will add all client side
URL’s with a forward to the main page of the client indext.html. This will
allow the server to accept the client endpoint and the redirect it back to the
client.
Error endpoint
This all works fine unless the client endpoint
is /error. Since this is a standard endpoint spring automatically implements
this endpoint for us. Since we do not want this we must override spring’s
default implementation as follows. In you configuration bean you need to add:
@Bean(name = Array("error"))
def defaultErrorView() : View = {
new RedirectView("index.html")
}
This implements the
error bean of spring and tells spring to not use the spring implementation but
in our case to redirect the error endpoint back to the client.
Great explanation Chaim.
ReplyDelete