Activiti 数据库表结构大小写转换

  • 背景
  • 实操
  • 修改数据库服务
  • 利用拦截器处理
  • 拦截器使用
  • 通过源码窥探实现方案
  • DbSqlSessionFactory&sqlSessionFactory


背景

项目中使用了activiti,activiti使用mysql作为数据源。上线时被DBA卡住了,原因是大写的表结构不符合sql规范,一番沟通后无果,无奈只能自己想办法解决了。经过一番探索,最终成功解决表结构大写问题,现总结如下。

:首先你肯定需要自己将建表语句改为小写,然后创建对应的表,这里需要注意的一点是

act_ge_property

需要有初始化数据,否则项目无法正常启动。

实操

修改数据库服务

这个是最直接也最简单的方式,直接修改数据库服务,使其大小写不敏感

  • 打开my.cnf文件,加入以下语句后重启。
lower_case_table_names = 1

但是此方法受是否可操作服务器的局限。很明显,这个方法在我这这里行不通。

利用拦截器处理

使用mybatis的拦截器在向数据库发送sql的时候进行拦截,将其转换为小写的语句。

拦截器使用

  1. 定义拦截器 xxInterceptor 使其实现 mybatis的 Interceptor接口;
  2. 在mybaits的全局配置文件中,声明这个插件。
@Intercepts({
        @Signature(
                type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class
        })
})
@Component
public class MySqlInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        //获取到原始sql语句
        String sql = boundSql.getSql();
        sql = convertTableNameToLower(sql);
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, sql);
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }


    private String convertTableNameToLower(String sql) {
        //TODO 对sql进行处理
        return sql;
    }


    @Override
    public void setProperties(Properties properties) {

    }
}
<!-- MyBatis全局配置文件:mybatis-config.xml -->
<plugins>  
	<plugin interceptor="xxx.xxx.xxx.xxx.MySqlInterceptor">    
		<!--此处可以定义一些用得着的属性-->
		<property name="propertyXXX" value="valueXXX"/>  
	</plugin>
</plugins>

OK,此时一个sql拦截器就定义好了,启动项目试试吧。
没错,正如你期待的那样。如果你的表结构此时已经改成小写了,那么在启动的时候项目会报错,告诉你表不存在。但是,到这肯定不甘心啊,我的拦截器到底生效了没,是不是白搞了。于是,我为了项目能起来,做了如下操作

  • 先把表全部干掉,让activiti启动的时候自己创建表,
  • 等项目起来以后,再把系统自动创建的大写的表干掉,改成自己定义的小写表。

此时再测试一下(在自己定义的拦截器那块加个断点试试),没错,进来了,数据也正常写入了。至少取得了阶段性胜利。

思考:为什么项目启动的时候拦截器未生效,而在正常启动之后可以正常运行?
补充:拦截器的添加是通过sqlSessionFactory添加的

sqlSessionFactory.getConfiguration().addInterceptor(interceptor);

猜测是activiti自己对mybatis的SqlSessionFactory做了封装,并且与mybatis的默认DefaultSqlSessionFactory建立了联系。在启动的时候使用自己的SqlSessionFactory,项目启动之后DefaultSqlSessionFactory也初始化完成,此时的DefaultSqlSessionFactory已经加载了自定义的拦截器。自定义的与默认的又建立了联系,所以启动后可以正常使用。

后来的探索也验证了这一点。activiti自己维护了一个DbSqlSessionFactory
那如果我能在DbSqlSessionFactory这个上面添加我自定义的拦截器,那应该就可以正常使用了。

通过源码窥探实现方案

从这里入手

org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl#init
public void init() {
    this.initConfigurators();
    this.configuratorsBeforeInit();
    // ……
    this.configuratorsAfterInit();
}

查看initConfigurators方法

public void initConfigurators() {
        this.allConfigurators = new ArrayList();
        //加载默认的configurators
        if (this.configurators != null) {
            Iterator var1 = this.configurators.iterator();

            while(var1.hasNext()) {
                ProcessEngineConfigurator configurator = (ProcessEngineConfigurator)var1.next();
                this.allConfigurators.add(configurator);
            }
        }

        if (this.enableConfiguratorServiceLoader) {
            ClassLoader classLoader = this.getClassLoader();
            //加载自定义的configurators

            ServiceLoader<ProcessEngineConfigurator> configuratorServiceLoader = ServiceLoader.load(ProcessEngineConfigurator.class, classLoader);
            
            for(var4 = configuratorServiceLoader.iterator(); var4.hasNext(); ++nrOfServiceLoadedConfigurators) {
              	// ……
                this.allConfigurators.add(configurator);
            }
			//……
            
        }

    }

为了查看清晰,代码仅保留了我们需要关注的内容,此处主要做了两件事情

  • 加载默认的configurators添加至allConfigurators
  • 加载自定义的configurators添加至allConfigurators

加载自定义配置的时候,注意到ProcessEngineConfigurator

public interface ProcessEngineConfigurator {
    void beforeInit(ProcessEngineConfigurationImpl var1);

    void configure(ProcessEngineConfigurationImpl var1);

    int getPriority();
}

然后在configuratorsBeforeInit()configuratorsAfterInit()中分别调用了beforeInit()及configure()方法。

SqlSessionFactory sqlSessionFactory = ProcessEngineConfigurationImpl.getSqlSessionFactory();

拿到sqlSessionFactory就好办了。

正如上文 “补充” 中提到,只要通过

sqlSessionFactory.getConfiguration().addInterceptor(interceptor)

