Semantic Versioning and Java
In this post, about semantic versioning, and how I believe it can be efficiently applied for the benefit of long-term interoperability of Java libraries.
Let us introduce the basic premise of semantic versioning (borrowed from their page), namely version numbers and the connection they have to the continued development of your software.
- MAJOR version when you make incompatible API changes,
- MINOR version when you add functionality in a backwards-compatible manner, and
- PATCH version when you make backwards-compatible bug fixes.
Hello Java
Java has a lot of things which could qualify as members your public API.
The most distinct feature in the language is the interface
, a fully abstract
class definition that forces you to describe all possible interactions that are
allowed with a given implementation.
So let’s build an API using that.
Consider @since
, here it doesn’t contain the patch version. It could, but it
wouldn’t make a difference.
A patch must never modify API, that privilege is left to the major, and
the minor version.
Maven plays an important role here as well.
The Java ecosystem relies on it to distribute libraries and resolve
dependencies.
The way you would expose your library is by putting the above in an API
artifact named eu.toolchain.mylib:mylib-api
.
You might also feel compelled to provide an implementation, this could be
eu.toolchain.mylib:mylib-core
.
The separation is not critical, but it helps in being explicit in what your public API is. Both for you and your users.
My intent is to have your users primarily interact with your library through interfaces, abstract classes, and value objects.
A Minor Change
Let us introduce a minor change to the library.
In library terms, we are exposing another symbol.
For Java, this is just another method with a given signature added to the
already existing MyLibrary
interface.
This only constitutes a minor change because consumers of the API which
happen to use 1.0
will happily continue to operate in a runtime containing
1.1
.
Anything linked against 1.0
will be oblivious to the fact that there is
added functionality in 1.1
.
This is due to indirection that is introduced by Java, method calls use a
very flexible symbolic reference to indicate the target of the invocation.
Removing a method and not fixing all callers of it would eventually cause
NoSuchMethodError
.
Eventually, because it would not be triggered until a caller attempts the
invocation at runtime. Ouch.
What qualifies as a minor change
Identifying what qualifies as a minor change, and what does not, is one of the harder aspects we need to deal with. It requires a bit of knowledge in how binary compatibility works.
The Eclipse project has compiled an excellent page on this topic which touches a few more cases. For all the gritty details, you should consult Chapter 13 of the Java Language Specification.
I’ll touch on a few things that are compatible, and why.
Increasing visibility
Increasing the visibility of a method is a minor change.
Visibility goes with the following modifiers, from least to most visible:
private
- package protected (no modifier)
protected
public
From the perspective of the user, a think is not part of your public API if it is not visible.
Adding a method
This works, because method invocations only consult the signature of the method being called, which is handled indirectly by the virtual machine who is responsible for looking up the method at runtime.
So this is good unless the client implements the given API.
If you are exposing an API that the client should implement, a very popular compromise is to provide an abstract class that the client must use as a base to maintain compatibility.
You as a library maintainer must maintain this class to make sure that between each minor release it does not force clients to have to implement methods they previously did were not required to.
To see this in action, check out SimpleTypeVisitor8 which is part of the interesting java.lang.model API.
Extending behaviour
This one is tricky, but probably the most important to understand.
If you have a documented behavior in your API, you are not allowed to remove or modify it.
In practice, it means that once your javadoc asserts something, that assertion must be versioned as well.
You may extend it in a manner, which does not violate the existing assertions.
You may not however, change the behavior from current Galaxy
to Milky Way
.
Your users will have operated under the assumption that the current
galaxy
will be consumed.
Imagine their surprise when they run the newly upgraded application in the
Andromeda Galaxy
and they inadvertently expedite their own extinction because
they didn’t expect a breaking change in behavior for a minor version :/.
A Major Change
Ok, so it’s time to rethink your library’s existence. The world changed, you’ve grown and realized the errors of your way. It’s time to fix all the design errors you made in the previous version.
In order to introduce a new major version, it is important to consider the following:
- Do I need to publish a new package?
- Do I need to publish a new Maven artifact?
- Should I introduce the changes using
@Deprecated
?
This sounds rough, but there are a few points to all this.
Publishing a new package
To maintain binary compatibility with the previous Major version.
There are no easy take-backs once an API has been published. You may communicate to your clients that something is deprecated, and it is time to upgrade. You cannot force an atomic upgrade.
If you introduce a Major change that cannot co-exist in a single classpath. Your users are in for a world of pain.
Publishing a new Maven artifact
To allow your users to co-depend on the various major versions of your
library.
Maven will only allow one version of a <groupId>:<artifactId>
combination to
exist within a given build solution.
For our example, we could go from eu.toolchain.mylib:mylib-api
to
eu.toolchain.mylib:mylib2-api
.
If you don’t change the artifact, Maven will not allow a user to install all your major versions. More importantly, any transitive dependencies requiring another major version will find themselves lacking.
Using @Deprecated to your advantage
@Deprecated
is a standard annotation discouraging the use of the element that is annotated.
This has wide support among IDEs, and will typically show up as a warning when used.
You can use this to your advantage when releasing a new Major version.
Assume that you are renaming a the following #badName()
method.
Into #goodName()
.
You can go back and release a new minor version of your 1.x
branch
containing the newly named method with a @Deprecated
annotation.
This is an excellent way of communicating what changes your users can expect, and can be applied to many situations.
Case studies
- Jackson performed a move between
1.x
and2.x
fromorg.codehaus.jackson
tocom.fasterxml.jackson.core
. The transition plan can be followed on their wiki. - In doing things the hard way we have Lucene Core, and their take on compatibility. Parts of their library use versioned packages in order to allow different implementations to co-exist. Most compatibility issues are handled by rarely breaking the public API, and doing version detection at runtime to determine which behavior to implement.
- Guava maintains compatibility for a long time, and communicate expectations through their @Beta annotation. Unfortunately, there are many things using @Beta at the moment, making this a real consideration when using the library.
- I’ve recently encouraged the Elasticsearch project to
consider the versioning implications
of their java client for the impending
2.x
release.
Project jigsaw
Project jigsaw is an initiative that could improve things in the near future by implementing a module system where dependencies and versions are more explicit.
The specification will not require implementations to support multiple versions of the same module, but it should be possible to hook into the module discovery process in a manner that supports it.
Final Words
Dependency hell is far from solved, but good practices can get us a long way.
Good luck, library maintainer. And may the releases be ever in your favor.