Grails Programmer : How to create a multi project build with Grails 3 (apps and plugins)?

Back in February I wrote a post about multi-project build. Since then, Graeme Rocher did a great Quickcast where he explains it much better. Because of that I have reviewed this post to include all the points covered in the video. Feel free to view the video or follow the post.

Creating a multi project build in Grails 3 is really easy.

Lets create a folder where we are going to place a core projects and a plugin.

$ mkdir multiproject
$ cd multiproject

Lets checkout the list of available profiles. We are going to use the REST profile

 
$ grails --version
 Grails Version: 3.1.10
| Groovy Version: 2.4.7
| JVM Version: 1.8.0_45
$ grails list-profiles
| Available Profiles
--------------------
* angular - A profile for creating applications using AngularJS
* rest-api - Profile for REST API applications
* base - The base profile extended by other profiles
* plugin - Profile for plugins designed to work across all profiles
* web - Profile for Web applications
* web-plugin - Profile for Plugins designed for Web applications

Lets create the core app project which we are going to use the web-api profile.

| grails create-app myapi --profile=rest-api
| Application created at /Users/groovycalamari/Documents/tests/multiproject/myapi

Lets create the a plugin which we are going to use from the main app.

| grails create-app myplugin --profile=plugin
| Application created at /Users/groovycalamari/Documents/tests/multiproject/myplugin

Lets create a service in the plugin which returns a name:

$ cd myplugin
$ grails create-service Name

Lets return the a string in a method within the service:
grails-app/services/myplugin/NameService.groovy

package myplugin

class NameService {

    def name() {
        'Sergio'
    }
}

Now, lets go back to the root of the multiproject and create a settings.gradle which includes both projects:

$ pwd
/Users/groovycalamari/Documents/tests/multiproject
$ echo "include 'myapi', 'myplugin'" >>  settings.gradle

Reference the plugin project – Gradle way

Although you can do this, don’t. Please, use the grails plugin block described later.

Edit myapi/build.gradle and add a reference to the plugin project.

buildscript {
    ext {
        grailsVersion = project.grailsVersion
    }
    repositories {
        mavenLocal()
        maven { url "https://repo.grails.org/grails/core" }
    }
    dependencies {
        classpath "org.grails:grails-gradle-plugin:$grailsVersion"
        classpath "org.grails.plugins:hibernate4:5.0.10"
        classpath "org.grails.plugins:views-gradle:1.0.12"
    }
}

version "0.1"
group "myapi"

apply plugin:"eclipse"
apply plugin:"idea"
apply plugin:"war"
apply plugin:"org.grails.grails-web"
apply plugin:"org.grails.plugins.views-json"

ext {
    grailsVersion = project.grailsVersion
    gradleWrapperVersion = project.gradleWrapperVersion
}

repositories {
    mavenLocal()
    maven { url "https://repo.grails.org/grails/core" }
}

dependencyManagement {
    imports {
        mavenBom "org.grails:grails-bom:$grailsVersion"
    }
    applyMavenExclusions false
}

dependencies {
    compile "org.springframework.boot:spring-boot-starter-logging"
    compile "org.springframework.boot:spring-boot-autoconfigure"
    compile "org.grails:grails-core"
    compile "org.springframework.boot:spring-boot-starter-actuator"
    compile "org.springframework.boot:spring-boot-starter-tomcat"
    compile "org.grails:grails-plugin-url-mappings"
    compile "org.grails:grails-plugin-rest"
    compile "org.grails:grails-plugin-codecs"
    compile "org.grails:grails-plugin-interceptors"
    compile "org.grails:grails-plugin-services"
    compile "org.grails:grails-plugin-datasource"
    compile "org.grails:grails-plugin-databinding"
    compile "org.grails:grails-plugin-async"
    compile "org.grails:grails-web-boot"
    compile "org.grails:grails-logging"
    compile "org.grails.plugins:cache"
    compile "org.grails.plugins:hibernate4"
    compile "org.hibernate:hibernate-ehcache"
    compile "org.grails.plugins:views-json"
    console "org.grails:grails-console"
    profile "org.grails.profiles:rest-api"
    runtime "com.h2database:h2"
    testCompile "org.grails:grails-plugin-testing"
    testCompile "org.grails.plugins:geb"
    testCompile "org.grails:grails-datastore-rest-client"
    testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1"
    testRuntime "net.sourceforge.htmlunit:htmlunit:2.18"
    compile project(":myplugin")
}

assets {
    minifyJs = true
    minifyCss = true
}

in the myapi project create a controller which uses the service:

