Liquibase 4.0 Extension Upgrade Guide

Changes introduced in 4.0.0

There are four major changes introduced in 4.0:

  1. Changed how extension classes are configured and found
  2. Changed how changelog files are found
  3. Changed how logging works
  4. Added a new Scope object

We also made a collection of minor API changes that we will cover in more detail below.

How extension classes are configured and found

Prior to 4.0, we had a custom ServiceLocator class which relied on custom logic to find all the classes that implemented our base interfaces. We'd specify a set of package names in a Liquibase-Packages section of MANIFEST.MF and Liquibase would go through all the configured classloaders to find classes that are in those packages.

Unfortunately, Java's classloader interface is not well set up for that type of logic. All of our attempts at workarounds often conflicted with logic in application-specific classloader implementations. It was a source of many issues.

Starting with 4.0, we switched to the standard java.util.ServiceLoader system to find extension classes. The Java ServiceLoader system works as follows:

  • Create a file in META-INF/services whose name matches the interface you are implementing
  • In that file, add the list of all classes that implement that interface.

For example, in Liquibase we have a file named META-INF/services/liquibase.database.Database that contains:

liquibase.database.core.DB2Database
liquibase.database.core.Db2zDatabase
liquibase.database.core.DerbyDatabase
liquibase.database.core.Firebird3Database
liquibase.database.core.FirebirdDatabase
liquibase.database.core.H2Database
liquibase.database.core.HsqlDatabase
liquibase.database.core.InformixDatabase
liquibase.database.core.Ingres9Database
liquibase.database.core.MSSQLDatabase
liquibase.database.core.MariaDBDatabase
liquibase.database.core.MockDatabase
liquibase.database.core.MySQLDatabase
liquibase.database.core.OracleDatabase
liquibase.database.core.PostgresDatabase
liquibase.database.core.SQLiteDatabase
liquibase.database.core.SybaseASADatabase
liquibase.database.core.SybaseDatabase
liquibase.database.core.UnsupportedDatabase

The ServiceLoader will look for all files named META-INF/services/liquibase.database.Database in the classpath and use the union of all the classes listed in them as implementations.

How this impacts your extension

Because Liquibase no longer scans the Liquibase-Packages directories anymore, extensions need to be updated to list all their implementations in META-INF/services.

To do

For each class you created that implements a Liquibase extension point, add your class name to a file in META-INF/services that matches the name of the Liquibase interface it implements.

For example, if you have created com.example.liquibase.MyCreateTableChange and com.example.liquibase.MyAlterTableChange classes that implement the liquibase.change.Change class, you must create the file META-INF/services/liquibase.change.Change containing the text:

com.example.liquibase.MyAlterTableChange
com.example.liquibase.MyCreateTableChange

Note: For extensions that have NOT been updated to list their classes this way, the liquibase-compat library includes the old classpath-scanning logic in it, so adding liquibase-compat to your classpath will allow extensions that have not made this change to continue to work for now.

The APIs around ServiceLocator have changed, so any extensions that have their own custom ServiceLocators or interact with the shipped ones will need to be updated. We have not introduced any backwards-compatibility into these classes because they are low-level and rarely-used directly in extensions. If you are using these classes and have questions on the changes, contact us by email, on Discord, or on the Liquibase Forum.

How changelog files are found

Prior to 4.0, the way we find changelog files (both top-level files as well as included/referenced files) was mixed in with the logic on how to look up extension classes. Logic around how to handle things like directory-delimiter differences and encoding handling was also scattered and duplicated throughout the code.

Starting with 4.0, we've completely split the ResourceAccessor code from the ServiceLocator code, allowing both to do what they do best and not get in each other's way.

How this impacts your extension

The APIs around ResourceAccessor have changed, so any extensions that have their own custom ResourceAccessor or interact with the shipped ones will need to be updated. We have not introduced any backwards-compatibility into these classes because they are low-level and rarely-used directly in extensions.

Most likely, extensions are using the stream handling that exists in ResourceAccessor. These APIs have been cleaned up and it was too difficult to introduce old-api compatible versions alongside the new. Most extensions used the StreamUtil.openStream() method which has been deprecated but still exists.

