Hi,
I think I have an explanation for this. Within the liquibase.database.DatabaseFactory class, we can see the following two methods :
protected DatabaseFactory() {
try {
Class[] classes = ServiceLocator.getInstance().findClasses(Database.class);
for (Class<? extends Database> clazz : classes) {
register(clazz.getConstructor().newInstance());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Database findCorrectDatabaseImplementation(DatabaseConnection connection) throws DatabaseException {
Database database = null;
boolean foundImplementation = false;
for (Database implementedDatabase : getImplementedDatabases()) {
database = implementedDatabase;
if (database.isCorrectDatabaseImplementation(connection)) {
foundImplementation = true;
break;
}
}
if (!foundImplementation) {
LogFactory.getLogger().warning("Unknown database: " + connection.getDatabaseProductName());
database = new UnsupportedDatabase();
}
Database returnDatabase;
try {
returnDatabase = database.getClass().newInstance();
} catch (Exception e) {
throw new UnexpectedLiquibaseException(e);
}
returnDatabase.setConnection(connection);
return returnDatabase;
}
First, the ServiceLocator.findClasses(Database.class) method returns all the instances of Database.class, even those extended. So the Class[] object returned by this method contains the original liquibase.database.core.MySQLDatabase as well as my extension : liquibase.ext.MySQLDatabase. The DatabaseFactory.findCorrectDatabaseImplementation(DatabaseConnection) methods returns the first found within the list.
Now lets take a look at the last part of the ServiceLocator.findClasses(Database.class) method :
public Class[] findClasses(Class requiredInterface) throws ServiceNotFoundException {
//… code to find the implementations
List classes = classesBySuperclass.get(requiredInterface);
HashSet uniqueClasses = new HashSet(classes);
return uniqueClasses.toArray(new Class[uniqueClasses.size()]);
}
And here is an extract of the description of the HashSet class : This class implements the Set interface, backed by a hash table (actually a HashMap instance). It makes no guarantees as to the iteration order of the set; in particular, it does not guarantee that the order will remain constant over time.
Since the list of implementation returned by the ServiceLocator.findClasses(Database.class) contains the original MySQLDatabase implementation as well as my extension and those two implementations are listed in a random order from call to call, this explains why the implementation used is not always the same.
I guess this issue applies to all extensions (TypeConverter, SnapShotGenerator, Executor, etc). IMHO, I see two way in order to avoid this issue :
- Change the HashSet for another Collection (LinkedHashSet for exemple)
- Make sure that extensions from **.ext packages are forced to be at the begining of the list.
Another good idea would be to place the liquibase.ext at the begining of the list within the MANIFEST.MF’s Liquibase-Package attibute.
Any confirmation or suggestion ?