Introduction
In this post I’ll show you how to serve an Angular (>2) frontend application using a Java Spring Boot application as the backend and static page web server, and a Gradle script to perform automated build and deploy tasks both for Spring and Angular.
Requirements
The technology stack required for this tutorial is:
The main goal is to achieve a npm install + build that integrates with Java’s gradle build and deploys the distribution files into Spring’s static resources directory. You could replace Angular CLI for any other Frontend build tool / framework such as React.
Backend
The backend of the stack will be a Spring Boot application.
We can generate a bootstrapped application using Spring’s Initializr utility as follows:
Select the basic options for a Gradle Java Project with support for Web and click “Generate Project” to download an initial application.
Next extract the project to a location of your choice.
In order to test frontend and backend integration, we are going to build a simple REST endpoint that generates a “Hello world!” String.
Create a new class (e.g. TestController) and modify it as follows:
1 2 3 4 5 6 7 8 |
@RestController() @RequestMapping(path="/api") public class TestController { @GetMapping(path = "/hello-world") public String helloWorld(){ return "Hello world!"; } } |
Frontend
Once the Spring bootstrapped application has been extracted and modified, we must initialize the Frontend application.
In this case we are going to use angular-cli to prepare a bootstrapped empty Angular application.
The Angular application should be placed in the project’s directory ./src/main/webapp. The easiest way to achieve this is to open a terminal, navigate to ./src/main in the project’s root and execute the following Angular CLI command:
1 |
ng new webapp |
Angular build configuration
Some additional settings must be made to the default angular-cli.json configuration in order to ease the deployment of the application with Spring Boot.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { "name": "Demo Application" }, "apps": [ { "root": "src", "outDir": "dist/static", "assets": [ "assets", "favicon.ico" ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "test": "test.ts", "tsconfig": "tsconfig.app.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, "lint": [ { "project": "src/tsconfig.app.json" }, { "project": "src/tsconfig.spec.json" }, { "project": "e2e/tsconfig.e2e.json" } ], "test": { "karma": { "config": "./karma.conf.js" } }, "defaults": { "styleExt": "css", "component": {} } } |
apps.outDir property should be changed from “dist” to “dist/static”. Also the project.name property should be changed from “webapp” to the real name of the application.
Backend integration
In order to test integration with the Backend, we are going to make some simple modifications to the newly created application to connect to the REST endpoint we just created:
1 2 3 4 5 |
<h1> {{title}} </h1> <button (click)="sayHello()">Say Hello</button> <span>{{result}}</span> |
We’ve added to the component’s html a button to trigger the REST request, and a placeholder to show the result.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { Component } from '@angular/core'; import { Http } from '@angular/http'; import { Observable } from 'rxjs'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app works!'; result = ''; constructor(private http: Http){ } private sayHello(): void { this.result = 'loading...'; this.http.get(`/api/hello-world`).subscribe(response => this.result = response.text()); } } |
We’ve added a function that will be triggered by the button to call the REST endpoint and show the result in the placeholder.
Gradle Build Script
Finally we are going to modify the build.gradle script to create frontend integration tasks.
The resulting script looks as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
buildscript { ext { springBootVersion = '1.5.4.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile('org.springframework.boot:spring-boot-starter-web') testCompile('org.springframework.boot:spring-boot-starter-test') } def webappDir = "$projectDir/src/main/webapp" sourceSets { main { resources { srcDirs = ["$webappDir/dist", "$projectDir/src/main/resources"] } } } processResources { dependsOn "buildAngular" } task buildAngular(type:Exec) { // installAngular should be run prior to this task dependsOn "installAngular" workingDir "$webappDir" inputs.dir "$webappDir" // Add task to the standard build group group = BasePlugin.BUILD_GROUP // ng doesn't exist as a file in windows -> ng.cmd if (System.getProperty("os.name").toUpperCase().contains("WINDOWS")){ commandLine "ng.cmd", "build" } else { commandLine "ng", "build" } } task installAngular(type:Exec) { workingDir "$webappDir" inputs.dir "$webappDir" group = BasePlugin.BUILD_GROUP if (System.getProperty("os.name").toUpperCase().contains("WINDOWS")){ commandLine "npm.cmd", "install" } else { commandLine "npm", "install" } } |
We’ve included a new variable webappDir that points to the Frontend’s application directory.
sourceSets have been modified to include Andular’s dist directory where the application is packaged and deployed by Angular CLI.
We’ve added a dependency to the standard processResources task to one of our newly generated tasks: buildAngular.
Two new tasks have been created. buildAngular will run ‘ng build’ command. This task will be added to the standard build tasks and be called after installAngular task,
It’s important to notice that both these tasks should execute slightly different commands when run on Windows. Neither npm nor ng exist as executables in windows, they are both cmd scripts that can be run in the console just by passing their name (without extensions) but are really .cmd scripts. In order for them to be run as an Exec task, full name of the executable must be provided.
Finally, installAngular task will run npm install before any other task related with processing resources is executed.
Download sources
You can find a full working project with the sources for this tutorial here: https://github.com/marcnuri-demo/angularboot
To get up and running (it takes a while, npm must download Angular dependencies [not verbose]):
1 2 |
git clone https://github.com/marcnuri-demo/angularboot.git gradle bootRun |
Open http://localhost:8080 in your browser
Very nice blog, exactly what I was looking for, thanks for such a simplified and understanding blog.
It’s impressive how fast and simple it is to get Gradle to manage a diverse set of tools. Thank you for this useful post.
Thank you for the blog and useful information. Can some guidance be provided how this code is built and deployed for use? Thank you
all sorted…was having a small problem with angular. Thank you!!
Thanks
Nice. Inspired me. Thx
This is an excellent tutorial! Is there any chance you could update to use Angular6? I’m having difficulty transitioning form the angular-cli.json contained here to the new angular.json format of Angular6.