Angular, Spring Boot, Maven, and all that Jazz

This post has 2539 words. Reading it will take approximately 13 minutes.

Angular is the “new” hotness is web-development, and it is easy to see why: web-developers (lol) are idiots. Simultaneously, Spring Boot is the way to develop cloud-native enterprise-ready applications in a Java ecosystem. It just seems nobody on the internet has thought to bring these two great tastes together yet. Sure, there’s (as always) a great Baeldung writeup about it, but it assumes you want to keep the two separate like some sort of apartheid for applications. We don’t want that, and in this post, I’ll go over a way to package up your Spring Boot applications with Angular user-interfaces using Maven in a way that feels natural to Java developers and web-developers (lol) alike.

Basic Setup

First, we want a (sort-of) sane folder structure. Maven keeps sources under src/ and binaries under target/. Angular, keeps everything is a big mess in a root folder, and binaries in a dist/ folder under that. To unify that, we’ll move the Angular project as a sub-folder of src/; to keep things clean, we put it in src/main/<module>, where <module> is the name of our module.

To build our application, we want everything to be buildable using a single command. So no “npm build” and then “mvn compile.” To achieve this, we use the excellent maven-frontend plugin, which can download and run npm, ng and other tools developed by a committee of web-developers (lol):

<plugin>
	<groupId>com.github.eirslett</groupId>
	<artifactId>frontend-maven-plugin</artifactId>
	<version>1.9.1</version>
	<configuration>
		<workingDirectory>src/main/module/</workingDirectory>
	</configuration>
	<executions>
		<!-- Install npm + ng -->
		<execution>
			<id>install-node-and-npm</id>
			<goals>
				<goal>install-node-and-npm</goal>
			</goals>
			<configuration>
				<nodeVersion>${node.version}</nodeVersion>
				<npmVersion>${npm.version}</npmVersion>
			</configuration>
			<phase>initialize</phase>
		</execution>
		<execution>
			<id>npm-install</id>
			<goals>
				<goal>npm</goal>
			</goals>
			<configuration>
				<arguments>install</arguments>
			</configuration>
		</execution>
		<!-- Set main + library versions to match artifact -->
		<execution>
			<id>npm-version</id>
			<goals>
				<goal>npm</goal>
			</goals>
			<configuration>
				<arguments>version --allow-same-version ${project.version}</arguments>
			</configuration>
			<phase>generate-resources</phase>
		</execution>
		<!-- Build -->
		<execution>
			<id>npm-build</id>
			<goals>
				<goal>npm</goal>
			</goals>
			<configuration>
				<arguments>run build</arguments>
			</configuration>
			<phase>generate-resources</phase>
		</execution>
	</executions>
</plugin>

We configure the plugin to run in our module directory (l. 6) and install npm + ng (ll. 9-29). In lines 30-40 we make the module update the version of the Angular module to match that of the Maven project. That way, we keep versioning in a single place. The build (ll. 41-51) is pretty standard and could probably be left out, but it retained to allow customization.

The Angular build leaves some built artifacts in /src/main/module/dist (and also in src/main/module/node_modules). At least the first one, we’d like to clean with Maven artifacts; the second we’ll probably want to keep around to avoid having to resolve thousands of Angular modules on each clean build, but your use case may differ. To ensure we clean out the slightly mislocated, Maven-wise, artifacts, we add the following section to the POM:

<!-- Remove Angular junk on clean -->
<plugin>
	<artifactId>maven-clean-plugin</artifactId>
	<version>3.1.0</version>
	<configuration>
		<filesets>
			<fileset>
				<directory>src/main/module/dist</directory>
				<followSymlinks>false</followSymlinks>
			</fileset>
		</filesets>
	</configuration>
</plugin>

To allow simple serving of our artifacts, we want to attach them to our project. Spring Boot automatically adds a number of folders as resource folders. That means, Spring Boot will automatically serve up files from those folders without any processing. This is perfect for Angular applications, so we just add the distribution to the list of resources as such:

