简介:Activiti 5.17.0 是一款面向 Java 开发者的开源工作流与 BPM 平台,适用于企业审批流、任务调度、业务自动化等场景。这份官方发布并打包好的 zip 资源总体积约 69.35MB,内含可运行的 jar 包、框架源码和使用手册,既能让开发者快速部署使用,也能通过阅读源码理解引擎的内部运行机制。整套资料围绕 BPMN 2.0 标准展开,覆盖流程建模、引擎解析与执行、任务分配与管理、用户与组权限、监听器与事件机制、表单集成、持久化事务以及监控报表等核心模块;其中流程定义支持图形化建模,任务节点可分配给用户或组,并支持条件分支、并发子流程等复杂逻辑,事件机制允许开发者在流程启动、任务完成等时机注入自定义业务逻辑,解决会签、驳回、多级审批等常见需求。配合表单引擎与持久化存储,还能保障数据在流程各节点间正确流转,并为系统提供事务一致性与操作安全边界。借助手册还可学习与 Spring、Spring Boot 等主流框架的集成方式,用于实际开发中的流程设计、异常处理和性能调优。资源已经获得 202 人浏览/学习,适合需要构建 OA 审批、工作流引擎或进行 BPM 技术选型的中高级后端工程师作为入门参考和工程实践资料。
1. 为什么还要折腾 Activiti 5.17.0:老项目里跑着的流程引擎
这几年维护了好几个启动于多年前的 Java 后台服务,流程模块全是 activiti,版本死死锁在 5.17.0。想升级?历史流程数据全部要迁移,代价高到没人敢签字;不升级?网上教程铺天盖地都是 6.x、7.x,照着抄一半就发现 API 对不上。于是「吃透老版本」成了接手这类系统的必修课。
这篇笔记不聊新特性,就讲 5.17.0 从初始化、Spring 集成、流程部署到任务流转的完整链路,重点把我在实际项目里踩过的坑摊开说。适合正在维护老系统、需要在 5.17.0 上做二次开发、或者刚接手一堆历史流程数据的开发者。看完你至少能独立部署一个流程,并知道出问题时该查哪张表。
2. 核心表结构:五类表的分工与初始化策略
2.1 五类表各管什么,先记住哪几张
Activiti 5.17.0 建完库会生成以ACT_开头的数十张表,按前缀可以分成五组。新手最容易在这里犯迷糊:运行时数据、历史数据、流程定义数据混在一起查,导致该看ACT_HI_*的时候去看ACT_RU_*,自然什么都查不到。
| 前缀 | 用途 | 关键表 | 典型场景 |
|---|---|---|---|
| ACT_RE_* | 流程定义、模型等静态资源 | ACT_RE_PROCDEF、ACT_RE_DEPLOYMENT | 查流程有几个版本、部署时间 |
| ACT_RU_* | 运行时数据,流程执行到一半的状态 | ACT_RU_TASK、ACT_RU_EXECUTION、ACT_RU_VARIABLE | 查当前待办任务、当前流程变量 |
| ACT_HI_* | 历史数据,流程结束后的归档 | ACT_HI_PROCINST、ACT_HI_TASKINST、ACT_HI_VARINST | 做流程审计、统计耗时 |
| ACT_ID_* | 用户、组、关系 | ACT_ID_USER、ACT_ID_GROUP | 身份管理,很多项目直接不用 |
| ACT_GE_* | 通用数据 | ACT_GE_PROPERTY、ACT_GE_BYTEARRAY | 存流程定义的二进制内容 |
平时排障最高频的就是三张:ACT_RU_TASK没有记录,说明当前没有待办任务,要么流程走完了,要么卡在别的节点;ACT_HI_PROCINST记录了流程实例的起止时间,用来判断流程是否走完;ACT_RE_PROCDEF每次部署都会产生新版本,同一个 key 下 VERSION 字段从 1 开始递增。
这里有个常见误用:想查「谁处理过这个流程」,去查ACT_RU_TASK是看不到的,运行时表只保留当前未完成的任务。历史处理记录必须查ACT_HI_TASKINST,它保留了每个任务的处理人、开始时间、结束时间。记住了这一点,后面第 6 章做流程审计就能直接上手。
2.2 引擎初始化:databaseSchemaUpdate 的四个取值
5.17.0 的引擎初始化核心是ProcessEngineConfiguration。不接 Spring 时默认加载 classpath 下的activiti.cfg.xml;接 Spring 后通常改用SpringProcessEngineConfiguration,由它来构建ProcessEngine。构建引擎时最关键的参数是databaseSchemaUpdate,它决定了表怎么建、什么时候建。
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> <property name="dataSource" ref="dataSource"/> <property name="databaseSchemaUpdate" value="true"/> <property name="transactionManager" ref="transactionManager"/> </bean>databaseSchemaUpdate的取值要分清:
false:启动时不做任何 DDL,表必须提前存在。生产环境推荐这种,避免引擎启动时跟数据库表结构较劲。true:启动时对比缺失的表,只创建不存在的,已有的表不动。本地开发最省心,我一般本地用 true,接生产前把表结构导出来改成 false。create-drop:启动时创建,销毁时删除。只适合单元测试,用一次后悔一次,数据全没了。no-verify:跳过表结构校验,适合对性能敏感的场景,但前提是你非常确定表结构没问题。
值得注意的是,5.17.0 的建表脚本并不是放在项目里,而是打在 activiti-engine 的 jar 包里,路径是org/activiti/db/create/,按数据库类型分文件,比如activiti.mysql.create.engine.sql。日常项目里如果数据库账号权限受限跑不了 DDL,手动导这个脚本也是一条路。
另外提一句 5.17.0 的底层映射:引擎内部走 MyBatis 3.2.8,实体映射文件在org/activiti/db/mapping/entity/下。如果哪天需要给表加索引、调整查询,去这里能找到对应 SQL 的入口。
2.3 依赖坐标与版本匹配:JDK 和 Spring 的兼容边界
接手老项目遇到的第一道坎是依赖版本。5.17.0 是 JDK 6/7 时代的产物,在 JDK 8 下运行基本没问题,但如果你把工程升到了 JDK 17 再回来用它,反射相关的报错会从启动阶段一路冒出来。
<dependency> <groupId>org.activiti</groupId> <artifactId>activiti-engine</artifactId> <version>5.17.0</version> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring</artifactId> <version>5.17.0</version> </dependency>这里要特别说明的是activiti-spring和activiti-engine的版本必须保持一致,这是 5.x 时代的硬规则,混用版本容易出现类找不到或者方法签名不一致。配套的 Spring 版本一般落在 3.2.x 到 4.x,如果你的工程已经用上 Spring 5,5.17.0 的很多内部接口跟新版本冲突,常见的报错包括序列化相关方法签名变化、ObjectMapper 行为不一致等。
数据库驱动也要注意年代差。比如 MySQL 5.6 时代常用的 mysql-connector-java 5.1.x,跟 5.17.0 配合得很好;如果直接跳到 8.x 驱动,连接参数和时区设置都变了,常见表现是The server time zone value '�й���ʱ��' is unrecognized这种乱码报错。这种情况不是 activiti 的问题,是驱动兼容问题,加上serverTimezone=Asia/Shanghai大多能解决。
3. Spring 工程接入:从依赖配置到部署第一个流程
3.1 最小工程:pom 依赖和目录结构
把 5.17.0 接进 Spring 工程,最小依赖就是上一章那三个坐标:activiti-engine、activiti-spring、数据库驱动。工程结构我一般这样组织:processes/目录放 BPMN 文件,config/放引擎配置。
<dependency> <groupId>org.activiti</groupId> <artifactId>activiti-engine</artifactId> <version>5.17.0</version> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring</artifactId> <version>5.17.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.39</version> </dependency>注意点有两个。第一,activiti-spring会传递引入spring-beans、spring-tx等模块,跟工程里已有的 Spring 版本冲突时,需要手动排除传递依赖再显式指定版本。第二,如果用到了activiti-rest模块,它内部带的 Jackson 版本通常比项目旧,JSON 序列化行为两者不一致,容易导致接口返回的字段跟预期不同。这类历史版本模块,能少引就少引。
3.2 ProcessEngineFactoryBean:数据源和事务绑定
Spring 接入的核心是通过ProcessEngineFactoryBean拿到ProcessEngine实例。它内部干了两件事:读取配置构建引擎对象,再把 Spring 的事务管理器绑定给引擎,让流程操作跟 Service 层事务走同一条路径。
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/activiti_db?characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="yourpassword"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean"> <property name="processEngineConfiguration" ref="processEngineConfiguration"/> </bean> <bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService"/> <bean id="runtimeService" factory-bean="processEngine" factory-method="getRuntimeService"/> <bean id="taskService" factory-bean="processEngine" factory-method="getTaskService"/>注意这里的factory-method,ProcessEngineFactoryBean暴露出来的其实是ProcessEngine对象,而repositoryService、runtimeService这些服务是从引擎对象上取出来的。很多初学者在这里犯糊涂,以为ProcessEngineFactoryBean直接就是流程引擎,其实它是一个工厂 bean,取出来的对象才是引擎。
数据源上我一般要求连接 URL 必须带characterEncoding=utf8,不然中文字符串全变问号。事务管理器用 Spring 自带的数据源事务管理器就够,不需要额外引入 JTA。这里最关键的一点是:配置里的transactionManager必须要和SpringProcessEngineConfiguration里引用的是同一个实例,一旦出现两个不同的事务管理器,流程操作和数据库操作会各管各的,异常时一边回滚一边提交,查起问题来十分难受。
3.3 部署第一个 BPMN:bpmn 文件和 RepositoryService
部署流程的第一步是准备一个合法的 BPMN 文件。5.17.0 识别的是 BPMN 2.0 规范,命名为.bpmn或.bpmn20.xml均可。一个最简单的请假审批流程长这样:
<?xml version="1.0" encoding="UTF-8"?> <bpmn2:definitions xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:activiti="http://activiti.org/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" targetNamespace="http://activiti.org/process"> <bpmn2:process id="leaveProcess" name="请假审批" isExecutable="true"> <bpmn2:startEvent id="start" name="开始"/> <bpmn2:userTask id="applyTask" name="提交申请" activiti:assignee="${applyUser}"/> <bpmn2:userTask id="managerApprove" name="主管审批" activiti:assignee="${managerUser}"/> <bpmn2:endEvent id="end" name="结束"/> <bpmn2:sequenceFlow id="flow1" sourceRef="start" targetRef="applyTask"/> <bpmn2:sequenceFlow id="flow2" sourceRef="applyTask" targetRef="managerApprove"/> <bpmn2:sequenceFlow id="flow3" sourceRef="managerApprove" targetRef="end"/> </bpmn2:process> </bpmn2:definitions>process的id是流程定义 key,启动流程的时候要用它;name是显示名称,部署后在ACT_RE_PROCDEF里看到的就是这个字段。activiti:assignee指定了任务的处理人,这里用了表达式applyUser和managerUser,说明处理人是在启动流程时通过变量传入的,而不是写死。
部署动作通过RepositoryService完成:
@Autowired private RepositoryService repositoryService; public String deployLeaveProcess() { Deployment deployment = repositoryService.createDeployment() .name("请假审批流程") .addClasspathResource("processes/leaveProcess.bpmn20.xml") .deploy(); return deployment.getId(); }部署后立刻查一下流程定义,确认是否成功:
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery() .processDefinitionKey("leaveProcess") .latestVersion() .singleResult(); System.out.println("流程定义ID: " + pd.getId()); System.out.println("流程版本号: " + pd.getVersion());这里先问一个问题:部署一次生成一条ACT_RE_PROCDEF记录,同一个 key 部署第二次会生成新的 VERSION。如果代码里启动流程时拿的是processDefinitionId,并且是从某张业务表里存的 ID 取出来的,那它永远指向第一次部署的版本,第二次部署的新逻辑就变成摆设。这个坑后面第 5 章会专门展开。
还有一点,.addClasspathResource是往 classpath 找文件,对应的文件要放在src/main/resources/processes/下。很多本地调试跑不通,其实就是文件路径没放对,报的错很容易误判成引擎问题。
4. 流程实例运行:变量、任务与监听的完整链路
4.1 启动流程实例并传递流程变量
流程定义是模板,流程实例才是实际跑起来的一条流程。启动实例用RuntimeService.startProcessInstanceByKey,第一个参数是 BPMN 文件里的 process id,第二个参数是流程变量 Map。
@Autowired private RuntimeService runtimeService; public String startLeaveProcess(String applyUser, String managerUser) { Map<String, Object> vars = new HashMap<String, Object>(); vars.put("applyUser", applyUser); vars.put("managerUser", managerUser); vars.put("leaveDays", 3); ProcessInstance pi = runtimeService.startProcessInstanceByKey("leaveProcess", vars); return pi.getId(); }启动之后,ACT_RU_EXECUTION会插入一条主执行流记录,ACT_RU_TASK会出现第一个待办任务,也就是applyTask。流程变量有两种存放水平:ACT_RU_VARIABLE存运行时的当前变量,流程结束后的归档记录在ACT_HI_VARINST。两者通过PROC_INST_ID_关联到同一个流程实例。
这里要解释一下变量的存储类型。5.17.0 变量支持最常用的是String、Integer、Long、Boolean、Date和Serializable对象。传对象时会把对象序列化到ACT_GE_BYTEARRAY,同时ACT_RU_VARIABLE里记一条指向字节数据的索引。传对象虽然方便,但流程变量会跨流程实例长期占用表空间,垃圾数据积累起来特别占库,非必要不用对象类型,能用String或数值就用简单的。
4.2 查询并完成用户任务
任务流转的核心是查任务、办任务。TaskService是 5.17.0 里用得最频的服务之一。按处理人查待办,是日常开发最常见的写法:
@Autowired private TaskService taskService; public void completeApplyTask(String assignee, boolean approved) { List<Task> tasks = taskService.createTaskQuery() .taskAssignee(assignee) .processDefinitionKey("leaveProcess") .list(); for (Task task : tasks) { Map<String, Object> taskVars = new HashMap<String, Object>(); taskVars.put("approved", approved); taskService.complete(task.getId(), taskVars); } }taskAssignee是按处理人精确匹配,注意它匹配的是ASSIGNEE_字段,不是候选人。如果流程节点配置的是activiti:candidateUsers,那这里应该用taskCandidateUser来查,这两个方法差别很大,用错就一条任务都查不到。
调用complete后,当前任务会被标记完成,同时流程按顺序流走到下一个节点。如果下一个节点是用户任务,ACT_RU_TASK会插入新的待办记录;如果是结束节点,整个流程实例结束,ACT_RU_EXECUTION和ACT_RU_TASK里的数据都会被清理掉,只留下ACT_HI_PROCINST里的一条历史记录。这解释了为什么查运行中的流程要查ACT_RU_*,查结束的流程必须查ACT_HI_*。
4.3 监听器实战:执行监听器和任务监听器
5.17.0 的监听器分两类,一类是执行监听器ExecutionListener,监听流程节点级别的状态变化,比如流程开始、节点结束;另一类是任务监听器TaskListener,监听任务级别的操作,比如任务创建、任务完成、指定处理人。
建议把执行监听器加在流程元素上:
<bpmn2:process id="leaveProcess" name="请假审批"> <bpmn2:extensionElements> <activiti:executionListener event="start" class="com.example.listener.ProcessStartListener"/> </bpmn2:extensionElements> </bpmn2:process>对应的 Java 监听类实现ExecutionListener接口:
public class ProcessStartListener implements ExecutionListener { @Override public void notify(DelegateExecution execution) throws Exception { String processInstanceId = execution.getProcessInstanceId(); String businessKey = execution.getProcessBusinessKey(); System.out.println("流程实例启动: " + processInstanceId + ", 业务键: " + businessKey); } }参数说明:event属性决定监听时机,start表示流程节点开始,end表示节点结束,take表示顺序流被走完时触发。class属性指定的类必须实现了对应监听接口,类需要有一个无参构造方法,否则引擎反射实例化时会直接报错。
实际项目里我见过很多人把监听器当成日志打印器来用,这是资源的错配。监听器最适合做的是:流程启动后调用外部系统接口同步数据、任务创建后自动发待办通知、流程结束时清理对应的业务状态。要注意监听器代码执行失败会影响整个流程操作,所以监听器内部一定要捕获异常,不能把业务错误抛到流程引擎这一层。
4.4 挂起与激活:让流程实例暂停
有时候流程需要临时停掉,比如节假日暂停审批,或者发现问题流程需要冻结处理。5.17.0 的挂起机制分两级:挂起流程定义和挂起流程实例。
// 挂起整个流程定义,新实例不能再启动 repositoryService.suspendProcessDefinitionByKey("leaveProcess"); // 挂起单个流程实例,当前任务不能再操作 runtimeService.suspendProcessInstanceById(processInstanceId);对应的激活接口是activateProcessDefinitionByKey和activateProcessInstanceById。挂起的流程实例,通过TaskService查询任务仍然能查到,但complete操作会抛异常。这个特性坑了不少人,现象就是任务在列表里,点办理却报流程被挂起,原因就是没注意流程实例的挂起状态。
排查时可以查一个字段:ACT_RU_EXECUTION里的SUSPENSION_STATE_,值为 1 是激活状态,值为 2 是挂起状态。在业务代码里,如果要在任务办理前做校验,可以先查这个字段,给用户一个明确的提示文案,而不是让后端抛一堆堆栈。
5. 避坑与排错:5.17.0 常见的翻车场景
在 5.17.0 上踩过的坑数得过来,但每一个都曾经让人在深夜里怀疑自己的基本功。下面这几条是按出现频率排序的,有同款症状直接照方抓药。
5.1 启动报错:表不存在或列不存在
【现象】:Spring 应用启动时 JVM 日志抛出Table 'ACT_RU_TASK' doesn't exist,或者数据库管理工具能查到表但启动依旧报列不存在。
【原因】:databaseSchemaUpdate设置成了false,但对应的建表脚本没有执行过;还有一种情况是执行了旧版本的 SQL 脚本,5.17.0 对部分表结构做了调整。
【解决】:本地开发先改成true启动一次,让引擎自动补表;生产环境则去 activiti-engine jar 包org/activiti/db/create/目录下找到对应数据库的脚本,按顺序执行engine、identity、history三类脚本。
5.2 部署了新流程,跑的却还是旧逻辑
【现象】:BPMN 文件修改后重新部署,日志显示部署成功,但流程实例跑到旧节点上,新加的条件分支不生效。
【原因】:同一 key 的流程定义会按版本递增,代码里启动流程时用的是旧的processDefinitionId,而不是最新的。
【解决】:确认启动方式用的是processDefinitionKey,配合.latestVersion()查询最新定义;如果你必须存固定 ID,部署成功后重新查一次ACT_RE_PROCDEF,把新的 ID 更新回业务表。
5.3 MySQL 表名大小写不一致导致查询失败
【现象】:本地 Windows 开发一切正常,部署到 Linux 后引擎启动也没报错,但某个 SQL 查询报Table 'act_ru_task' doesn't exist。
【原因】:Activiti 5.17.0 生成的 DDL 里表名是大写ACT_RU_TASK,MySQL 在lower_case_table_names=0的 Linux 环境下区分大小写,而应用里某些查询拼的是小写表名。
【解决】:统一大小写。最简单的方法是数据库初始化时强制使用大写表名,连接 URL 不要加lowerCaseTableNames这类参数;如果已经乱了,就把小写表重命名成大写,再确认所有自定义 SQL 里的表名与库中实际表名完全一致。
5.4 流程名称和任务名称中文乱码
【现象】:流程部署后,ACT_RE_PROCDEF.NAME_或任务标题显示成????,或者存储后读出来是乱码。
【原因】:JDBC 连接没有指定characterEncoding=utf8,数据库表本身字符集不是 utf8,两者叠加导致中文在写入时被转成了无法识别的字节。
【解决】:连接 URL 加characterEncoding=utf8,数据库建库语句指定DEFAULT CHARACTER SET utf8,已经乱的表要去掉脏数据后重建。BPMN 文件本身要确保是 UTF-8 编码保存,IDE 默认编码如果不是 UTF-8,即使数据库没问题,部署时解析 XML 也会出现中文反转义。
5.5 事务边界混乱导致数据不一致
【现象】:流程启动之后,业务表数据回滚了,但ACT_RU_*表里却留下了流程实例记录,或者反过来流程记录回滚了,业务表却提交了。
【原因】:5.17.0 的引擎自身带事务机制,接 Spring 后必须让引擎使用外部事务管理器。如果配置里没有正确绑定transactionManager,引擎会用自己的事务提交,跟业务事务各管各的。
【解决】:检查SpringProcessEngineConfiguration中的transactionManager是否和 Spring 中的数据源事务管理器是同一个 bean。同时,业务方法加上@Transactional,把流程操作和业务操作放在同一个事务里,异常时统一回滚。
5.6 日志里看不到引擎执行的 SQL
【现象】:想调试流程走某一步时执行了什么 SQL,但控制台没有任何输出,排查效率很低。
【原因】:5.17.0 的日志默认输出级别没有包含实体查询包的 SQL。
【解决】:配置 log4j 或 logback,把org.activiti.engine.impl.persistence.entity的级别设为DEBUG,就能看到 MyBatis 生成的 SQL 和执行参数。这一条在排查任务查询、变量更新这一类逻辑时实测非常管用,能让引擎这个「黑匣子」直接打开给你看。
6. 进阶:用历史表做流程审计与状态自查
6.1 没有管理界面时,SQL 就是审计工具
很多老项目的流程管理后台做得极其简陋,连流程走到哪一步都看不到。这时候历史表就是最好的审计工具。我常用这样一条 SQL 拉出一个流程实例的完整轨迹:
SELECT p.ID_ AS PROC_INST_ID, p.PROC_DEF_ID_, p.START_TIME_, p.END_TIME_, v.NAME_ AS VAR_NAME, v.TEXT_ AS VAR_VALUE FROM ACT_HI_PROCINST p LEFT JOIN ACT_HI_VARINST v ON v.PROC_INST_ID_ = p.ID_ WHERE p.PROC_DEF_ID_ LIKE 'leaveProcess:%' ORDER BY p.START_TIME_ DESC;这条查询不需要接口,不需要写 Java,直接在数据库客户端就能跑。END_TIME_为空说明流程还没走完,变量字段可以看到每次申请时传入了什么参数。
再看每个任务由谁什么时候处理的:
SELECT t.NAME_ AS TASK_NAME, t.ASSIGNEE_, t.START_TIME_, t.END_TIME_, t.DELETE_REASON_ FROM ACT_HI_TASKINST t WHERE t.PROC_INST_ID_ = '10001' ORDER BY t.START_TIME_;DELETE_REASON_有几个固定值,completed表示正常办理完成,deleted表示被删除或流程被终止。如果流程实例是异常中断的,同一实例下的后续任务可能没有END_TIME_,从这里就能快速定位是哪一步断了。
6.2 把引擎当工具而不是黑匣子
刚接流程引擎那阵子,我对 5.17.0 的心态是「能跑就行」,出了问题先重启,实在不行再看日志。后来吃了几次亏才明白,这类开源引擎的表结构、日志开关都是暴露在外的调试接口,只是平时没人给你指路。
从那以后我每次改完流程都强制走一遍三板斧:先查ACT_RE_PROCDEF确认部署的是不是最新版本,再启动到目标节点查ACT_RU_TASK核对当前待办,最后看ACT_HI_PROCINST和ACT_HI_TASKINST确认整个执行轨迹没有跑偏。一条流程从部署到走通,这三个检查点能覆盖九成以上的问题,省下的全是排查时间。
希望这篇笔记能帮你在 5.17.0 上少走几步弯路。
本文还有配套的精品资源,点击获取