japitools: Java API compatibility testing tools

japitools consists of two simple tools designed to test for compatibility between Java APIs. They were originally designed for testing free implementations of Java itself for compatibility with Sun's JDK, but they can also be used for testing backward compatibility between versions of any API.

The tools are japize and japicompat. Japize is a java program which uses the jode.bytecode library to emit a listing of an API in a machine-readable format. Japicompat then takes two such listings and compares them for binary compatibility, as defined by Sun in the Java Language Specification (and as amended here).

News

Installation

You can download japitools-0.8.5.tar.gz here. This tarball contains japitools.jar which should be placed in your CLASSPATH, and a bin/ directory containing all the tools for working with japi files. It also contains sources for everything in case you want to do development. You will need Jode 1.1.1, available from here (local mirror of jode jarball).

The tools in the bin directory assume that perl is /usr/bin/perl.

Usage

Using japitools is a two-step process:

japize

The general usage of japize is as follows:

$ japize zip|unzip [as <name>] apis <zipfile> | <dir> ... +|-<pkgpath> ...

At least one +<pkgpath> is required. The word "reflect" can appear before "apis", but this is unreliable and deprecated. <name> will have ".japi" and/or ".gz" appended as appropriate.

The word "apis" can be replaced by "explicitly", "byname", "packages" or "classes". These options indicate whether something of the form "a.b.C" should be treated as a class or a package. You may specify this unambiguously by using one of the forms "a.b.cpackage," or "a.b,CClass".

That's the one-paragraph overview, pretty much equivalent to what you get if you type "japize" with no arguments. In detail, the options available are as follows:

zip|unzip

Specifying the "zip" option indicates that japize should gzip its output. Specifying "unzip" indicates that it shouldn't. Zipping the output is highly recommended since it saves huge amounts of space (japi files are large but extremely compressable because they contain large numbers of duplicate strings. Factor-of-ten compression seems to be typical). The only situations where you might not want to use gzip compression are when memory and CPU usage are extremely tight (zipping and unzipping both require more memory the larger the file gets, and require more CPU usage - on todays computers this is rarely an issue, though) or if your JVM does not implement GZIPOutputStream correctly (in which case you might still want to gzip the resulting file manually).

In previous releases "zip" was an option and "unzip" was not available as an option, because it was the implicit default. Since zipping is the recommended mode of operation, "zip" will become the implicit default in future versions. Japitools 0.8.5 requires an explicit selection of this option to avoid surprises caused by silently changing the default. Japitools 0.9 will accept "zip" as a no-op; future versions will refuse it entirely.

as <name>

Specifying this option tells japize to write its output to a file with the specified name. If this option is omitted, japize will write to standard output. When writing to a file, japize insists on writing to a filename ending in .japi for uncompressed files, or .japi.gz for compressed files. If the filename you specify doesn't have the right extension, japize will add parts to it to ensure that it does.

reflect

This option is unreliable in several common scenarios, and therefore considered deprecated. It tells japize to use reflection to look up API information, instead of using Jode to parse the class files. Reflection does not provide access to information about whether a static final field is constant or not, and also uses far more memory. I have a hard time imagining any legitimate use for this option.

As if that weren't enough reason for you to steer clear, bear in mind too that when invoked in this way, japize cannot load classes from the zipfiles or directories that you specify. Instead it will scan the paths you give it for class names, but use a regular Class.forName() call to actually look at the class contents. This means that to get API information for a given Java implementation with this option, you must use that JVM to run japize. The japize executable uses whatever is "java" in your system path.

apis | explicitly | byname | packages | classes

This option has a dual role: it indicates the boundary between japize options (zip, as, reflect) and other arguments (files and packages), but also tells japize how to deal with ambiguously specified arguments. See "+|-<pkgpath>" below for details on the behavior of each option. If you are unsure which to specify, "apis" is a safe choice.

<zipfile> | <dir>

Any arguments after "apis" that do not start with "+" or "-" are taken to be zipfiles or directories. These should be specified exactly as you would put them in your CLASSPATH (except separated by spaces rather than colons). Anything that's a file will be assumed to be a zip (or jar) file, so you can't specify a .class file directly - if you need to do that you should specify the folder containing it and then name the class for processing.

+|-<pkgpath>

To specify which classes are included, use +pkgpath to add pkgpaths to be scanned and -pkgpath to exclude sub-pkgpaths of these. You MUST specify at least one +pkgpath option to specify which pkgpath to include, otherwise Japize could happily scan through all the zipfiles and directories but not actually process any of the classes. Since that would be a useless thing to do, japize gives an error instead.

