Spring Boot + Grunt + Bower

After I wrote some comments about the integration of Grunt + Bower with Maven, it happens that I had to apply this in the context of Spring Boot.

Out of the box, Spring Boot allows you serve some static content by placing it in the resources under /static (or /public, or other locations, but let's stick to /static in this blog entry). It is all very nice if you need only to serve a few pages and files, but if you need to code a complex Javascript application and wish to use proper tools like Grunt or Bower, you may have an issue.

It seems obvious that you cannot setup your Grunt project directly under the /src/main/resources/static directory. That would mean an awkward SCM setup and an even more awkward URL pattern.

What I chose to do was to create a separate Maven module to hold my Grunt project, and to follow the guidelines I had established previously, with a few differences.

The general idea is that in development mode, the Spring Boot application would serve the static content directly from the target/dev directory of the Grunt module, and that in release mode, the target/prod directory of the Grunt module would be copied into the /src/main/resources/static directory of the Spring Boot module, and packaged together with the resulting JAR file.

Development mode

In development mode, I needed to find a way to tell Spring Boot to serve the static content from the target/dev directory of the Grunt module. It is easily done by defining a ResourceHandler for the development profile only:

@Configuration
@Profile("dev")
public class DevWebConfig extends WebMvcConfigurerAdapter {

@Autowired
private DevSettings devSettings;

/**
 * At development time, we want the static resources served directly
 * from the <code>myproject-web</code> project, under the <code>target/dev</code>
 * directory.
 */
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    String staticDir = devSettings.getStaticDir();
    String prefix = "file:";
    registry.addResourceHandler("/app/**").addResourceLocations(prefix + staticDir + "/app/");
    registry.addResourceHandler("/assets/**").addResourceLocations(prefix + staticDir + "/assets/");
    registry.addResourceHandler("/vendor/**").addResourceLocations(prefix + staticDir + "/vendor/");
    registry.addResourceHandler("index.html").addResourceLocations(prefix + staticDir + "/index.html");
    }
}

Additionally, I needed a configuration object to map the application properties:

@Component
@ConfigurationProperties(prefix = "myproject.dev")
public class DevSettings {
    private String target;
    private String staticDir;
    // Getters & setters...
}

In the application-dev.properties, activated by the dev profile, I just define the following properties:

myproject.dev.target=target/dev
myproject.dev.staticDir=${user.dir}/myproject-web/${myproject.dev.target}

When I want to run the application, I just have to run the Web module in watch mode:

cd myproject-web
grunt watch

and to launch my Boot application with dev profile enabled by adding --spring.profiles.active=devto the program arguments.

Release mode

When the application has to be released, I active a release Maven profile in order to enable the following code in the Spring Boot POM:

<profile>
    <id>release</id>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-resources-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>release-web-resources</id>
                            <phase>compile</phase>
                            <goals>
                                <goal>copy-resources</goal>
                            </goals>
                            <configuration>
                                <outputDirectory>
                               ${project.basedir}/src/main/resources/static
                                </outputDirectory>
                                <resources>
                                    <resource>
                                        <directory>
                                      ../myproject-web/target/prod
                                        </directory>
                                    </resource>
                                </resources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</profile>

I have added the myproject-web Grunt module into my dependencies in order to make sure it is ready before the copy:

<dependency>
  <groupid>${project.groupId}</groupid>
  <artifactid>myproject-web</artifactid>
  <version>${project.version}</version>
  <scope>runtime</scope>
</dependency>

And that's it! Again, in order to see the details of how the Grunt project itself is integrated with the Maven lifecyle, have a look at Grunt + Bower with Maven.