<resources>
	<resource>
		<directory>src/main/module/dist/module</directory>
		<targetPath>static</targetPath>
		<includes><include>**/*</include></includes>
	</resource>
	<resource>
		<directory>src/main/resources</directory>
	</resource>
</resources>

We add the regular resources (ll. 7-9) and mount our Angular distributable under the /static folder which is served automatically by Spring Boot (ll. 2-6).

We need to make sure that the Angular application expects to run from the proper base URL matching that of our Spring Boot application. If that is / we don’t need to do anything, but if it is, say, /application-name, we need to alter our package.json, altering line 4:

	"scripts": {
		"ng": "ng",
		"start": "ng serve",
		"build": "ng build --prod --base-href /application-name/",
		"test": "ng test",
		"lint": "ng lint",
		"e2e": "ng e2e"
	},

Now, our application pretty much just works; we can build Angular artifacts using all the web-developer (lol) tools and serve them automatically in our Spring Boot application. Pretty much everything “just works,” i.e., the Angular development server works for quick development of the web-interface, the Spring Boot devtools works for code changes.

Angular Modules

If you develop larger applications, at some point, you might want to also use shared libraries to bundle shared code/resources. For Java applications, Maven makes this easy, but Angular has its own ideas of how that works (and doesn’t work).

To make an Angular library, we create an empty Angular namespace and a library inside it using

ng new library-name --create-application=false
cd library-name
ng generate library library-name

This creates a project (library-name) containing no application, but with one library (also called library-name). The project and library can of course have different names, but in my case, I want just a single library, so they don’t.

I assume the library resides in a different Maven module to the application project above; this new module also has a maven-frontend section which in addition to the stuff from the application also includes:

<plugin>
	<groupId>com.github.eirslett</groupId>
	<artifactId>frontend-maven-plugin</artifactId>
	<version>1.9.1</version>
	<configuration>
		<workingDirectory>src/main/library-name/</workingDirectory>
	</configuration>
	<executions>
		...
		<!-- Set main + library versions to match artifact -->
		<execution>
			<id>npm-version-commons-web</id>
			<goals>
				<goal>npm</goal>
			</goals>
			<configuration>
				<workingDirectory>src/main/library-name/projects/library-name</workingDirectory>
				<arguments>version --allow-same-version ${project.version}</arguments>
			</configuration>
			<phase>generate-resources</phase>
		</execution>
		<!-- Pack up individual libraries -->
		<execution>
			<id>npm-pack-library-name</id>
			<goals>
				<goal>npm</goal>
			</goals>
			<configuration>
				<workingDirectory>src/main/library-name/dist/library-name</workingDirectory>
				<arguments>pack</arguments>
			</configuration>
			<phase>package</phase>
		</execution>
	</executions>
</plugin>

We use the library workspace as working directory (l. 6). Since we have an Angular workspace with a library, we need to also set the library version (ll. 10-21); note that this is in addition to, not instead of, setting the version of the module itself as in the application example at the top. We also add a step to build a npm module of the application during the maven package phase (ll. 22-33).

In my case, I did not want to publish the npm module to an npm repository. Basically, the code is not open source, and I don’t want to bother setting up a private npm repository as we already have a Maven repository. If you want to publish to an npm repository, you can skip some of the next parts and have to replace them by other parts, but you’re on your own there. Instead, we want to attach the npm module to our Maven artifacts so it gets published directly into our Maven repository. We do that by adding this to our POM:

<!-- Attach Angular artifacts -->
<plugin>
	<groupId>org.codehaus.mojo</groupId>
	<artifactId>build-helper-maven-plugin</artifactId>
	<version>3.0.0</version>
	<executions>
		<execution>
			<id>attach-artifacts</id>
			<phase>package</phase>
			<goals>
				<goal>attach-artifact</goal>
			</goals>
			<configuration>
				<artifacts>
					<artifact>
						<file>src/main/commons-web/dist/library-name/library-name-${project.version}.tgz</file>
						<type>tgz</type>
					</artifact>
				</artifacts>
			</configuration>
		</execution>
	</executions>
</plugin>

This allows us to easily fetch artifacts using Maven and whatever credentials are already set up. It also works running locally using SNAPSHOT versions, so it feels reasonably natural and allows fast turnaround.

To use our library, we need to include it in our application POM. This means we can use both shared application code and UI code in a single go. In our application POM, we add:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-dependency-plugin</artifactId>
	<version>3.1.1</version>
	<executions>
		<execution>
			<id>copy</id>
			<phase>initialize</phase>
			<goals>
				<goal>copy</goal>
			</goals>
		</execution>
	</executions>
	<configuration>
		<artifactItems>
			<artifactItem>
				<groupId>nl.customer</groupId>
				<artifactId>library-name</artifactId>
				<version>${nl.customer.library-name.version}</version>
				<type>tgz</type>
				<overWrite>true</overWrite>
				<destFileName>library-name.tgz</destFileName>
			</artifactItem>
		</artifactItems>
		<outputDirectory>${project.build.directory}/angular</outputDirectory>
		<overWriteReleases>false</overWriteReleases>
		<overWriteSnapshots>true</overWriteSnapshots>
	</configuration>
</plugin>

I just download the artifact using the regular Maven identification (ll. 17-19). I configure the version using a property (l. 19) defined earlier so I can use the same to load the Java bits. I put the file under target/angular (l. 25) and give it a predictable name (l. 22). This allows me to include it in my Angular project using a fixed path in package.json:

	"dependencies": {
		"@angular/animations": "~8.2.14",
		...
		"zone.js": "~0.9.1",
		"library-name": "../../../target/angular/library-name.tgz"
	},

This allows my to load the file with the correct version on the build server. The maven-dependency plugin makes sure to fetch the proper artifact from the Maven repository and place it is a recognizable location, which is then automatically picked up by npm install in the application. There is one small issue, though: if I make changes to the library locally after doing npm install in the application, no amount of cleaning will update the module. A way to circumvent this is to filter out the entry for library-name from the package-lock.json file. We do that by adding a postschrinkwrap hook to package.json of the application:

	"scripts": {
		...
		"postshrinkwrap": "cat package-lock.json | jq '. | with_entries(if .key == \"dependencies\" then { key: .key, value: .value | with_entries( if .key == \"library-name\" then empty else . end ) } else . end)' > tmp && mv tmp package-lock.json"
	},

This elegant one-liner depends on the jq command and just filters out the secting locking the version of the library-name dependency. That way, it will be installed from scratch every time npm install is run. Now, we can just do a mvn install in our library directory and a mvn install in the application directory, and the depdency is properly reloaded, also for SNAPSHOT versions.

Sharing Resources in Angular

One of the reasons, I want an Angular library is to share resources (images, fonts, Javascript, CSS, Angular components). Being developed by web-developers (lol), Angular barely supports this and the solutions online are frankly hilarious (which is a nice way of saying insane). Basically, libraries cannot contain assets, and applications cannot import assets that have been bundled with libraries anyway.

Tow work around this insanity, the accepted solution in the web-developer (lol) community is to manually copy resources into the dist folder of a library and copy them into the application. To do that, I created a generic shall script for libraries, copy_assets.sh:

#!/bin/bash

for SRC in projects/*/src/{assets,scripts,styles}; do
	TMP="${SRC/projects/dist}"
	DST="${TMP/\/src/}"
	if [ -e "$DST" ]; then
		rm -Rf "$DST"
	fi
	if [ -d "$SRC" ]; then
		cp -R "$SRC" "$DST"
	fi
