Working With Extensions in Liquibase 4.0 Beta 2

 

Overview

As a platform built around extensibility, Liquibase strives to preserve API-level compatibility from release to release. However, preserving compatibility sometimes comes at a cost of not being able to use new technologies, patterns, and use cases that do not fit well into that original API structure. 

In our quest to balance stability with innovation, Liquibase 4.0 introduces some API breaking changes. To ease the transition, we also included an update compatibility layer to keep existing extensions working, and give everyone time to make the (minor) extension changes required to stay fully Liquibase compatible.

End User Impact

For most extensions, you can still run a Liquibase 3.x-designed version of the extension on Liquibase 4.0 as long as you include the liquibase-compat.jar to your classpath.

If you’re using a 3.x version of an extension, please download the compatibility library from Maven Central or from Github at https://github.com/liquibase/liquibase-compat/releases.

If your extension has more complexity and falls outside of the basic development patterns, it's possible that some of the functionality will be broken. The indicator of problems is the occurrence of method not found errors when performing certain operations. If this occurs, please let the extension developer know.

Extension Author Impact

As an extension author, the liquibase-compat library should give you time to get your library up-to-date with Liquibase 4.x so you aren’t relying on the liquibase-compat library or the deprecated methods for very long.

To update your library, you will need to:

  1. Compile your extension against the 4.0 version of Liquibase without liquibase-compat.jar in your classpath

  2. Fix any compilation errors

  3. Fix any deprecated warnings

  4. Re-test functionality

By releasing a new version of your extension to work with the Liquibase 4.0, you keep your users from having to rely on the liquibase-compat library. Removing this library will reduce installation overhead and get rid of unexpected behaviors.

How to run Liquibase 4.x with pre-4.x Extensions

We recognize the importance of backward compatibility to extension writers and extension users. To better support extension compatibility, we created a new liquibase-compat library for Liquibase 4.0. We intend to include new compatibility code, as changes are made to Liquibase Core, to maintain compatibility.

Here are the details about the three changes we made to this new liquibase-compat library:

  1. Introduced @Deprecated versions of classes/methods that have been shifted around

    1. We renamed the Log.debug() method to Log.fine(), added a Log.debug() method marked as deprecated that calls out to the new fine() method.

    2. We replaced how extension classes are found in Liquibase Core, so we added the old logic for locating both classes and files into the new liquibase-compat library.

  2. Added feature/functionality flags to enable old functionality

    1. Since we changed how Liquibase handled the translation between database-specific types and generic types, we added a configurable flag to change back to the old behavior.

Even though we added these tools, we are not able to promise 100% compatibility with extensions built against earlier Liquibase versions. We are striving to provide as much compatibility as possible. However, to be fully compatible with new versions of Liquibase you should build your extensions without using the liquibase-compat library and without using deprecated methods.

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 work-arounds 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 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

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

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.Target parameter 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

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

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.

To Do

Search and replace all instances of the following method:

Old Method

New Method

Old Method

New Method

liquibase.sdk.database.MockDatabase

liquibase.database.core.MockDatabase

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.