添加拦截器即可。接下来开始自定义configurators

  1. 实现ProcessEngineConfigurator(这里选择extend ProcessEngineConfigurator的抽象类AbstractProcessEngineConfigurator
public class MyProcessEngineConfigurator extends AbstractProcessEngineConfigurator {

    public void configure(ProcessEngineConfigurationImpl processEngineConfiguration) {

        SqlSessionFactory sqlSessionFactory = processEngineConfiguration.getSqlSessionFactory();
        //这里是自定义的MySqlInterceptor,跟上文一样
        sqlSessionFactory.getConfiguration().addInterceptor(new MySqlInterceptor());

    }

}
  1. 添加到配置文件
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
        <!--
			……
		-->
        <property name="configurators">
            <list>        
                   <bean class="com.xxx.xxx.MyProcessEngineConfigurator"></bean>        
            </list>
        </property>
</bean>

如果是springBoot的项目,直接使用配置类的方式

@Configuration
public class MyAbstractProcessEngineConfigurator implements ProcessEngineConfigurationConfigurer {


    @Override
    public void configure(SpringProcessEngineConfiguration springProcessEngineConfiguration) {
        List<ProcessEngineConfigurator> list = new ArrayList<>();
        list.add(new MyProcessEngineConfigurator());
        springProcessEngineConfiguration.setConfigurators(list);
    }
}

这样,在项目启动的时候,就可以将自定义的拦截器加载进activiti内,至此,改造结束。

DbSqlSessionFactory&sqlSessionFactory

activiti自己维护的DbSqlSessionFactory与mybatis的sqlSessionFactory的关系是如何维护的呢?

public void init() {
    this.initConfigurators();
    this.configuratorsBeforeInit();
    // ……
    if (this.usingRelationalDatabase) {
            this.initSqlSessionFactory();
        }

    this.initSessionFactories();
    // ……
    this.configuratorsAfterInit();
}

看到这里initSqlSessionFactory,创建mybatis的DefaultSqlSessionFactory

public void initSqlSessionFactory() {
        if (this.sqlSessionFactory == null) {
            InputStream inputStream = null;

            try {
                inputStream = this.getMyBatisXmlConfigurationStream();
                Environment environment = new Environment("default", this.transactionFactory, this.dataSource);
                Reader reader = new InputStreamReader(inputStream);
                Properties properties = new Properties();
                properties.put("prefix", this.databaseTablePrefix);
                String wildcardEscapeClause = "";
                if (this.databaseWildcardEscapeCharacter != null && this.databaseWildcardEscapeCharacter.length() != 0) {
                    wildcardEscapeClause = " escape '" + this.databaseWildcardEscapeCharacter + "'";
                }

                properties.put("wildcardEscapeClause", wildcardEscapeClause);
                properties.put("limitBefore", "");
                properties.put("limitAfter", "");
                properties.put("limitBetween", "");
                properties.put("limitOuterJoinBetween", "");
                properties.put("limitBeforeNativeQuery", "");
                properties.put("orderBy", "order by ${orderByColumns}");
                properties.put("blobType", "BLOB");
                properties.put("boolValue", "TRUE");
                if (this.databaseType != null) {
                    properties.load(this.getResourceAsStream("org/activiti/db/properties/" + this.databaseType + ".properties"));
                }

                Configuration configuration = this.initMybatisConfiguration(environment, reader, properties);
                this.sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
            } catch (Exception var10) {
                throw new ActivitiException("Error while building ibatis SqlSessionFactory: " + var10.getMessage(), var10);
            } finally {
                IoUtil.closeSilently(inputStream);
            }
        }

    }

然后将DefaultSqlSessionFactory赋给dbSqlSessionFactory

public void initSessionFactories() {
        if (this.sessionFactories == null) {
            this.sessionFactories = new HashMap();
            if (this.usingRelationalDatabase) {
                this.initDbSqlSessionFactory();
            }

            this.addSessionFactory(new GenericManagerFactory(EntityCache.class, EntityCacheImpl.class));
        }

        if (this.customSessionFactories != null) {
            Iterator var1 = this.customSessionFactories.iterator();

            while(var1.hasNext()) {
                SessionFactory sessionFactory = (SessionFactory)var1.next();
                this.addSessionFactory(sessionFactory);
            }
        }

    }



public void initDbSqlSessionFactory() {
        if (this.dbSqlSessionFactory == null) {
            this.dbSqlSessionFactory = this.createDbSqlSessionFactory();
        }

        this.dbSqlSessionFactory.setDatabaseType(this.databaseType);
        this.dbSqlSessionFactory.setIdGenerator(this.idGenerator);
        //这里添加
        this.dbSqlSessionFactory.setSqlSessionFactory(this.sqlSessionFactory);
        this.dbSqlSessionFactory.setDbIdentityUsed(this.isDbIdentityUsed);
        this.dbSqlSessionFactory.setDbHistoryUsed(this.isDbHistoryUsed);
        this.dbSqlSessionFactory.setDatabaseTablePrefix(this.databaseTablePrefix);
        this.dbSqlSessionFactory.setTablePrefixIsSchema(this.tablePrefixIsSchema);
        this.dbSqlSessionFactory.setDatabaseCatalog(this.databaseCatalog);
        this.dbSqlSessionFactory.setDatabaseSchema(this.databaseSchema);
        this.dbSqlSessionFactory.setBulkInsertEnabled(this.isBulkInsertEnabled, this.databaseType);
        this.dbSqlSessionFactory.setMaxNrOfStatementsInBulkInsert(this.maxNrOfStatementsInBulkInsert);
        this.addSessionFactory(this.dbSqlSessionFactory);
    }