How to Configure Revapi
The maven plugin itself is configured using the properties of the goals. The configuration of
the API analysis is a matter of configuring different Revapi extensions that are to be run. This is done using the
analysisConfiguration
element in the Maven plugin’s configuration. This element contains the configuration of
the individual extensions in either XML or JSON.
Failing level
By default, mvn revapi:check
fails if at least one potentially breaking change is found. To fail only for breaking changes, one has to set up failSeverity
as follows.
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<configuration>
<failSeverity>breaking</failSeverity>
...
Specifying The Analysis Configuration Using XML
Ignoring classes
As an example, let’s configure the java extension to ignore the classes it finds missing from the API
rather than reporting them and also only include the archives with com.acme
groupId in the analysis::
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<version>0.14.6</version>
<configuration>
<analysisConfiguration>
<revapi.java>
<missing-classes>
<behavior>ignore</behavior>
</missing-classes>
</revapi.java>
<revapi.filter>
<archives>
<include>
<item>com\.acme:.*</item>
</include>
</archives>
</revapi.filter>
</analysisConfiguration>
</configuration>
Each extension has a unique "extension id" which is used as the root tag for its configuration under the
analysisConfiguration
tag. Under the extension configuration’s root tag an XML representation of the configuration
as specified by the extension documentation (and JSON schema - yes, the XML is validated against a JSON schema ;) ).
Multiple Configurations Per Extension
There can be multiple configurations for a single extension. Optionally, each extension configuration "instance" can be assigned an ID such that it can be effectively merged (see below).
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<version>0.14.6</version>
...
<configuration>
<analysisConfiguration>
<revapi.reporter.text id="stdout">
<output>out</output>
</revapi.reporter.text>
<revapi.reporter.text id="custom-report">
<output>${project.build.directory}/revapi-custom-report.xml</output>
<template>${custom-report.template.location}</template>
</revapi.reporter.text>
</analysisConfiguration>
</configuration>
This configuration will cause the Revapi’s text reporter (if is included as a dependency of the plugin) to output the results of the analysis both to standard output and a custom file using a custom template.
Analysis Configuration And Maven Inheritance
Having the Revapi analysis configuration specified in XML enables Maven to apply its configuration inheritance logic to Revapi analysis configuration, too.
Here is a couple of tips on how to make the Maven configuration inheritance work nice with Revapi analysis configuration.
mvn help:effective-pom ,
combine.self and combine.children are your friends when inheriting
more complex analysis configurations.
|
One Configuration Per Extension
Parent POM:
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<configuration>
<analysisConfiguration>
<revapi.ignore>
<item>
<code>java.class.removed</code>
</item>
<item>
<code>java.class.added</code>
</item>
</revapi.ignore>
</analysisConfiguration>
</configuration>
...
Child POM:
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<configuration>
<analysisConfiguration>
<revapi.ignore>
<item>
<code>java.class.nowFinal</code>
</item>
</revapi.ignore>
</analysisConfiguration>
</configuration>
...
Effective Child POM:
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<configuration>
<analysisConfiguration>
<revapi.ignore>
<item>
<code>java.class.nowFinal</code>
</item>
</revapi.ignore>
</analysisConfiguration>
</configuration>
...
Notice that revapi.ignore
doesn’t contain the item
s defined in the parent POM. That is the default Maven behavior.
To be able to inherit the configuration of the revapi.ignore
extension from the parent POM, you have to specify
how to merge the `item`s in the child POM like so:
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<configuration>
<analysisConfiguration>
<revapi.ignore combine.children="append">
<item>
<code>java.class.nowFinal</code>
</item>
</revapi.ignore>
</analysisConfiguration>
</configuration>
...
After that, the effective child POM will indeed contain configuration combined from both parent and child:
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<configuration>
<analysisConfiguration>
<revapi.ignore>
<item>
<code>java.class.nowFinal</code>
</item>
<item>
<code>java.class.removed</code>
</item>
<item>
<code>java.class.added</code>
</item>
</revapi.ignore>
</analysisConfiguration>
</configuration>
...
Multiple Configurations Per Extension
As mentioned in the previous chapters, revapi supports multiple configurations per extension. This gets a little bit complicated in conjunction with inheritance. Let’s see an example.
Parent POM
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<version>0.14.6</version>
...
<configuration>
<analysisConfiguration>
<revapi.reporter.text id="stdout">
<output>out</output>
</revapi.reporter.text>
<revapi.reporter.text id="custom-report">
<output>${project.build.directory}/revapi-custom-report.xml</output>
<template>${custom-report.template.location}</template>
</revapi.reporter.text>
</analysisConfiguration>
</configuration>
Child POM
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<version>0.14.6</version>
...
<configuration>
<analysisConfiguration>
<revapi.reporter.text id="stdout">
<output>err</output>
</revapi.reporter.text>
</analysisConfiguration>
</configuration>
I.e. the child POM wants to reconfigure the "stdout" configuration of revapi text reporter to report to standard error output instead of the standard output.
If we inspect the effective child POM, we’ll see this though:
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<version>0.14.6</version>
...
<configuration>
<analysisConfiguration>
<revapi.reporter.text id="stdout">
<output>err</output>
</revapi.reporter.text>
</analysisConfiguration>
</configuration>
I.e. the configuration for the custom output is lost in the child POM (again, this is standard Maven behavior. These are just examples to save you from ripping your hair out unnecessarily ;) ). To also inherit the other reporter configuration, you have to mention it like this in the child POM
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<version>0.14.6</version>
...
<configuration>
<analysisConfiguration>
<revapi.reporter.text id="stdout">
<output>err</output>
</revapi.reporter.text>
<revapi.reporter.text id="custom-report"/>
</analysisConfiguration>
</configuration>
Now the effective child POM contains the custom report configuration as well as the modified stdout configuration.
Specifying The Analysis Configuration Using JSON
Revapi has been around for a little bit and over the time it has evolved. Originally (up until Revapi API 0.8.0), each extension was instantiated exactly once and therefore also configured exactly once. Since Revapi API 0.8.0, supported by Revapi Maven plugin 0.9.0, there can be multiple configurations for each extension (and the extension can be therefore instantiated multiple times). This brings the ability to e.g. have 2 differently configured text reporter instances, each generating a different kind of output. Unfortunately, this complicates the configuration, because it is no longer possible to have a single "configuration tree" where extensions would read their configurations from their declared locations.
Therefore, since Revapi API 0.8.0 there is a new kind of JSON format for configuration (which in turn also enables maven plugin to support XML configuration btw). To ease the migration to the new versions, the old configuration format is still supported (but mixing the two formats can lead to unresolvable situations, see the Multi-file Configuration section for more details).
The JSON Configuration Format
As explained above, each extension can be configured multiple times. To support this in JSON, the JSON configuration looks like this:
[
{
"extension": "revapi.reporter.text",
"id": "optional-id",
"configuration": {
... the actual configuration of the extension according to its schema ...
}
},
{
"extension": "revapi.reporter.text",
"configuration": {
...
}
},
{
"extension": "revapi.ignore",
"configuration": {
...
}
},
...
]
The configuration object is a list. The members of the list are individual configurations for the extensions.
The extension being configured is specified by the extension
key and the configuration (conforming to the schema
specified by the extension) is present under the configuration
key.
The optional id
key is useful if there are multiple configuration sources (see
multi file configuration for example) as it affects how the configurations from the
different sources are merged together.
The Legacy JSON Configuration Format
This describes the obsolete JSON configuration format that cannot handle multiple configurations per extension. If you still use it, rest assured that it is still supported (with the exception of certain scenarios during merging of multiple configuration sources) but you are encouraged to start using the new configuration format. |
The JSON data contains the configuration of all the extensions. Each of the extensions declares a "root" in the JSON
data from which it reads its configuration (for example, ignoring specific problems found during the analysis can be
done using the IgnoreDifferenceTransform
extension from the basic
features under the root revapi.ignore
).
So, without further ado, let’s configure the java extension to report the classes it finds missing from the API
rather than failing the analysis upon encountering them and also only include the archives with com.acme
groupId in
the analysis:
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<version>0.14.6</version>
<configuration>
<analysisConfiguration><![CDATA[
{
"revapi": {
"java": {
"missing-classes": {
"behavior": "report"
}
},
"filter": {
"archives": {
"include": ["com\\.acme:.*"]
}
}
}
}
]]></analysisConfiguration>
</configuration>
<executions>
<execution>
<goals><goal>check</goal></goals>
</execution>
</executions>
The configuration options of the various extensions can be found in their respective docs: basic features documentation, java extension documentation.
Evolving The Library Using Semver Rules
By default Revapi will report all API changes of configured severity (by default "potentiallyBreaking") and the maven plugin will break the build if such differences are found since the last released version.
One then has to somehow "persuade" the plugin to let the build pass again. One way of doing it is to use the ignore extension and list all the changes and provide them with a "justification" for why such change happened.
This is very rigorous but also laborious approach that isn’t always worth the effort - especially in the early stages of development. Also one can argue that any change made to the codebase is intentional and therefore specifically listing it somewhere in a file that would justify such change to a tool is superfluous. On the other such strict policy might be required for some critical libraries that require high level of stability and any change should be vetted and approved.
There is also another way though. One can use the combination of the semver-ignore extension and the update-versions goal to (semi-)automatically increase the version of the library even during the development such that its version corresponds to the API changes it contains since the last released version. No other action like listing the and justifying the changes is necessary in this case.
For that you need to configure Revapi maven plugin to use and enable the semver-ignore extension:
<build>
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<configuration>
<analysisConfiguration><![CDATA[
{
"revapi": {
"semver": {
"ignore": {
"enabled": true
}
}
}
}
]]></analysisConfiguration>
</configuration>
</plugin>
</build>
Then, when you try to build your project, revapi might find a change that is incompatible with the current version increase (like an API breaking change when you only increased a micro version since the last release) and fail your build. At that moment, it is enough to invoke:
mvn revapi:update-versions
and the version will be updated to reflect the API changes made. When you build the project again, the build should pass.
You can even embed the update-versions goal in your regular build and have the versions increase automagically
(at the cost of having to run the build twice when an incompatible change is made).
|
Multi-file Configuration
Sometimes it can be handy to split the configuration of Revapi analysis in separate files - this can be done for various reasons - you might want to keep the config of different extensions separate or you want to compose the config from various contributing locations, etc.
For Revapi, this can be achieved by using the analysisConfigurationFiles
configuration element instead of (or in
addition to) the analysisConfiguration
element which provides the in-POM way of configuring Revapi.
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<version>0.14.6</version>
...
<configuration>
<analysisConfigurationFiles>
<file>${project.basedir}/config/filter.json</file>
<file>${project.basedir}/config/ignore.xml</file>
</analysisConfigurationFiles>
</configuration>
<executions>
<execution>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>
Each of the configuration files (e.g. filter.json
and ignore.xml
in the above example) is a JSON or XML document
with the configuration. The maven plugin then merges the files together (in an unspecified order) and uses the
result as the final configuration for the analysis.
Using Configuration Defined In Other Modules
It is possible to define a JAR artifact that contains "common" configuration of the Revapi analysis shared by many modules. To reference it, simply add the artifact as a dependency of the revapi maven plugin and reference the configuration file inside that artifact like:
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<version>0.14.6</version>
<dependencies>
...
<dependency>
<groupId>my.group.id</groupId>
<artifactId>artifact-with-common-config</artifact>
<version>...</version>
</dependency>
</dependencies>
<configuration>
<analysisConfigurationFiles>
<configurationFile>
<resource>path/to/the/config/file/in/the/shared/artifact</resource>
</configurationFile>
...
</analysisConfigurationFiles>
...
</configuration>
...
</plugin>
Merging Configuration From Multiple Files
When the analysis configuration is split amongst several files, it needs to be merged together before it is applied to the Revapi extensions. This process is slightly complex with the ability for a single extension to be configured multiple times but in the end is somewhat similar to the way Maven merges the executions of a plugin - as long as the executions are defined once in the effective POM, they don’t need to be assigned IDs. If there are multiple executions and you override them in child POMs, they need to have the IDs assigned so that it is clear what executions in child POM need to be merged with what executions in the parent POM.
In Revapi, too, an extension configuration can optionally have an ID. In JSON this is expressed like this:
...
{
"extension": "my.extension",
"id": "id",
"configuration": ...
}
...
and in XML like so:
...
<my.extension id="myid">
...
</my.extension>
...
When merging configurations without an explicit ID, everything works as long as there is at most a single configuration for each extension in each configuration file to be merged. As soon as there is more than one configuration for some extension in one of the configuration files, you need to assign IDs to the configurations of that extension so that it is clear what configs should be merged with what.
Custom Root Element of Configuration
It might sometimes be useful to be able to only use a part of a JSON document as configuration for Revapi. This might be because the same file might be used for holding other data, too, or because the file(s) contain(s) multiple Revapi configurations. Note that the custom root is only applicable to configuration files, not the in-POM configuration.
The below example illustrates the usage of the custom configuration root.
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<version>0.14.6</version>
...
<configuration>
<analysisConfigurationFiles>
<file>${project.basedir}/config/filter.json</file>
<file>${project.basedir}/config/ignore.json</file>
<file>
<path>${project.basedir}/config/json-data-for-many-things.json</path>
<roots>
<root>configuration/revapi</root>
</roots>
</file>
<file>
<path>${project.basedir}/config/xml-data-for-many-things.xml</path>
<roots>
<root>configuration/revapi</root>
</roots>
</file>
</analysisConfigurationFiles>
</configuration>
<executions>
<execution>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>
The above would read the configuration from the filter.json
and ignore.json
files. In addition it would also read
the configuration from the json-data-for-many-things.json
and xml-data-for-many-things.xml
files but would only
consider the data from the "configuration/revapi" subpath in those file. E.g. if the files looked like:
{
"itest-setup": {...},
"configuration": {
"our-custom-tool": {
},
"revapi": {
... HERE WE ARE ...
}
}
}
<configuration>
<ci>...</ci>
<revapi>
... HERE WE ARE ...
</revapi>
</configuration>
The Revapi configurations would only be read from the "… HERE WE ARE …" part of the documents.
Revapi itself uses this approach to track the changes made to its API across the versions using a single file. Each Revapi module can have a "api-changes.json" file in its base directory. The contents of this file follow this pattern:
{
"version1": {
"revapi": {
"ignore": [
...
]
}
},
"version2": {
"revapi": {
"ignore": [
...
]
}
},
...
}
I.e. in that file, the root elements are the released versions of revapi and under them there are configurations for revapi for the particular version to pass the build. Usually, this is just a list of ignored API changes - i.e. the API changes made in that release that are to be purposefully ignored by the tool so that the build passes. To make this work, revapi build contains this profile:
<profile>
<id>api-check</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>parse-version</id>
<goals>
<goal>parse-version</goal>
</goals>
<phase>validate</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.revapi</groupId>
<artifactId>revapi-maven-plugin</artifactId>
<version>${self-api-check.maven-version}</version>
<dependencies>
<dependency>
<groupId>org.revapi</groupId>
<artifactId>revapi-java</artifactId>
<version>${self-api-check.java-extension-version}</version>
</dependency>
</dependencies>
<configuration>
<failOnMissingConfigurationFiles>false</failOnMissingConfigurationFiles>
<analysisConfigurationFiles>
<configurationFile>
<path>api-changes.json</path>
<roots>
<root>${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}</root>
</roots>
</configurationFile>
</analysisConfigurationFiles>
</configuration>
<executions>
<execution>
<id>api-check</id>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>