Build Profiles
Chapter 5
5.1. What Are They For?
Profiles allow for the ability to customize a particular build for a particular environment; profiles enable portability between different build environments.
What do we mean by different build environments? Two example build environments are production and development. When you are working in a development environment, your system might be configured to read from a development database instance running on your local machine while in production, your system is configured to read from the production database. Maven allows you to define any number of build environments (build profiles) which can override any of the settings in the pom.xml. You could configure your application to read from your local, development instance of a database in your "development" profile, and you can configure it to read from the production database in the "production" profile. Profiles can also be activated by the environment and platform, you can customize a build to run differently depending the Operating System or the installed JDK version. Before we talk about using and configuring Maven profiles, we need to define the concept of Build Portability.
A build’s "portability" is a measure of how easy it is to take a particular project and build it in different environments. A build which works without any custom configuration or customization of properties files is more portable than a build which requires a great deal of work to build from scratch. The most portable projects tend to be widely used open source projects like Apache Commons or Apache Velocity which ship with Maven builds which require little or no customization. Put simply, the most portable project builds tend to just work, out of the box, and the least portable builds require you to jump through hoops and configure platform specific paths to locate build tools. Before we show you how to achieve build portability, let’s survey the different kinds of portability we are talking about.
The lack of portability is exactly what all build tools are made to prevent - however, any tool can be configured to be non-portable (even Maven). A non-portable project is buildable only under a specific set of circumstances and criteria (e.g., your local machine). Unless you are working by yourself and you have no plans on ever deploying your application to another machine, it is best to avoid non-portability entirely. A non-portable build only runs on a single machine, it is a "one-off". Maven is designed to discourage non-portable builds by offering the ability to customize builds using profiles.
When a new developer gets the source for a non-portable project, they will not be able to build the project without rewriting large portions of a build script.
A build exhibits environment portability if it has a mechanism for customizing behavior and configuration when targeting different environments. A project that contains a reference to a test database in a test environment, for example, and a production database in a production environment, is environmentally portable. It is likely that this build has a different set of properties for each environment. When you move to a different environment, one that is not defined and has no profile created for it, the project will not work. Hence, it is only portable between defined environments.
When a new developer gets the source for an environmentally portable project, they will have to run the build within a defined environment or they will have to create a custom environment to successfully build the project.
The center of this level of portability is a project’s requirement that only a select few may access internal resources such as source control or an internally-maintained Maven repository. A project at a large corporation may depend on a database available only to in-house developers, or an open source project might require a specific level of credentials to publish a web site and deploy the products of a build to a public repository.
If you attempt to build an in-house project from scratch outside of the in-house network (for example, outside of a corporate firewall), the build will fail. It may fail because certain required custom plugins are unavailable, or project dependencies cannot be found because you don’t have the appropriate credentials to retrieve dependencies from a custom remote repository. Such a project is portable only across environments in a single organization.
Anyone may download a widely portable project’s source, compile, and install it without customizing a build for a specific environment. This is the highest level of portability; anything less requires extra work for those who wish to build your project. This level of portability is especially important for open source projects, which depend on the ability for would-be contributors to easily download and build from source.
Any developer could download the source for a widely portable project.
Clearly, you’ll want to avoid creating the worst-case scenario: the non-portable build. You may have had the misfortune to work or study at an organization that had critical applications with non-portable builds. In such organizations, you cannot deploy an application without the help of a specific individual on a specific machine. In such an organization, it is also very difficult to introduce new project dependencies or changes without coordinating the change with the single person who maintains such a non-portable build. Non-portable builds tend to grow in highly political environments when one individual or group needs to exert control over how and when a project is built and deployed. "How do we build the system? Oh, we’ve got to call Jack and ask him to build it for us, no one else deploys to production." That is a dangerous situation which is more common that you would think. If you work for this organization, Maven and Maven profiles provide a way out of this mess.
On the opposite end of the portability spectrum are widely portable builds. Widely portable builds are generally the most difficult build systems to attain. These builds restrict your dependencies to those projects and tools that may be freely distributed and are publicly available. Many commercial software packages might be excluded from the most-portable builds because they cannot be downloaded before you have accepted a certain license. Wide portability also restricts dependencies to those pieces of software that may be distributed as Maven artifacts. For example, if you depend upon Oracle JDBC drivers, your users will have to download and install them manually; this is not widely portable as you will have to distribute a set of environment setup instructions for people interested in building your application. On the other hand, you could use a JDBC driver which is available from the public Maven repositories like MySQL or HSQLDB.
As stated previously, open source projects benefit from having the most widely portable build possible. Widely portable builds reduce the inefficiencies associated with contributing to a project. In an open source project (such as Maven) there are two distinct groups: end-users and developers. When an end-user uses a project like Maven and decides to contribute a patch to Maven, they have to make the transition from using the output of a build to running a build. They have to first become a developer, and if it is difficult to learn how to build a project, this end-user has a disincentive to take the time to contribute to a project. In a widely portable project, an end-user doesn’t have to follow a set of arcane build instructions to start becoming a developer, they can download the source, modify the source, build, and submit a contribution without asking someone to help them set up a build environment. When the cost of contributing source back to an open-source project is lower, you’ll see an increase in source code contributions, especially casual contributions which can make the difference between a project’s success and a project’s failure. One side-effect of Maven’s adoption across a wide group of open source projects is that it has made it easier for developers to contribute code to various open source projects.
5.2. Portability through Maven Profiles
A profile in Maven is an alternative set of configuration values which set or override default values. Using a profile, you can customize a build for different environments. Profiles are configured in the pom.xml and are given an identifier. Then you can run Maven with a command-line flag that tells Maven to execute goals in a specific profile. The following pom.xml uses a production
profile to override the default settings of the Compiler plugin.
This excerpt from the debug output of Maven shows the configuration of the Compiler plugin under the production profile. As shown in the output, debug
is set to false and optimize
is set to true.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.sonatype.mavenbook</groupId> <artifactId>simple</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>simple</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies>
<profiles> (1) <profile> <id>production</id> (2) <build> (3) <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration>
<debug>false</debug> (4) <optimize>true</optimize> </configuration> </plugin> </plugins> </build> </profile> </profiles> </project>
In this example, we’ve added a profile named production
that overrides the default configuration of the Maven Compiler plugin. Let’s examine the syntax of this profile in detail.
The |
|
Each profile has to have an |
|
A |
|
We’re overriding the configuration of the Maven Compiler plugin. We’re making sure that the bytecode produced by the production profile doesn’t contain debug information and that the bytecode has gone through the compiler’s optimization routines. |
To execute mvn install
under the production
profile, you need to pass the -Pproduction
argument on the command-line. To verify that the production
profile overrides the default Compiler plugin configuration, execute Maven with debug output enabled (-X
) as follows:
~/examples/profile $ mvn clean install -Pproduction -X ... (omitting debugging output) ... [DEBUG] Configuring mojo 'o.a.m.plugins:maven-compiler-plugin:2.0.2:testCompile' [DEBUG] (f) basedir = ~\examples\profile [DEBUG] (f) buildDirectory = ~\examples\profile\target ... [DEBUG] (f) compilerId = javac [DEBUG] (f) *debug = false* [DEBUG] (f) failOnError = true [DEBUG] (f) fork = false [DEBUG] (f) *optimize = true* [DEBUG] (f) outputDirectory = \ ~\svnw\sonatype\examples\profile\target\test-classes [DEBUG] (f) outputFileName = simple-1.0-SNAPSHOT [DEBUG] (f) showDeprecation = false [DEBUG] (f) showWarnings = false [DEBUG] (f) staleMillis = 0 [DEBUG] (f) verbose = false [DEBUG] -- end configuration -- ... (omitting debugging output) ...
This excerpt from the debug output of Maven shows the configuration of the Compiler plugin under the production profile. As shown in the output, debug
is set to false and optimize
is set to true.
While the previous example showed you how to override the default configuration properties of a single Maven plugin, you still don’t know exactly what a Maven profile is allowed to override. The short-answer to that question is that a Maven profile can override almost everything that you would have in a pom.xml. The Maven POM contains an element under project called profiles
containing a project’s alternate configurations, and under this element are profile elements which define each profile. Each profile must have an id
, and other than that, it can contain almost any of the elements one would expect to see under project. The following XML document shows all of the elements, a profile is allowed to override.
Elements Allowed in a Profile.
<project> <profiles> <profile> <build> <defaultGoal>...</defaultGoal> <finalName>...</finalName> <resources>...</resources> <testResources>...</testResources> <plugins>...</plugins> </build> <reporting>...</reporting> <modules>...</modules> <dependencies>...</dependencies> <dependencyManagement>...</dependencyManagement> <distributionManagement>...</distributionManagement> <repositories>...</repositories> <pluginRepositories>...</pluginRepositories> <properties>...</properties> </profile> </profiles> </project>
A profile can override an element shown with ellipses. A profile can override the final name of a project’s artifact in a profile, the dependencies, and the behavior of a project’s build via plugin configuration. A profile can also override the configuration of distribution settings depending on the profile; for example, if you need to publish an artifact to a staging server in a staging profile, you would create a staging profile which overrides the distributionManagement
element in a profile.
In the previous section we showed you how to create a profile that overrides default behavior for a specific target environment. In the previous build the default build was designed for development and the production
profile exists to provide configuration for a production environment. What happens when you need to provide customizations based on variables like operating systems or JDK version? Maven provides a way to "activate" a profile for different environmental parameters, this is called profile activation.
Take the following example, assume that we have a Java library that has a specific feature only available in the Java 6 release: the Scripting Engine as defined in JSR-223. You’ve separated the portion of the library that deals with the scripting library into a separate Maven project, and you want people running Java 5 to be able to build the project without attempting to build the Java 6 specific library extension. You can do this by using a Maven profile that adds the script extension module to the build only when the build is running within a Java 6 JDK. First, let’s take a look at our project’s directory layout and how we want developers to build the system.
When someone runs mvn install
with a Java 6 JDK, you want the build to include the simple-script
project’s build, when they are running in Java 5, you would like to skip the simple-script
project build. If you failed to skip the simple-script
project build in Java 5, your build would fail because Java 5 does not have the ScriptEngine
on the classpath. Let’s take a look at the library project’s pom.xml:
Dynamic Inclusion of Submodules Using Profile Activation.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.sonatype.mavenbook</groupId> <artifactId>simple</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>simple</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> <profiles> <profile> <id>jdk16</id>
<activation> (1) <jdk>1.6</jdk> </activation> <modules> (2) <module>simple-script</module> </modules> </profile> </profiles> </project>
If you run mvn install
under Java 1.6, you will see Maven descending into the simple-script subdirectory to build the simple-script
project. If you are running mvn install
in Java 1.5, the build will not try to build the simple-script
submodule. Exploring this activation configuration in more detail:
The |
|
In this profile we are adding the module |
Activations can contain one of more selectors including JDK versions, Operating System parameters, files, and properties. A profile is activated when all activation criteria has been satisfied. For example, a profile could list an Operating System family of Windows, and a JDK version of 1.4, this profile will only be activated when the build is executed on a Windows machine running Java 1.4. If the profile is active then all elements override the corresponding project-level elements as if the profile were included with the -P
command-line argument. The following example, lists a profile which is activated by a very specific combination of operating system parameters, properties, and a JDK version.
Profile Activation Parameters: JDK Version, OS Parameters, and Properties.
<project> ... <profiles> <profile> <id>dev</id> <activation>
<activeByDefault>false</activeByDefault> (1) <jdk>1.5</jdk> (2) <os>
<name>Windows XP</name> (3) <family>Windows</family> <arch>x86</arch> <version>5.1.2600</version> </os> <property>
<name>customProperty</name> (4) <value>BLUE</value> </property> <file>
<exists>file2.properties</exists> (5) <missing>file1.properties</missing> </file> </activation> ... </profile> </profiles> </project>
This previous example defines a very narrow set of activation parameters. Let’s examine each activation criterion in detail:
The |
|
This profile will only be active for JDK versions that begin with "1.5". This includes "1.5.0_01", "1.5.1". |
|
This profile targets a very specific version of Windows XP, version 5.1.2600 on a 32-bit platform. If your project uses the native plugin to build a C program, you might find yourself writing projects for specific platforms. |
|
The |
|
The |
You can activate a profile based on the value of a property like environment.type
. You can activate a development
profile if environment.type
equals dev
, or a production
profile if environment.type
equals prod
. You can also activate a profile in the absence of a property. The following configuration activates a profile if the property environment.type
is not present during Maven execution.
Activating Profiles in the Absence of a Property.
<project> ... <profiles> <profile> <id>development</id> <activation> <property> <name>!environment.type</name> </property> </activation> </profile> </profiles> </project>
Note the exclamation point prefixing the property name. The exclamation point is often referred to as the "bang" character and signifies "not". This profile is activated when no ${environment.type} property is set.
Maven profiles can be defined in either pom.xml, profiles.xml, ~/.m2/settings.xml, or ${M2_HOME}/conf/settings.xml. With these four levels, there’s no good way of keeping track of profiles available to a particular project without remembering which profiles are defined in these four files. To make it easier to keep track of which profiles are available, and where they have been defined, the Maven Help plugin defines a goal, active-profiles
, which lists all the active profiles and where they have been defined. You can run the active-profiles
goal, as follows:
$ mvn help:active-profiles Active Profiles for Project 'My Project': The following profiles are active: - my-settings-profile (source: settings.xml) - my-external-profile (source: profiles.xml) - my-internal-profile (source: pom.xml)
Profiles can encourage build portability. If your build needs subtle customizations to work on different platforms or if you need your build to produce different results for different target platforms, project profiles increase build portability. Settings profiles generally decrease build portability by adding extra-project information that must be communicated from developer to developer. The following sections provide some guidelines and some ideas for applying Maven profiles to your project.
One of the core motivations for Maven project profiles was to provide for environment-specific configuration settings. In a development environment, you might want to produce bytecode with debug information and you might want to configure your system to use a development database instance. In a production environment you might want to produce a signed JAR and configure the system to use a production database. In this chapter, we defined a number of environments with identifiers like dev
and prod
. A simpler way to do this would be to define profiles that are activated by environment properties and to use these common environment properties across all of your projects.
For example, if every project had a development
profile activated by a property named environment.type
having a value of dev
, and if those same projects had a production
profile activated by a property named environment.type
having a value of prod
, you could simply pass in the appropriate property value on the command-line to ensure that your builds target the correct environment. You can then use this property to activate profiles defined in a project’s pom.xml as follows. Let’s take a look at how a project’s pom.xml would define a profile activated by environment.type
having the value dev
.
Project Profile Activated by setting environment.type to dev.
<project> ... <profiles> <profile> <id>development</id> <activation> <activeByDefault>true</activeByDefault> <property> <name>environment.type</name> <value>dev</value> </property> </activation> <properties> <database.driverClassName>com.mysql.jdbc.Driver</database.driverClassName> <database.url> jdbc:mysql://localhost:3306/app_dev </database.url> <database.user>development_user</database.user> <database.password>development_password</database.password> </properties> </profile> <profile> <id>production</id> <activation> <property> <name>environment.type</name> <value>prod</value> </property> </activation> <properties> <database.driverClassName>com.mysql.jdbc.Driver</database.driverClassName> <database.url>jdbc:mysql://master01:3306,slave01:3306/app_prod</database.url> <database.user>prod_user</database.user> </properties> </profile> </profiles> </project>
This project defines some properties like database.url
and database.user
which might be used to configure another Maven plugin configured in the pom.xml. There are plugins available that can manipulate the database, run SQL, and plugins like the Maven Hibernate3 plugin which can generate annotated model objects for use in persistence frameworks. A few of these plugins, can be configured in a pom.xml using these properties. These properties could also be used to filter resources. If we needed to target the development environment, we would just run the following command:
~/examples/profiles $ mvn install
Because the development profile is active by default, and because there are no other profiles activated, running mvn
help:active-profiles
will show that the development profile is active. Now, the activeByDefault option will only work if no other profiles are active. If you wanted to be sure that the development profile would be active for a given build, you could explicitly pass in the environment.type variable as follows:
~/examples/profiles $ mvn install -Denvironment.type=dev
Alternatively, if we need to activate the production profile, we could always run Maven with:
~/examples/profiles $ mvn install -Denvironment.type=prod
To test which profiles are active for a given build, use mvn
help:active-profiles
.
This best practice builds upon the previous section. In Project Profile Activated by setting environment.type to dev, the production profile does not contain the database.password
property. I’ve done this on purpose to illustrate the concept of putting secrets in you user-specific settings.xml. If you were developing an application at a large organization which values security, it is likely that the majority of the development group will not know the password to the production database. In an organization that draws a bold line between the development group and the operations group, this will be the norm. Developers may have access to a development and a staging environment, but they might not have (or want to have) access to the production database. There are a number of reasons why this makes sense, particularly if an organization is dealing with extremely sensitive financial, intelligence, or medical information. In this scenario, the production environment build may only be carried out by a lead developer or by a member of the production operations group. When they run this build using the prod
environment.type
, they will need to define this variable in their settings.xml as follows:
Storing Secrets in a User-specific Settings Profile.
<settings> <profiles> <profile> <activeByDefault>true</activeByDefault> <properties> <environment.type>prod</environment.type> <database.password>m1ss10nimp0ss1bl3</database.password> </properties> </profile> </profiles> </settings>
This user has defined a default profile which sets the environment.type
to prod
and which also sets the production password. When the project is executed, the production profile is activated by the environment.type
property and the database.password
property is populated. This way, you can put all of the production-specific configuration into a project’s pom.xml and leave out only the single secret necessary to access the production database.
Note
Secrets usually conflict with wide portability, but this makes sense. You wouldn’t want to share your secrets openly.
Let’s assume that you have a library or a project that produces platform-specific customizations. Even though Java is platform-neutral, there are times when you might need to write some code that invokes platform-specific native code. Another possibility is that you’ve written some C code which is compiled by the Maven Native plugin and you want to produce a qualified artifact depending on the build platform. You can set a classifier with the Maven Assembly plugin or with the Maven Jar plugin. The following pom.xml produces a qualified artifact using profiles which are activated by Operating System parameters. For more information about the Maven Assembly plugin, see Chapter 8, Maven Assemblies.
Qualifying Artifacts with Platform Activated Project Profiles.
<project> ... <profiles> <profile> <id>windows</id> <activation> <os> <family>windows</family> </os> </activation> <build> <plugins> <plugin> <artifactId>maven-jar-plugin</artifactId> <configuration> <classifier>win</classifier> </configuration> </plugin> </plugins> </build> </profile> <profile> <id>linux</id> <activation> <os> <family>unix</family> </os> </activation> <build> <plugins> <plugin> <artifactId>maven-jar-plugin</artifactId> <configuration> <classifier>linux</classifier> </configuration> </plugin> </plugins> </build> </profile> </profiles> </project>
If the Operating System is in the Windows family, this pom.xml qualifies the JAR artifact with "-win". If the Operating System is in the Unix family, the artifact is qualified with "-linux". This pom.xml successfully adds the qualifiers to the artifacts, but it is more verbose than it need to be due to the redundant configuration of the Maven Jar plugin in both profiles. This example could be rewritten to use variable substitution to minimize redundancy as follows:
Qualifying Artifacts with Platform Activated Project Profiles and Variable Substitution.
<project> ... <build> <plugins> <plugin> <artifactId>maven-jar-plugin</artifactId> <configuration> <classifier>${envClassifier}</classifier> </configuration> </plugin> </plugins> </build> ... <profiles> <profile> <id>windows</id> <activation> <os> <family>windows</family> </os> </activation> <properties> <envClassifier>win</envClassifier> </properties> </profile> <profile> <id>linux</id> <activation> <os> <family>unix</family> </os> </activation> <properties> <envClassifier>linux</envClassifier> </properties> </profile> </profiles> </project>
In this pom.xml, each profile doesn’t need to include a build
element to configure the Jar plugin. Instead, each profile is activated by the Operating System family and sets the envClassifier
property to either win
or linux
. This envClassifier
is then referenced in the default pom.xml build
element to add a classifier to the project’s JAR artifact. The JAR artifact will be named ${finalName}-${envClassifier}.jar and included as a dependency using the following dependency syntax:
Depending on a Qualified Artifact.
<dependency> <groupId>com.mycompany</groupId> <artifactId>my-project</artifactId> <version>1.0</version> <classifier>linux</classifier> </dependency>
When used judiciously, profiles can make it very easy to customize a build for different platforms. If something in your build needs to define a platform-specific path for something like an application server, you can put these configuration points in a profile which is activated by an operating system parameter. If you have a project which needs to produce different artifacts for different environments, you can customize the build behavior for different environments and platforms via profile-specific plugin behavior. Using profiles, builds can become portable, there is no need to rewrite your build logic to support a new environment, just override the configuration that needs to change and share the configuration points which can be shared.