myapi$ grails create-controller Salute 
| Created grails-app/controllers/myapi/SaluteController.groovy
| Created src/test/groovy/myapi/SaluteControllerSpec.groovy
package myapi


import grails.rest.*
import grails.converters.*
import static org.springframework.http.HttpStatus.*

class SaluteController {

    def nameService

    def index() { 
        render text:"Hello ${nameService.name()}", status: OK
    }
}

Start the app:

$ grails run-app
Grails application running at http://localhost:8080 in environment: development

Hit the endpoint:

$ curl -H "Accept: application/json" http://localhost:8080/salute 
Hello Sergio

We are calling service defined on its own plugin form the main app.

Reference the plugin project – Grails Plugin block

Note the recommended way to add a project dependency between a grails project and a grails plugin is by means of the grails plugin block. It allow us to get automatically reloading of subproject dependencies which is really important from a development productivity stand point.

buildscript {
    ext {
        grailsVersion = project.grailsVersion
    }
    repositories {
        mavenLocal()
        maven { url "https://repo.grails.org/grails/core" }
    }
    dependencies {
        classpath "org.grails:grails-gradle-plugin:$grailsVersion"
        classpath "org.grails.plugins:hibernate4:5.0.10"
        classpath "org.grails.plugins:views-gradle:1.0.12"
    }
}

version "0.1"
group "myapi"

apply plugin:"eclipse"
apply plugin:"idea"
apply plugin:"war"
apply plugin:"org.grails.grails-web"
apply plugin:"org.grails.plugins.views-json"

ext {
    grailsVersion = project.grailsVersion
    gradleWrapperVersion = project.gradleWrapperVersion
}

repositories {
    mavenLocal()
    maven { url "https://repo.grails.org/grails/core" }
}

dependencyManagement {
    imports {
        mavenBom "org.grails:grails-bom:$grailsVersion"
    }
    applyMavenExclusions false
}

grails {
    plugins {
        compile project(':myplugin')
    }
}

dependencies {
    compile "org.springframework.boot:spring-boot-starter-logging"
    compile "org.springframework.boot:spring-boot-autoconfigure"
    compile "org.grails:grails-core"
    compile "org.springframework.boot:spring-boot-starter-actuator"
    compile "org.springframework.boot:spring-boot-starter-tomcat"
    compile "org.grails:grails-plugin-url-mappings"
    compile "org.grails:grails-plugin-rest"
    compile "org.grails:grails-plugin-codecs"
    compile "org.grails:grails-plugin-interceptors"
    compile "org.grails:grails-plugin-services"
    compile "org.grails:grails-plugin-datasource"
    compile "org.grails:grails-plugin-databinding"
    compile "org.grails:grails-plugin-async"
    compile "org.grails:grails-web-boot"
    compile "org.grails:grails-logging"
    compile "org.grails.plugins:cache"
    compile "org.grails.plugins:hibernate4"
    compile "org.hibernate:hibernate-ehcache"
    compile "org.grails.plugins:views-json"
    console "org.grails:grails-console"
    profile "org.grails.profiles:rest-api"
    runtime "com.h2database:h2"
    testCompile "org.grails:grails-plugin-testing"
    testCompile "org.grails.plugins:geb"
    testCompile "org.grails:grails-datastore-rest-client"
    testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1"
    testRuntime "net.sourceforge.htmlunit:htmlunit:2.18"
}

If we run the app with the verbose flag. We will see how the plugin is built first and placed on the classpath before the application.

$ grails run-app -verbose
| Running application...
:myplugin:compileAstJava UP-TO-DATE
:myplugin:compileAstGroovy UP-TO-DATE
:myplugin:processAstResources UP-TO-DATE
:myplugin:astClasses UP-TO-DATE
:myplugin:compileJava UP-TO-DATE
:myplugin:configScript UP-TO-DATE
:myplugin:compileGroovy UP-TO-DATE
:myplugin:copyCommands UP-TO-DATE
:myplugin:copyTemplates UP-TO-DATE
:myplugin:processResources UP-TO-DATE
:myapi:compileJava UP-TO-DATE
:myapi:compileGroovy
:myapi:buildProperties UP-TO-DATE
:myapi:processResources UP-TO-DATE
:myapi:classes
:myapi:findMainClass
:myapi:bootRun
...
..
.
Grails application running at http://localhost:8080 in environment: development

You can drag and drop the settings.gradle to IntelliJ and it will configure correctly your multi-project build there.

Create a Gradle wrapper

In order to avoid Gradle configuration duplication we can create a build.gradle file in the root directory of our multi project build and move most of the configuration there.

Lets start by creating the build.gradle