done

This copies everything under src/assets, src/scripts, src/styles under any library in our library folder into the appropriate sub-folders under dist. To achieve single-command builds that work for both Angular and Maven, I alter the build script in package.json of the library:

	"scripts": {
		...
		"build": "ng build && ./copy_assets.sh"
	},

This makes sure that the script is called whenever we run a npm build, including when this is done from Maven. Of course, the shell script only works on Posix platforms (so Linux and OS X are good, Windows is not).

To make this work on the application side, we alter our build options in angular.json in the application:

"assets": [
	"src/favicon.ico",
	"src/assets"
],
"styles": [
	"src/styles.css",
	"node_modules/library-name/styles/bootstrap.min.css",
	"node_modules/library-name/styles/styles.css"
],
"scripts": [
	"node_modules/library-name/scripts/jquery.min.js",
	"node_modules/library-name/scripts/bootstrap.min.js",
	"node_modules/library-name/scripts/scripts.js"
]

We directly include styles (ll. 7-8) and scripts (ll. 11-13) from our library module. We can include styles and scripts directly from the modules having been copied in there by our script, but unfortunately we have to import each script individually (as far as I can tell, if I find a good way around that I’ll make a new post). There may be a way around that by using SCSS or Javascript includes.

What there is no way around is that we cannot import assets (images./fonts) directly from libraries. Because fuck you, that’s why. Instead, we have to manually copy them into the assets folder of our application. Yes, that’s insane but that’s what happens if you ask web-developers (lol) do do anything more complicated than not shitting their pants during a 5 minute meeting. To automate this, I created a script similar to the one for copying library resources into the dist folder living in the application as copy_lib_assets.sh:

