Gradle doesn’t come with built-in “build profile” support. This blog post walks through how to manually implement build profile support for a multi-project Spring Boot application.
What’s a “Multi-Project Spring Boot Application”?
Spring Boot is a popular choice for Java micro-services. Related micro-service projects can be stored in a single repository using a parent project to group them together. This parent project can share dependencies across all sub-projects using Gradle or Maven, and it can host Java projects intended to be included as libraries for the executable micro-services. Some micro-service projects may wish to utilize shared property files for configuration values, and some of these properties will be environment specific. For example, developers may want to periodically switch from using a local database to a shared development database, and each micro-service will need a different set of database credentials for QA and production environments. “Build profiles” are a convenient way to accomplish this.
What’s a “Build Profile”?
Maven has a “build profile” concept that allows applications to be loaded in different configuration states. This feature can be used to set environment-specific dependencies and properties across each sub-project. For example, instead of altering configuration files to connect to a different database, developers can just run the application using a different profile to select from a group of pre-defined properties.
However, unlike Maven, profiles are a little more complicated to set up in Gradle, especially when Spring Boot is involved. The following sections explain how to scaffold a multi-project Spring Boot application with Gradle profile support using a GitHub sample project.
What Are Our Profiles?
The end goal of this walkthrough is to be able to load the sample project’s micro-services in three different profile modes:
A “test” profile that uses an embedded H2 database and an embedded Undertow webserver.
A “dev” profile that connects to a local MySQL database and uses an embedded Tomcat server.
A “prod” profile that connects to a remote MySQL database whose credentials are passed as command line arguments, which also uses an embedded Tomcat server.
When an application is booted using a Spring Profile, specific beans with @Profile annotations can be loaded or excluded. This trick is used to load profile specific property files. Additionally, custom Gradle build scripts can associate each profile with unique dependencies. Using these two features together will allow each profile to connect to different database types.
Gradle Profile Support
Profiles are implemented in Gradle by using the -P (or –project-prop) command line argument. Custom build script modifications will read values passed using -P and branch dependencies based on the value provided. For example, using the following Gradle task on the command line from a sub-project’s root folder (not the parent project’s root folder) will build and execute it using the “dev” profile:
gradle -Pdev bootRun
Similar commands can be executed from the parent project’s root folder that target individual sub-projects. For example, this command executes a sub-project named “Profiles-Blog-Sender”:
gradle -Pdev Profiles-Blog-Sender:bootRun
The sample parent project’s build.gradle file includes a profiles.gradle file which contains all profile-related build scripts. The following code samples originate from profiles.gradle.
All of the contents of profiles.gradle are enclosed within a sub-projects section, which means that they will apply to all sub-projects and not the parent project.
subprojects {
}
The value of the command line -P property is compared against expected values to set the local propertyDrivenProfiles variable and load a profile-specific build script for unique dependencies. If none of the expected values are matched, the “test” profile is used by default.
def prodProfiles='prod'
def devProfiles='dev'
def testProfiles='test'
def propertyDrivenProfiles;
if (project.hasProperty('prod')) {
// Used for production environment
propertyDrivenProfiles = prodProfiles
apply from: rootProject.file('gradle/profile_prod.gradle');
} else if (project.hasProperty('dev')) {
// Used for local development
propertyDrivenProfiles = devProfiles
apply from: rootProject.file('gradle/profile_dev.gradle');
} else {
// Default when no profile property is specified, used for testing
propertyDrivenProfiles = testProfiles
apply from: rootProject.file('gradle/profile_test.gradle');
}
println 'Using profile: "' + propertyDrivenProfiles + '" for ' + project.getName()
The profile-specific build scripts simply list dependencies that may not be common to other profiles. For example, the profile_dev.gradle file uses MySQL and an embedded Tomcat server, and the profile_test.gradle file uses H2 and an embedded Undertow server.
Excerpt from profile_dev.gradle:
dependencies {
// Start: Webserver
providedCompile 'org.springframework.boot:spring-boot-starter-tomcat'
// End: Webserver
// Start: Database
compile 'mysql:mysql-connector-java:5.1.6'
// End: Database
}
Excerpt from profile_test.gradle:
dependencies {
// Start: Webserver
providedCompile 'org.springframework.boot:spring-boot-starter-undertow'
// End: Webserver
// Start: Database
compile 'com.h2database:h2:1.4.185'
// End: Database
}
The value of the propertyDrivenProfiles variable must be set into each Java project’s spring.profiles.active property, which drives @Profile bean injection. The profiles.gradle file provides two alternatives for injecting the spring.profiles.active property into executing sub-projects:
1. Extending the bootRun task the parent build.gradle file imports from the Spring Boot plugin.
bootRun {
systemProperties = [
'spring.profiles.active': propertyDrivenProfiles
]
}
This method has one caveat: If a sub-project is executed without using Gradle’s bootRun task, the spring.profiles.active property must be provided via an alternate mechanism (such as Java’s -Dproperty=value argument if a micro-service’s JAR is manually executed or its WAR is run from an application server).
2. Intercepting Gradle’s classes compilation task to inject the profiles.active property into compiled properties files.
classes << {
FileTree tree =
fileTree(dir: "build/resources/main").
include("application.properties")
tree.each { File file ->
file.append('\nspring.profiles.active=' + propertyDrivenProfiles);
}
}
This method ensures all WAR and JAR artifacts will contain a spring.profiles.active property whose value corresponds to the profile the artifact was built with. This can be desirable if profile-specific dependencies lock artifacts to specific profiles (such as the H2 and MySQL dependency differences between the test and dev profiles). However, this type of injection would not be appropriate if artifacts are meant to be reused across different environments using different profiles (such as using the same JAR in a dev environment and a production environment without recompiling it).
Setting Additional Properties
The bootRun task can also be extended to allow additional Java properties to be passed to Spring Boot applications in addition to the spring.profiles.active property. For example, the following version of the bootRun extension allows database properties to be passed on the command line instead of reading them from a properties file. This can be useful when it is inappropriate to commit properties to a repository, such as production database credentials.
bootRun {
systemProperties = [
'spring.profiles.active': propertyDrivenProfiles
]
// Use -Pproperty.name=value in addition to a -P profile argument
// for each property below to pass database connection properties.
//
// These will only be used if profile specific property files
// do not override them.
// They are intended for use with the "prod" profile.
ext.applyPropertyIfExists = { propertyKey ->
if(project.hasProperty(propertyKey)) {
systemProperties[propertyKey] = project.getProperty(propertyKey)
}
}
applyPropertyIfExists('spring.datasource.url')
applyPropertyIfExists('spring.datasource.username')
applyPropertyIfExists('spring.datasource.password')
}
For example, this command executes a sub-project in the prod profile with database credentials:
gradle –Pdev –Pspring.datasource.url=jdbc:mysql://localhost:3306/profileblog –Pspring.datasource.username=root -Pspring.datasource.password=root bootRun
This sample can be modified to add support for any Java property—just make sure that any properties set this way will not be overwritten by loaded Java properties files or the Gradle command line versions will not be used.
Spring Profile Support
The sample project contains a CommonPropertiesConfiguration.java file that is imported into all sub-projects. It is responsible for loading profile-specific property files in lieu of Spring configuration XML. It uses @Profile annotations on its @Bean methods to load different property file sets for different profiles, as seen in this code snippet:
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)public class CommonPropertiesConfiguration extends PropertiesConfiguration {
@Bean
@Profile(Profiles.TEST)
public static PropertySourcesPlaceholderConfigurer testProperties() {
return createPropertySourcesPlaceholderConfigurer(
"common_application.properties",
"common_test_application.properties");
}
The snippet’s superclass contains the implementation of the static method used to actually load the properties from the given properties files’ names, as seen on GitHub in PropertiesConfiguration.java. It creates a Spring PropertySourcesPlaceholderConfigurer bean by loading matching properties files found in the common project’s resources directory.
The snippet also assumes a Profiles.java file exists with static Strings that match all possible values of the propertyDrivenProfiles variable from the profiles.gradle build script.
public class Profiles {
public static final String PROD = "prod";
public static final String DEV = "dev";
public static final String TEST = "test";}
Additionally, all unit tests should have the following annotation on their classes (or one of the classes they extend from):
@ActiveProfiles(Profiles.TEST)
This allows the unit tests to set the spring.profiles.active property to “test” without involving Gradle or the bootRun task. This comes in handy when running unit tests directly from an integrated development environment (IDE), or using the “gradle build” or “gradle test” command without specifying any -P profile arguments.
Command Line? I Want to Use My IDE!
Gradle tasks can be executed with command line arguments natively within Eclipse and IntelliJ using the “External Tools Configurations” and “Run/Debug Configurations” menus respectively. Executing Gradle tasks within an IDE ensures all console output will be kept within the IDE’s console tab instead of a separate command prompt window. This makes local development more convenient, primarily due to the ability to click on stack trace classes printed within IDE console tabs to automatically jump to the line of code specified.
Note that Eclipse’s “Gradle Tasks” view is not capable of specifying profiles; “External Tools Configurations” must be used instead.
Eclipse Instructions
1. Import the GitHub sample project as a Gradle project.
2. Leave “Enable dependency management” unchecked to allow Eclipse to automatically download external library source code.
3. Click on the arrow on the “External Tools” button at the top bar
4. Go to “External Tools Configurations”
5. Create a new launch configuration within the Gradle Build section
6. Give the new task a name, pick a sub-project, and enter bootRun as the task value
7. Set a -P argument (e.g. -Pdev) in the “Arguments” tab under “Program Args” using the desired profile name, click apply, and then run
8. The new profile will now show up when clicking the arrow on the “External Tools” button, along with other recently run profiles.
IntelliJ Instructions
1. Import the GitHub sample project as a Gradle project.
2. Select “Edit Configurations” under the Run menu at the top bar
3. Create a new Gradle configuration. Give it a name, pick the sub-project’s build.gradle file, enter bootRun as the task value, set a -P argument (e.g. -Pdev) in the “Script parameters” tab under “Program Args” using the desired profile name, and click apply
4. The new configuration will now show up under the Run menu at the top bar and can be clicked to execute the application using the specified profile.
Debugging
Each sample executable sub-project starts with remote debugging enabled for every profile except “prod”. The build.gradle file in the root folder of each executable sub-project contains a “remoteDebugPort” value that IDEs can connect on to enable debugging. For example:
// Ensure each project uses a different remote debug port
def remoteDebugPort = '5001'
if (!project.hasProperty('prod')) {
bootRun.jvmArgs = ["-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=" + remoteDebugPort]
}
The if condition at the end of the sub-project’s build.gradle file appends a remote debugging argument to the Spring Boot application’s JVM in all profiles except “prod”. This is present because Eclipse cannot execute a Gradle task in debug mode directly, unlike IntelliJ. Instead, the application must be loaded using the command line or one of the aforementioned IDE Gradle task methods, and then a separate remote debugging task must be executed.
IntelliJ’s direct debug mode execution cannot be used when Gradle sets the debug JVM arguments because it would cause the arguments to be set twice, so two steps are still required for IntelliJ debugging. If all developers are using IntelliJ, the debugging related items in each sub-project’s build.gradle files can be removed to reduce one step from their workflows.
If the “suspend=n” argument in the build.gradle file is changed to “suspend=y”, the application will hang immediately after being executed until a remote debugger is attached. This is useful for placing breakpoints within code that is only executed on project startup.
Eclipse Debug Configuration Example
Go to the “Debug Configurations” menu and select Remote Java application.
Enter a debug profile name.
Select the sub-project to debug.
Enter localhost for the host.
Enter the port that corresponds to the targeted project’s “remoteDebugPort” value.
Click apply.
Click debug after running the application to hook in the remote debugger.
IntelliJ Debug Configuration Example
1. Go to IntelliJ’s Run/Debug Configurations menu and create a new Remote configuration
2. Enter a debug profile name.
3. Enter localhost for the host.
4. Enter the port that corresponds to the targeted project’s build.gradle “remoteDebugPort” value.
5. Click apply
6. The new configuration will now show up under the Run menu at the top bar and can be clicked to hook the remote debugger into an application that is already running.
Sample Multi-Project Hierarchy
The GitHub sample project has the following root project folder hierarchy:
Profiles-Blog
|
\---Profiles-Blog-Common
|
\---Profiles-Blog-Receiver
|
\---Profiles-Blog-Sender
The root “Profiles Blog” folder is the parent project, with three sub-projects. In Eclipse, all four will show up as separate Java projects, but in IntelliJ (and in the filesystem) they are all structured in the hierarchy shown above.
How Does the Parent Project Interact With the Sub-Projects?
The parent Profiles-Blog folder contains parent Gradle files that all sub-projects will inherit shared dependencies and tasks from. It also contains profile-specific Gradle files that will only be used when running an application in that profile mode.
Profiles-Blog
| build.gradle
|
\---gradle
eclipse.gradle
profiles.gradle
profile_dev.gradle
profile_prod.gradle
profile_test.gradle
The parent project’s build.gradle file has a category called “subprojects”. All dependences and plugins present in that section are inherited by each of the sub-projects listed in the parent’s settings.gradle file. It also includes an eclipse.gradle file that improves Gradle integration with Eclipse by adding code formatting defaults.
subprojects {
apply plugin: 'java'
apply plugin: 'eclipse-wtp'
apply plugin: 'war'
apply plugin: 'spring-boot'
apply plugin: 'io.spring.dependency-management'
apply from: rootProject.file('gradle/eclipse.gradle');
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
[...]
}
}
rootProject.name = 'Profiles-Blog'
include 'Profiles-Blog-Common'
include 'Profiles-Blog-Sender'
include 'Profiles-Blog-Receiver'
The parent build.gradle also includes profiles.gradle, which is the key to Gradle’s profile support as its name implies. The profiles.gradle file is covered in the “Gradle Profile Support” section.
What About Common Code?
The Profiles-Blog-Common project will be included as a dependency in the remaining projects and is a source of shared code and properties files.
While Spring Boot can automatically read property files from pre-defined locations within each project’s resources folder (such as any file named application.properties or application-{profileName}.properties as per sections 23.3 and 23.4 in the Spring Boot documentation), there isn’t a pre-defined way to set common properties across multiple sub-projects. To accomplish this, the common project is responsible for loading common properties from its resources directory using the CommonPropertiesConfiguration.java file mentioned in the “Spring Profile Support” section. It loads a “common_application.properties” file for every project that uses it as a dependency, as well as profile–specific properties files that are only used when running an application in that profile mode.
Profiles-Blog
|
\---Profiles-Blog-Common
\---src\main\
\---java\org\jdw\blog
| \---config
| | PropertiesConfiguration.java
| | CommonPropertiesConfiguration.java
| | Profiles.java
| \---common
| Shared code
|
\---resources
common_application.properties
common_test_application.properties
common_dev_application.properties
common_prod_application.properties
The common project also contains the Profiles.java file with static String profile names to use in @Profile and @ActiveProfiles annotation values across all sub-projects.
All other shared code unrelated to profiles is located in in the “common” package.
What’s Unique About Each Micro-Service Project?
The Profiles-Blog-Sender and Profiles-Blog-Receiver projects are separate Spring Boot applications that send and receive each other’s HTTP requests respectively. They contain their own unique properties files (two application.properties files) and Gradle dependencies (two build.gradle files) that are not shared with each other.
Profiles-Blog
|
\---Profiles-Blog-Common
|
\---Profiles-Blog-Sender
| | build.gradle
| |
| \---src\main\
| \---java\org\jdw\blog
| | \---config
| | \---sender
| | Sender specific code
| |
| \---resources
| application.properties
|
\---Profiles-Blog-Receiver
| build.gradle
|
\---src\main\
\---java\org\jdw\blog
| \---config
| \---receiver
| Receiver specific code
|
\---resources
application.properties
Each build.gradle file declares the dependency on the common project.
dependencies {
compile project(':Profiles-Blog-Common')
}
They also specify a unique remote debugging port number and JVM arguments, as detailed in the “Debugging” section.
All other dependencies are inherited from the parent project’s Gradle files.
What Does the Sample Project Do?
The Profiles-Blog-Sender project sends simple HTTP POST requests to a Profiles-Blog-Receiver project running on the same host. Both projects share a common database with one table that records the number of requests sent and received for each payload type.
TEST1 and TEST2 are the only available payload types. Each service has several URLs for HTTP GET requests that use these payload types in their names. The available endpoints for each project are:
Profiles-Blog-Sender Project:
http://localhost:9001/TEST1/send
Sends a payload of the type specified in the URL to the Profiles-Blog-Sender project.
Sample response:
{"payloadType":"TEST1","timesSent":1}
http://localhost:9001/TEST1/count
Displays the current number of times the specified payload type has been sent.
Sample Response:
{"payloadType":"TEST1","timesSent":2}
Profiles-Blog-Receiver Project:
http://localhost:9002/TEST1/count
Displays the current number of times the specified payload type has been received.
Sample Response:
{"payloadType":"TEST1","timesReceived":1}
The sample project’s functionality is intentionally rudimentary and only meant to serve as an example of two Spring Boot projects that utilize a shared database, with profile support to switch between databases and embedded webserver types.
Wrapping Up
Even though Gradle doesn’t come with native “build profile” support as found in Maven, following this blog post’s steps reproduces that functionality for Spring Boot projects. Developers can easily run and debug their applications using different profiles with a few clicks in their IDE of choice, and CI servers such as Jenkins can easily configure applications without using properties file replacement techniques by specifying Gradle command line arguments.
Profiles can be used for much more than just database configurations. They can drive profile specific dependencies in Gradle and the injection of any Spring bean or component with an @Profile annotation. Feel free to use the GitHub sample project as a scaffold for your own project’s needs, adding new profiles and profile-dependent resources as necessary.
Please post any questions to the comments section below, and be sure to keep in touch with us on LinkedIn and Twitter at @CrederaOpen.
Contact Us
Ready to achieve your vision? We're here to help.
We'd love to start a conversation. Fill out the form and we'll connect you with the right person.
Searching for a new career?
View job openings