Fork me on GitHub

Description

A simple maven plugin that detects some illegal changes to a Java project's API.

How it works

APILyzer looks for deviations from the following rules.

  • Public API members should use other Public API types.
  • Public API members should use an approved set of types.

APILyzer allows easy declaration of Public API Types and approved types. The public members of the public API types are analyzed to ensure only expected types are used. Public members include public methods, public fields, and public inner classes. Protected classes and members are treated as public during analysis.

Other tools

This tool fills a niche not covered by other tools, like Animal Sniffer and checkstyle import control. A project that wants to ensure API stability would likely use these tools in addition to APILyzer. Import analysis differs because it's ok for a public API class to import a non-public API class for use only in its implementation. Comparing new API changes to signatures of a previous API differs because it only validates that the old API is not broken.

How to use it

To add this plugin to your project, configure the plugin similarly to:

  <build>
    <plugins>
      <plugin>
        <groupId>net.revelc.code</groupId>
        <artifactId>apilyzer-maven-plugin</artifactId>
        <version>1.3.0</version>
        <executions>
          <execution>
            <id>apilyzer</id>
            <goals>
              <goal>analyze</goal>
            </goals>
            <configuration>
              <includes>
                <!--Specify one or more regular expressions that define the
                    public API.  Each regex is matched agains all fully
                    qualified class names.  Any class that matches (and is
                    public) is added to the set of public API classes.-->
                <include>org[.]apache[.]accumulo[.]minicluster[.].*</include>
              </includes>
              <excludes>
                <!-- Specifiy zero or more regular expressions. Any regex that
                     matches will exclude a prevously included class from the
                     set of API classes -->
                <exclude>.*[.]impl[.].*</exclude>
                <exclude>.*Impl</exclude>
              </excludes>
              <allows>
                <!-- Specify zero or more regular expressions defining the set
                     of non-API classes thats it ok for public API members to
                     reference.  These regular expressions are matched against
                     fully qualified type names referenced by public API
                     members.  Conceptually, public API classes and Java classes
                     are automatically added to this set, so there is no need
                     to add those here. -->
                <allow>org[.]apache[.]accumulo[.]core[.]client[.].*</allow>
                <allow>org[.]apache[.]accumulo[.]core[.]data[.](Mutation|Key|Value|Condition|ConditionalMutation|Range|ByteSequence|PartialKey|Column)</allow>
                <allow>org[.]apache[.]accumulo[.]core[.]security[.](ColumnVisibility|Authorizations)</allow>
              </allows>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

and build your project, similarly to (it runs at the verify phase by default):

mvn verify

Below is the output of running the above command.

Includes: [org[.]apache[.]accumulo[.]minicluster[.].*]
Excludes: [.*[.]impl[.].*, .*Impl]
Allowed: [org[.]apache[.]accumulo[.]core[.]client[.].*, org[.]apache[.]accumulo[.]core[.]data[.](Mutation|Key|Value|Condition|ConditionalMutation|Range|ByteSequence|PartialKey|Column), org[.]apache[.]accumulo[.]core[.]security[.](ColumnVisibility|Authorizations)]

Public API:
org.apache.accumulo.minicluster.MemoryUnit
org.apache.accumulo.minicluster.MiniAccumuloCluster
org.apache.accumulo.minicluster.MiniAccumuloConfig
org.apache.accumulo.minicluster.MiniAccumuloInstance
org.apache.accumulo.minicluster.MiniAccumuloRunner
org.apache.accumulo.minicluster.MiniAccumuloRunner$Opts
org.apache.accumulo.minicluster.MiniAccumuloRunner$PropertiesConverter
org.apache.accumulo.minicluster.ServerType

CONTEXT              TYPE                                                         FIELD/METHOD                        NON-PUBLIC REFERENCE

Method return        org.apache.accumulo.minicluster.MiniAccumuloInstance         getConfigProperties(...)            org.apache.commons.configuration.PropertiesConfiguration
Method param         org.apache.accumulo.minicluster.MiniAccumuloInstance         lookupInstanceName(...)             org.apache.accumulo.fate.zookeeper.ZooCache

The output shows the 8 types that APILyzer determined to be public API types based on its configuration and the maven dependencies. These are the 8 types that APILyzer analyzed.

The output also shows two problems APILyzer found with these 8 types. Both problems are with public API methods. The first problem is MiniAccumuloInstance.getConfigProperties() returns PropertiesConfiguration which is not a public API type. The second problem is MiniAccumuloInstance.lookupInstanceName() takes a parameter of type ZooCache which is not a public API type.

Annotation Example

Hadoop has two annotations that it uses to communicate who should use its APIs. First, the InterfaceAudience annotation has values of Public, LimitedPrivate, and Private. Second, the InterfaceStability annotation has values of Stable, Evolving, and Unstable. In Hadoop the convention is to mark each API with an audience and stability annotation. The following example finds types marked Public+Stable using types that are not Public+Stable.

When trying to analyze a classes annotations, the class must be loaded. In the example below some classes could not be loaded because their dependencies were not on the classpath. However these classes were not in a package we cared about. Excluding classes not in the org.apache.hadoop package fixed this problem.

  <configuration>
     <!--Look for Public+Stable APIs using Types that are not Public+Stable-->
     <includes>
       <!-- This class has no annotation, but seems like it sould be in API -->
       <include>org[.]apache[.]hadoop[.]fs[.]RemoteIterator</include>
     </includes>
     <includeAnnotations>
       <include>
         [@]org[.]apache[.]hadoop[.]classification[.]InterfaceAudience[$]Public.*
       </include>
     </includeAnnotations>
     <excludeAnnotations>
       <exclude>
         [@]org[.]apache[.]hadoop[.]classification[.]InterfaceStability[$]Evolving.*
       </exclude>
       <exclude>
         [@]org[.]apache[.]hadoop[.]classification[.]InterfaceStability[$]Unstable.*
       </exclude>
       <!-- If a class is included in the API, then its public inner classes are also (unless excluded).
            This exclude is for the case where a type w/ Private annotation is an inner class of a class
            with a Public annotation -->
       <exclude>
         [@]org[.]apache[.]hadoop[.]classification[.]InterfaceAudience[$](Limited)?Private.*
       </exclude>
    </excludeAnnotations>
     <excludes>
        <!-- Exclude all classes not in the org.apache.hadoop package -->
        <exclude>(?!org[.]apache[.]hadoop.*).*</exclude>
     </excludes>
  </configuration>