To do

Search and replace all instances of the following methods:

Old Method New Method
StreamUtil.openStream() resourceAccessor.openStream()
new FileSystemResourceAccessor(String) new FileSystemResourceAccessor(File)

If you have questions on making the changes, you can contact us by email, on Discord, or on the Liquibase Forum.

How logging works

Prior to 4.0, we used a custom logging interface around slf4j. The custom interface was there to allow alternate logging methods through extensions. The logging API was also used for both logs and user messages that should be sent to the console.

Starting with 4.0, we still have the custom logging API, but switched the default implementation to use java.util.Logging. The extra dependency on slf4j and indirection it provided was not giving us enough value for the cost. We also split out a new liquibase.ui.UIService for user messages that need to be routed to the “UI”.

How this impacts your extension

The logging API has shifted slightly, but we tried to keep it compatible for most use cases without introducing too much deprecated code.

As part of the shift to java.util.Logging, we better defined what the various levels are based on how java.util.Logging defines them. The main change is that they have a “fine” level, not a ”debug” level. Because log.debug(“message here“) is used so much, we kept that a deprecated method so that code continues to work.

We also used to have a version of the log methods that took an initial liquibase.logging.Targetparameter to specify if the messages went to the log or to the UI. Because most extensions are simply specifying the default method without the parameter, we didn't bother to include a deprecated Target enum and methods.

To do

If you are using one of those logging methods, either remove the liquibase.logging.Target parameter to send the message to the log, or use the new UIService.

A new scope object

One issue around preserving API compatibility is handling new parameters that need to be passed to methods. For example, if we realize a piece of code needs access to the Database object, we need to change a whole chain of method parameters to pass that Database object along from the point where we have it to the point where we need it. Method changes like that can be very API-breaking.

Starting with 4.0, we are introducing a new liquibase.Scope object. The job of the Scope object is to be a mid-point between global variables and method-level parameters. It allows us to set objects in one place in the code and access it from another point without to pass them along in method parameters.

You can access the scope with Scope.getCurrentScope() and add to the scope with Scope.child()

We are still experimenting with best practices in what goes in the Scope and what does not, and are just starting to utilize it. But, the plan is to have it fully incorporated in to the API design by the end of the 4.x series.

For now, we are using the Scope object to better manage singleton instances, and have replaced methods that took ResourceAccessors, ClassLoaders, and ServiceLocators with new methods that do not take those as parameters and instead get them off the Scope.

How this impacts your extension

If you are accessing singleton objects like the Logger or DatabaseFactory, the correct way to access them is now Scope.getCurrentScope().getSingleton(DatabaseFactory.class) rather than DatabaseFactory.getInstance(). We do still have the getInstance() methods in the API, but they are marked as deprecated.

For commonly used singletons, we have helper methods directly on the Scope object. The Logger in particular should now be accessed by Scope.getCurrentScope().getLog(getClass() rather than LogFactory.getLogger().getLog(getClass()).

To do

Search and replace all instances of the following methods:

Old Method New Method
DatabaseFactory.getInstance() Scope.getCurrentScope().getSingleton(DatabaseFactory.class)
LogFactory.getLogger().getLog(getClass()) Scope.getCurrentScope().getLog(getClass())

Misc API Cleanup

Beyond those major changes in 4.0, we did some additional API cleanup work as well.

liquibase.util.StringUtils → liquibase.util.StringUtil

In 3.x, we had the plural liquibase.util.StringUtils which differed than the singular naming in all our other “Util” classes. We fixed that difference in core, and introduced a deprecated StringUtils in liquibase-compat.jar.

To do

Search and replace all instances of the following method:

Old Method New Method
liquibase.util.StringUtils liquibase.util.StringUtil

liquibase.sdk.database.MockDatabase → liquibase.database.core.MockDatabase

If you were using the MockDatabase class for testing, it has been moved to a different package because we cleared out the entire sdk package.

Others

There were other misc method signature changes as well, but they are all very low level and little used by extensions.

Questions and Problems

If you have questions on the API changes or issues with your extension supporting Liquibase 4.0, don't hesitate to contact us by email, on Discord, or on the Liquibase Forum.