Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Integrating Java and npm Builds Using Gradle

DZone's Guide to

Integrating Java and npm Builds Using Gradle

We take a look at building a Java-based app that serves up a JavaScript/npm-based app as a static resource, all using Gradle.

Free Resource

Access over 20 APIs and mobile SDKs, up to 250k transactions free with no credit card required

This article describes how to automate building Java and JavaScript npm-based applications within a single Gradle build.

As examples we are going to use a Java backend application based on Spring Boot and a JavaScript front-end application based on React. Though there are no obstacles to replacing them with any similar technologies like DropWizard or Angular, using TypeScript instead of JavaScript, etc.

Our main focus is Gradle build configuration, both applications' details are of minor importance.

Goal

We want to serve the JavaScript front-end application as static resources from the Java backend application. The full production package, i.e. a fat JAR containing all the resources, should be automatically created via Gradle.

The npm project should be built using Gradle, without any direct interaction with npm or node CLIs. Going further, it should not be necessary to have them installed on the system at all — especially important when building on a CI server.

The Plan

The Java project is built with Gradle in a regular way, no fancy things here.

The npm build is done using gradle-node-plugin, which integrates Node.js-based projects with Gradle without requiring to have Node.js installed on the system.

Output of the npm build is packaged into a JAR file and added as a regular dependency to the Java project.

Digression - gradle-node-plugin

During work on this article an actively developed fork of gradle-node-plugin has appeared. It's good news since the original plugin seemed abandoned. However, due to the early phase of the fork development, we decided to stick with the original plugin, and eventually upgrade in the future.

Initial Setup

Create a root Gradle project, let's call it java-npm-gradle-integration-example, then java-app and npm-app as its subprojects.

Create the Root Project

Create java-npm-gradle-integration-example Gradle project with the following configuration.

java-npm-gradle-integration-example/build.gradle

defaultTasks 'build'

wrapper {
    description "Regenerates the Gradle Wrapper files"
    gradleVersion = '5.0'
    distributionUrl = "http://services.gradle.org/distributions/gradle-${gradleVersion}-all.zip"
}

java-npm-gradle-integration-example/settings.gradle

rootProject.name = 'java-npm-gradle-integration-example'

The directory structure is expected to be as below:

java-npm-gradle-integration-example/
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle

Create a java-app Project

Generate a Spring Boot application using Spring Initializr, with Web dependency and Gradle as build type. Place the generated project under java-npm-gradle-integration-example directory.

Create an npm-app Project

Generate npm-app React application using create-react-app under java-npm-gradle-integration-example directory.

Adapt java-app to be a Gradle Subproject of java-npm-gradle-integration-example

Remove the gradle directory, gradlew, gradlew.bat and settings.gradle files from java-app as they are provided by the root project.

Update the root project to include java-app by adding the following line:

include 'java-app'

to java-npm-gradle-integration-example/settings.gradle.

Now building the root project, i.e. running ./gradlew inside java-npm-gradle-integration-example directory should build the java-app as well.

Make npm-app Be Built by Gradle

This is the essential part consisting of converting npm-app to a Gradle subproject and executing the npm build via a Gradle script.

Create an npm-app/build.gradle file with the following content, already including the gradle-node-plugin dependency.

buildscript {
    repositories {
        mavenCentral()
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }

    dependencies {
        classpath 'com.moowork.gradle:gradle-node-plugin:1.2.0'
    }
}

apply plugin: 'base'
apply plugin: 'com.moowork.node' // gradle-node-plugin

Below, we have added the configuration for gradle-node-plugin, declaring the versions of npm/Node.js to be used. The download flag is crucial here as it decides whether to download npm/Node.js via the plugin or by using the ones installed in the system.

node {
    /* gradle-node-plugin configuration
       https://github.com/srs/gradle-node-plugin/blob/master/docs/node.md

       Task name pattern:
       ./gradlew npm_<command> Executes an NPM command.
    */

    // Version of node to use.
    version = '10.14.1'

    // Version of npm to use.
    npmVersion = '6.4.1'

    // If true, it will download node using above parameters.
    // If false, it will try to use globally installed node.
    download = true
}

Now it's time to configure the build task. Normally, the build would be done via the npm run build command. gradle-node-plugin allows for the execution of npm commands using the following underscore notation: /gradlew npm_<command>. Behind the scenes, it dynamically generates a Gradle task. So, for our purposes, the Gradle task is npm_run_build.

Let's customize its behavior. We want to be sure it is executed only when the appropriate files change and avoid any unnecessary building. In order to do so, we define inputs and outputs pointing files or directories to be monitored for changes between executions of the task. Not to be confused with specifying files the task consumes or produces. In case a change is detected the task is going to be executed otherwise it will be treated as up-to-date and skipped.

npm_run_build {
    inputs.files fileTree("public")
    inputs.files fileTree("src")

    inputs.file 'package.json'
    inputs.file 'package-lock.json'

    outputs.dir 'build'
}

One would say we are missing node_modules as inputs here, though this directory appeared not reliable for dependency change detection. The task was rerun without changes, probably enormous number of node_modules files does not help here either. Instead we monitor only package.json and package-lock.json as they reflect state of dependencies enough.

Finally, make the Gradle build depend on executing npm build:

assemble.dependsOn npm_run_build

