写点什么

Activiti 工作流自动生成 28 张数据库表的底层原理分析

用户头像
朱季谦
关注
发布于: 2021 年 03 月 26 日

原创/朱季谦

网上关于工作流引擎 Activiti 生成表的机制大多仅限于四种策略模式,但其底层是如何实现的,相关文章还是比较少,因此,觉得撸一撸其生成表机制的底层原理。

 

我接触工作流引擎 Activiti 已有两年之久,但一直都只限于熟悉其各类 API 的使用,对底层的实现,则存在较大的盲区。

 

Activiti 这个开源框架在设计上,其实存在不少值得学习和思考的地方,例如,框架用到以命令模式、责任链模式、模板模式等优秀的设计模式来进行框架的设计。

 

故而,是值得好好研究下 Activiti 这个框架的底层实现。

 

我在工作当中现阶段用的比较多是 Activiti6.0 版本,本文就以这个版本来展开分析。

 

在使用 Activiti 工作流引擎过程中,让我比较好奇的一个地方,是框架自带一套数据库表结构,在首次启动时,若设计了相应的建表策略时,将会自动生成 28 张表,而这些表都是以 ACT_开头。

 

那么问题来了,您是否与我一样,曾好奇过这些表都是怎么自动生成的呢?

 

下面,就开始一点点深入研究——