A "pkgpath" refers to either a package (which includes, by implication, all sub-packages of it) or a single class. A pkgpath for a package looks like "com.foo.pkg.sub," and a pkgpath for a class looks like "com.foo.pkg,Cls". The existence and placement of the comma indicates unambiguously which type of path is intended.

Most of the time, though, it's a pain to have to put in commas in names that are familiar with dots instead, and get the comma placement exactly right. For this reason, japize accepts pkgpaths containing only dots, and lets you tell it what to make of those names. The interpretation of "a.b.c" as a pkgpath depends on whether you specified apis, explicitly, byname, packages, or classes.

apis
a.b.c is tried both as a package and a class. This will always do what you want (which is why apis is described as the safe default) but at the expense of possibly doing extra unnecessary processing trying to find the wrong thing.
explicitly
pkgpaths of the form a.b.c are illegal - you must use the explicit form.
byname
a.b.c will be processed as a package if "c" starts with a lowercase letter, or as a class if it starts with an uppercase one. This usually does what you want but fails on things like org.omg.CORBA.
packages
a.b.c will be processed as a package. If processing for a class is needed, it must be specified explicitly.
classes
a.b.c will be processed as a class. If processing for a package is needed, it must be specified explicitly.

Example

As an example, Sun's JDK 1.1 includes classes in java.awt.peer and in java.text.resources that are not part of the public API, even though they are public classes; however, every other class in the java.* package hierarchy is part of the public API. The syntax to construct a useful jdk11.japi.gz would therefore be:

$ japize zip as jdk11 apis classes.zip +java -java.awt.peer -java.text.resources

Note that since all pkgpath arguments here are packages, you could save a small amount of processing by doing this instead:

$ japize zip as jdk11 packages classes.zip +java -java.awt.peer -java.text.resources

or even this:

$ japize zip as jdk11 explicitly classes.zip +java, -java.awt.peer, -java.text.resources,

Another example, this time doing the same thing for kaffe:

$ japize zip as kaffe packages $KAFFEHOME/share/kaffe/Klasses.jar $KAFFEHOME/share/kaffe/rmi.jar +java -java.awt.peer -java.text.resources

japicompat

Next, you can perform the test for compatibility between these two files:

$ japicompat jdk11.japi.gz kaffe.japi.gz

Either compressed (.japi.gz) or uncompressed (.japi) files can be passed to japicompat: The file extension is used to determine whether or not to pipe input through gzip or not. japicompat reports its progress to standard error and the specific errors to standard output, so you can redirect standard output to watch the progress without getting spammed by verbose error messages. However, the output should happen in a sensible order even if this is not done. Future versions of japicompat will support more flexible output, including HTML.

By default, japicompat tests for binary compatibility as defined by the JLS, plus a couple of additions (see below for details). You can turn off these additions by passing the -s flag to japicompat, for example:

$ japicompat -s jdk11.japi.gz kaffe.japi.gz

The s stands for "sun", "standard", "specification", or if you like more colorful language, "single-buttocked" (one buttock=half an...)

japicompat can also check that Serializable classes have the same SerialVersionUID number from one release to the next. Because in most cases this is likely to produce lots of errors, it needs to be explicitly turned on by passing the -v flag to japicompat, for example:

$ japicompat -v jdk11.japi.gz kaffe.japi.gz

The v stands for serial "version" or "verbose".

What exactly does japicompat test?

As mentioned above, japicompat tests for binary compatibility as defined by Sun in the JLS. A full summary of what does and does not break binary compatibility according to Sun is here.

However, japicompat also performs some checks that are not specified by the JLS, for the simple reason that I believe the JLS is wrong to omit them. You can omit these four extra checks by passing the "-s" flag to japicompat, although I'm not sure why you would want to...

The specific checks that I believe the JLS should include are:

japicompat specifically tests that the second argument is backwardly-compatible with the first. Therefore, a perfect implementation of JDK 1.1 would produce no errors regardless of the order of the arguments, but a perfect implementation of JDK1.1 plus *parts* of JDK1.2 should be tested as follows:

$ japicompat jdk11.japi.gz kaffe.japi.gz
$ japicompat kaffe.japi.gz jdk12.japi.gz

It is probably impossible to make an implementation that passes both these tests, since Sun's own JDK1.2 produces numerous errors when tested against JDK1.1.

License

These programs are made available under the GNU General Public License, version 2 or later.

To do

Here is an incomplete list of what still needs to be done:

Another to-do list with most of these same todos broken down by hypothetical future release number can be found in the jarball in design/TODO.