flowable适配人大金仓Kingbase数据库

发布时间 2024-01-10 17:13:01作者: 星耀寂夜

背景

因为国产化的需求,需要把现有项目的数据库改成人大金仓,适配某个项目的时候因为使用了没适配Kingbase的flowable,导致无法启动。
原本使用的是Oracle数据库,kingbase兼容Oracle数据库,可以直接当成Oracle来使用。

错误1: couldn't deduct database type from database product name 'KingbaseES'

image

Caused by: org.flowable.common.engine.api.FlowableException: couldn't deduct database type from database product name 'KingbaseES'
	at org.flowable.common.engine.impl.AbstractEngineConfiguration.initDatabaseType(AbstractEngineConfiguration.java:486)
	at org.flowable.common.engine.impl.AbstractEngineConfiguration.initDataSource(AbstractEngineConfiguration.java:456)
	at org.flowable.app.engine.AppEngineConfiguration.init(AppEngineConfiguration.java:198)
	at org.flowable.app.engine.AppEngineConfiguration.buildAppEngine(AppEngineConfiguration.java:183)
	at org.flowable.app.spring.SpringAppEngineConfiguration.buildAppEngine(SpringAppEngineConfiguration.java:63)
	at org.flowable.app.spring.AppEngineFactoryBean.getObject(AppEngineFactoryBean.java:59)
	at org.flowable.app.spring.AppEngineFactoryBean.getObject(AppEngineFactoryBean.java:31)
	at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBeanRegistrySupport.java:169)
	... 213 common frames omitted

解决方法

先说解决方法,通过AOP直接指定databaseType为oracle

import lombok.RequiredArgsConstructor;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.flowable.app.engine.AppEngineConfiguration;
import org.springframework.stereotype.Component;

@Aspect
@Component
@RequiredArgsConstructor
public class KingbaseSupport {

    private final AppEngineConfiguration appEngineConfiguration;

    @Pointcut("execution(* org.flowable.app.engine.AppEngineConfiguration.buildAppEngine())")
    public void access() {

    }

    @Before("access()")
    public void before() {
        appEngineConfiguration.setDatabaseType("oracle");
    }
}

分析原因

查看报错的堆栈信息,报错是在AbstractEngineConfiguration.initDatabaseType方法上, 代码如下(省略部分代码):

public void initDatabaseType() {
        Connection connection = null;

        try {
            connection = this.dataSource.getConnection();
            DatabaseMetaData databaseMetaData = connection.getMetaData();
            String databaseProductName = databaseMetaData.getDatabaseProductName();
            this.logger.debug("database product name: '{}'", databaseProductName);

            this.databaseType = this.databaseTypeMappings.getProperty(databaseProductName);
            if (this.databaseType == null) {
                throw new FlowableException("couldn't deduct database type from database product name '" + databaseProductName + "'");
            }

            this.logger.debug("using database type: {}", this.databaseType);
        } catch (SQLException var56) {
            this.logger.error("Exception while initializing Database connection", var56);
        } finally {
            try {
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException var49) {
                this.logger.error("Exception while closing the Database connection", var49);
            }

        }

        if ("mssql".equals(this.databaseType)) {
            this.maxNrOfStatementsInBulkInsert = this.DEFAULT_MAX_NR_OF_STATEMENTS_BULK_INSERT_SQL_SERVER;
        }

    }

调试后发现,databaseProductName的值是KingbaseES, 从databaseTypeMappings中没有获取到名为KingbaseES的Property。

那么我们只要添加KingbaseES进去就可以了

继续查看代码,databaseTypeMappings的值来自AbstractEngineConfiguration.getDefaultDatabaseTypeMappings()方法

