Angular & Spring Boot

Problem

We want to combine Spring and Angular use the Angular developing features but also be able to use the Spring IDE, in the end, everything should be build using maven and nicely packed into a JAR.
Let’s get started.

Overview

  • Create a multi-module project
  • Pack the frontend into an own JAR
  • Include the frontend JAR into the Spring application
  • Add a caching configuration

Less text more code get me to the source.

Multi-Module project

  • Create a new simple Spring project using https://start.spring.io/
  • Generate a new angular project ng new frontend
  • Create a new pom and include frontend and backend
<modules>
  <module>frontend</module> 
  <module>backend</module> 
</modules>

It may make sense to move the spring parent into the root-pom.

Why a multi-module?

You might think why the hack we don’t just pull everything into one maven artifact? Well, separation both may be considered good practice but the real reason is:
We can in that way use, during the development, the best tools for the job. Means use the angular dev server for the front-end and the Spring STS IDE for the backend. Means also just restart/ reload the UI or the Backend if the change that. Even more, build the UI and use the backend running somewhere else.

Build angular with maven

Overall we have here to choose, either we use the frontend-maven-plugin, assuming the CI server has no nodeJS installed or we go with the exec-maven-plugin. The last one is faster assuming the CI server has not to pull nodeJS first. What we want to run is:

  • npm install
  • ng build –prod

Build angular with exec-maven-plugin

<plugin>
    <artifactId>exec-maven-plugin</artifactId>
    <groupId>org.codehaus.mojo</groupId>
    <executions>
        <execution>
            <id>npm install</id>
            <goals>
                <goal>exec</goal>
            </goals>
            <phase>generate-resources</phase>
            <configuration>
                <executable>npm</executable>
                <arguments>
                    <argument>install</argument>
                </arguments>
            </configuration>
        </execution>

        <execution>
            <id>angular-cli build</id>
            <goals>
                <goal>exec</goal>
            </goals>
            <phase>compile</phase>
            <configuration>
                <executable>ng</executable>
                <arguments>
                    <argument>build</argument>
                  	<!-- only neded in old versions -->
                    <!-- <argument>--prod</argument> -->
                    <argument>--output-path</argument>
                    <argument>${project.build.outputDirectory}/META-INF/resources/webjars/${project.artifactId}/${project.version}</argument>
                </arguments>
            </configuration>
        </execution>
    </executions>
</plugin>

This will not delete the node_modules folder; which is intended. As this takes usually some unnecessary time.

Note: We added here already the --output-path to emulate a kind of web-jar, we will use this later to configure the Spring resource handler. We could of course remove the version here.

Add node_modules to mvn clean

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-clean-plugin</artifactId>
	<configuration>
		<filesets>
			<fileset>
				<directory>node_modules</directory>
			</fileset>
		</filesets>
	</configuration>
</plugin>

Include the Angular frontend in Spring

Again we have two ways to go, we could either include the JAR file and configure the resource handler, or just copy the files over into the static folder of spring during the build. The last one is very easy the first one has the advantage that we could include easily multiple angular JARs into our project.

Include the frontend as JAR

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>frontend</artifactId>
    <version>${project.version}</version>
</dependency>

In your Spring application add implements WebMvcConfigurer. This will allow you to extend

@Autowired private BuildProperties buildProperties;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    final String jarResourcePath = "classpath:/META-INF/resources/webjars/frontend/" + buildProperties.getVersion() + "/";
        // no caching for HTML pages
        registry.addResourceHandler("/ui/*.html")
                .setCacheControl(CacheControl.noStore().sMaxAge(0, TimeUnit.SECONDS).mustRevalidate())
                .setCachePeriod(0)
                .addResourceLocations(jarResourcePath);

        // 365 days for JS and CSS files, which have a hash code in the file name 
        // generated by angular
        registry.addResourceHandler("/ui/*.js", "/ui/*.css")
                .addResourceLocations(jarResourcePath)
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
                .resourceChain(true);
        
        // apply custom config as needed to the remaining resources
        registry.addResourceHandler("/ui/**")
                .addResourceLocations(jarResourcePath)
                .setCacheControl(CacheControl.maxAge(7, TimeUnit.DAYS))
                .resourceChain(true);
	
}
Note: We added here already a caching configuration.

In addition we have to tell Spring that by default if anybody opens / we want to display the index.html. This is only needed if we include the angular app as JAR, as we can just delete the static folder.