在工作流 Springboot+Activiti6.0 集成框架,网上最常见的引擎启动配置教程一般长这样:


 1 @Configuration 2 public class SpringBootActivitiConfig  { 3 @Bean 4 public ProcessEngine processEngine(){ 5      ProcessEngineConfiguration pro=ProcessEngineConfiguration.createStandaloneProcessEngineConfiguration(); 6      pro.setJdbcDriver("com.mysql.jdbc.Driver"); 7      pro.setJdbcUrl("xxxx"); 8      pro.setJdbcUsername("xxxx"); 9      pro.setJdbcPassword("xxx");10      //避免发布的图片和xml中文出现乱码11      pro.setActivityFontName("宋体");12      pro.setLabelFontName("宋体");13      pro.setAnnotationFontName("宋体");14      //数据库更更新策略15      pro.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);16      return pro.buildProcessEngine();17 }18     19      @Bean20      public RepositoryService repositoryService(){21          return processEngine().getRepositoryService();22      }23     24      @Bean25      public RuntimeService runtimeService(){26          return processEngine().getRuntimeService();27      }28     29      @Bean30      public TaskService taskService(){31          return processEngine().getTaskService();32      }33      ......34     35 }
复制代码


其中,方法 pro.setDatabaseSchemaUpdate()可对工作流引擎自带的 28 张表进行不同策略的更新。

Activiti6.0 版本总共有四种数据库表更新策略。

查看这三种策略的静态常量标识,分别如下:

 1 public abstract class ProcessEngineConfiguration { 2      public static final String DB_SCHEMA_UPDATE_FALSE = "false"; 3      public static final String DB_SCHEMA_UPDATE_CREATE_DROP = "create-drop"; 4      public static final String DB_SCHEMA_UPDATE_TRUE = "true"; 5      ...... 6 } 7  8 public abstract class ProcessEngineConfigurationImpl extends ProcessEngineConfiguration { 9      public static final String DB_SCHEMA_UPDATE_DROP_CREATE = "drop-create";10      ......11 }
复制代码


  • flase:默认值,引擎启动时,自动检查数据库里是否已有表,或者表版本是否匹配,如果无表或者表版本不对,则抛出异常。(常用在生产环境);

  • true:若表不存在,自动更新;若存在,而表有改动,则自动更新表,若表存在以及表无更新,则该策略不会做任何操作。(一般用在开发环境);

  • create_drop:启动时自动建表,关闭时就删除表,有一种临时表的感觉。(需手动关闭,才会起作用);

  • drop-create:启动时删除旧表,再重新建表。(无需手动关闭就能起作用);

 

整个启动更新数据库的过程都是围绕这四种策略,接下来就以这四种策略为主题,撸一下自动更新据库表的底层原理,这一步骤是在引擎启动时所执行的 buildProcessEngine()方法里实现。

从该 buildProcessEngine 方法名上便可以看出,这是一个初始化工作流引擎框架的方法。

image-20210311223459139


从这里开始,一步一步 debug 去分析源码实现。

一.初始化工作流的 buildProcessEngine()方法——

processEngineConfiguration.buildProcessEngine()是一个抽象方法,主要功能是初始化引擎,获取到工作流的核心 API 接口:ProcessEngine。通过该 API,可获取到引擎所有的 service 服务。

进入 processEngine 接口,可以看到,其涵盖了 Activiti 的所有服务接口:

 1 public interface ProcessEngine { 2  3    public static String VERSION = "6.0.0.4";  4  5    String getName(); 6  7   void close(); 8    //流程运行服务类,用于获取流程执行相关信息 9    RepositoryService getRepositoryService();10    //流程运行服务类,用于获取流程执行相关信息11    RuntimeService getRuntimeService();12    //内置表单,用于工作流自带内置表单的设置13    FormService getFormService();14    //任务服务类,用户获取任务信息15    TaskService getTaskService();16    //获取正在运行或已经完成的流程实例历史信息17    HistoryService getHistoryService();18    //创建、更新、删除、查询群组和用户19    IdentityService getIdentityService();20    //流程引擎的管理与维护21    ManagementService getManagementService();22    //提供对流程定义和部署存储库的访问的服务。23    DynamicBpmnService getDynamicBpmnService();24    //获取配置类25    ProcessEngineConfiguration getProcessEngineConfiguration();26    //提供对内置表单存储库的访问的服务。27    FormRepositoryService getFormEngineRepositoryService();28   29    org.activiti.form.api.FormService getFormEngineFormService();30 }
复制代码


buildProcessEngine()有三个子类方法的重写,默认是用 ProcessEngineConfigurationImpl 类继承重写 buildProcessEngine 初始化方法,如下图所示:

image


该 buildProcessEngine 重写方法如下:

 1 @Override 2 public ProcessEngine buildProcessEngine() { 3    //初始化的方法 4    init(); 5    //创建ProcessEngine 6    ProcessEngineImpl processEngine = new ProcessEngineImpl(this); 7  8   // Activiti 5引擎的触发装置 9    if (isActiviti5CompatibilityEnabled && activiti5CompatibilityHandler != null) {10      Context.setProcessEngineConfiguration(processEngine.getProcessEngineConfiguration());11      activiti5CompatibilityHandler.getRawProcessEngine();12    }13 14   postProcessEngineInitialisation();15 16   return processEngine;17 }
复制代码


init()方法里面包含各类需要初始化的方法,涉及到很多东西,这里先暂不一一展开分析,主要先分析与数据库连接初始化相关的逻辑。Activiti6.0 底层是通过 mybatis 来操作数据库的,下面主要涉及到 mybatis 的连接池与 SqlSessionFactory 的创建。

1.initDataSource():实现动态配置数据库 DataSource 源

1 protected boolean usingRelationalDatabase = true;2 if (usingRelationalDatabase) {3    initDataSource();4 }
复制代码

该数据库连接模式初始化的意义如何理解,这就需要回到最初引擎配置分析,其中里面有这样一部分代码:

1   pro.setJdbcDriver("com.mysql.jdbc.Driver");2   pro.setJdbcUrl("xxxx");3   pro.setJdbcUsername("xxxx");4   pro.setJdbcPassword("xxx");
复制代码

这部分设置的东西,都是数据库相关的参数,它将传到 initDataSource 方法里,通过 mybatis 默认的连接池 PooledDataSource 进行设置,可以说,这个方法主要是用来创建 mybatis 连接数据库的连接池,从而生成数据源连接。


 1 public void initDataSource() { 2     //判断数据源dataSource是否存在 3    if (dataSource == null) { 4        / 5        //判断是否使用JNDI方式连接数据源 6      if (dataSourceJndiName != null) { 7        try { 8          dataSource = (DataSource) new InitialContext().lookup(dataSourceJndiName); 9        } catch (Exception e) {10          ......11        }12       //使用非JNDI方式且数据库地址不为空,走下面的设置13      } else if (jdbcUrl != null) {14          //jdbc驱动为空或者jdbc连接账户为空15        if ((jdbcDriver == null) || (jdbcUsername == null)) {16         ......17        }18 19        //创建mybatis默认连接池PooledDataSource对象,这里传进来的,就是上面pro.setJdbcDriver("com.mysql.jdbc.Driver")配置的参数,20        //debug到这里,就可以清晰明白,配置类里设置的JdbcDriver、JdbcUrl、JdbcUsername、JdbcPassword等,就是为了用来创建连接池需要用到的;21        PooledDataSource pooledDataSource = new PooledDataSource(ReflectUtil.getClassLoader(), jdbcDriver, jdbcUrl, jdbcUsername, jdbcPassword);22       23        if (jdbcMaxActiveConnections > 0) {24          //设置最大活跃连接数25          pooledDataSource.setPoolMaximumActiveConnections(jdbcMaxActiveConnections);26        }27        if (jdbcMaxIdleConnections > 0) {28          // 设置最大空闲连接数29          pooledDataSource.setPoolMaximumIdleConnections(jdbcMaxIdleConnections);30        }31        if (jdbcMaxCheckoutTime > 0) {32          // 最大checkout 时长33          pooledDataSource.setPoolMaximumCheckoutTime(jdbcMaxCheckoutTime);34        }35        if (jdbcMaxWaitTime > 0) {36          // 在无法获取连接时,等待的时间37          pooledDataSource.setPoolTimeToWait(jdbcMaxWaitTime);38        }39        if (jdbcPingEnabled == true) {40          //是否允许发送测试SQL语句41          pooledDataSource.setPoolPingEnabled(true);42       43         ......44       45        dataSource = pooledDataSource;46      }47 48        ......49    }50    //设置数据库类型51    if (databaseType == null) {52      initDatabaseType();53    }54 }
复制代码


initDatabaseType()作用是设置工作流引擎的数据库类型。在工作流引擎里,自带的 28 张表,其实有区分不同的数据库,而不同数据库其建表语句存在一定差异。

进入到 initDatabaseType()方法看看其是如何设置数据库类型的——

 1 public void initDatabaseType() { 2      Connection connection = null; 3      try { 4          connection = this.dataSource.getConnection(); 5          DatabaseMetaData databaseMetaData = connection.getMetaData(); 6          String databaseProductName = databaseMetaData.getDatabaseProductName(); 7          this.databaseType = databaseTypeMappings.getProperty(databaseProductName); 8          ...... 9      } catch (SQLException var12) {10       ......11      } finally {12       ...... 13      }14 }
复制代码


进入到 databaseMetaData.getDatabaseProductName()方法里,可以看到这是一个接口定义的方法:

String getDatabaseProductName() throws SQLException;

 

这个方法在 java.sql 包中的 DatabaseMetData 接口里被定义,其作用是搜索并获取数据库的名称。这里配置使用的是 mysql 驱动,那么就会被 mysql 驱动中的 jdbc 中的 DatabaseMetaData 实现,如下代码所示:

1 package com.mysql.cj.jdbc;2 3 public class DatabaseMetaData implements java.sql.DatabaseMetaData 
复制代码

在该实现类里,其重写的方法中,将会返回 mysql 驱动对应的类型字符串:

1 @Override2 public String getDatabaseProductName() throws SQLException {3      return "MySQL";4 }
复制代码

故而,就会返回“MySql”字符串,并赋值给字符串变量 databaseProductName,再将 databaseProductName 当做参数传给

databaseTypeMappings.getProperty(databaseProductName),最终会得到一个 this.databaseType =“MySQL”,也就是意味着,设置了数据库类型 databaseType 的值为 mysql。注意,这一步很重要,因为将在后面生成表过程中,会判断该 databaseType 的值究竟是代表什么数据库类型。

1  String databaseProductName = databaseMetaData.getDatabaseProductName();2  this.databaseType = databaseTypeMappings.getProperty(databaseProductName);
复制代码
  1. 该方法对 SqlSessionFactory 进行 初始化创建:SqlSessionFactory 是 mybatis 的核心类,简单的讲,创建这个类,接下来就可以进行增删改查与事务操作了。

1 protected boolean usingRelationalDatabase = true;2 if (usingRelationalDatabase) {3   initSqlSessionFactory();4 }
复制代码

init()主要都是初始化引擎环境的相关操作,里面涉及到很多东西,但在本篇文中主要了解到这里面会创建线程池以及 mybatis 相关的初始创建即可。

二、开始进行 processEngine 的创建

ProcessEngineImpl processEngine = new ProcessEngineImpl(this);
复制代码


这部分代码,就是创建 Activiti 的各服务类了:

 1 public ProcessEngineImpl(ProcessEngineConfigurationImpl processEngineConfiguration) { 2   this.processEngineConfiguration = processEngineConfiguration; 3   this.name = processEngineConfiguration.getProcessEngineName(); 4   this.repositoryService = processEngineConfiguration.getRepositoryService(); 5   this.runtimeService = processEngineConfiguration.getRuntimeService(); 6   this.historicDataService = processEngineConfiguration.getHistoryService(); 7   this.identityService = processEngineConfiguration.getIdentityService(); 8   this.taskService = processEngineConfiguration.getTaskService(); 9   this.formService = processEngineConfiguration.getFormService();10   this.managementService = processEngineConfiguration.getManagementService();11   this.dynamicBpmnService = processEngineConfiguration.getDynamicBpmnService();12   this.asyncExecutor = processEngineConfiguration.getAsyncExecutor();13   this.commandExecutor = processEngineConfiguration.getCommandExecutor();14   this.sessionFactories = processEngineConfiguration.getSessionFactories();15   this.transactionContextFactory = processEngineConfiguration.getTransactionContextFactory();16   this.formEngineRepositoryService = processEngineConfiguration.getFormEngineRepositoryService();17   this.formEngineFormService = processEngineConfiguration.getFormEngineFormService();18 ​19   if (processEngineConfiguration.isUsingRelationalDatabase() && processEngineConfiguration.getDatabaseSchemaUpdate() != null) {20     commandExecutor.execute(processEngineConfiguration.getSchemaCommandConfig(), new SchemaOperationsProcessEngineBuild());21   }22  ......23 }
复制代码


注意,这里面有一段代码,整个引擎更新数据库的相应策略是具体实现,就在这里面:

1 if (processEngineConfiguration.isUsingRelationalDatabase() && processEngineConfiguration.getDatabaseSchemaUpdate() != null) {2     commandExecutor.execute(processEngineConfiguration.getSchemaCommandConfig(), new SchemaOperationsProcessEngineBuild());3   }
复制代码
  • processEngineConfiguration.isUsingRelationalDatabase()默认是 true,即代表需要对数据库模式做设置,例如前面初始化的 dataSource 数据源,创建 SqlSessionFactory 等,这些都算是对数据库模式进行设置;若为 false,则不会进行模式设置与验证,需要额外手动操作,这就意味着,引擎不能验证模式是否正确。

  • processEngineConfiguration.getDatabaseSchemaUpdate()是用户对数据库更新策略的设置,如,前面配置类里设置了 pro.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE),若设置四种模式当中的任何一种,就意味着,需要对引擎的数据库进行相应策略操作。

综上,if()判断为 true,就意味着,将执行括号里的代码,这块功能就是根据策略去对数据库进行相应的增删改查操作。

commandExecutor.execute()是一个典型的命令模式,先暂时不深入分析,直接点开 new SchemaOperationsProcessEngineBuild()方法。

 1 public final class SchemaOperationsProcessEngineBuild implements Command<Object> { 2 ​ 3   public Object execute(CommandContext commandContext) { 4     DbSqlSession dbSqlSession = commandContext.getDbSqlSession(); 5     if (dbSqlSession != null) { 6       dbSqlSession.performSchemaOperationsProcessEngineBuild(); 7    } 8     return null; 9   }10 }
复制代码


进入到 dbSqlSession.performSchemaOperationsProcessEngineBuild()方法中,接下来,将会看到,在这个方法当中,将根据不同的 if 判断,执行不同的方法——而这里的不同判断,正是基于四种数据库更新策略来展开的,换句话说,这个 performSchemaOperationsProcessEngineBuild 方法,才是真正去判断不同策略,从而根据不同策略来对数据库进行对应操作:

 1 public void performSchemaOperationsProcessEngineBuild() { 2     String databaseSchemaUpdate = Context.getProcessEngineConfiguration().getDatabaseSchemaUpdate(); 3     log.debug("Executing performSchemaOperationsProcessEngineBuild with setting " + databaseSchemaUpdate); 4      //drop-create模式 5     if ("drop-create".equals(databaseSchemaUpdate)) { 6         try { 7             this.dbSchemaDrop(); 8        } catch (RuntimeException var3) { 9        }10    }11    12     if (!"create-drop".equals(databaseSchemaUpdate) && !"drop-create".equals(databaseSchemaUpdate) && !"create".equals(databaseSchemaUpdate)) {13          //false模式14         if ("false".equals(databaseSchemaUpdate)) {15             this.dbSchemaCheckVersion();16        } else if ("true".equals(databaseSchemaUpdate)) {17               //true模式18             this.dbSchemaUpdate();19        }20    } else {21         //create_drop模式22         this.dbSchemaCreate();23    }24 ​25 
复制代码


这里主要以 true 模式来讲解,其他基本都类似的实现。

 1 public String dbSchemaUpdate() { 2 ​ 3   String feedback = null; 4   //判断是否需要更新,默认是false 5   boolean isUpgradeNeeded = false; 6   int matchingVersionIndex = -1; 7   //判断是否需要更新或者创建引擎核心engine表,若isEngineTablePresent()为true,表示需要更新,若为false,则需要新创建 8  if (isEngineTablePresent()) { 9   ......10   } else {11      //创建表方法,稍后会详细分析12     dbSchemaCreateEngine();13   }14  15  //判断是否需要创建或更新历史相关表16  if (this.isHistoryTablePresent()) {17       if (isUpgradeNeeded) {18            this.dbSchemaUpgrade("history", matchingVersionIndex);19        }20   } else if (this.dbSqlSessionFactory.isDbHistoryUsed()) {21        this.dbSchemaCreateHistory();22   }23   //判断是否需要更新群组和用户24   if (this.isIdentityTablePresent()) {25        if (isUpgradeNeeded) {26          this.dbSchemaUpgrade("identity", matchingVersionIndex);27        }28   } else if (this.dbSqlSessionFactory.isDbIdentityUsed()) {29          this.dbSchemaCreateIdentity();30        }31   return feedback;32 }
复制代码


这里以判断是否需要创建 engine 表为例,分析下 isEngineTablePresent()里面是如何做判断的。其他如历史表、用户表,其判断是否需要创建的逻辑,是类型的。

点击 isEngineTablePresent()进去——

1 public boolean isEngineTablePresent() {2   return isTablePresent("ACT_RU_EXECUTION");3 }
复制代码

进入到 isTablePresent("ACT_RU_EXECUTION")方法里,其中有一句最主要的代码:

1 tables = databaseMetaData.getTables(catalog, schema, tableName, JDBC_METADATA_TABLE_TYPES);2 return tables.next();
复制代码

这两行代码大概意思是,通过"ACT_RU_EXECUTION"表名去数据库中查询该 ACT_RU_EXECUTION 表是否存在,若不存在,返回 false,说明还没有创建;若存在,返回 true。

返回到该方法上层,当 isEngineTablePresent()返回值是 false 时,说明还没有创建 Activiti 表,故而,将执行 dbSchemaCreateEngine()方法来创建 28 表张工作流表。


1  if (isEngineTablePresent()) {2   ......3   } else {4      //创建表方法5     dbSchemaCreateEngine();6   }7 ​
复制代码


进入到 dbSchemaCreateEngine()方法——里面调用了 executeMandatorySchemaResource 方法,传入"create"与 "engine",代表着创建引擎表的意思。

1 protected void dbSchemaCreateEngine() {2     this.executeMandatorySchemaResource("create", "engine");3 }
复制代码

继续进入到 executeMandatorySchemaResource 里面——

1 public void executeMandatorySchemaResource(String operation, String component) {2     this.executeSchemaResource(operation, component, this.getResourceForDbOperation(operation, operation, component), false);3 }
复制代码

跳转到这里时,有一个地方需要注意一下,即调用的 this.getResourceForDbOperation(operation, operation, component)方法,这方法的作用,是为了获取 sql 文件所存放的相对路径,而这些 sql,就是构建工作流 28 张表的数据库 sql。因此,我们先去 executeSchemaResource()方法里看下——

1 public String getResourceForDbOperation(String directory, String operation, String component) {2     String databaseType = this.dbSqlSessionFactory.getDatabaseType();3     return "org/activiti/db/" + directory + "/activiti." + databaseType + "." + operation + "." + component + ".sql";4 }
复制代码

这里的 directory 即前边传进来的"create",databaseType 的值就是前面获取到的“mysql”,而 component 则是"engine",因此,这字符串拼接起来,就是:"org/activiti/db/create/activiti.mysql.create.engine.sql"。

根据这个路径,我们去 Activiti 源码里查看,可以看到在 org/activiti/db/路径底下,总共有 5 个文件目录。根据其名字,可以猜测出,create 目录下存放的,是生成表的 sql 语句;drop 目录下,存放的是删除表是 sql 语句;mapping 目录下,是 mybatis 映射 xml 文件;properties 是各类数据库类型在分页情况下的特殊处理;upgrade 目录下,则是更新数据库表的 sql 语句。

image-20210313004622812


展开其中的 create 目录,可以进一步发现,里面根据名字区分了不同数据库类型对应的执行 sql 文件,其中,有 db2、h2、hsql、mssql、mysql、mysql55、oracle、postgres 这八种类型,反过来看,同时说明了 Activiti 工作流引擎支持使用这八种数据库。通常使用比较多的是 mysql。根据刚刚的路径 org/activiti/db/create/activiti.mysql.create.engine.sql,可以在下面截图中,找到该对应路径下的 engine.sql 文件——


image-20210313005508035


点击进去看,会发现,这不就是我们常见的 mysql 建表语句吗!没错,工作流 Activiti 就是在源码里内置了一套 sql 文件,若要创建数据库表,就直接去到对应数据库文件目录下,获取到相应的建表文件,执行 sql 语句建表。这跟平常用 sql 语句构建表结构没太大区别,区别只在于执行过程的方式而已,但两者结果都是一样的。


image-20210313005623292


到这里,我们根据其拼接的 sql 存放路径,找到了 create 表结构的 sql 文件,那么让我们回到原来代码执行的方法里:

1 public void executeMandatorySchemaResource(String operation, String component) {2      this.executeSchemaResource(operation, component, this.getResourceForDbOperation(operation, operation, component), false);3 }
复制代码

这里通过 this.getResourceForDbOperation(operation, operation, component), false)拿到 了 mysql 文件路径,接下来,将同其他几个参数,一块传入到 this.executeSchemaResource()方法里,具体如下:


 1 public void executeSchemaResource(String operation, String component, String resourceName, boolean isOptional) { 2      InputStream inputStream = null; 3      try { 4          //根据resourceName路径字符串,获取到对应engine.sql文件的输入流inputStream,即读取engine.sql文件 5          inputStream = ReflectUtil.getResourceAsStream(resourceName); 6          if (inputStream == null) { 7          ...... 8          } else { 9              //将得到的输入流inputStream传入该方法10              this.executeSchemaResource(operation, component, resourceName, inputStream);11          }12      } finally {13         ......14      }15 }
复制代码


这一步主要通过输入流 InputStream 读取 engine.sql 文件的字节,然后再传入到 this.executeSchemaResource(operation, component, resourceName, inputStream)方法当中,而这个方法,将是 Activiti 建表过程中的核心所在。

下面删除多余代码,只留核心代码来分析:


 1 private void executeSchemaResource(String operation, String component, String resourceName, InputStream inputStream) { 2     //sql语句字符串 3     String sqlStatement = null; 4      5     try { 6  //1、jdbc连接mysql数据库 7         Connection connection = this.sqlSession.getConnection(); 8  //2、分行读取resourceName="org/activiti/db/create/activiti.mysql.create.engine.sql"目录底下的文件数据 9         byte[] bytes = IoUtil.readInputStream(inputStream, resourceName);10  //3.将engine.sql文件里的数据分行转换成字符串,换行的地方,可以看到字符串用转义符“\n”来代替11         String ddlStatements = new String(bytes);12         try {13             14             if (this.isMysql()) {15                 DatabaseMetaData databaseMetaData = connection.getMetaData();16                 int majorVersion = databaseMetaData.getDatabaseMajorVersion();17                 int minorVersion = databaseMetaData.getDatabaseMinorVersion();18                 if (majorVersion <= 5 && minorVersion < 6) {19                     //若数据库类型是在mysql 5.6版本以下,需要做一些替换,因为低于5.6版本的MySQL是不支持变体时间戳或毫秒级的日期,故而需要在这里对sql语句的字符串做替换。(注意,这里的majorVersion代表主版本,minorVersion代表主版本下的小版本)20                     ddlStatements = this.updateDdlForMySqlVersionLowerThan56(ddlStatements);21                }22            }23        } catch (Exception var26) {24            ......25        }26         //4.以字符流形式读取字符串数据27         BufferedReader reader = new BufferedReader(new StringReader(ddlStatements));28         //5.根据字符串中的转义符“\n”分行读取29         String line = this.readNextTrimmedLine(reader);30         //6.循环每一行31         for(boolean inOraclePlsqlBlock = false; line != null; line = this.readNextTrimmedLine(reader)) {32            33             if (line.startsWith("# ")) {34            ......35            } 36             //7.若下一行line还有数据,证明还没有全部读取,仍可执行读取37         else if (line.length() > 0) {38                     if (this.isOracle() && line.startsWith("begin")) {39                      .......40                        41                    } 42             /**43             8.在没有拼接够一个完整建表语句时,!line.endsWith(";")会为true,即一直循环进行拼接,当遇到";"就跳出该if语句44             **/45             else if ((!line.endsWith(";") || inOraclePlsqlBlock) && (!line.startsWith("/") || !inOraclePlsqlBlock)) {46                         sqlStatement = this.addSqlStatementPiece(sqlStatement, line);47                    } else {48                 /**49              9.循环拼接中若遇到符号";",就意味着,已经拼接形成一个完整的sql建表语句,例如50             create table ACT_GE_PROPERTY ( 51             NAME_ varchar(64), 52             VALUE_ varchar(300), 53             REV_ integer, 54             primary key (NAME_) 55             ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin56             这样,就可以先通过代码来将该建表语句执行到数据库中,实现如下:57                 **/58                         if (inOraclePlsqlBlock) {59                             inOraclePlsqlBlock = false;60                        } else {61                    62                             sqlStatement = this.addSqlStatementPiece(sqlStatement, line.substring(0, line.length() - 1));63                        }64                    //10.将建表语句字符串包装成Statement对象65                         Statement jdbcStatement = connection.createStatement();66                         try {67                    //11.最后,执行建表语句到数据库中68                             jdbcStatement.execute(sqlStatement);69                             jdbcStatement.close();70                        } catch (Exception var27) {71                        ......72                        } finally {73                     //12.到这一步,意味着上一条sql建表语句已经执行结束,若没有出现错误话,这时已经证明第一个数据库表结构已经创建完成,可以开始拼接下一条建表语句,74                             sqlStatement = null;75                        }76                    }77                }78            }79 ​80            ......81        } catch (Exception var29) {82            ......83        }84    }
复制代码


以上步骤可以归纳下:

  1. jdbc 连接 mysql 数据库;

  2. 分行读取 resourceName="org/activiti/db/create/activiti.mysql.create.engine.sql"目录底下的 sql 文件数据;

  3. 将整个 engine.sql 文件数据分行转换成字符串 ddlStatements,有换行的地方,用转义符“\n”来代替;

  4. 以 BufferedReader 字符流形式读取字符串 ddlStatements 数据;

  5. 循环字符流里的每一行,拼接成 sqlStatement 字符串,若读取到该行结尾有“;”符号,意味着已经拼接成一个完整的 create 建表语句,这时,跳出该次拼接,直接包装成成 Statement 对象;值得注意一点是,Statement 是 Java 执行数据库操作的一个重要接口,用于在已经建立数据库连接的基础上,向数据库发送要执行的 SQL 语句。Statement 对象是用于执行不带参数的简单 SQL 语句,例如本次的 create 建表语句。

  6. 最后,执行 jdbcStatement.execute(sqlStatement),将 create 建表语句执行进数据库中;

  7. 生成对应的数据库表;

根据 debug 过程截图,可以更为直观地看到,这里获取到的 ddlStatements 字符串,涵盖了 sql 文件里的所有 sql 语句,同时,每一个完整的 creat 建表语句,都是以";"结尾的:


image-20210313020402762


每次执行到";"时,都会得到一个完整的 create 建表语句:


image-20210313020651548


执行完一个建表语句,就会在数据库里同步生成一张数据库表,如上图执行的是 ACT_GE_PROPERTY 表,数据库里便生成了这张表:


image


在执行完之后,看 idea 控制台打印信息,可以看到,我的数据库是 5.7 版本,引擎在启动过程中分别执行了 engine.sql、history.sql、identity.sql 三个 sql 文件来进行数据库表结构的构建。


image-20210313020945718


到这一步,引擎整个生成表的过程就结束了,以上主要是基于 true 策略模式,通过对 engine.sql 的执行,来说明工作流引擎生成表的底层逻辑,其余模式基本都类似,这里就不一一展开分析了。

最后,进入到数据库,可以看到,已成功生成 28 张 ACT 开头的工作流自带表——


image-20210313070032185


发布于: 2021 年 03 月 26 日阅读数: 10
用户头像

朱季谦

关注

一个以编程为生的荒诞小说家 2018.06.29 加入

汤圆创作APP签约作者; 简书优秀认证作者; Java后端程序员; PMP认证人员; 学生时代著有《黑色玫瑰》一书已上架微信读书APP;

评论

发布
暂无评论
Activiti工作流自动生成28张数据库表的底层原理分析