#!/bin/bash

MODULE=application-name
FOLDERS="assets"

while [ ! -z "$1" ]; do
	for FOLDER in $FOLDERS; do
		DST="dist/$MODULE/$FOLDER/"
		SRC="node_modules/$1/$FOLDER/"
		if [ -d "$SRC" ]; then
			echo "$SRC -> $DST"
			mkdir -p dist/$MODULE/$FOLDER
			rsync -r "$SRC" "$DST"
		fi
	done
	shift
done

for FOLDER in $FOLDERS; do
	DST="dist/$MODULE/$FOLDER/"
	SRC="src/$FOLDER/"
	if [ -d "$SRC" ]; then
		echo "$SRC -> $DST"
		mkdir -p dist/$MODULE/$FOLDER
		rsync -r "$SRC" "$DST"
	fi
done

This script is not completely generic and requires configuration of the application name (l. 3) and the folders to include (l. 4). It copies in assets and allows us to override assets in the main application. The script is called with the libraries we need to process, from the application package.json:

	"scripts": {
		...
		"build": "ng build --prod --base-href /application-name/ && ./copy_lib_assets.sh library-name"
	},

And that, finally, is it. Using this setup, I can locally develop making use of npm for simple Angular things and using Maven for managing inter-module dependencies and versioning. I can develop locally using SNAPSHOT dependencies, and get reproducible builds on my CI server including managing binary artifacts in a single repository.

3 thoughts on “Angular, Spring Boot, Maven, and all that Jazz

  1. Nice and elaborate article, Michael.

    A normal (sane) person would of course build an rpm, separating the Angular dist that you build with the front-end maven plug-in from the backend (fat-jar), deploying the front-end in a web server (nginx, apache) and installing the backend as a service. For local development, you of course use the local development capabilities Angular already has, knowing your IDE is perfectly capable to support two work streams. But, sure, why not go crazy and put it all in one single spot. 😉

    1. Thanks! I can see the separation between frontend and backend, but that gets less meaningful or at least with different tradeoffs in a cloud environment. There, you’d not have a web-server or services, but rather Docker images and pods. Then, your frontend would be one image and the backend another.

      That architecture still makes a ton of sense for a complex application, but increases complexity in a small application. For just a couple of pages and a handful services, suddenly you need to make sure that two images and their corresponding configuration are synchronised across 2-4 or more environments. Bundling everything up in a single image reduces that complexity as the frontend and backend are automatically in sync.

      I guess it all comes down to how you slice up your application; here, in the underlying case, we have separate modules each packing up their own functionality, database and UI, so it is nice to keep everything together. Basically, it is microservices for not only the backend but also the frontend.

      I can easily imagine building the backend as microservices and externally viewing it as a collection of APIs (possibly via an ESB, API manager or service mesh depending on how fancy you want to sound 🙂 ) consumed by a single frontend, multiple frontends, or frontends sliced up differently from the backend, and there the traditional way of deploying frontend and backend separately makes more sense and comes with advantages like independent scaling of frontend and backend.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.