public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("forward:/index.html");
}

// in the addResourceHandlers we can also alternatively add the view ot the base e.g. 
// / or ui/
// handy in case of PathLocationStrategy
registry.addResourceHandler("/")
    .setCacheControl(CacheControl.noStore().sMaxAge(0, TimeUnit.SECONDS).mustRevalidate()).setCachePeriod(0)
    .addResourceLocations(jarResourcePath + "index.html");

Copy the Angular files

The most simple way to just include the angular into the build JAR is to copy them during the package phase into the static directory from Spring. From where it will just behave like a regular application.

<plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <executions>
        <execution>
            <id>copy frontend resources</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.outputDirectory}/static</outputDirectory>
                <resources>
                    <resource>
                        <directory>../frontend/target/classes/META-INF/resources/webjars/frontend/${project.version}</directory>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>

If we copy the resources, we can just set optional onto the frontend JAR or provided. The only reason we keep it in the pom.xml is just to ensure that it gets build first by maven; even if somebody re-sorts the modules in the root pom.

Note: If we copy the resources we can also remove the addResourceHandlers configuration from the app. As we don’t need to tell Spring to check the JAR file too.

Handling HTML 5 routes in Spring

Using a Regex (not recommended)

A common problem is to handle any HTML5 routes from angular, if why are directly opened in the browser. We don’t really want to add a view handler for each angular route. We have just to add a simple @Controller with the following code.

@RequestMapping(value = "/{[path:[^\\.]*}")
public String redirect() {
    return "forward:/index.html";
}

Using a fallback route

registry.addResourceHandler("/**").addResourceLocations(jarResourcePath)
        .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)).resourceChain(true)
        .addResolver(new PathResourceResolver() {
            // fall back to our index.html for any other stuff
            // to support angular PathLocationStrategy
            @Override
            protected Resource getResource(String resourcePath, Resource location) throws IOException {
                final Resource requestedResource = location.createRelative(resourcePath);
                Resource result = requestedResource;
                if (!requestedResource.exists()) {
                    result = new ClassPathResource("/META-INF/resources/index.html");
                }
                return result;
            }
        });

Custom UI base path

Another solution would be to publish the API and the UI with different prefixes e.g.:

  • /api/* or /rest/* for all REST services
  • /ui/* for the angular UI

This makes the controller match clearer and more predictable if also other services like the Swagger UI etc. are hosted too. But it requires some adjustments too:

  • base href for the HTML5 angular navigation has to be set --base-href /ui/
  • The resource handler above need have /ui/* prefix
  • We need a filter or a controller
// custom filter, which does the same as the controller
@Bean
public OncePerRequestFilter angularForwardFilter() {
    return new OncePerRequestFilter() {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            final String requestURI = request.getRequestURI();
            if (requestURI.startsWith("/ui") && !requestURI.endsWith("/ui/index.html") && !requestURI.contains(".")) {
                request.getRequestDispatcher("/ui/index.html").forward(request, response);
            } else {
                filterChain.doFilter(request, response);
            }
        }
    };
}
// custom controller class with a regex instead of the OncePerRequestFilter
@Controller
@RequestMapping("/ui")
public class AngularUiController {
    @GetMapping("{[path:[^\\.]*}")
    String index() {
        return "forward:/ui/index.html";
    }
}

Explicit match

Last but not least we can, of course, duplicate any angular route in our request mapping.

@RequestMapping({"/", "/home", "/home/*", "/person", "/person/list"})
public String redirect() {
    return "forward:/index.html";
}

Avoid reloading the backend on frontend changes

Including the dev-tools from Spring will unfortunately also reload the backend if we change the frontend. We could of course just remove the frontend JAR dependency but we could also tell Spring to stop. For this we have to create the spring-devtools.properties in the META-INF directory telling Spring not to do so:

restart.exclude.frontend=.*/frontend/.*

Proxy /api calls to the backend

Now we have to forward any calls going to /api to our backend otherwise we cannot get any data from any REST call during the usage of the angular dev web-server. For that, we have to add the proxy.conf.json to the project

{
    "/api": {
        "target": "http://localhost:8080",
        "secure": false
    }
}

and extend package.json to add a short cut add the proxy config if we run ng serve.

{
  "scripts": {
    "start": "ng serve --proxy-config proxy.conf.json",

Now we can run the Angular dev server with the proxy config using npm start.

Links

Paul Sterl has written 51 articles

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>