protected Properties databaseTypeMappings = getDefaultDatabaseTypeMappings();
public static Properties getDefaultDatabaseTypeMappings() {
        Properties databaseTypeMappings = new Properties();
        databaseTypeMappings.setProperty("H2", "h2");
        databaseTypeMappings.setProperty("HSQL Database Engine", "hsql");
        databaseTypeMappings.setProperty("MySQL", "mysql");
        databaseTypeMappings.setProperty("MariaDB", "mysql");
        databaseTypeMappings.setProperty("Oracle", "oracle");
        databaseTypeMappings.setProperty("PostgreSQL", "postgres");
        databaseTypeMappings.setProperty("Microsoft SQL Server", "mssql");
        databaseTypeMappings.setProperty("db2", "db2");
        databaseTypeMappings.setProperty("DB2", "db2");
        databaseTypeMappings.setProperty("DB2/NT", "db2");
        databaseTypeMappings.setProperty("DB2/NT64", "db2");
        databaseTypeMappings.setProperty("DB2 UDP", "db2");
        databaseTypeMappings.setProperty("DB2/LINUX", "db2");
        databaseTypeMappings.setProperty("DB2/LINUX390", "db2");
        databaseTypeMappings.setProperty("DB2/LINUXX8664", "db2");
        databaseTypeMappings.setProperty("DB2/LINUXZ64", "db2");
        databaseTypeMappings.setProperty("DB2/LINUXPPC64", "db2");
        databaseTypeMappings.setProperty("DB2/400 SQL", "db2");
        databaseTypeMappings.setProperty("DB2/6000", "db2");
        databaseTypeMappings.setProperty("DB2 UDB iSeries", "db2");
        databaseTypeMappings.setProperty("DB2/AIX64", "db2");
        databaseTypeMappings.setProperty("DB2/HPUX", "db2");
        databaseTypeMappings.setProperty("DB2/HP64", "db2");
        databaseTypeMappings.setProperty("DB2/SUN", "db2");
        databaseTypeMappings.setProperty("DB2/SUN64", "db2");
        databaseTypeMappings.setProperty("DB2/PTX", "db2");
        databaseTypeMappings.setProperty("DB2/2", "db2");
        databaseTypeMappings.setProperty("DB2 UDB AS400", "db2");
        databaseTypeMappings.setProperty("CockroachDB", "cockroachdb");
        return databaseTypeMappings;
    }

这里完全写死了,没办法修改,同时也没有找到添加的方法。

通过不停的下断点, 调用链是:

AppEngineConfiguration.buildAppEngine()
↓
AppEngineConfiguration.init()
↓
AbstractEngineConfiguration.initDataSource()
↓
AbstractEngineConfiguration.initDatabaseType()

initDataSource方法中,有判断databaseType是否为空,如果为则会调用initDatabaseType()方法:

if (this.databaseType == null) {
    this.initDatabaseType();
}

因此提前设置好databaseType就可以了。

错误2: com.kingbase8.util.KSQLException: 错误: 语法错误 在输入的末尾

解决方法

添加一个包名liquibase.database,在里面添加一个KingbaseDatabase类:

package liquibase.database;

import liquibase.database.core.OracleDatabase;
import liquibase.exception.DatabaseException;

public class KingbaseDatabase extends OracleDatabase {
    @Override
    public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
        return super.isCorrectDatabaseImplementation(conn) || "KingbaseES".equals(conn.getDatabaseProductName());
    }
}

分析原因

断点后发现在JdbcExecutorpublic Object execute(CallableStatementCallback action, List<SqlVisitor> sqlVisitors) throws DatabaseException方法执行了一条sqlcall current_schema,不支持这种语法就报错了。

image

通过调用堆栈发现sql语句在这个方法中获取AbstractJdbcDatabase.getConnectionSchemaName

    protected String getConnectionSchemaName() {
        if (this.connection == null) {
            return null;
        } else if (this.connection instanceof OfflineConnection) {
            return ((OfflineConnection)this.connection).getSchema();
        } else {
            try {
                SqlStatement currentSchemaStatement = this.getConnectionSchemaNameCallStatement();
                return (String)ExecutorService.getInstance().getExecutor(this).queryForObject(currentSchemaStatement, String.class);
            } catch (Exception var2) {
                LogService.getLog(this.getClass()).info(LogType.LOG, "Error getting default schema", var2);
                return null;
            }
        }
    }

关键点聚焦在getConnectionSchemaNameCallStatement()这个方法上, 默认返回的就是call current_schema这条SQL。