$ touch build.gradle

Add the gradle wrapper tasks to it. You may cut it from the myapi/build.gradle file

task wrapper(type: Wrapper) {
	gradleVersion = gradleWrapperVersion
}

We will create a gradle.properties in the root directory

$ touch gradle.properties

Add the wrapper version

gradleWrapperVersion=2.13

Run

gradle wrapper

This will generated the Gradle wrapper which will help you import your project into IntelliJ or run your multi-project in a CI server.

Remove duplication between myapi/build.gradle and myplugin/build.gradle

We are going to move as much duplication to the root build.gradle file to keep the build as dry as possible.

Add the grails version to root gradle.propeties

grailsVersion=3.1.10
gradleWrapperVersion=2.13

You can remove the subproject gradle.properties

$ rm myapi/gradle.properties
rm myplugin/gradle.properties

The root build.gradle will look like:

$ cat build.gradle
buildscript {
    ext {
        grailsVersion = project.grailsVersion
    }
    repositories {
        mavenLocal()
        maven { url "https://repo.grails.org/grails/core" }
    }
    dependencies {
        classpath "org.grails:grails-gradle-plugin:$grailsVersion"
        classpath "org.grails.plugins:hibernate4:5.0.10"
        classpath "org.grails.plugins:views-gradle:1.0.12"
    }
}

task wrapper(type: Wrapper) {
	gradleVersion = gradleWrapperVersion
}

version "0.1"
group "myapi"

ext {
    grailsVersion = project.grailsVersion
    gradleWrapperVersion = project.gradleWrapperVersion
}

subprojects { project ->
	apply plugin:"eclipse"
	apply plugin:"idea"
	if ( project.name.endsWith('api') ) {
		apply plugin:"war"
		apply plugin:"org.grails.grails-web"
		apply plugin:"org.grails.plugins.views-json"

	} else if ( project.name.endsWith('plugin') ) {
		apply plugin:"org.grails.grails-plugin"
		apply plugin:"org.grails.grails-plugin-publish"
	}

	repositories {
    	mavenLocal()
    	maven { url "https://repo.grails.org/grails/core" }
	}

	dependencyManagement {
    	imports {
        	mavenBom "org.grails:grails-bom:$grailsVersion"
    	}
    	applyMavenExclusions false
	}

	dependencies {
    	compile "org.springframework.boot:spring-boot-starter-logging"
    	compile "org.springframework.boot:spring-boot-autoconfigure"
    	compile "org.grails:grails-core"
    	console "org.grails:grails-console"
    	testCompile "org.grails:grails-plugin-testing"    	
	}
}

myapp build.gradle will look like:

grails {
    plugins {
        compile project(':myplugin')
    }
}

dependencies {

    compile "org.springframework.boot:spring-boot-starter-actuator"
    compile "org.springframework.boot:spring-boot-starter-tomcat"
    compile "org.grails:grails-plugin-url-mappings"
    compile "org.grails:grails-plugin-rest"
    compile "org.grails:grails-plugin-codecs"
    compile "org.grails:grails-plugin-interceptors"
    compile "org.grails:grails-plugin-services"
    compile "org.grails:grails-plugin-datasource"
    compile "org.grails:grails-plugin-databinding"
    compile "org.grails:grails-plugin-async"
    compile "org.grails:grails-web-boot"
    compile "org.grails:grails-logging"
    compile "org.grails.plugins:cache"
    compile "org.grails.plugins:hibernate4"
    compile "org.hibernate:hibernate-ehcache"
    compile "org.grails.plugins:views-json"
    profile "org.grails.profiles:rest-api"
    runtime "com.h2database:h2"
    testCompile "org.grails.plugins:geb"
    testCompile "org.grails:grails-datastore-rest-client"
    testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1"
    testRuntime "net.sourceforge.htmlunit:htmlunit:2.18"
}

and myplugin build.gradle will look like:

dependencies {    
    profile "org.grails.profiles:plugin"
    provided "org.grails:grails-plugin-services"
    provided "org.grails:grails-plugin-domain-class"    
}
grailsPublish {
    // TODO: Provide values here
    user = 'user'
    key = 'key'
    githubSlug = 'foo/bar'
    license {
        name = 'Apache-2.0'
    }
    title = "My Plugin"
    desc = "Full plugin description"
    developers = [johndoe:"John Doe"]
    portalUser = ""
    portalPassword = ""    
}

Do you like to read about Groovy/Grails development? Yes, then Subscribe to Groovy Calamari a weekly curated email newsletter about the Groovy ecosystem which I write 

Leave a Reply

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