Now include npm-app in the root project by adding the following line to java-npm-gradle-integration-example/settings.gradle:

include 'npm-app'

At this moment, you should be able to build the root project and see the npm build results under thenpm-app/build directory.

Pack npm Build Result Into a JAR and Expose to the Java Project

Now we need to somehow put the npm build result into a Java package. We would like to do it without awkwardly copying external files into Java project resources during the build. A much more elegant and reliable way is to add them as a regular dependency, just like any other library.

Let's update npm-app/build.gradle to achieve this.

At first, define the task and pack the results of the build into a JAR file:

task packageNpmApp(type: Zip) {
    dependsOn npm_run_build
    baseName 'npm-app'
    extension 'jar'
    destinationDir file("${projectDir}/build_packageNpmApp")
    from('build') {
        // optional path under which output will be visible in Java classpath, e.g. static resources path
        into 'static' 
    }
}

Now we need to define a custom configuration to be used for publishing the JAR artifact:

configurations {
    npmResources
}

configurations.default.extendsFrom(configurations.npmResources)

We do not use here any predefined configurations, like archives, in order to be sure no other dependencies are included in the published scope.

Then expose the artifact created by the packaging task:

artifacts {
    npmResources(packageNpmApp.archivePath) {
        builtBy packageNpmApp
        type "jar"
    }
}

where archivePath points the created JAR file.

Next make the build depend on packageNpmApp task rather than the directly on the build task by replacing line

assemble.dependsOn npm_run_build

with

assemble.dependsOn packageNpmApp

Don't forget to configure proper cleaning as now the output doesn't go to the standard Gradle build directory:

clean {
    delete packageNpmApp.archivePath
}

Finally, include npm-app project as a dependency of java-app by adding

runtimeOnly project(':npm-app')

to the dependencies { } block of java-app/build.gradle. Here the scope (configuration) is runtimeOnly since we do not want to include the dependency during compilation time.

Now executing the root project build, i.e. inside java-npm-gradle-integration-example running a single command

./gradlew 

should result in creating java-app JAR containing, apart of the Java project's classes and resources, the npm-appbundle packaged into a JAR.

In our case, the npm-app.jar resides in java-app/build/libs/java-app-0.0.1-SNAPSHOT.jar:

zipinfo -1 java-app/build/libs/java-app-0.0.1-SNAPSHOT.jar
...
BOOT-INF/classes/eu/xword/labs/gc/JavaAppApplication.class
BOOT-INF/classes/application.properties
BOOT-INF/lib/
BOOT-INF/lib/spring-boot-starter-web-2.1.1.RELEASE.jar
BOOT-INF/lib/npm-app.jar
BOOT-INF/lib/spring-boot-starter-json-2.1.1.RELEASE.jar
BOOT-INF/lib/spring-boot-starter-2.1.1.RELEASE.jar
BOOT-INF/lib/spring-boot-starter-tomcat-2.1.1.RELEASE.jar
...

Last, but not the least, check to see that all of this works. Start the Java application with the following command:

java -jar java-app/build/libs/java-app-0.0.1-SNAPSHOT.jar

And open http://localhost:8080/ in your browser. You should see the React app welcome page.

What About Tests?

The Java tests are handled in a standard way by the Java plugin, no changes here.

In order to run JavaScript tests during the Gradle build we need to create a task that would execute thenpm run testcommand.

Here it's important to make sure the process started by such tasks exits with a proper status code, i.e. 0 for success and non-0 for failure — we don't want our Gradle build to pass smoothly, ignoring JavaScript tests that are blowing up. In our example, it's enough to set a CI environment variable — the Jest testing platform (default for create-react-app) is going to behave correctly.

String testsExecutedMarkerName = "${projectDir}/.tests.executed"

task test(type: NpmTask) {
    dependsOn assemble

    // force Jest test runner to execute tests once and finish the process instead of starting watch mode
    environment CI: 'true'

    args = ['run', 'test']

    inputs.files fileTree('src')
    inputs.file 'package.json'
    inputs.file 'package-lock.json'

    // allows easy triggering re-tests
    doLast {
        new File(testsExecutedMarkerName).text = 'delete this file to force re-execution JavaScript tests'
    }
    outputs.file testsExecutedMarkerName
}

We also add a file marker for making re-execution of tests easier.

Finally, make the project depend on the tests' execution:

check.dependsOn test

And update clean task:

clean {
    delete packageNpmApp.archivePath
    delete testsExecutedMarkerName
}

That's it. Now your build includes both Java and JavaScript tests execution. In order to execute the latter individually just run ./gradlew npm-app:test.

Summary

We integrated building Java and JavaScript/npm projects into a single Gradle project. The Java project is build in a standard manner, whereas the JavaScript one is build by npm tool wrapped with Gradle script using gradle-node-plugin. The plugin can provide npm and node so they do not need to be installed on the system.

The result of the build is a standard Java package (fat JAR), additionally including a JavaScript package as classpath resource to be served as a static asset.

Such a setup can be useful for simple front-end/backend stacks when there is no need to serve front-end applications from a separate server.

Full implementation of this example can be found on GitHub.

#1 for location developers in quality, price and choice, switch to HERE.

Topics:
java ,npm ,web dev ,full-stack application development ,gradle tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}