protected SqlStatement getConnectionSchemaNameCallStatement() {
    return new RawCallStatement("call current_schema");
}

getConnectionSchemaName()方法只有在getDefaultSchemaName()方法中被调用,而且getDefaultSchemaName()方法是实现Database接口的方法。

public abstract class AbstractJdbcDatabase implements Database {
    // 省略部分代码
        public String getDefaultSchemaName() {
        if (!this.supportsSchemas()) {
            return this.getDefaultCatalogName();
        } else {
            if (this.defaultSchemaName == null && this.connection != null) {
                this.defaultSchemaName = this.getConnectionSchemaName();
            }

            return this.defaultSchemaName;
        }
    }
}

也就是说不同类型的数据库应该会有不同类型的Database实现,接下来要找到是如何确定Database实现的。

继续通过调用堆栈向上查找,发现在LiquibaseBasedSchemaManagerschemaUpdate()方法上database消失了,说怕databsecreateLiquibaseInstance()方法上被确定下来。

image

protected Liquibase createLiquibaseInstance(LiquibaseDatabaseConfiguration databaseConfiguration) throws SQLException {
        Connection jdbcConnection = null;
        boolean closeConnection = false;

        try {
            CommandContext commandContext = Context.getCommandContext();
            if (commandContext == null) {
                jdbcConnection = databaseConfiguration.getDataSource().getConnection();
                closeConnection = true;
            } else {
                jdbcConnection = ((DbSqlSession)commandContext.getSession(DbSqlSession.class)).getSqlSession().getConnection();
            }

            if (!jdbcConnection.getAutoCommit()) {
                jdbcConnection.commit();
            }

            DatabaseConnection connection = new JdbcConnection(jdbcConnection);
            Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(connection);
            database.setDatabaseChangeLogTableName(this.changeLogPrefix + database.getDatabaseChangeLogTableName());
            database.setDatabaseChangeLogLockTableName(this.changeLogPrefix + database.getDatabaseChangeLogLockTableName());
            String databaseSchema = databaseConfiguration.getDatabaseSchema();
            if (StringUtils.isNotEmpty(databaseSchema)) {
                database.setDefaultSchemaName(databaseSchema);
                database.setLiquibaseSchemaName(databaseSchema);
            }

            String databaseCatalog = databaseConfiguration.getDatabaseCatalog();
            if (StringUtils.isNotEmpty(databaseCatalog)) {
                database.setDefaultCatalogName(databaseCatalog);
                database.setLiquibaseCatalogName(databaseCatalog);
            }

            return new Liquibase(this.changeLogFile, new ClassLoaderResourceAccessor(), database);
        } catch (Exception var9) {
            if (jdbcConnection != null && closeConnection) {
                jdbcConnection.close();
            }

            throw new FlowableException("Error creating " + this.context + " liquibase instance", var9);
        }
    }

关键在这一行:

Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(connection);

findCorrectDatabaseImplementation方法implementedDatabases变量中获取对应的database

Database implementedDatabase = (Database)var3.next();
if (connection instanceof OfflineConnection) {
    if (((OfflineConnection)connection).isCorrectDatabaseImplementation(implementedDatabase)) {
        foundDatabases.add(implementedDatabase);
    }
} else if (implementedDatabase.isCorrectDatabaseImplementation(connection)) {
    foundDatabases.add(implementedDatabase);
}

implementedDatabases从哪里来?

原来在DatabaseFactory的构造方法中进行了初始化, 搜索所有实现了Database的类并调用register方法注册。

image

注册方法register虽然是public,但是我们代码中获取不到实例,所以考虑添加支持Kingbase的Database并让它被扫描到。

findClasses()方法实际是调用了findClassesImpl()方法,方法中使用了一个packagesToScan,应该是用来限制扫描包名范围的。

image

setResourceAccessor()方法中会初始化packagesToScan, 先读取系统属性liquibase.scan.packages,否则读取META-INF/MANIFEST.MF文件,再不然就使用默认的包名。

image

image

因此只需要把自己实现的Database丢到默认的包名下就可以被扫面描到。