
1.概述前言 一直以来都是从事大量的工作流相关的项目,用过很多商用的工作流产品,包括国内与国外的,尽管商用的工作产品在UI操作上比较人性化,但个人用户觉得,这东西只需要一些初级用户,对于我们一直在为一些高级的客户提供一些专业的数据整合、流程梳理、系统间的数据穿透时,这些系统因为不开源,给项目的实施带来巨大的风险,在一些项目栽过跟头后,我更偏向于使用开源的平台了。但开源平台最大的难点是在于你是否有足够的技术人员来学习及掌握它,否则,它也一样面临项目实施失败的风险。后来在一些项目上使用JBPM4,Activiti5,发现Activiti5的流程功能真的很强大,几乎是无所不能。套用一句广告语,老板再也不担心我的流程实现了。在实施国外的项目时,流程的设计几乎是交给开发人员来处理的,因此用Activiti的合适的。但在国内,我们的客户则提出更高的要求,要求普通的人员也可以参与流程的设计要求。Activiti后续的版本也在完善这些功能,特别是Activiti-5.18版本,Activiti-Modeler的建模工具几乎进行了重写,看来Activiti的开源团队也慢慢意识了这点,加大了人力在这方面的投入,以目前的使用,可以达到商用级别,通过功能的扩展,可以很好实现在线流程建模。 为了平台未来的延伸扩展,我建议直接使用该团队的Activiti-Modeler,原因很简单,可以有效跟着团队进行产品的升级,当然我们也需要扩展自己的特色功能,这块我在后面不断把文章写出来,以供大家学习。 在此,先展示一下我在JSAAS平台上初步整合Activiti-Modeler的效果: 说实话,虽然这设计器还有一些小小的缺陷,但仍然阻止不了我爱它,因为全新自己开发这东西,那是比较要命的,呵呵,苦逼的程序员呀。于是我多么希望在我的Activiti的流程应用里,直接就带这么一款应用。 现实提美好的, 整合是苦逼的,于是就有本文的出现。 在我的博客前一篇中,已经有说明如何利用Activiti-Modeler的源码跑起来,加至eclipse下运行起来,在本文中即是以该文为基础,进行本文的说明及整合。 2. 整合Activiti-Modeler的要求 Activiti-Modeler 5.18用了新的WEB框架,其是基于Spring-Mvc 4.0以上的框架,同时用了VAADIN的RIA的UI,特别是后者,这框架带有太多的jar包,虽然它也是结合了spring来使用,要整合这玩意,几乎就得把这东西加入我们的项目中,同时还需要整合它的用户管理,这是要命的。我们的出发点,仅是用它的前端画图处理功能,后端的流程逻辑处理即由我们来实现。 于是我研究了一下Activiti-webapp-explorer2项目,发现要实现我以上的目标,原来很简单。以下假定我的环境要求,以下为我的原项目的环境,是基于Spring 3的,我的平台可直接转为Spring4.0,特别是Spring-MVC的环境也转成4.0 3. 整合步骤 3.1. 把前端的设计器文件从activiti-webapp-explorer2拷至我平台上新建的目录process-editor,如下图所示: 同时把resources下的stencilset.json文件拷至我的项目中的resources目录下。 3.2.在Spring项目中加入Activiti 5.18的依赖引用,注意,不能直接从explorer项目中直接取,那样会带有很多无用的jar包,以下为精简后的pom引用。 <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-engine</artifactId> <version>5.19.0</version> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring</artifactId> <version>5.19.0</version> <exclusions> <exclusion> <artifactId>commons-dbcp</artifactId> <groupId>commons-dbcp</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-diagram-rest</artifactId> <version>5.19.0</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-transcoder</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-dom</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-json-converter</artifactId> <version>5.19.0</version> <exclusions> <exclusion> <artifactId>commons-collections</artifactId> <groupId>commons-collections</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-bridge</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-css</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-anim</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-codec</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-ext</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-gvt</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-script</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-parser</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-svg-dom</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-svggen</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-util</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-xml</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>org.apache.xmlgraphics</groupId> <artifactId>batik-js</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>xml-apis</groupId> <artifactId>xml-apis-ext</artifactId> <version>1.3.04</version> </dependency> <dependency> <groupId>xml-apis</groupId> <artifactId>xml-apis</artifactId> <version>1.3.04</version> </dependency> <dependency> <groupId>org.apache.xmlgraphics</groupId> <artifactId>xmlgraphics-commons</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-awt-util</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> 如不采用common-dbcp的数据源时,以上配置排除该包的引用, Batik的包主要是用来解析html中的svg的内容,包比较多,但都不大。 3.3.配置如下的spring-activiti.xml文件,其格式如下所示(可从activiti-webapp-explorer2下的resources的activiti-custom-context.xml文件拷出来),把以下的一些用到explorer表单的配置信息删除。因为我们不采用其表单的配置信息。 注意点: 1. 扩展实现自身的idGenerator 目的是为了产生唯一的数据主键,方法很多,请自行实现,不扩展也可以。 2. 配置对应的数据连接信息及数据源、事务等 3.4 .在Spring的配置文件中引入spring-activiti.xml,启动应用程序即可,可看到其已经把数据库表创建出来。 3.5. 处理Activiti-Moderler的后台处理的配置。即创建模型设计、保存、更新等内容,它需要与后端进行交互处理。我们看了activiti-webapp-explorer2的web.xml就清楚其后台交互的处理模式。 简要说明:explorer2这个项目在启动后,就会spring mvc4进行包扫描,把(请参考org.activiti.explorer.servlet.DispatcherServletConfiguration),org.activiti.rest.editor、org.activiti.rest.diagram包下的Controller扫描至响应映射中来,为的就是实现编辑器及设计模型的流程展示时,相应有对应的controller服务。 因此,我们比较好的办法就是重写这些controller即可,这些controller的实现也很简单,在这里,我最简单的做法就是把这些类直接拷到我的项目中,重命名了包名。(当然也可以直接把以上两包通过pom依赖加进来),本人不想自己的项目带有太多的依赖包,所以不直接引用了。 拷完后,我这里的包如下所示: 在SpringMVC中加载这些包,注意,SpringMvc需要为4.0以上的,这样才能比较好支持RestController的注解方式,否则,请用旧的方式来支持这种Rest URL访问。 <!--加入Spring Activiti-Modeler的运行配置--> <context:component-scan base-package="com.redxun.bpm.rest.diagram"/> <context:component-scan base-package="com.redxun.bpm.rest.editor"/> 在web.xml中配置拦截这些访问路径 <servlet-mapping> <servlet-name>springMvc</servlet-name> <url-pattern>/service/*</url-pattern> </servlet-mapping> 3.6. 修改process-editor下的一些配置文件,以支持我们的在线流程设计 A)去掉Activiti Afresco的logo标题栏,并且把样式上的空白栏去掉 修改modeler.html中的以下内容,注意不要把该文本删除,建议加style=”display:none”,删除后其会造成底层下的一些内容有40个像数的东西显示不出来。 <div class="navbar navbar-fixed-top navbar-inverse" role="navigation" id="main-header"> <div class="navbar-header"> <a href="" ng-click="backToLanding()" class="navbar-brand" title="{{'GENERAL.MAIN-TITLE' | translate}}"><span class="sr-only">{{'GENERAL.MAIN-TITLE' | translate}}</span></a> </div> </div> B)在editor-app/css/style-common.css中,把以下样式的padding-top部分改为0px; .wrapper.full { padding: 40px 0px 0px 0px; overflow: hidden; max-width: 100%; min-width: 100%; } C)在modeler.html中加上CloseWindow的函数 <script type="text/javascript"> function CloseWindow(action) { if (window.CloseOwnerWindow) return window.CloseOwnerWindow(action); else window.close(); } </script> 目的是为了保存模型时,可以关闭当前的弹出的mini窗口,修改保存后弹出的窗口的保存及关闭动作,如下所示:
Activiti数据表结构 1 Activiti数据库表结构 1.1 数据库表名说明 Activiti工作流总共包含23张数据表,所有的表名默认以“ACT_”开头。 并且表名的第二部分用两个字母表明表的用例,而这个用例也基本上跟Service API匹配。 u ACT_GE_* : “GE”代表“General”(通用),用在各种情况下; u ACT_HI_* : “HI”代表“History”(历史),这些表中保存的都是历史数据,比如执行过的流程实例、变量、任务,等等。Activit默认提供了4种历史级别: Ø none: 不保存任何历史记录,可以提高系统性能; Ø activity:保存所有的流程实例、任务、活动信息; Ø audit:也是Activiti的默认级别,保存所有的流程实例、任务、活动、表单属性; Ø full:最完整的历史记录,除了包含audit级别的信息之外还能保存详细,例如:流程变量。 对于几种级别根据对功能的要求选择,如果需要日后跟踪详细可以开启full。 u ACT_ID_* : “ID”代表“Identity”(身份),这些表中保存的都是身份信息,如用户和组以及两者之间的关系。如果Activiti被集成在某一系统当中的话,这些表可以不用,可以直接使用现有系统中的用户或组信息; u ACT_RE_* : “RE”代表“Repository”(仓库),这些表中保存一些‘静态’信息,如流程定义和流程资源(如图片、规则等); u ACT_RU_* : “RU”代表“Runtime”(运行时),这些表中保存一些流程实例、用户任务、变量等的运行时数据。Activiti只保存流程实例在执行过程中的运行时数据,并且当流程结束后会立即移除这些数据,这是为了保证运行时表尽量的小并运行的足够快; 1.2 数据库表结构 1.2.1 Activiti数据表清单: 表分类 表名 解释 一般数据 ACT_GE_BYTEARRAY 通用的流程定义和流程资源 ACT_GE_PROPERTY 系统相关属性 流程历史记录 ACT_HI_ACTINST 历史的流程实例 ACT_HI_ATTACHMENT 历史的流程附件 ACT_HI_COMMENT 历史的说明性信息 ACT_HI_DETAIL 历史的流程运行中的细节信息 ACT_HI_IDENTITYLINK 历史的流程运行过程中用户关系 ACT_HI_PROCINST 历史的流程实例 ACT_HI_TASKINST 历史的任务实例 ACT_HI_VARINST 历史的流程运行中的变量信息 用户用户组表 ACT_ID_GROUP 身份信息-组信息 ACT_ID_INFO 身份信息-组信息 ACT_ID_MEMBERSHIP 身份信息-用户和组关系的中间表 ACT_ID_USER 身份信息-用户信息 流程定义表 ACT_RE_DEPLOYMENT 部署单元信息 ACT_RE_MODEL 模型信息 ACT_RE_PROCDEF 已部署的流程定义 运行实例表 ACT_RU_EVENT_SUBSCR 运行时事件 ACT_RU_EXECUTION 运行时流程执行实例 ACT_RU_IDENTITYLINK 运行时用户关系信息 ACT_RU_JOB 运行时作业 ACT_RU_TASK 运行时任务 ACT_RU_VARIABLE 运行时变量表 1.2.2表名:ACT_GE_BYTEARRAY(通用的流程定义和流程资源) 用来保存部署文件的大文本数据。 保存流程定义图片和xml、Serializable(序列化)的变量,即保存所有二进制数据,特别注意类路径部署时候,不要把svn等隐藏文件或者其他与流程无关的文件也一起部署到该表中,会造成一些错误(可能导致流程定义无法删除)。 ACT_GE_BYTEARRAY(act_ge_bytearray) 是否主键 字段名 字段描述 数据类型 可空 约束 缺省值 取值说明 是 ID_ 主键ID,资源文件编号,自增长 VARCHAR(64) REV_ 版本号 INT(11) 是 Version NAME_ 部署的文件名称, VARCHAR(255) 是 mail.bpmn、mail.png 、mail.bpmn20.xml DEPLOYMENT_ID_ 来自于父表ACT_RE_DEPLOYMENT的主键 VARCHAR(64) 是 部署的ID BYTES_ 大文本类型,存储文本字节流 LONGBLOB 是 GENERATED_ 是否是引擎生成。 TINYINT(4) 是 0为用户生成 1为Activiti生成 1.2.3 表名:ACT_GE_PROPERTY(系统相关属性) 属性数据表。存储这个流程引擎级别的数据。 ACT_GE_PROPERTY(act_ge_property) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 取值说明 是 NAME_ 属性名称 VARCHAR(64) 64 schema.version schema.history next.dbid VALUE_ 属性值 VARCHAR(300) 300 是 5.* create(5.*) REV_INT 版本号 INT(11) 11 是 1.2.4表名:ACT_HI_ACTINST(历史节点表) 历史活动信息。这里记录流程流转过的所有节点,与HI_TASKINST不同的是,taskinst只记录usertask内容。 ACT_HI_ACTINST(act_hi_actinst) 是否主键 字段名 字段描述 数据类型 可空 约束 取值说明 是 ID_ ID_ VARCHAR(64) PROC_DEF_ID_ 流程定义ID VARCHAR(64) PROC_INST_ID_ 流程实例ID VARCHAR(64) EXECUTION_ID_ 流程执行ID VARCHAR(64) ACT_ID_ 活动ID VARCHAR(255) 节点定义ID TASK_ID_ 任务ID VARCHAR(64) 是 任务实例ID 其他节点类型实例ID在这里为空 CALL_PROC_INST_ID_ 请求流程实例ID VARCHAR(64) 是 调用外部流程的流程实例ID' ACT_NAME_ 活动名称 VARCHAR(255) 是 节点定义名称 ACT_TYPE_ 活动类型 VARCHAR(255) 如startEvent、userTask ASSIGNEE_ 代理人员 VARCHAR(64) 是 节点签收人 START_TIME_ 开始时间 DATETIME 2013-09-15 11:30:00 END_TIME_ 结束时间 DATETIME 是 2013-09-15 11:30:00 DURATION_ 时长,耗时 BIGINT(20) 是 毫秒值 1.2.5 表名:ACT_HI_ATTACHMENT(附件信息) ACT_HI_ATTACHMENT(act_hi_attachment) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 取值说明 是 ID_ ID_ VARCHAR(64) 64 主键ID REV_ REV_ INT(11) 11 是 Version USER_ID_ 用户id VARCHAR(255) 255 是 用户ID NAME_ 名称 VARCHAR(255) 255 是 附件名称 DESCRIPTION_ 描述 VARCHAR(4000) 4000 是 描述 TYPE_ 类型 VARCHAR(255) 255 是 附件类型 TASK_ID_ 任务Id VARCHAR(64) 64 是 节点实例ID PROC_INST_ID_ 流程实例ID VARCHAR(64) 64 是 流程实例ID URL_ 连接 VARCHAR(4000) 4000 是 附件地址 CONTENT_ID_ 内容Id 字节表的ID VARCHAR(64) 64 是 ACT_GE_BYTEARRAY的ID 1.2.6 表名:ACT_HI_COMMENT(历史审批意见表) ACT_HI_COMMENT(act_hi_comment) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 取值说明 是 ID_ ID_ VARCHAR(64) 64 主键ID TYPE_ 意见记录类型,为comment时,为处理意见 VARCHAR(255) 255 是 类型:event(事件) comment(意见) TIME_ 记录时间 DATETIME 填写时间 USER_ID_ 用户Id VARCHAR(255) 255 是 填写人 TASK_ID_ 任务Id VARCHAR(64) 64 是 节点实例ID PROC_INST_ID_ 流程实例Id VARCHAR(64) 64 是 流程实例ID ACTION_ 行为类型。 为addcomment时,为处理意见 VARCHAR(255) 255 是 值为下列内容中的一种: AddUserLink、DeleteUserLink、AddGroupLink、DeleteGroupLink、AddComment、AddAttachment、DeleteAttachment MESSAGE_ 处理意见 VARCHAR(4000) 4000 是 用于存放流程产生的信息,比如审批意见 FULL_MSG_ 全部消息 LONGBLOB 是 1.2.7表名:ACT_HI_DETAIL(历史详细信息) 历史详情表:流程中产生的变量详细,包括控制流程流转的变量,业务表单中填写的流程需要用到的变量等。 ACT_HI_DETAIL(act_hi_detail) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 取值说明 是 ID_ ID_ VARCHAR(64) 64 主键 TYPE_ 数据类型 VARCHAR(255) 255 类型: FormProperty, //表单 VariableUpdate //参数 PROC_INST_ID_ 流程实例ID VARCHAR(64) 64 是 流程实例ID EXECUTION_ID_ 执行实例Id VARCHAR(64) 64 是 执行实例ID TASK_ID_ 任务Id VARCHAR(64) 64 是 任务实例ID ACT_INST_ID_ 活动实例Id VARCHAR(64) 64 是 ACT_HI_ACTINST表的ID NAME_ 名称 VARCHAR(255) 255 名称 VAR_TYPE_ 变量类型 VARCHAR(255) 255 是 参见VAR_TYPE_类型说明 REV_ REV_ INT(11) 11 是 Version TIME_ 创建时间 DATETIME 创建时间 BYTEARRAY_ID_ 字节数组Id VARCHAR(64) 64 是 ACT_GE_BYTEARRAY表的ID DOUBLE_ DOUBLE_ DOUBLE 是 存储变量类型为Double LONG_ LONG_ BIGINT(20) 20 是 存储变量类型为long TEXT_ 值 VARCHAR(4000) 4000 是 存储变量值类型为String TEXT2_ 值2 VARCHAR(4000) 4000 是 此处存储的是JPA持久化对象时,才会有值。此值为对象ID 备注:VAR_TYPE_类型说明: jpa-entity、boolean、bytes、serializable(可序列化)、自定义type(根据你自身配置)、 CustomVariableType、date、double、integer、long、null、short、string 1.2.8 表名:ACT_HI_IDENTITYLINK (历史流程人员表) 任务参与者数据表。主要存储历史节点参与者的信息。 ACT_HI_IDENTITYLINK(act_hi_identitylink) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 取值说明 是 ID_ ID_ VARCHAR(64) 64 ID_ GROUP_ID_ 用户组ID VARCHAR(255) 255 是 组ID TYPE_ 用户组类型 VARCHAR(255) 255 是 类型,主要分为以下几种: assignee、 candidate、 owner、starter 、participant USER_ID_ 用户ID VARCHAR(255) 255 是 用户ID TASK_ID_ 任务Id VARCHAR(64) 64 是 节点实例ID PROC_INST_ID_ 流程实例Id VARCHAR(64) 64 是 流程实例ID 1.2.9 表名:ACT_HI_PROCINST(历史流程实例信息)核心表 ACT_HI_PROCINST(act_hi_procinst) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 ID_ ID_ VARCHAR(64) 64 PROC_INST_ID_ 流程实例ID VARCHAR(64) 64 BUSINESS_KEY_ 业务Key VARCHAR(255) 255 是 PROC_DEF_ID_ 流程定义Id VARCHAR(64) 64 START_TIME_ 开始时间 DATETIME END_TIME_ 结束时间 DATETIME 是 DURATION_ 时长 BIGINT(20) 20 是 START_USER_ID_ 发起人员Id VARCHAR(255) 255 是 START_ACT_ID_ 开始节点 VARCHAR(255) 255 是 END_ACT_ID_ 结束节点 VARCHAR(255) 255 是 SUPER_PROCESS_INSTANCE_ID_ 超级流程实例Id VARCHAR(64) 64 是 DELETE_REASON_ 删除理由 VARCHAR(4000) 4000 是 1.2.10 表名:ACT_HI_TASKINST(历史任务流程实例信息)核心表 ACT_HI_TASKINST(act_hi_taskinst) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 ID_ ID_ VARCHAR(64) 64 主键ID PROC_DEF_ID_ 流程定义Id VARCHAR(64) 64 是 流程定义ID TASK_DEF_KEY_ 任务定义Key VARCHAR(255) 255 是 节点定义ID PROC_INST_ID_ 流程实例ID VARCHAR(64) 64 是 流程实例ID EXECUTION_ID_ 执行ID VARCHAR(64) 64 是 执行实例ID NAME_ 名称 VARCHAR(255) 255 是 名称 PARENT_TASK_ID_ 父任务iD VARCHAR(64) 64 是 父节点实例ID DESCRIPTION_ 描述 VARCHAR(4000) 4000 是 描述 OWNER_ 实际签收人 任务的拥有者 VARCHAR(255) 255 是 签收人(默认为空,只有在委托时才有值) ASSIGNEE_ 代理人 VARCHAR(255) 255 是 签收人或被委托 START_TIME_ 开始时间 DATETIME 开始时间 CLAIM_TIME_ 提醒时间 DATETIME 是 提醒时间 END_TIME_ 结束时间 DATETIME 是 结束时间 DURATION_ 时长 BIGINT(20) 20 是 耗时 DELETE_REASON_ 删除理由 VARCHAR(4000) 4000 是 删除原因(completed,deleted) PRIORITY_ 优先级 INT(11) 11 是 优先级别 DUE_DATE_ 应完成时间 DATETIME 是 过期时间,表明任务应在多长时间内完成 FORM_KEY_ 表单key VARCHAR(255) 255 是 desinger节点定义的 form_key属性 1.2.11 表名:ACT_HI_VARINST(历史变量信息) ACT_HI_VARINST(act_hi_varinst) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 ID_ ID_ VARCHAR(64) 64 ID_ PROC_INST_ID_ 流程实例ID VARCHAR(64) 64 是 流程实例ID EXECUTION_ID_ 执行ID VARCHAR(64) 64 是 执行实例ID TASK_ID_ 任务Id VARCHAR(64) 64 是 任务实例ID NAME_ 名称 VARCHAR(255) 255 参数名称(英文) VAR_TYPE_ 变量类型 VARCHAR(100) 100 是 参见VAR_TYPE_类型说明 REV_ REV_ INT(11) 11 是 Version BYTEARRAY_ID_ 字节数组ID VARCHAR(64) 64 是 ACT_GE_BYTEARRAY表的主键 DOUBLE_ DOUBLE_ DOUBLE 是 存储DoubleType类型的数据 LONG_ LONG_ BIGINT(20) 20 是 存储LongType类型的数据 TEXT_ TEXT_ VARCHAR(4000) 4000 是 存储变量值类型为String,如此处存储持久化对象时,值jpa对象的class TEXT2_ TEXT2_ VARCHAR(4000) 4000 是 此处存储的是JPA持久化对象时,才会有值。此值为对象ID 1.2.12 表名:ACT_ID_GROUP(用户组表) 用来存储用户组信息。 ACT_ID_GROUP(act_id_group) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 ID_ 用户组ID VARCHAR(64) 64 REV_ 版本号 INT(11) 11 是 NAME_ 用户组描述信息 VARCHAR(255) 255 是 TYPE_ 用户组类型 VARCHAR(255) 255 是 1.2.13 表名:ACT_ID_INFO(用户扩展信息表) 用户扩展信息表。目前该表未用到。 ACT_ID_INFO(act_id_info) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 ID_ VARCHAR(64) 64 REV_ 版本号 INT(11) 11 是 USER_ID_ 用户ID VARCHAR(64) 64 是 TYPE_ 类型 VARCHAR(64) 64 是 KEY_ formINPut名称 VARCHAR(255) 255 是 VALUE_ 值 VARCHAR(255) 255 是 PASSWORD_ 密码 LONGBLOB 是 PARENT_ID_ 父节点 VARCHAR(255) 255 是 1.2.14 表名:ACT_ID_MEMBERSHIP(用户用户组关联表) 用来保存用户的分组信息 ACT_ID_MEMBERSHIP(act_id_membership) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 USER_ID_ 用户Id VARCHAR(64) 64 是 GROUP_ID_ 用户组Id VARCHAR(64) 64 1.2.15 表名:ACT_ID_USER(用户信息表) ACT_ID_USER(act_id_user) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 ID_ ID_ VARCHAR(64) 64 REV_ 版本号 INT(11) 11 是 FIRST_ 用户名称 VARCHAR(255) 255 是 LAST_ 用户姓氏 VARCHAR(255) 255 是 EMAIL_ 邮箱 VARCHAR(255) 255 是 PWD_ 密码 VARCHAR(255) 255 是 PICTURE_ID_ 头像Id VARCHAR(64) 64 是 1.2.16 表名:ACT_RE_DEPLOYMENT(部署信息表) 用来存储部署时需要持久化保存下来的信息 ACT_RE_DEPLOYMENT(act_re_deployment) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 ID_ 部署编号,自增长 VARCHAR(64) 64 NAME_ 部署包的名称 VARCHAR(255) 255 是 CATEGORY_ 类型 VARCHAR(255) 255 是 TENANT_ID_ 租户 VARCHAR(255) 255 是 多租户通常是在软件需要为多个不同组织服务时产生的概念 DEPLOY_TIME_ 部署时间 TIMESTAMP CURRENT_TIMESTAMP 1.2.17 表名:ACT_RE_MODEL(流程设计模型表) 创建流程的设计模型时,保存在该数据表中。 ACT_RE_MODEL(act_re_model) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 ID_ ID_ VARCHAR(64) 64 ID_ REV_ INT(11) 11 是 乐观锁 NAME_ 模型的名称: 比如:收文管理 VARCHAR(255) 255 是 名称 KEY_ 模型的关键字,流程引擎用到。 比如:FTOA_SWGL VARCHAR(255) 255 是 分类,例如: http://www.mossle.com/docs/activiti/ CATEGORY_ 类型,用户自己对流程模型的分类。 VARCHAR(255) 255 是 分类 CREATE_TIME_ 创建时间 TIMESTAMP 是 创建时间 LAST_UPDATE_TIME_ 最后修改时间 TIMESTAMP 是 最新修改时间 VERSION_ 版本,从1开始。 INT(11) 11 是 版本 META_INFO_ 数据源信息,比如: {"name":"FTOA_SWGL","revision":1,"description":"丰台财政局OA,收文管理流程"} VARCHAR(4000) 4000 是 以json格式保存流程定义的信息 DEPLOYMENT_ID_ 部署ID VARCHAR(64) 64 是 部署ID EDITOR_SOURCE_VALUE_ID_ 编辑源值ID VARCHAR(64) 64 是 是 ACT_GE_BYTEARRAY 表中的ID_值。 EDITOR_SOURCE_EXTRA_VALUE_ID_ 编辑源额外值ID(外键ACT_GE_BYTEARRAY ) VARCHAR(64) 64 是 是 ACT_GE_BYTEARRAY 表中的ID_值。 TENANT_ID_ 租户 VARCHAR(255) 255 是 1.2.18 表名:ACT_RE_PROCDEF(流程定义:解析表) 流程解析表,解析成功了,在该表保存一条记录。业务流程定义数据表 ACT_RE_PROCDEF(act_re_procdef) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省 备注 是 ID_ 流程ID,由“流程编号:流程版本号:自增长ID”组成 VARCHAR(64) 64 ID_ REV_ 版本号 INT(11) 11 是 乐观锁 CATEGORY_ 流程命名空间(该编号就是流程文件targetNamespace的属性值) VARCHAR(255) 255 是 流程定义的Namespace就是类别 NAME_ 流程名称(该编号就是流程文件process元素的name属性值) VARCHAR(255) 255 是 名称 KEY_ 流程编号(该编号就是流程文件process元素的id属性值) VARCHAR(255) 255 流程定义ID VERSION_ 流程版本号(由程序控制,新增即为1,修改后依次加1来完成的) INT(11) 11 版本 DEPLOYMENT_ID_ 部署编号 VARCHAR(64) 64 是 部署表ID RESOURCE_NAME_ 资源文件名称 VARCHAR(4000) 4000 是 流程bpmn文件名称 DGRM_RESOURCE_NAME_ 图片资源文件名称 VARCHAR(4000) 4000 是 png流程图片名称 DESCRIPTION_ 描述信息 VARCHAR(4000) 4000 是 描述 HAS_START_FORM_KEY_ 是否从key启动 TINYINT(4) 4 是 start节点是否存在formKey 0否 1是 SUSPENSION_STATE_ 是否挂起 INT(11) 11 是 1激活 2挂起 注:此表和ACT_RE_DEPLOYMENT是多对一的关系,即,一个部署的bar包里可能包含多个流程定义文件,每个流程定义文件都会有一条记录在ACT_RE_PROCDEF表内,每个流程定义的数据,都会对于ACT_GE_BYTEARRAY表内的一个资源文件和PNG图片文件。和ACT_GE_BYTEARRAY的关联是通过程序用ACT_GE_BYTEARRAY.NAME与ACT_RE_PROCDEF.NAME_完成的,在数据库表结构中没有体现。 1.2.19 表名:ACT_RU_EVENT_SUBSCR(运行时事件) ACT_RU_EVENT_SUBSCR(act_ru_event_subscr) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 ID_ ID VARCHAR(64) 64 REV_ 版本号 INT(11) 11 是 EVENT_TYPE_ 事件类型 VARCHAR(255) 255 EVENT_NAME_ 事件名称 VARCHAR(255) 255 是 EXECUTION_ID_ 流程执行ID VARCHAR(64) 64 是 PROC_INST_ID_ 流程实例ID VARCHAR(64) 64 是 ACTIVITY_ID_ 活动ID VARCHAR(64) 64 是 CONFIGURATION_ 配置信息 VARCHAR(255) 255 是 CREATED_ 创建时间 TIMESTAMP CURRENT_TIMESTAMP 1.2.20 表名:ACT_RU_EXECUTION(运行时流程执行实例) 核心,我的代办任务查询表 ACT_RU_EXECUTION(act_ru_execution) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 ID_ ID_ VARCHAR(64) 64 ID_ REV_ 版本号 INT(11) 11 是 乐观锁 PROC_INST_ID_ 流程实例编号 VARCHAR(64) 64 是 流程实例ID BUSINESS_KEY_ 业务编号 VARCHAR(255) 255 是 业务主键ID PARENT_ID_ 父执行流程 VARCHAR(64) 64 是 父节点实例ID PROC_DEF_ID_ 流程定义Id VARCHAR(64) 64 是 流程定义ID SUPER_EXEC_ VARCHAR(64) 64 是 ACT_ID_ 实例id VARCHAR(255) 255 是 节点实例ID即 ACT_HI_ACTINST中ID IS_ACTIVE_ 激活状态 TINYINT(4) 4 是 是否存活 IS_CONCURRENT_ 并发状态 TINYINT(4) 4 是 是否为并行(true/false) IS_SCOPE_ TINYINT(4) 4 是 IS_EVENT_SCOPE_ TINYINT(4) 4 是 SUSPENSION_STATE_ 暂停状态_ INT(11) 11 是 挂起状态 1激活 2挂起 CACHED_ENT_STATE_ 缓存结束状态_ INT(11) 11 是 1.2.21 表名:ACT_RU_IDENTITYLINK(身份联系) 主要存储当前节点参与者的信息,任务参与者数据表。 ACT_RU_IDENTITYLINK(act_ru_identitylink) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 取值说明 是 ID_ ID_ VARCHAR(64) 64 REV_ 版本号 INT(11) 11 是 GROUP_ID_ 用户组ID VARCHAR(255) 255 是 TYPE_ 用户组类型 VARCHAR(255) 255 是 主要分为以下几种:assignee、candidate、 owner、starter、participant。即:受让人,候选人,所有者、起动器、参与者 USER_ID_ 用户ID VARCHAR(255) 255 是 TASK_ID_ 任务Id VARCHAR(64) 64 是 PROC_INST_ID_ 流程实例ID VARCHAR(64) 64 是 PROC_DEF_ID_ 流程定义Id VARCHAR(64) 64 是 1.2.22 表名:ACT_RU_JOB(运行中的任务) 运行时定时任务数据表 ACT_RU_JOB(act_ru_job) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 取值说明 是 ID_ ID_ VARCHAR(64) 64 标识 REV_ 版本号 INT(11) 11 是 版本 TYPE_ TYPE_ VARCHAR(255) 255 类型 LOCK_EXP_TIME_ LOCK_EXP_TIME_ TIMESTAMP 是 锁定释放时间 LOCK_OWNER_ LOCK_OWNER_ VARCHAR(255) 255 是 挂起者 EXCLUSIVE_ EXCLUSIVE_ TINYINT(1) 1 是 EXECUTION_ID_ EXECUTION_ID_ VARCHAR(64) 64 是 执行实例ID PROCESS_INSTANCE_ID_ PROCESS_INSTANCE_ID_ VARCHAR(64) 64 是 流程实例ID PROC_DEF_ID_ PROC_DEF_ID_ VARCHAR(64) 64 是 流程定义ID RETRIES_ RETRIES_ INT(11) 11 是 EXCEPTION_STACK_ID_ EXCEPTION_STACK_ID_ VARCHAR(64) 64 是 异常信息ID EXCEPTION_MSG_ EXCEPTION_MSG_ VARCHAR(4000) 4000 是 异常信息 DUEDATE_ DUEDATE_ TIMESTAMP 是 到期时间 REPEAT_ REPEAT_ VARCHAR(255) 255 是 重复 HANDLER_TYPE_ HANDLER_TYPE_ VARCHAR(255) 255 是 处理类型 HANDLER_CFG_ HANDLER_CFG_ VARCHAR(4000) 4000 是 标识 1.2.23 表名:ACT_RU_TASK(运行时任务数据表) (执行中实时任务)代办任务查询表 ACT_RU_TASK(act_ru_task) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 取值说明 是 ID_ ID_ VARCHAR(64) 64 ID_ REV_ 版本号 INT(11) 11 是 乐观锁 EXECUTION_ID_ 实例id(外键EXECUTION_ID_) VARCHAR(64) 64 是 执行实例ID PROC_INST_ID_ 流程实例ID(外键PROC_INST_ID_) VARCHAR(64) 64 是 流程实例ID PROC_DEF_ID_ 流程定义ID VARCHAR(64) 64 是 流程定义ID NAME_ 任务名称 VARCHAR(255) 255 是 节点定义名称 PARENT_TASK_ID_ 父节任务ID VARCHAR(64) 64 是 父节点实例ID DESCRIPTION_ 任务描述 VARCHAR(4000) 4000 是 节点定义描述 TASK_DEF_KEY_ 任务定义key VARCHAR(255) 255 是 任务定义的ID OWNER_ 所属人(老板) VARCHAR(255) 255 是 拥有者(一般情况下为空,只有在委托时才有值) ASSIGNEE_ 代理人员 (受让人) VARCHAR(255) 255 是 签收人或委托人 DELEGATION_ 代理团 VARCHAR(64) 64 是 委托类型,DelegationState分为两种:PENDING,RESOLVED。如无委托则为空 PRIORITY_ 优先权 INT(11) 11 是 优先级别,默认为:50 CREATE_TIME_ 创建时间 TIMESTAMP 创建时间,CURRENT_TIMESTAMP DUE_DATE_ 执行时间 DATETIME 是 耗时 SUSPENSION_STATE_ 暂停状态 INT(11) 11 是 1代表激活 2代表挂起 1.2.24 表名:ACT_RU_VARIABLE(运行时流程变量数据表) ACT_RU_VARIABLE(act_ru_variable) 是否主键 字段名 字段描述 数据类型 长度 可空 约束 缺省值 备注 是 ID_ ID_ VARCHAR(64) 64 主键标识 REV_ 版本号 INT(11) 11 是 乐观锁 TYPE 编码类型 VARCHAR(255) 255 参见VAR_TYPE_类型说明 NAME_ 变量名称 VARCHAR(255) 255 变量名称 EXECUTION_ID_ 执行实例ID VARCHAR(64) 64 是 执行的ID PROC_INST_ID_ 流程实例Id VARCHAR(64) 64 是 流程实例ID TASK_ID_ 任务id VARCHAR(64) 64 是 节点实例ID(Local) BYTEARRAY_ID_ 字节组ID VARCHAR(64) 64 是 字节表的ID (ACT_GE_BYTEARRAY) DOUBLE_ DOUBLE_ DOUBLE 是 存储变量类型为Double LONG_ LONG_ BIGINT(20) 20 是 存储变量类型为long TEXT_ TEXT_ VARCHAR(4000) 4000 是 存储变量值类型为String 如此处存储持久化对象时,值jpa对象的class TEXT2_ TEXT2_ VARCHAR(4000) 4000 是 此处存储的是JPA持久化对象时,才会有值。此值为对象ID 2 Activiti中主要对象的关系 本节主要介绍在工作流中出现的几个对象及其之间的关系,以及在Activiti中各个对象是如何关联的。 在开始之前先看看下图,对整个对象结构有个了解,再结合实例详细介绍理解。 图1.Activiti中几个对象之间的关系 我们模拟一个请假的流程进行分析介绍,该流程主要包含以下几个步骤: u 员工申请请假 u 部门领导审批 u 人事审批 u 员工销假 ProcessInstance对象 员工开始申请请假流程,通过runtimeService.startProcessInstance()方法启动,引擎会创建一个流程实例(ProcessInstance)。 简单来说流程实例就是根据一次(一条)业务数据用流程驱动的入口,两者之间是一对一的关系。流程引擎会创建一条数据到ACT_RU_EXECUTION表,同时也会根据history的级别决定是否查询相同的历史数据到ACT_HI_PROCINST表。 启动完流程之后业务和流程已经建立了关联关系,第一步结束。 启动流程和业务关联区别: u 对于自定义表单来说启动的时候会传入businessKey作为业务和流程的关联属性 u 对于动态表单来说不需要使用businessKey关联,因为所有的数据都保存在引擎的表中 u 对于外部表单来说businessKey是可选的,但是一般不会为空,和自定义表单类似 Execution对象 对于初学者来说,最难理解的地方就是ProcessInstance与Execution之间的关系,要分两种情况说明。Execution的含义就是一个流程实例(ProcessInstance)具体要执行的过程对象。 不过在说明之前先声明两者的对象映射关系: ProcessInstance(1)→ Execution(N),(其中N>=1)。 1) 值相等的情况: 除了在流程中启动的子流程之外,流程启动之后在表ACT_RU_EXECUTION中的字段ID_和PROC_INST_ID_字段值是相同的。 图2.ID_和PROC_INST_ID_相等 2) 值不相等的情况: 不相等的情况目前只会出现在子流程中(包含:嵌套、引入),例如一个购物流程中除了下单、出库节点之外可能还有一个付款子流程,在实际企业应用中付款流程通常是作为公用的,所以使用子流程作为主流程(购物流程)的一部分。 当任务到达子流程时引擎会自动创建一个付款流程,但是这个流程有一个特殊的地方,在数据库可以直观体现,如下图。 图3.ID_和PROC_INST_ID_不相等 上图中有两条数据,第二条数据(嵌入的子流程)的PARENT_ID_等于第一条数据的ID_和PROC_INST_ID_,并且两条数据的PROC_INST_ID_相同。 上图中还有一点特殊的地方,字段IS_ACTIVE_的值分别是0和1,说明正在执行子流程主流程挂起。 Task对象 前面说了ProcessInstance和业务是一对一关联的,和业务数据最亲密;而Task则和用户最亲密的(UserTask),用户每天的待办事项就是一个个的Task对象。 从图1中看得出Execution和Task是一对一关系,Task可以是任何类型的Task实现,可以是用户任务(UserTask)、Java服务(JavaServiceTask)等,在实际流程运行中只不过面向对象不同,用户任务(UserTask)需要有人为参与完成(complete),Java服务需要由系统自动执行(execution)。 图4. 表ACT_RU_TASK Task是在流程定义中看到的最大单位,每当一个Task完成的时候引擎会把当前的任务移动到历史中,然后插入下一个任务插入到表ACT_RU_TASK中。结合请假流程来说就是让用户点击“完成”按钮提交当前任务是的动作,引擎自动根据任务的顺序流或者排他分支判断走向。 HistoryActivity(历史活动) 图5. 表ACT_HI_ACTINST Activity包含了流程中所有的活动数据,例如开始事件(图5表中的第1条数据)、各种分支(排他分支、并行分支等,图5表中的第2条数据)、以及刚刚提到的Task执行记录(如图5表中的第3、4条数据)。 有些人认为Activity和Task是多对一关系,其实不是,从上图中可以看出来根本没有Task相关的字段。 结合请假流程来说,如Task中提到的当完成流程的时候所有下一步要执行的任务(包括各种分支)都会创建一个Activity记录到数据库中。例如领导审核节点点击“同意”按钮就会流转到人事审批节点,如果“驳回”那就流转到调整请假内容节点,每一次操作的Task背后实际记录更详细的活动(Activity)。
前言Ionic是目前较为流行的Hybird App解决方案,在Ionic开发过程中会遇到很多常见的开发问题,本文尝试对这些问题给出解决方案。 一些常识与技巧list 有延迟,可以在ion-content处使用 overflow-scroll="true"尝试在上用ng-click上是没效果的标签内的事件会在整个label内被触发,点哪都触发快捷修改背景色style="background-color: #212326;"能用ng-if就用ng-if,ng-if的效率比ng-show和ng-hide高直接在ion-list中的ion-item中并不能触发ng-click事件,可以在item中的元素上再套一层div可以用ng-class="{'important': post.important}"配合css 根据列表元素显示不同的效果获取日期用$filter,var postdate = $filter('date')(date, 'yyyy-MM-dd HH:mm:ss');列表中的元素不能写成 id : 4,应写成 id : "4",注意在创建id变量的时候也需要转成string,如var id = InfoListService.getListLength()+1+"";使用$log进行log输出,为什么用$log而不是console.log呢?可以看看这个在安卓上的体验比较差,动画有延迟?可以试试ionic集成的crosswalkcontrollers和services 的文件名可能会重合,但是他们意义差不多,可以将controllers中的文件名小写,对应的services中的文件名大写进行区分,或者加后缀xxxControler,xxxService安装cordova插件的时候用ionic plugin add ...的方式添加,这样会在package.json中添加这个插件的条目,如果有人clone了你的项目想在本地运行,可以用ionic state restore它会根据cordovaPlugins条目安装对应的插件。如果直接用cordova plugin add 安装则不会更新package.json。上传base64编码的时候如果提示413错误,是因为文件过大导致的,可以在nodejs中设置bodyparser的文件限制:var bodyParser = require('body-parser');app.use(bodyParser.json({limit: '50mb'}));app.use(bodyParser.urlencoded({limit: '50mb', extended: true}));img 中 base64编码的图片无法显示?在源码中发现angular添加了unsafe标签?需要在白名单中添加data:image$compileProvider.imgSrcSanitizationWhitelist(/^s*(https?|ftp|mailto|content|file|assets-library):|data:image//);有时候pm2运行有问题,重启一下即可在ios设备上运行ionic run ios --device 问题列表1.如何在某个界面中去掉导航栏?2.如何在ionic中加载本地图片?3.如何在ionic中嵌入网页代码?4.如何将template加载到某个tab或某个sidemenu项目下?5.运行serve命令时ionic报错?6.用docker跑ionic的时候,不能把地址绑定到0.0.0.0怎么处理?7.加载页面的时候会看到双括号一闪而过?8.更新了数据,如何让界面更新呢?9.如何实现IonicView中card上面有一列分割线的效果?10.controller.js和service.js文件越来越大怎么办?11.如何寻找优秀的范例代码?12.如何显示相对时间?13.发布应用的时候如果遇到翻译错误即MissingTranslation怎么办?14.如何在列表右下方添加时间等信息?15.如何回到上一页面?16.如何关闭应用?17.在安卓设备上如何让title居中?18.如何让在sidemenu中的headerbar能够显示头像等其他信息?19.ionic的subheader挡住了内容区域怎么办?20.对于需要添加数据的list,在添加数据后页面不能及时刷新造成卡顿怎么办?21.ionic如何处理回退按钮?例如询问用户是否真的要退出应用22.ionic如何实现对每个请求都添加认证信息或认证失败自动重新登录?23.ionic如何实现搜索框内的全部清除按钮?如何在某个界面中去掉导航栏?如果某个界面上不想要导航栏,可以简单地在最顶端的标签中添加hide-nav-bar="true" 如何在ionic中加载本地图片?对于css文件夹中的样式文件中如果要调用本地的图片的话,从该css文件所在的文件夹开始算,例如www/css/style.css要加../,否则在浏览器中可以正常显示,在设备上不行,结构如下所示:.login-page { background:url(../img/signup_bg.png); background-size: cover; background-repeat: no-repeat;}但是对于在页面中定义的图片路径,从www路径开始算,否则浏览器中可显示,但设备上不行,img文件夹和index.html在一级,如: 如何在ionic中嵌入网页代码?使用ng-bind-html这个类,不过它会过滤原始html的标签,我们可以引入$sce模块,用$sce.trustAsHtml()方法信任我们获取的网页 如何将template加载到某个tab或某个sidemenu项目下? 可以指定name,然后在子状态中使用该name,ionic就知道该把该状态的template渲染到哪边了。例如: // signup page .state('auth.signup', { url: '/signup', views: { 'auth-signup': { templateUrl: 'templates/auth-signup.html', controller: 'SignUpCtrl' } } }) 另有一个tabs中声明该auth-signup: icon-off="ion-ios-personadd-outline" href="#/auth/signup"> 运行serve命令时ionic报错?ionic $ An uncaught exception occured and has been reported to Ionic看看你是不还有一个终端在运行着serve呢? 用docker跑ionic的时候,不能把地址绑定到0.0.0.0怎么处理?可以用ionic serve -all的方法解决 加载页面的时候会看到双括号一闪而过?angularjs 在使用双括号的时候,第一个加载的页面,也就是应用中的index.html,其未被渲染好的模版可能会被用户看到。用ng-bind就不会遇到这个问题。造成这种现象的原因是,浏览器需要首先加载HTML页面,渲染它,然后Angular才有机会把它解释成你期望看到的内容。不过好消息是,在大多数的模版中你依然可以使用双括号.但是对于index.html页面中的数据绑定操作,建议使用ng-bind。ng-bind使用方式如下: 更新了数据,如何让界面更新呢?可以用广播,注意$broadcast 和 $emit的区别 如何实现IonicView中card上面有一列分割线的效果?在css里定义 info-up { border-top: 4px solid #f06336;} controller.js和service.js文件越来越大怎么办?所有的控制器不必都放在controllers.js这一个文件中,可以新建controllers文件夹,然后把每个controller都建一个.js文件,同理services和utils等都是.但注意要在index.html中head部分声明.但是为了避免他们相互覆盖,第一个加载的js中模块中要加[…],其他都不需要。如:// File : /js/directives/mainDirective.jsangular.module('app.directives',[]); // File : /js/directives/myGreatDirective.jsangular.module('app.directives') .directive('myGreatDirective', function(){ return { //... } }); // File : /js/directives/myBetterDirective.jsangular.module('app.directives') .directive('myBetterDirective', function(){ return { //... } }); ...看angularjs-code-organization了解更多,嗯这篇文章写的还不是best practice,因为你还得记着自己把[]写到那个模块里了,统一地写在app.js中即可,在app.js最下面加上类似:angular.module('fcws.controllers',['ionic', 'fcws.services']);angular.module('fcws.services', []);可以达到和上面一样的效果,而且可以统一管理. 如何寻找优秀的范例代码?目前有些ionic 的app没有进行代码混淆,至少ionic官方的ionic view没有进行代码混淆,下载他们的app,文件名改成zip,解压,所有的 www文件都在assets文件夹中,相当于开源了有木有,看看那些最优秀的practice。看中哪些优秀的app,下下来,如何在googleplay上下载?把googleplay应用的地址贴到apps.evozi中。 如何显示相对时间?如几分钟前,几天前等,可以用momentjs,看这篇教程 发布应用的时候如果遇到翻译错误即MissingTranslation怎么办?暂时的解决方法是,不进行翻译校正, 在 /platforms/android/build.gradle 中的android {}节中加入:lintOptions { disable 'MissingTranslation' disable 'ExtraTranslation' } 如何在列表右下方添加时间等信息?span 可以用来将时间之类的附加信息显示到列表右边,如下面会将创建时间显示在name的右边: <img src="../../img/commander.jpg"> <span class="item-note">{{message.create_at}}</span> <h2 >{{message.name}}</h2> <p > {{message.content}}</p> 如何回到上一页面?用$ionicHistory这个模块,引入该模块后使用goBack([backCount]),backCount指定回去多少个页面(-1代表回去一个页面),默认为-1 如何关闭应用?ionic.Platform.exitApp(); 在安卓设备上如何让title居中?在headerbar中添加align-title="center",如: <h1 class="title">{{username}}</h1> 不过这个设置对ion-view无效,亲测,如果要统一让所有navbar上的title居中(包括上面的headerbar),可以在config里设置,如:.config(function($stateProvider, $urlRouterProvider,$ionicConfigProvider) { $ionicConfigProvider.navBar.alignTitle('center'); ...如果要让某一个view title居中,可以用$ionicNavBarDelegate,参考ionic官方文档 如何让在sidemenu中的headerbar能够显示头像等其他信息?解决方案是去掉headerbar,添加一个avatar到sidemenu content中,如: <ion-content class="bar-positive"> <ion-list> <ion-item class="item item-avatar item-positive" href="#"> <img src="img/commander.jpg"> <h2 class=" light"> <i class="icon ion-ios-star"></i>{{title}} </h2> <a>{{username}}</a> </ion-item> ionic的subheader挡住了内容区域怎么办?解决方案是给加类has-subheader,同理也可以加has-header。如下: 对于需要添加数据的list,在添加数据后页面不能及时刷新造成卡顿怎么办?可以使用$ionicScrollDelegate.resize();在添加数据后手动进行重新刷新,记得添加依赖 ionic如何处理回退按钮?例如询问用户是否真的要退出应用可以在app.js的.run方法中增加对硬件回退按钮的注册处理,这里我在大部分页面都想注册该事件,除去有二级历史页面的我单独判断了下,注意增加依赖。$ionicPlatform.registerBackButtonAction(function(e) { var current_state_name = $state.current.name; if(current_state_name !== 'sidemenu.post' && current_state_name !== 'sidemenu.contact_town' && current_state_name !== 'sidemenu.contact_people'){ $ionicPopup.confirm({ title: '退出应用', template: '您确定要退出xxxx吗?' }).then(function (res) { if (res) { //ionic.Platform.exitApp(); navigator.app.exitApp(); } else { console.log('You are not sure'); } }); e.preventDefault(); return false; }else{ navigator.app.backHistory(); } },100); ionic如何实现对每个请求都添加认证信息或认证失败自动重新登录?在应用的注册或者登录部分,不记名token响应了这个请求并且这个token被存储到本地存储中。当你向后端请求一个服务时,你需要把这个token放在头部中。你可以在app.js的.config方法中使用AngularJS的拦截器实现这个。每次请求都会被拦截并且会把认证头部和值放到头部中,同理如果服务器端响应401或403,跳转到重新登录页面.$httpProvider.interceptors.push(function ($q, $location, User, $rootScope) { return { 'request': function (config) { config.headers = config.headers || {}; if (User.getToken()) { config.headers.Authorization = 'Bearer ' + User.getToken(); } return config; }, 'responseError': function (response) { if (response.status === 401 || response.status === 403) { //如果之前登陆过 if (User.getToken()) { $rootScope.$broadcast('unAuthenticed'); } } return $q.reject(response); } }; }); ionic如何实现搜索框内的全部清除按钮?在label中的input不能嵌入按钮,因为ionic对于label中的tap事件会进行重定向到input上。解决方案是将label替换成span或div。如下面的搜索框,注意ng-model需要是一个对象才能置空,变量不行: <i class="icon ion-ios-search placeholder-icon"></i> <input type="search" placeholder="请输入姓名前缀" ng-model="search.key"> <i class="icon ion-close-circled placeholder-icon" style="vertical-align: middle;" on-tap="clearSearch()" ng-if="search.key.length"></i> 中添加这个插件的条目,如果有人clone了你的项目想在本地运行,可以用ionic state restore它会根据cordovaPlugins条目安装对应的插件。如果直接用cordova plugin add 安装则不会更新package.json。上传base64编码的时候如果提示413错误,是因为文件过大导致的,可以在nodejs中设置bodyparser的文件限制:var bodyParser = require('body-parser');app.use(bodyParser.json({limit: '50mb'}));app.use(bodyParser.urlencoded({limit: '50mb', extended: true}));img 中 base64编码的图片无法显示?在源码中发现angular添加了unsafe标签?需要在白名单中添加data:image$compileProvider.imgSrcSanitizationWhitelist(/^s*(https?|ftp|mailto|content|file|assets-library):|data:image//);有时候pm2运行有问题,重启一下即可在ios设备上运行ionic run ios --device
1 安装JDK 选择安装目录 安装过程中会出现两次 安装提示 。第一次是安装 jdk ,第二次是安装 jre 。建议两个都安装在同一个 java文件夹中的不同文件夹中。(不能都安装在java文件夹的根目录下,jdk和jre安装在同一文件夹会出错) 如下图所示 2 (1):安装jdk 随意选择目录 只需把默认安装目录 \java 之前的目录修改即可 (2):安装jre→更改→ \java 之前目录和安装 jdk 目录相同即可 注:若无安装目录要求,可全默认设置。无需做任何修改,两次均直接点下一步。 3 安装完JDK后配置环境变量 计算机→属性→高级系统设置→高级→环境变量 4 系统变量→新建 JAVA_HOME 变量 。 变量值填写jdk的安装目录(本人是 E:\Java\jdk1.7.0) 5 系统变量→寻找 Path 变量→编辑 在变量值最后输入 %JAVA_HOME%\bin;%JAVA_HOME%\jre\bin; (注意原来Path的变量值末尾有没有;号,如果没有,先输入;号再输入上面的代码) 6 系统变量→新建 CLASSPATH 变量 变量值填写 .;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar(注意最前面有一点) 系统变量配置完毕 7 检验是否配置成功 运行cmd 输入 java -version (java 和 -version 之间有空格) 若如图所示 显示版本信息 则说明安装和配置成功。
创建一个只管的用户界面,并允许你控制图片的大小。上传到服务器端的数据,并不需要处理enctype为 multi-part/form-data 的情况,仅仅一个简单的POST表单处理程序就可以了. 好了,下面附上完整的代码示例 Canvas简介 canvas 是一个HTML5新增的DOM元素,允许用户在页面上直接地绘制图形,通常是使用JavaScript.而不同的格式标准也是不同的,比如SVG是光栅API(raster API) 而VML却是向量API(vector API).可以考虑使用Adobe Illustrator(矢量图)作图与使用 Adobe Photoshop (光栅图)作图的区别。 在canvas(画布)上能做的事情就是读取和渲染图像,并且允许你通过JavaScript操纵图像数据。已经有很多现存的文章来为你演示基本的图像处理——主要关注与各种不同的图像过滤技术( image filtering techniques)——但我们需要的仅仅是缩放图片并转换到特定的文件格式,而canvas完全可以做到这些事情。 我们假定的需求,比如图像高度不超过100像素,不管原始图像有多高。基本的代码如下所示: // 参数,最大高度 var MAX_HEIGHT = 100; // 渲染 function render(src){ // 创建一个 Image 对象 var image = new Image(); // 绑定 load 事件处理器,加载完成后执行 image.onload = function(){ // 获取 canvas DOM 对象 var canvas = document.getElementById("myCanvas"); // 如果高度超标 if(image.height > MAX_HEIGHT) { // 宽度等比例缩放 *= image.width *= MAX_HEIGHT / image.height; image.height = MAX_HEIGHT; } // 获取 canvas的 2d 环境对象, // 可以理解Context是管理员,canvas是房子 var ctx = canvas.getContext("2d"); // canvas清屏 ctx.clearRect(0, 0, canvas.width, canvas.height); // 重置canvas宽高 canvas.width = image.width; canvas.height = image.height; // 将图像绘制到canvas上 ctx.drawImage(image, 0, 0, image.width, image.height); // !!! 注意,image 没有加入到 dom之中 }; // 设置src属性,浏览器会自动加载。 // 记住必须先绑定事件,才能设置src属性,否则会出同步问题。 image.src = src; }; 在上面的例子中,你可以使用canvas 的 toDataURL() 方法获取图像的 Base64编码的值(可以类似理解为16进制字符串,或者二进制数据流). 注意: canvas 的 toDataURL() 获取的URL以字符串开头,有22个无用的数据 "data:image/png;base64,",需要在客户端或者服务端进行过滤. 原则上只要浏览器支持,URL地址的长度是没有限制的,而1024的长度限制,是老一代IE所独有的。 请问,如何获取我们需要的图像呢? 好孩子,很高兴你能这么问。你并不能通过File 输入框来直接处理,你从这个文件输入框元素所能获取的仅仅是用户所选择文件的path路径。按照常规想象,你可以通过这个path路径信息来加载图像,但是,在浏览器里面这是不现实的。(译者注:浏览器厂商必须保证自己的浏览器绝对安全,才能获得市场,至少避免媒体的攻击,如果允许这样做,那恶意网址可以通过拼凑文件路径来尝试获取某些敏感信息). 为了实现这个需求,我们可以使用HTML5的File API 来读取用户磁盘上的文件,并用这个file来作为图像的源(src,source). File API简介 新的File API接口是在不违背任何安全沙盒规则下,读取和列出用户文件目录的一个途径—— 通过沙盒(sandbox)限制,恶意网站并不能将病毒写入用户磁盘,当然更不能执行。 我们要使用的文件读取对象叫做 FileReader,FileReader允许开发者读取文件的内容(具体浏览器的实现方式可能大不相同)。 假设我们已经获取了图像文件的path路径,那么依赖前面的代码,使用FileReader来加载和渲染图像就变得很容易了: // 加载 图像文件(url路径) function loadImage(src){ // 过滤掉 非 image 类型的文件 if(!src.type.match(/image.*/)){ if(window.console){ console.log("选择的文件类型不是图片: ", src.type); } else { window.confirm("只能选择图片文件"); } return; } // 创建 FileReader 对象 并调用 render 函数来完成渲染. var reader = new FileReader(); // 绑定load事件自动回调函数 reader.onload = function(e){ // 调用前面的 render 函数 render(e.target.result); }; // 读取文件内容 reader.readAsDataURL(src); }; 请问,如何获取文件呢? 小白兔,要有耐心!我们的下一步就是获取文件,当然有好多方法可以实现啦。例如:你可以用文本框让用户输入文件路径,但很显然大多数用户都不是开发者,对输入什么值根本就不了解. 为了用户使用方便,我们采用 Drag and Drop API接口。 使用 Drag and Drop API 拖拽接口(Drag and Drop)非常简单——在大多数的DOM元素上,你都可以通过绑定事件处理器来实现. 只要用户从磁盘上拖动一个文件到dom对象上并放开鼠标,那我们就可以读取这个文件。代码如下: function init(){ // 获取DOM元素对象 var target = document.getElementById("drop-target"); // 阻止 dragover(拖到DOM元素上方) 事件传递 target.addEventListener("dragover", function(e){e.preventDefault();}, true); // 拖动并放开鼠标的事件 target.addEventListener("drop", function(e){ // 阻止默认事件,以及事件传播 e.preventDefault(); // 调用前面的加载图像 函数,参数为dataTransfer对象的第一个文件 loadImage(e.dataTransfer.files[0]); }, true); var setheight = document.getElementById("setheight"); var maxheight = document.getElementById("maxheight"); setheight.addEventListener("click", function(e){ // var value = maxheight.value; if(/^\d+$/.test(value)){ MAX_HEIGHT = parseInt(value); } e.preventDefault(); },true); var btnsend = document.getElementById("btnsend"); btnsend.addEventListener("click", function(e){ // sendImage(); },true); }; 我们还可以做一些其他的处理,比如显示预览图。但如果不想压缩图片的话,那很可能没什么用。我们将采用Ajax通过HTTP 的post方式上传图片数据。下面的例子是使用Dojo框架来完成请求的,当然你也可以采用其他的Ajax技术来实现. Dojo 代码如下: // 译者并不懂Dojo,所以将在后面附上jQuery的实现 // Remember that DTK 1.7+ is AMD! require(["dojo/request"], function(request){ // 设置请求URL,参数,以及回调。 request.post("image-handler.php", { data: { imageName: "myImage.png", imageData: encodeURIComponent(document.getElementById("canvas").toDataURL("image/png")) } }).then(function(text){ console.log("The server returned: ", text); }); }); jQuery 实现如下: // 上传图片,jQuery版 function sendImage(){ // 获取 canvas DOM 对象 var canvas = document.getElementById("myCanvas"); // 获取Base64编码后的图像数据,格式是字符串 // "data:image/png;base64,"开头,需要在客户端或者服务器端将其去掉,后面的部分可以直接写入文件。 var dataurl = canvas.toDataURL("image/png"); // 为安全 对URI进行编码 // data%3Aimage%2Fpng%3Bbase64%2C 开头 var imagedata = encodeURIComponent(dataurl); //var url = $("#form").attr("action"); // 1. 如果form表单不好处理,可以使用某个hidden隐藏域来设置请求地址 // <input type="hidden" name="action" value="receive.jsp" /> var url = $("input[name='action']").val(); // 2. 也可以直接用某个dom对象的属性来获取 // <input id="imageaction" type="hidden" action="receive.jsp"> // var url = $("#imageaction").attr("action"); // 因为是string,所以服务器需要对数据进行转码,写文件操作等。 // 个人约定,所有http参数名字全部小写 console.log(dataurl); //console.log(imagedata); var data = { imagename: "myImage.png", imagedata: imagedata }; jQuery.ajax( { url : url, data : data, type : "POST", // 期待的返回值类型 dataType: "json", complete : function(xhr,result) { //console.log(xhr.responseText); var $tip2 = $("#tip2"); if(!xhr){ $tip2.text('网络连接失败!'); return false; } var text = xhr.responseText; if(!text){ $tip2.text('网络错误!'); return false; } var json = eval("("+text+")"); if(!json){ $tip2.text('解析错误!'); return false; } else { $tip2.text(json.message); } //console.dir(json); //console.log(xhr.responseText); } }); }; OK,搞定!你还需要做的,就是创建一个只管的用户界面,并允许你控制图片的大小。上传到服务器端的数据,并不需要处理enctype为 multi-part/form-data 的情况,仅仅一个简单的POST表单处理程序就可以了. 好了,下面附上完整的代码示例: <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> <!DOCTYPE html> <html> <head> <title>通过Canvas及File API缩放并上传图片</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="Canvas,File,Image"> <meta http-equiv="description" content="2013年8月8日,renfufei@qq.com"> <script src="http://code.jquery.com/jquery-1.7.1.min.js"></script> <script> // 参数,最大高度 var MAX_HEIGHT = 100; // 渲染 function render(src){ // 创建一个 Image 对象 var image = new Image(); // 绑定 load 事件处理器,加载完成后执行 image.onload = function(){ // 获取 canvas DOM 对象 var canvas = document.getElementById("myCanvas"); // 如果高度超标 if(image.height > MAX_HEIGHT) { // 宽度等比例缩放 *= image.width *= MAX_HEIGHT / image.height; image.height = MAX_HEIGHT; } // 获取 canvas的 2d 环境对象, // 可以理解Context是管理员,canvas是房子 var ctx = canvas.getContext("2d"); // canvas清屏 ctx.clearRect(0, 0, canvas.width, canvas.height); // 重置canvas宽高 canvas.width = image.width; canvas.height = image.height; // 将图像绘制到canvas上 ctx.drawImage(image, 0, 0, image.width, image.height); // !!! 注意,image 没有加入到 dom之中 }; // 设置src属性,浏览器会自动加载。 // 记住必须先绑定事件,才能设置src属性,否则会出同步问题。 image.src = src; }; // 加载 图像文件(url路径) function loadImage(src){ // 过滤掉 非 image 类型的文件 if(!src.type.match(/image.*/)){ if(window.console){ console.log("选择的文件类型不是图片: ", src.type); } else { window.confirm("只能选择图片文件"); } return; } // 创建 FileReader 对象 并调用 render 函数来完成渲染. var reader = new FileReader(); // 绑定load事件自动回调函数 reader.onload = function(e){ // 调用前面的 render 函数 render(e.target.result); }; // 读取文件内容 reader.readAsDataURL(src); }; // 上传图片,jQuery版 function sendImage(){ // 获取 canvas DOM 对象 var canvas = document.getElementById("myCanvas"); // 获取Base64编码后的图像数据,格式是字符串 // "data:image/png;base64,"开头,需要在客户端或者服务器端将其去掉,后面的部分可以直接写入文件。 var dataurl = canvas.toDataURL("image/png"); // 为安全 对URI进行编码 // data%3Aimage%2Fpng%3Bbase64%2C 开头 var imagedata = encodeURIComponent(dataurl); //var url = $("#form").attr("action"); // 1. 如果form表单不好处理,可以使用某个hidden隐藏域来设置请求地址 // <input type="hidden" name="action" value="receive.jsp" /> var url = $("input[name='action']").val(); // 2. 也可以直接用某个dom对象的属性来获取 // <input id="imageaction" type="hidden" action="receive.jsp"> // var url = $("#imageaction").attr("action"); // 因为是string,所以服务器需要对数据进行转码,写文件操作等。 // 个人约定,所有http参数名字全部小写 console.log(dataurl); //console.log(imagedata); var data = { imagename: "myImage.png", imagedata: imagedata }; jQuery.ajax( { url : url, data : data, type : "POST", // 期待的返回值类型 dataType: "json", complete : function(xhr,result) { //console.log(xhr.responseText); var $tip2 = $("#tip2"); if(!xhr){ $tip2.text('网络连接失败!'); return false; } var text = xhr.responseText; if(!text){ $tip2.text('网络错误!'); return false; } var json = eval("("+text+")"); if(!json){ $tip2.text('解析错误!'); return false; } else { $tip2.text(json.message); } //console.dir(json); //console.log(xhr.responseText); } }); }; function init(){ // 获取DOM元素对象 var target = document.getElementById("drop-target"); // 阻止 dragover(拖到DOM元素上方) 事件传递 target.addEventListener("dragover", function(e){e.preventDefault();}, true); // 拖动并放开鼠标的事件 target.addEventListener("drop", function(e){ // 阻止默认事件,以及事件传播 e.preventDefault(); // 调用前面的加载图像 函数,参数为dataTransfer对象的第一个文件 loadImage(e.dataTransfer.files[0]); }, true); var setheight = document.getElementById("setheight"); var maxheight = document.getElementById("maxheight"); setheight.addEventListener("click", function(e){ // var value = maxheight.value; if(/^\d+$/.test(value)){ MAX_HEIGHT = parseInt(value); } e.preventDefault(); },true); var btnsend = document.getElementById("btnsend"); btnsend.addEventListener("click", function(e){ // sendImage(); },true); }; window.addEventListener("DOMContentLoaded", function() { // init(); },false); </script> </head> <body> <div> <h1>通过Canvas及File API缩放并上传图片</h1> <p>从文件夹拖动一张照片到下方的盒子里, canvas 和 JavaScript将会自动的进行缩放.</p> <div> <input type="text" id="maxheight" value="100"/> <button id="setheight">设置图片最大高度</button> <input type="hidden" name="action" value="receive.jsp" /> </div> <div id="preview-row"> <div id="drop-target" style="width:400px;height:200px;min-height:100px;min-width:200px;background:#eee;cursor:pointer;">拖动图片文件到这里...</div> <div> <div> <button id="btnsend"> 上 传 </button> <span id="tip2" style="padding:8px 0;color:#f00;"></span> </div> </div> <div><h4>缩略图:</h4></div> <div id="preview" style="background:#f4f4f4;width:400px;height:200px;min-height:100px;min-width:200px;"> <canvas id="myCanvas"></canvas> </div> </div> </div> </body> </html> 服务端页面,receive.jsp <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@page import="sun.misc.BASE64Decoder"%> <%@page import="java.io.*"%> <%@page import="org.springframework.web.util.UriComponents"%> <%@page import="java.net.URLDecoder"%> <%! // 本文件:/receive.jsp // 图片存放路径 String photoPath = "D:/blog/upload/photo/"; File photoPathFile = new File(photoPath); // references: http://blog.csdn.net/remote_roamer/article/details/2979822 private boolean saveImageToDisk(byte[] data,String imageName) throws IOException{ int len = data.length; // // 写入到文件 FileOutputStream outputStream = new FileOutputStream(new File(photoPathFile,imageName)); outputStream.write(data); outputStream.flush(); outputStream.close(); // return true; } private byte[] decode(String imageData) throws IOException{ BASE64Decoder decoder = new BASE64Decoder(); byte[] data = decoder.decodeBuffer(imageData); for(int i=0;i<data.length;++i) { if(data[i]<0) { //调整异常数据 data[i]+=256; } } // return data; } %> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> <% //如果是IE,那么需要设置为text/html,否则会弹框下载 //response.setContentType("text/html;charset=UTF-8"); response.setContentType("application/json;charset=UTF-8"); // String imageName = request.getParameter("imagename"); String imageData = request.getParameter("imagedata"); int success = 0; String message = ""; if(null == imageData || imageData.length() < 100){ // 数据太短,明显不合理 message = "上传失败,数据太短或不存在"; } else { // 去除开头不合理的数据 imageData = imageData.substring(30); imageData = URLDecoder.decode(imageData,"UTF-8"); //System.out.println(imageData); byte[] data = decode(imageData); int len = data.length; int len2 = imageData.length(); if(null == imageName || imageName.length() < 1){ imageName = System.currentTimeMillis()+".png"; } saveImageToDisk(data,imageName); // success = 1; message = "上传成功,参数长度:"+len2+"字符,解析文件大小:"+len+"字节"; } // 后台打印 System.out.println("message="+message); %> { "message": "<%=message %>", "success": <%=success %> }
整合Acitiviti在线流程设计器(Activiti-Modeler 5.18.0) 1.概述前言 一直以来都是从事大量的工作流相关的项目,用过很多商用的工作流产品,包括国内与国外的,尽管商用的工作产品在UI操作上比较人性化,但个人用户觉得,这东西只需要一些初级用户,对于我们一直在为一些高级的客户提供一些专业的数据整合、流程梳理、系统间的数据穿透时,这些系统因为不开源,给项目的实施带来巨大的风险,在一些项目栽过跟头后,我更偏向于使用开源的平台了。但开源平台最大的难点是在于你是否有足够的技术人员来学习及掌握它,否则,它也一样面临项目实施失败的风险。后来在一些项目上使用JBPM4,Activiti5,发现Activiti5的流程功能真的很强大,几乎是无所不能。套用一句广告语,老板再也不担心我的流程实现了。在实施国外的项目时,流程的设计几乎是交给开发人员来处理的,因此用Activiti的合适的。但在国内,我们的客户则提出更高的要求,要求普通的人员也可以参与流程的设计要求。Activiti后续的版本也在完善这些功能,特别是Activiti-5.18版本,Activiti-Modeler的建模工具几乎进行了重写,看来Activiti的开源团队也慢慢意识了这点,加大了人力在这方面的投入,以目前的使用,可以达到商用级别,通过功能的扩展,可以很好实现在线流程建模。 为了平台未来的延伸扩展,我建议直接使用该团队的Activiti-Modeler,原因很简单,可以有效跟着团队进行产品的升级,当然我们也需要扩展自己的特色功能,这块我在后面不断把文章写出来,以供大家学习。 在此,先展示一下我在JSAAS平台上初步整合Activiti-Modeler的效果: 说实话,虽然这设计器还有一些小小的缺陷,但仍然阻止不了我爱它,因为全新自己开发这东西,那是比较要命的,呵呵,苦逼的程序员呀。于是我多么希望在我的Activiti的流程应用里,直接就带这么一款应用。 现实提美好的, 整合是苦逼的,于是就有本文的出现。 在我的博客前一篇中,已经有说明如何利用Activiti-Modeler的源码跑起来,加至eclipse下运行起来,在本文中即是以该文为基础,进行本文的说明及整合。 2. 整合Activiti-Modeler的要求 Activiti-Modeler 5.18用了新的WEB框架,其是基于Spring-Mvc 4.0以上的框架,同时用了VAADIN的RIA的UI,特别是后者,这框架带有太多的jar包,虽然它也是结合了spring来使用,要整合这玩意,几乎就得把这东西加入我们的项目中,同时还需要整合它的用户管理,这是要命的。我们的出发点,仅是用它的前端画图处理功能,后端的流程逻辑处理即由我们来实现。 于是我研究了一下Activiti-webapp-explorer2项目,发现要实现我以上的目标,原来很简单。以下假定我的环境要求,以下为我的原项目的环境,是基于Spring 3的,我的平台可直接转为Spring4.0,特别是Spring-MVC的环境也转成4.0 3. 整合步骤 3.1. 把前端的设计器文件从activiti-webapp-explorer2拷至我平台上新建的目录process-editor,如下图所示: 同时把resources下的stencilset.json文件拷至我的项目中的resources目录下。 3.2.在Spring项目中加入Activiti 5.18的依赖引用,注意,不能直接从explorer项目中直接取,那样会带有很多无用的jar包,以下为精简后的pom引用。 <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-engine</artifactId> <version>5.18.0</version> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring</artifactId> <version>5.18.0</version> <exclusions> <exclusion> <artifactId>commons-dbcp</artifactId> <groupId>commons-dbcp</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-diagram-rest</artifactId> <version>5.18.0</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-transcoder</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-dom</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-json-converter</artifactId> <version>5.18.0</version> <exclusions> <exclusion> <artifactId>commons-collections</artifactId> <groupId>commons-collections</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-bridge</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-css</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-anim</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-codec</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-ext</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-gvt</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-script</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-parser</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-svg-dom</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-svggen</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-util</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-xml</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>org.apache.xmlgraphics</groupId> <artifactId>batik-js</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>xml-apis</groupId> <artifactId>xml-apis-ext</artifactId> <version>1.3.04</version> </dependency> <dependency> <groupId>xml-apis</groupId> <artifactId>xml-apis</artifactId> <version>1.3.04</version> </dependency> <dependency> <groupId>org.apache.xmlgraphics</groupId> <artifactId>xmlgraphics-commons</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>batik</groupId> <artifactId>batik-awt-util</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> 如不采用common-dbcp的数据源时,以上配置排除该包的引用, Batik的包主要是用来解析html中的svg的内容,包比较多,但都不大。 3.3.配置如下的spring-activiti.xml文件,其格式如下所示(可从activiti-webapp-explorer2下的resources的activiti-custom-context.xml文件拷出来),把以下的一些用到explorer表单的配置信息删除。因为我们不采用其表单的配置信息。 注意点: 1. 扩展实现自身的idGenerator 目的是为了产生唯一的数据主键,方法很多,请自行实现,不扩展也可以。 2. 配置对应的数据连接信息及数据源、事务等 3.4 .在Spring的配置文件中引入spring-activiti.xml,启动应用程序即可,可看到其已经把数据库表创建出来。 3.5. 处理Activiti-Moderler的后台处理的配置。即创建模型设计、保存、更新等内容,它需要与后端进行交互处理。我们看了activiti-webapp-explorer2的web.xml就清楚其后台交互的处理模式。 简要说明:explorer2这个项目在启动后,就会spring mvc4进行包扫描,把(请参考org.activiti.explorer.servlet.DispatcherServletConfiguration),org.activiti.rest.editor、org.activiti.rest.diagram包下的Controller扫描至响应映射中来,为的就是实现编辑器及设计模型的流程展示时,相应有对应的controller服务。 因此,我们比较好的办法就是重写这些controller即可,这些controller的实现也很简单,在这里,我最简单的做法就是把这些类直接拷到我的项目中,重命名了包名。(当然也可以直接把以上两包通过pom依赖加进来),本人不想自己的项目带有太多的依赖包,所以不直接引用了。 拷完后,我这里的包如下所示: 在SpringMVC中加载这些包,注意,SpringMvc需要为4.0以上的,这样才能比较好支持RestController的注解方式,否则,请用旧的方式来支持这种Rest URL访问。 <!--加入Spring Activiti-Modeler的运行配置--> <context:component-scan base-package="com.redxun.bpm.rest.diagram"/> <context:component-scan base-package="com.redxun.bpm.rest.editor"/> 在web.xml中配置拦截这些访问路径 <servlet-mapping> <servlet-name>springMvc</servlet-name> <url-pattern>/service/*</url-pattern> </servlet-mapping> 3.6. 修改process-editor下的一些配置文件,以支持我们的在线流程设计 A)去掉Activiti Afresco的logo标题栏,并且把样式上的空白栏去掉 修改modeler.html中的以下内容,注意不要把该文本删除,建议加style=”display:none”,删除后其会造成底层下的一些内容有40个像数的东西显示不出来。 <div class="navbar navbar-fixed-top navbar-inverse" role="navigation" id="main-header"> <div class="navbar-header"> <a href="" ng-click="backToLanding()" class="navbar-brand" title="{{'GENERAL.MAIN-TITLE' | translate}}"><span class="sr-only">{{'GENERAL.MAIN-TITLE' | translate}}</span></a> </div> </div> B)在editor-app/css/style-common.css中,把以下样式的padding-top部分改为0px; .wrapper.full { padding: 40px 0px 0px 0px; overflow: hidden; max-width: 100%; min-width: 100%; } C)在modeler.html中加上CloseWindow的函数 <script type="text/javascript"> function CloseWindow(action) { if (window.CloseOwnerWindow) return window.CloseOwnerWindow(action); else window.close(); } </script> 目的是为了保存模型时,可以关闭当前的弹出的mini窗口,修改保存后弹出的窗口的保存及关闭动作,如下所示:
对于无论是Activtit还是jbpm来说,业务与流程的整合均类似,启动流程是绑定业务,流程与业务的整合放到动态代理中 [java] view plain copy print? /** * 启动修改课程流程Leave leave, * * @param leave */ @RequestMapping(value = "start", method = RequestMethod.POST) public String startWorkflow(Leave leave,RedirectAttributes redirectAttributes, HttpSession session) { try { User user = UserUtil.getUserFromSession(session); // 用户未登录不能操作,实际应用使用权限框架实现,例如Spring Security、Shiro等 if (user == null || StringUtils.isBlank(user.getId())) { return "redirect:/login?timeout=true"; } leave.setUserId(user.getId()); Map<String, Object> variables = new HashMap<String, Object>(); variables.put("leave", leave); //保存业务实体 leave.setTestId(new Date().toString()); leaveBean.saveBeforeEntity(leave); Leave Leavetest=null; Leavetest=leaveBean.queryByTestid(leave.getTestId()); leave=Leavetest; logger.debug("save entity: {}", leave); //不再获取id,改为获取类 .getClass().getSimpleName().toString(); //String businessKey = "leave"; String businessKey = leave.getId().toString(); ProcessInstance processInstance = null; /*添加的代码--begin--Proxy*/ // 调用业务,保存申请信息 startNode.common(businessKey, variables,runtimeService,identityService); LogHandler1 logHandler = startNode.new LogHandler1(); //放到代理中设置值了 //stuCourseApply.setExecuteId(pi.getId()); LeaveBean leaveBeanProxy=(LeaveBean)logHandler.newProxyInstanceStart(leaveBean); leaveBeanProxy.updeatChangeApply(leave); /*添加的代码--end--Proxy*/ /*放到代理中--begin--Proxy*/ /* try { // 用来设置启动流程的人员ID,引擎会自动把用户ID保存到activiti:initiator中 identityService.setAuthenticatedUserId(leave.getUserId()); processInstance = runtimeService.startProcessInstanceByKey("easyChangeCourse", businessKey, variables); String processInstanceId = processInstance.getId(); leave.setProcessInstanceId(processInstanceId); logger.debug("start process of {key={}, bkey={}, pid={}, variables={}}", new Object[]{"easyChangeCourse", processInstanceId, variables}); } finally { identityService.setAuthenticatedUserId(null); }*/ /*放到代理中--end--Proxy*/ //+ processInstance.getId() redirectAttributes.addFlashAttribute("message", "流程已启动" ); } catch (ActivitiException e) { if (e.getMessage().indexOf("no processes deployed with key") != -1) { logger.warn("没有部署流程!", e); redirectAttributes.addFlashAttribute("error", "没有部署流程,请在[工作流]->[流程管理]页面点击<重新部署流程>"); } else { logger.error("启动请假流程失败:", e); redirectAttributes.addFlashAttribute("error", "系统内部错误!"); } } catch (Exception e) { logger.error("启动请假流程失败:", e); redirectAttributes.addFlashAttribute("error", "系统内部错误!"); } return "redirect:/oa/leave/apply"; } 动态代理: [java] view plain copy print? package com.tgb.itoo.activiti.controller; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Map; import org.activiti.engine.IdentityService; import org.activiti.engine.RuntimeService; import org.activiti.engine.runtime.ProcessInstance; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import com.tgb.itoo.basic.entity.Leave; @Component @Transactional public class StartNode{ private Logger logger = LoggerFactory.getLogger(getClass()); //定义一个属性变量 private Map<String, Object> variables; private String businessKey; //设置人人员 protected IdentityService identityService; @Autowired public void setIdentifyService(IdentityService identityService) { this.identityService = identityService; } protected RuntimeService runtimeService; @Autowired public void setRuntimeService(RuntimeService runtimeService) { this.runtimeService = runtimeService; } @Autowired RuntimeService runtimeService1; public void common(String businessKey,Map<String, Object> variables,RuntimeService runtimeService,IdentityService identityService){ this.variables=variables; this.businessKey=businessKey; this.runtimeService=runtimeService; this.identityService=identityService; } //想尝试能否根据其他方式传参,new的话太耗费资源 /*public StartAbstractJBPM(String pdKey,Map<String, Object> variablesMap,JBPMService jbpmService){ this.variablesMap=variablesMap; this.pdKey=pdKey; this.jbpmService=jbpmService; }*/ //动态代理类只能代理接口(不支持抽象类),代理类都需要实现InvocationHandler类,实现invoke方法。该invoke方法就是调用被代理接口的所有方法时需要调用的,该invoke方法返回的值是被代理接口的一个实现类 public class LogHandler1 implements InvocationHandler{ // 目标对象 private Object targetObject; //绑定关系,也就是关联到哪个接口(与具体的实现类绑定)的哪些方法将被调用时,执行invoke方法。 public Object newProxyInstanceStart(Object targetObject){ this.targetObject=targetObject; //该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例 //第一个参数指定产生代理对象的类加载器,需要将其指定为和目标对象同一个类加载器 //第二个参数要实现和目标对象一样的接口,所以只需要拿到目标对象的实现接口 //第三个参数表明这些被拦截的方法在被拦截时需要执行哪个InvocationHandler的invoke方法 //根据传入的目标返回一个代理对象 return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(),this); } @Override //关联的这个实现类的方法被调用时将被执行 // InvocationHandler接口的方法,proxy表示代理,method表示原对象被调用的方法,args表示方法的参数 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("start-->>"); for(int i=0;i<args.length;i++){ System.out.println(args[i]); } Object ret=null; try{ //原对象方法调用前处理日志信息 System.out.println("satrt-->>"); //启动流程 //调用目标方法 Leave leave=(Leave)args[0]; // 用来设置启动流程的人员ID,引擎会自动把用户ID保存到activiti:initiator中 try { identityService.setAuthenticatedUserId(leave.getUserId()); ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("ChangeCourse", businessKey, variables); String processInstanceId = processInstance.getId(); leave.setProcessInstanceId(processInstanceId); logger.debug("start process of {key={}, bkey={}, pid={}, variables={}}", new Object[]{"ChangeCourse", processInstanceId, variables}); } finally { identityService.setAuthenticatedUserId(null); } args[0]=leave; ret=method.invoke(targetObject, args); //调用完成当前结点 // >> 办理完第1个任务“提交申请” //jbpmService.completeFirstTask(pi); //原对象方法调用后处理日志信息 System.out.println("success-->>"); }catch(Exception e){ e.printStackTrace(); System.out.println("error-->>"); throw e; } return ret; } } } [java] view plain copy print? /** * 任务列表ERROR [stderr] (http-localhost/127.0.0.1:8080-3) ScriptEngineManager providers.next(): javax.script.ScriptEngineFactory: Provider com.sun.script.javascript.RhinoScriptEngineFactory not found * * @param leave */ @RequestMapping(value = "list/task") public ModelAndView taskList(HttpSession session, HttpServletRequest request) { List<Map<String, Object>> results = new ArrayList<Map<String, Object>>(); String userId = UserUtil.getUserFromSession(session).getId(); results=abstractTaskList(userId); return new ModelAndView("/oa/leave/taskList","results",results); } [java] view plain copy print? /** * 抽象出来的查看任务列表,与基本业务无关 * * @param userId 用户id * @return */ public List<Map<String, Object>> abstractTaskList(String userId){ List<Leave> results = new ArrayList<Leave>(); // 根据当前人的ID查询 TaskQuery taskQuery = taskService.createTaskQuery().taskCandidateOrAssigned(userId); List<Task> tasks = taskQuery.list(); int i=0; List<Map<String, Object>> mapList = new ArrayList<Map<String, Object>>(); // 根据流程的业务ID查询实体并关联 for (Task task : tasks) { String processInstanceId = task.getProcessInstanceId(); ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).active().singleResult(); String businessKey = processInstance.getBusinessKey(); if (businessKey == null) { continue; } Map<String, Object> map = new HashMap<String, Object>(); Leave leave = leaveBean.findEntityById(businessKey); //leave.setProcessInstance(processInstance); //leave.setProcessDefinition(getProcessDefinition(processInstance.getProcessDefinitionId())); //leave.setTask(task); map.put("leave", leave);//存入“申请信息” map.put("task", task); map.put("processDefinition", getProcessDefinition(processInstance.getProcessDefinitionId())); map.put("processInstance", processInstance);//存入“流程实例” mapList.add(map); /*Leave leave=updateEntity(processInstance,task,businessKey); results.add(leave); */ i=i+1; } return mapList; } [java] view plain copy print? /** * 读取运行中的流程实例(查看我的申请)involvedUser(userId)(涉及到的用户) * * @return */ @RequestMapping(value = "list/running") public ModelAndView runningList(HttpSession session,HttpServletRequest request) { String userId = UserUtil.getUserFromSession(session).getId(); List<Map<String, Object>> results = new ArrayList<Map<String, Object>>(); results=abstractRuningList(userId); return new ModelAndView ("/oa/leave/running","results",results); } [java] view plain copy print? /** * 抽象出来读取运行中的流程实例(查看我的申请),与基本业务无关 * * @param userId 用户id * @return */ public List<Map<String, Object>> abstractRuningList(String userId){ List<Leave> results = new ArrayList<Leave>(); ProcessInstanceQuery query = runtimeService.createProcessInstanceQuery().processDefinitionKey("ChangeCourse").involvedUser(userId).active().orderByProcessInstanceId().desc();//根据流程定义Key查询流程实例 List<ProcessInstance> list = query.list(); List<Map<String, Object>> mapList = new ArrayList<Map<String, Object>>(); // 关联业务实体 for (ProcessInstance processInstance : list) { String businessKey = processInstance.getBusinessKey(); if (businessKey == null) { continue; } // 设置当前任务信息 List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).active().orderByTaskCreateTime().desc().listPage(0, 1); Map<String, Object> map = new HashMap<String, Object>(); Leave leave = leaveBean.findEntityById(businessKey); /*leave.setProcessInstance(processInstance); leave.setProcessDefinition(getProcessDefinition(processInstance.getProcessDefinitionId())); leave.setTask(tasks.get(0));*/ map.put("leave", leave);//存入“考试信息” map.put("task", tasks.get(0)); map.put("processDefinition", getProcessDefinition(processInstance.getProcessDefinitionId())); map.put("processInstance", processInstance);//存入“流程实例” mapList.add(map); /*Leave leave=updateEntity(processInstance,task,businessKey); results.add(leave); */ //Leave leave=updateEntity(processInstance,tasks.get(0),businessKey); } return mapList; } [java] view plain copy print? /** * 读取完成的流程实例(已经完成的流程申请-我) * * @return */ @RequestMapping(value = "list/finished") public ModelAndView finishedList(HttpSession session,HttpServletRequest request) { String userId = UserUtil.getUserFromSession(session).getId(); List<Leave> results = new ArrayList<Leave>(); HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery().processDefinitionKey("ChangeCourse").involvedUser(userId).finished().orderByProcessInstanceEndTime().desc(); List<HistoricProcessInstance> list = query.list(); List<Map<String, Object>> mapList = new ArrayList<Map<String, Object>>(); // 关联业务实体 for (HistoricProcessInstance historicProcessInstance : list) { Map<String, Object> map = new HashMap<String, Object>(); String businessKey = historicProcessInstance.getBusinessKey(); Leave leave = leaveBean.findEntityById(businessKey); /* leave.setProcessDefinition(getProcessDefinition(historicProcessInstance.getProcessDefinitionId())); leave.setHistoricProcessInstance(historicProcessInstance); results.add(leave);*/ map.put("leave", leave);//存入“申请信息” map.put("processDefinition", getProcessDefinition(historicProcessInstance.getProcessDefinitionId())); map.put("historicProcessInstance", historicProcessInstance);//存入“流程实例” mapList.add(map); } return new ModelAndView("/oa/leave/finished","results",mapList); } [java] view plain copy print? /** * 完成任务 * * @param id * @return */ @RequestMapping(value = "/complete/{id}", method = {RequestMethod.POST, RequestMethod.GET}) @ResponseBody public String complete(@PathVariable("id") String taskId, Variable var) { try { //deptLeaderPass=true or hrBackReason=666, hrPass=false-----{leaderBackReason=78, deptLeaderPass=false} Map<String, Object> variables = var.getVariableMap(); //taskService.getVariables(taskId); //Object variablesResult=variables.get("deptLeaderPass"); //variablesResult=variables.get("hrPass"); taskService.complete(taskId, variables); //获取map中的值 // if(hrPass=true){ // //更新业务表信息 // } return "success"; } catch (Exception e) { logger.error("error on complete task {}", new Object[]{taskId, e}); return "error"; } } 总结: 对于在流程与业务的整合中应用动态代理也不知道是否符合AOP的理念,类似其他方面的流程操作还未抽取出来(交互太多),在这里记录一下学习Activiti的一个过程,在之后的尝试中可以换个方式,换个思路,直接将整个工作流应用抽取出来(请假,修改课程等)。
Java 并发性支持 在 Java 平台诞生之初,并发性支持就是它的一个特性,线程和同步的实现为它提供了超越其他竞争语言的优势。Scala 基于 Java 并在 JVM 上运行,能够直接访问所有 Java 运行时(包括所有并发性支持)。所以在分析 Scala 特性之前,我首先会快速回顾一下 Java 语言已经提供的功能。 Java 线程基础 在 Java 编程过程中创建和使用线程非常容易。它们由 java.lang.Thread 类表示,线程要执行的代码为 java.lang.Runnable 实例的形式。如果需要的话,可以在应用程序中创建大量线程,您甚至可以创建数千个线程。在有多个核心时,JVM 使用它们来并发执行多个线程;超出核心数量的线程会共享这些核心。 Java 5:并发性的转折点 Java 从一开始就包含对线程和同步的支持。但在线程间共享数据的最初规范不够完善,这带来了 Java 5 的 Java 语言更新中的重大变化 (JSR-133)。Java Language Specification for Java 5 更正并规范化了 synchronized 和 volatile 操作。该规范还规定不变的对象如何使用多线程。(基本上讲,只要在执行构造函数时不允许引用 “转义”,不变的对象始终是线程安全的。)以前,线程间的交互通常需要使用阻塞的 synchronized 操作。这些更改支持使用 volatile 在线程间执行非阻塞协调。因此,在 Java 5 中添加了新的并发集合类来支持非阻塞操作 — 这与早期仅支持阻塞的线程安全方法相比是一项重大改进。 线程操作的协调难以让人理解。只要从程序的角度让所有内容保持一致,Java 编译器和 JVM 就不会对您代码中的操作重新排序,这使得问题变得更加复杂。例如:如果两个相加操作使用了不同的变量,编译器或 JVM 可以安装与指定的顺序相反的顺序执行这些操作,只要程序不在两个操作都完成之前使用两个变量的总数。这种重新排序操作的灵活性有助于提高 Java 性能,但一致性只被允许应用在单个线程中。硬件也有可能带来线程问题。现代系统使用了多种缓存内存级别,一般来讲,不是系统中的所有核心都能同样看到这些缓存。当某个核心修改内存中的一个值时,其他核心可能不会立即看到此更改。 由于这些问题,在一个线程使用另一个线程修改的数据时,您必须显式地控制线程交互方式。Java 使用了特殊的操作来提供这种控制,在不同线程看到的数据视图中建立顺序。基本操作是,线程使用 synchronized 关键字来访问一个对象。当某个线程在一个对象上保持同步时,该线程将会获得此对象所独有的一个锁的独占访问。如果另一个线程已持有该锁,等待获取该锁的线程必须等待,或者被阻塞,直到该锁被释放。当该线程在一个 synchronized 代码块内恢复执行时,Java 会保证该线程可以 “看到了” 以前持有同一个锁的其他线程写入的所有数据,但只是这些线程通过离开自己的 synchronized 锁来释放该锁之前写入的数据。这种保证既适用于编译器或 JVM 所执行的操作的重新排序,也适用于硬件内存缓存。一个 synchronized 块的内部是您代码中的一个稳定性孤岛,其中的线程可依次安全地执行、交互和共享信息。 在变量上对 volatile 关键字的使用,为线程间的安全交互提供了一种稍微较弱的形式。synchronized 关键字可确保在您获取该锁时可以看到其他线程的存储,而且在您之后,获取该锁的其他线程也会看到您的存储。volatile 关键字将这一保证分解为两个不同的部分。如果一个线程向volatile 变量写入数据,那么首先将会擦除它在这之前写入的数据。如果某个线程读取该变量,那么该线程不仅会看到写入该变量的值,还会看到写入的线程所写入的其他所有值。所以读取一个 volatile 变量会提供与输入 一个 synchronized 块相同的内存保证,而且写入一个volatile 变量会提供与离开 一个 synchronized 块相同的内存保证。但二者之间有很大的差别:volatile 变量的读取或写入绝不会受阻塞。 抽象 Java 并发性 同步很有用,而且许多多线程应用程序都是在 Java 中仅使用基本的 synchronized 块开发出来的。但协调线程可能很麻烦,尤其是在处理许多线程和许多块的时候。确保线程仅在安全的方式下交互并 避免潜在的死锁(两个或更多线程等待对方释放锁之后才能继续执行),这很困难。支持并发性而不直接处理线程和锁的抽象,这为开发人员提供了处理常见用例的更好方法。 java.util.concurrent 分层结构包含一些集合变形,它们支持并发访问、针对原子操作的包装器类,以及同步原语。这些类中的许多都是为支持非阻塞访问而设计的,这避免了死锁的问题,而且实现了更高效的线程。这些类使得定义和控制线程之间的交互变得更容易,但他们仍然面临着基本线程模型的一些复杂性。 java.util.concurrent 包中的一对抽象,支持采用一种更加分离的方法来处理并发性:Future<T> 接口、Executor 和ExecutorService 接口。这些相关的接口进而成为了对 Java 并发性支持的许多 Scala 和 Akka 扩展的基础,所以更详细地了解这些接口和它们的实现是值得的。 Future<T> 是一个 T 类型的值的持有者,但奇怪的是该值一般在创建 Future 之后才能使用。正确执行一个同步操作后,才会获得该值。收到Future 的线程可调用方法来: 查看该值是否可用 等待该值变为可用 在该值可用时获取它 如果不再需要该值,则取消该操作 Future 的具体实现结构支持处理异步操作的不同方式。 Executor 是一种围绕某个执行任务的东西的抽象。这个 “东西” 最终将是一个线程,但该接口隐藏了该线程处理执行的细节。Executor 本身的适用性有限,ExecutorService 子接口提供了管理终止的扩展方法,并为任务的结果生成了 Future。Executor 的所有标准实现还会实现ExecutorService,所以实际上,您可以忽略根接口。 线程是相对重量级的资源,而且与分配并丢弃它们相比,重用它们更有意义。ExecutorService 简化了线程间的工作共享,还支持自动重用线程,实现了更轻松的编程和更高的性能。ExecutorService 的 ThreadPoolExecutor 实现管理着一个执行任务的线程池。 应用 Java 并发性 并发性的实际应用常常涉及到需要与您的主要处理逻辑独立的外部交互的任务(与用户、存储或其他系统的交互)。这类应用很难浓缩为一个简单的示例,所以在演示并发性的时候,人们通常会使用简单的计算密集型任务,比如数学计算或排序。我将使用一个类似的示例。 任务是找到离一个未知的输入最近的已知单词,其中的最近 是按照Levenshtein 距离 来定义的:将输入转换为已知的单词所需的最少的字符增加、删除或更改次数。我使用的代码基于 Wikipedia 上的 Levenshtein 距离 文章中的一个示例,该示例计算了每个已知单词的 Levenshtein 距离,并返回最佳匹配值(或者如果多个已知的单词拥有相同的距离,那么返回结果是不确定的)。 清单 1 给出了计算 Levenshtein 距离的 Java 代码。该计算生成一个矩阵,将行和列与两个对比的文本的大小进行匹配,在每个维度上加 1。为了提高效率,此实现使用了一对大小与目标文本相同的数组来表示矩阵的连续行,将这些数组包装在每个循环中,因为我只需要上一行的值就可以计算下一行。 清单 1. Java 中的 Levenshtein 距离计算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 /** * Calculate edit distance from targetText to known word. * * @param word known word * @param v0 int array of length targetText.length() + 1 * @param v1 int array of length targetText.length() + 1 * @return distance */ privateint editDistance(String word, int[] v0, int[] v1) { // initialize v0 (prior row of distances) as edit distance for empty 'word' for(inti = 0; i < v0.length; i++) { v0[i] = i; } // calculate updated v0 (current row distances) from the previous row v0 for(inti = 0; i < word.length(); i++) { // first element of v1 = delete (i+1) chars from target to match empty 'word' v1[0] = i + 1; // use formula to fill in the rest of the row for(intj = 0; j < targetText.length(); j++) { intcost = (word.charAt(i) == targetText.charAt(j)) ? 0: 1; v1[j + 1] = minimum(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost); } // swap v1 (current row) and v0 (previous row) for next iteration int[] hold = v0; v0 = v1; v1 = hold; } // return final value representing best edit distance returnv0[targetText.length()]; } 如果有大量已知词汇要与未知的输入进行比较,而且您在一个多核系统上运行,那么您可以使用并发性来加速处理:将已知单词的集合分解为多个块,将每个块作为一个独立任务来处理。通过更改每个块中的单词数量,您可以轻松地更改任务分解的粒度,从而了解它们对总体性能的影响。清单 2 给出了分块计算的 Java 代码,摘自 示例代码 中的 ThreadPoolDistance 类。清单 2 使用一个标准的 ExecutorService,将线程数量设置为可用的处理器数量。 清单 2. 在 Java 中通过多个线程来执行分块的距离计算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 privatefinal ExecutorService threadPool; privatefinal String[] knownWords; privatefinal int blockSize; publicThreadPoolDistance(String[] words, intblock) { threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); knownWords = words; blockSize = block; } publicDistancePair bestMatch(String target) { // build a list of tasks for matching to ranges of known words List<DistanceTask> tasks = newArrayList<DistanceTask>(); intsize = 0; for(intbase = 0; base < knownWords.length; base += size) { size = Math.min(blockSize, knownWords.length - base); tasks.add(newDistanceTask(target, base, size)); } DistancePair best; try{ // pass the list of tasks to the executor, getting back list of futures List<Future<DistancePair>> results = threadPool.invokeAll(tasks); // find the best result, waiting for each future to complete best = DistancePair.WORST_CASE; for(Future<DistancePair> future: results) { DistancePair result = future.get(); best = DistancePair.best(best, result); } }catch(InterruptedException e) { thrownew RuntimeException(e); }catch(ExecutionException e) { thrownew RuntimeException(e); } returnbest; } /** * Shortest distance task implementation using Callable. */ publicclass DistanceTask implementsCallable<DistancePair> { privatefinal String targetText; privatefinal int startOffset; privatefinal int compareCount; publicDistanceTask(String target, intoffset, intcount) { targetText = target; startOffset = offset; compareCount = count; } privateint editDistance(String word, int[] v0, int[] v1) { ... } /* (non-Javadoc) * @see java.util.concurrent.Callable#call() */ @Override publicDistancePair call() throwsException { // directly compare distances for comparison words in range int[] v0 = newint[targetText.length() + 1]; int[] v1 = newint[targetText.length() + 1]; intbestIndex = -1; intbestDistance = Integer.MAX_VALUE; booleansingle = false; for(inti = 0; i < compareCount; i++) { intdistance = editDistance(knownWords[i + startOffset], v0, v1); if(bestDistance > distance) { bestDistance = distance; bestIndex = i + startOffset; single = true; }elseif (bestDistance == distance) { single = false; } } returnsingle ? newDistancePair(bestDistance, knownWords[bestIndex]) : newDistancePair(bestDistance); } } 清单 2 中的 bestMatch() 方法构造一个 DistanceTask 距离列表,然后将该列表传递给 ExecutorService。这种对 ExecutorService 的调用形式将会接受一个 Collection<? extends Callable<T>> 类型的参数,该参数表示要执行的任务。该调用返回一个 Future<T> 列表,用它来表示执行的结果。ExecutorService 使用在每个任务上调用 call() 方法所返回的值,异步填写这些结果。在本例中,T 类型为DistancePair— 一个表示距离和匹配的单词的简单的值对象,或者在没有找到惟一匹配值时近表示距离。 bestMatch() 方法中执行的原始线程依次等待每个 Future 完成,累积最佳的结果并在完成时返回它。通过多个线程来处理 DistanceTask 的执行,原始线程只需等待一小部分结果。剩余结果可与原始线程等待的结果并发地完成。 并发性性能 要充分利用系统上可用的处理器数量,必须为 ExecutorService 配置至少与处理器一样多的线程。您还必须将至少与处理器一样多的任务传递给ExecutorService 来执行。实际上,您或许希望拥有比处理器多得多的任务,以实现最佳的性能。这样,处理器就会繁忙地处理一个接一个的任务,近在最后才空闲下来。但是因为涉及到开销(在创建任务和 future 的过程中,在任务之间切换线程的过程中,以及最终返回任务的结果时),您必须保持任务足够大,以便开销是按比例减小的。 图 1 展示了我在使用 Oracle 的 Java 7 for 64-bit Linux® 的四核 AMD 系统上运行测试代码时测量的不同任务数量的性能。每个输入单词依次与 12,564 个已知单词相比较,每个任务在一定范围的已知单词中找到最佳的匹配值。全部 933 个拼写错误的输入单词会重复运行,每轮运行之间会暂停片刻供 JVM 处理,该图中使用了 10 轮运行后的最佳时间。从图 1 中可以看出,每秒的输入单词性能在合理的块大小范围内(基本来讲,从 256 到大于 1,024)看起来是合理的,只有在任务变得非常小或非常大时,性能才会极速下降。对于块大小 16,384,最后的值近创建了一个任务,所以显示了单线程性能。 图 1. ThreadPoolDistance 性能 Fork-Join Java 7 引入了 ExecutorService 的另一种实现:ForkJoinPool 类。ForkJoinPool 是为高效处理可反复分解为子任务的任务而设计的,它使用 RecursiveAction 类(在任务未生成结果时)或 RecursiveTask<T> 类(在任务具有一个 T 类型的结果时)来处理任务。RecursiveTask<T> 提供了一种合并子任务结果的便捷方式,如清单 3 所示。 清单 3. RecursiveTask<DistancePair> 示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 privateForkJoinPool threadPool = newForkJoinPool(); privatefinal String[] knownWords; privatefinal int blockSize; publicForkJoinDistance(String[] words, intblock) { knownWords = words; blockSize = block; } publicDistancePair bestMatch(String target) { returnthreadPool.invoke(newDistanceTask(target, 0, knownWords.length, knownWords)); } /** * Shortest distance task implementation using RecursiveTask. */ publicclass DistanceTask extendsRecursiveTask<DistancePair> { privatefinal String compareText; privatefinal int startOffset; privatefinal int compareCount; privatefinal String[] matchWords; publicDistanceTask(String from, intoffset, intcount, String[] words) { compareText = from; startOffset = offset; compareCount = count; matchWords = words; } privateint editDistance(intindex, int[] v0, int[] v1) { ... } /* (non-Javadoc) * @see java.util.concurrent.RecursiveTask#compute() */ @Override protectedDistancePair compute() { if(compareCount > blockSize) { // split range in half and find best result from bests in each half of range inthalf = compareCount / 2; DistanceTask t1 = newDistanceTask(compareText, startOffset, half, matchWords); t1.fork(); DistanceTask t2 = newDistanceTask(compareText, startOffset + half, compareCount - half, matchWords); DistancePair p2 = t2.compute(); returnDistancePair.best(p2, t1.join()); } // directly compare distances for comparison words in range int[] v0 = newint[compareText.length() + 1]; int[] v1 = newint[compareText.length() + 1]; intbestIndex = -1; intbestDistance = Integer.MAX_VALUE; booleansingle = false; for(inti = 0; i < compareCount; i++) { intdistance = editDistance(i + startOffset, v0, v1); if(bestDistance > distance) { bestDistance = distance; bestIndex = i + startOffset; single = true; }elseif (bestDistance == distance) { single = false; } } returnsingle ? newDistancePair(bestDistance, knownWords[bestIndex]) : newDistancePair(bestDistance); } } 图 2 显示了清单 3 中的 ForkJoin 代码与 清单 2 中的 ThreadPool 代码的性能对比。ForkJoin 代码在所有块大小中稳定得多,仅在您只有单个块(意味着执行是单线程的)时性能会显著下降。标准的 ThreadPool 代码仅在块大小为 256 和 1,024 时会表现出更好的性能。 图 2. ThreadPoolDistance 与 ForkJoinDistance 的性能对比 这些结果表明,如果可调节应用程序中的任务大小来实现最佳的性能,那么使用标准 ThreadPool 比 ForkJoin 更好。但请注意,ThreadPool的 “最佳性能点” 取决于具体任务、可用处理器数量以及您系统的其他因素。一般而言,ForkJoin 以最小的调优需求带来了优秀的性能,所以最好尽可能地使用它。 Scala 并发性基础 Scala 通过许多方式扩展了 Java 编程语言和运行时,其中包括添加更多、更轻松的处理并发性的方式。对于初学者而言,Future<T> 的 Scala 版本比 Java 版本灵活得多。您可以直接从代码块中创建 future,可向 future 附加回调来处理这些 future 的完成。清单 4 显示了 Scala future 的一些使用示例。该代码首先定义了 futureInt() 方法,以便按需提供 Future<Int>,然后通过三种不同的方式来使用 future。 清单 4. Scala Future<T> 示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 importExecutionContext.Implicits.global val lastInteger = newAtomicInteger def futureInt() = future { Thread sleep 2000 lastInteger incrementAndGet } // use callbacks for completion of futures val a1 = futureInt val a2 = futureInt a1.onSuccess { casei1 => { a2.onSuccess { casei2 => println("Sum of values is " + (i1 + i2)) } } } Thread sleep 3000 // use for construct to extract values when futures complete val b1 = futureInt val b2 = futureInt for(i1 <- b1; i2 <- b2) yield println("Sum of values is " + (i1 + i2)) Thread sleep 3000 // wait directly for completion of futures val c1 = futureInt val c2 = futureInt println("Sum of values is " + (Await.result(c1, Duration.Inf) + Await.result(c2, Duration.Inf))) 清单 4 中的第一个示例将回调闭包附加到一对 future 上,以便在两个 future 都完成时,将两个结果值的和打印到控制台上。回调是按照创建它们的顺序直接嵌套在 future 上,但是,即使更改顺序,它们也同样有效。如果在您附加回调时 future 已完成,该回调仍会运行,但无法保证它会立即运行。原始执行线程会在 Thread sleep 3000 行上暂停,以便在进入下一个示例之前完成 future。 第二个示例演示了使用 Scala for comprehension 从 future 中异步提取值,然后直接在表达式中使用它们。for comprehension 是一种 Scala 结构,可用于简洁地表达复杂的操作组合(map、filter、flatMap 和 foreach)。它一般与各种形式的集合结合使用,但 Scala future 实现了相同的单值方法来访问集合值。所以可以使用 future 作为一种特殊的集合,一种包含最多一个值(可能甚至在未来某个时刻之前之后才包含该值)的集合。在这种情况下,for 语句要求获取 future 的结果,并在表达式中使用这些结果值。在幕后,这种技术会生成与第一个示例完全相同的代码,但以线性代码的形式编写它会得到更容易理解的更简单的表达式。和第一个示例一样,原始执行线程会暂停,以便在进入下一个示例之前完成 future。 第三个示例使用阻塞等待来获取 future 的结果。这与 Java future 的工作原理相同,但在 Scala 中,一个获取最大等待时间参数的特殊Await.result() 方法调用会让阻塞等待变得更为明显。 清单 4 中的代码没有显式地将 future 传递给 ExecutorService 或等效的对象,所以如果没有使用过 Scala,那么您可能想知道 future 内部的代码是如何执行的。答案取决于 清单 4 中最上面一行:import ExecutionContext.Implicits.global。Scala API 常常为代码块中频繁重用的参数使用 implicit 值。future { } 结构要求 ExecutionContext 以隐式参数的形式提供。这个 ExecutionContext 是 JavaExecutorService 的一个 Scala 包装器,以相同方式用于使用一个或多个托管线程来执行任务。 除了 future 的这些基本操作之外,Scala 还提供了一种方式将任何集合转换为使用并行编程的集合。将集合转换为并行格式后,您在集合上执行的任何标准的 Scala 集合操作(比如 map、filter 或 fold)都会自动地尽可能并行完成。(本文稍后会在 清单 7 中提供一个相关示例,该示例使用 Scala 查找一个单词的最佳匹配值。) 错误处理 Java 和 Scala 中的 future 都必须解决错误处理的问题。在 Java 中,截至 Java 7,future 可抛出一个 ExecutionException 作为返回结果的替代方案。应用程序可针对具体的失败类型而定义自己的 ExecutionException 子类,或者可连锁异常来传递详细信息,但这限制了灵活性。 Scala future 提供了更灵活的错误处理。您可以通过两种方式完成 Scala future:成功时提供一个结果值(假设要求一个结果值),或者在失败时提供一个关联的 Throwable。您也可以采用多种方式处理 future 的完成。在 清单 4 中,onSuccess 方法用于附加回调来处理 future 的成功完成。您还可以使用 onComplete 来处理任何形式的完成(它将结果或 throwable 包装在一个 Try 中来适应两种情况),或者使用 onFailure 来专门处理错误结果。Scala future 的这种灵活性扩展到了您可以使用 future 执行的所有操作,所以您可以将错误处理直接集成到代码中。 这个 Scala Future<T> 还有一个紧密相关的 Promise<T> 类。future 是一个结果的持有者,该结果在某个时刻可能可用(或不可用 — 无法内在地确保一个 future 将完成)。future 完成后,结果是固定的,不会发生改变。promise 是这个相同契约的另一端:结果的一个一次性、可分配的持有者,具有结果值或 throwable 的形式。可从 promise 获取 future,在 promise 上设置了结果后,就可以在该 future 上设置此结果。 应用 Scala 并发性 现在您已熟悉一些基本的 Scala 并发性概念,是时候来了解一下解决 Levenshtein 距离问题的代码了。清单 5 显示了 Levenshtein 距离计算的一个比较符合语言习惯的 Scala 实现,该代码基本上与 清单 1 中的 Java 代码类似,但采用了函数风格。 清单 5. Scala 中的 Levenshtein 距离计算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 val limit = targetText.length /** Calculate edit distance from targetText to known word. * * @param word known word * @param v0 int array of length targetText.length + 1 * @param v1 int array of length targetText.length + 1 * @return distance */ def editDistance(word: String, v0: Array[Int], v1: Array[Int]) = { val length = word.length @tailrec def distanceByRow(rnum: Int, r0: Array[Int], r1: Array[Int]): Int = { if(rnum >= length) r0(limit) else{ // first element of r1 = delete (i+1) chars from target to match empty 'word' r1(0) = rnum + 1 // use formula to fill in the rest of the row for(j <- 0until limit) { val cost = if(word(rnum) == targetText(j)) 0else 1 r1(j + 1) = min(r1(j) + 1, r0(j + 1) + 1, r0(j) + cost); } // recurse with arrays swapped for next row distanceByRow(rnum + 1, r1, r0) } } // initialize v0 (prior row of distances) as edit distance for empty 'word' for(i <- 0to limit) v0(i) = i // recursively process rows matching characters in word being compared to find best distanceByRow(0, v0, v1) } 清单 5 中的代码对每个行值计算使用了尾部递归 distanceByRow() 方法。此方法首先检查计算了多少行,如果该数字与检查的单词中的字符数匹配,则返回结果距离。否则会计算新的行值,然后递归地调用自身来计算下一行(将两个行数组包装在该进程中,以便正确地传递新的最新的行值)。Scala 将尾部递归方法转换为与 Java while 循环等效的代码,所以保留了与 Java 代码的相似性。 但是,此代码与 Java 代码之间有一个重大区别。清单 5 中的 for comprehension 使用了闭包。闭包并不总是得到了当前 JVM 的高效处理(参阅Why is using for/foreach on a Range slow?,了解有关的详细信息),所以它们在该计算的最里层循环上增加了大量开销。如上所述,清单 5 中的代码的运行速度没有 Java 版本那么快。清单 6 重写了代码,将 for comprehension 替换为添加的尾部递归方法。这个版本要详细得多,但执行效率与 Java 版本相当。 清单 6. 为提升性能而重新构造的计算代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 val limit = targetText.length /** Calculate edit distance from targetText to known word. * * @param word known word * @param v0 int array of length targetText.length + 1 * @param v1 int array of length targetText.length + 1 * @return distance */ def editDistance(word: String, v0: Array[Int], v1: Array[Int]) = { val length = word.length @tailrec def distanceByRow(row: Int, r0: Array[Int], r1: Array[Int]): Int = { if(row >= length) r0(limit) else{ // first element of v1 = delete (i+1) chars from target to match empty 'word' r1(0) = row + 1 // use formula recursively to fill in the rest of the row @tailrec def distanceByColumn(col: Int): Unit = { if(col < limit) { val cost = if(word(row) == targetText(col)) 0else 1 r1(col + 1) = min(r1(col) + 1, r0(col + 1) + 1, r0(col) + cost) distanceByColumn(col + 1) } } distanceByColumn(0) // recurse with arrays swapped for next row distanceByRow(row + 1, r1, r0) } } // initialize v0 (prior row of distances) as edit distance for empty 'word' @tailrec def initArray(index: Int): Unit = { if(index <= limit) { v0(index) = index initArray(index + 1) } } initArray(0) // recursively process rows matching characters in word being compared to find best distanceByRow(0, v0, v1) } 清单 7 给出的 Scala 代码执行了与 清单 2 中的 Java 代码相同的阻塞的距离计算。bestMatch() 方法找到由 Matcher 类实例处理的特定单词块中与目标文本最匹配的单词,使用尾部递归 best() 方法来扫描单词。*Distance 类创建多个 Matcher 实例,每个对应一个单词块,然后协调匹配结果的执行和组合。 清单 7. Scala 中使用多个线程的一次阻塞距离计算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 classMatcher(words: Array[String]) { def bestMatch(targetText: String) = { val limit = targetText.length val v0 = newArray[Int](limit + 1) val v1 = newArray[Int](limit + 1) def editDistance(word: String, v0: Array[Int], v1: Array[Int]) = { ... } @tailrec /** Scan all known words in range to find best match. * * @param index next word index * @param bestDist minimum distance found so far * @param bestMatch unique word at minimum distance, or None if not unique * @return best match */ def best(index: Int, bestDist: Int, bestMatch: Option[String]): DistancePair = if(index < words.length) { val newDist = editDistance(words(index), v0, v1) val next = index + 1 if(newDist < bestDist) best(next, newDist, Some(words(index))) elseif (newDist == bestDist) best(next, bestDist, None) elsebest(next, bestDist, bestMatch) }elseDistancePair(bestDist, bestMatch) best(0, Int.MaxValue, None) } } classParallelCollectionDistance(words: Array[String], size: Int) extendsTimingTestBase { val matchers = words.grouped(size).map(l => newMatcher(l)).toList def shutdown = {} def blockSize = size /** Find best result across all matchers, using parallel collection. */ def bestMatch(target: String) = { matchers.par.map(m => m.bestMatch(target)). foldLeft(DistancePair.worstMatch)((a, m) => DistancePair.best(a, m)) } } classDirectBlockingDistance(words: Array[String], size: Int) extendsTimingTestBase { val matchers = words.grouped(size).map(l => newMatcher(l)).toList def shutdown = {} def blockSize = size /** Find best result across all matchers, using direct blocking waits. */ def bestMatch(target: String) = { importExecutionContext.Implicits.global val futures = matchers.map(m => future { m.bestMatch(target) }) futures.foldLeft(DistancePair.worstMatch)((a, v) => DistancePair.best(a, Await.result(v, Duration.Inf))) } } 清单 7 中的两个 *Distance 类显示了协调 Matcher 结果的执行和组合的不同方式。ParallelCollectionDistance 使用前面提到的 Scala 的并行集合 feature 来隐藏并行计算的细节,只需一个简单的 foldLeft 就可以组合结果。 DirectBlockingDistance 更加明确,它创建了一组 future,然后在该列表上为每个结果使用一个 foldLeft 和嵌套的阻塞等待。 性能再分析 清单 7 中的两个 *Distance 实现都是处理 Matcher 结果的合理方法。(它们不仅合理,而且非常高效。示例代码 包含我在试验中尝试的其他两种实现,但未包含在本文中。)在这种情况下,性能是一个主要问题,所以图 3 显示了这些实现相对于 Java ForkJoin 代码的性能。 图 3. ForkJoinDistance 与 Scala 替代方案的性能对比 图 3 显示,Java ForkJoin 代码的性能比每种 Scala 实现都更好,但 DirectBlockingDistance 在 1,024 的块大小下提供了更好的性能。两种 Scala 实现在大部分块大小下,都提供了比 清单 1 中的 ThreadPool 代码更好的性能。 这些性能结果仅是演示结果,不具权威性。如果您在自己的系统上运行计时测试,可能会看到不同的性能,尤其在使用不同数量的核心的时候。如果希望为距离任务获得最佳的性能,那么可以实现一些优化:可以按照长度对已知单词进行排序,首先与长度和输入相同的单词进行比较(因为编辑距离总是不低于与单词长度之差)。或者我可以在距离计算超出之前的最佳值时,提前退出计算。但作为一个相对简单的算法,此试验公平地展示了两种并发操作是如何提高性能的,以及不同的工作共享方法的影响。 在性能方面,清单 7 中的 Scale 控制代码与 清单 2 和 清单 3 中的 Java 代码的对比结果很有趣。Scala 代码短得多,而且(假设您熟悉 Scala!)比 Java 代码更清晰。Scala 和 Java 可很好的相互操作,您可以在本文的 完整示例代码 中看到:Scala 代码对 Scala 和 Java 代码都运行了计时测试,Java 代码进而直接处理 Scala 代码的各部分。得益于这种轻松的互操作性,您可以将 Scala 引入现有的 Java 代码库中,无需进行通盘修改。最初使用 Scala 为 Java 代码实现高水平控制常常很有用,这样您就可以充分利用 Scala 强大的表达特性,同时没有闭包或转换的任何重大性能影响。 清单 7 中的 ParallelCollectionDistance Scala 代码的简单性非常具有吸引力。使用此方法,您可以从代码中完全抽象出并发性,从而编写类似单线程应用程序的代码,同时仍然获得多个处理器的优势。幸运的是,对于喜欢此方法的简单性但又不愿意或无法执行 Scala 开发的人而言,Java 8 带来了一种执行直接的 Java 编程的类似特性。
@JsonView是jackson json中的一个注解,Spring webmvc也支持这个注解。 这个注解的作用就是控制输入输出后的json. 假设我们有一个用户类,其中包含用户名和密码,一般情况下如果我们需要序列化用户类时,密码也会被序列化,在一般情况下我们肯定不想见到这样的情况。但是也有一些情况我们需要把密码序列化,如何解决这两种不同的情况呢? 使用@JsonView就可以解决。 看下面的简单例子: [java] view plain copy public class User { public interface WithoutPasswordView {}; public interface WithPasswordView extends WithoutPasswordView {}; private String username; private String password; public User() { } public User(String username, String password) { this.username = username; this.password = password; } @JsonView(WithoutPasswordView.class) public String getUsername() { return this.username; } @JsonView(WithPasswordView.class) public String getPassword() { return this.password; } } 有这样一个简单的User对象,包含两个简单的属性。 在这个对象中定义了两个接口,其中WithoutPasswordView指的就是不带密码的视图,WithPasswordView指的是带密码的视图,并且继承了WithoutPasswordView的视图。 @JsonView 中的这个视图不仅可以用接口,也可以是一般的类,或者说只要有Class属性就能当成视图使用。 类或接口间的继承,也是视图之间的继承,继承后的视图会包含上级视图注解的方法。 @JsonView 可以写到方法上或者字段上。 下面通过代码测试上面的User对象: [java] view plain copy public static void main(String[] args) throws IOException { ObjectMapper objectMapper = new ObjectMapper(); //创建对象 User user = new User("isea533","123456"); //序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); objectMapper.writerWithView(User.WithoutPasswordView.class).writeValue(bos, user); System.out.println(bos.toString()); bos.reset(); objectMapper.writerWithView(User.WithPasswordView.class).writeValue(bos, user); System.out.println(bos.toString()); } 先创建一个objectMapper,然后通过writerWithView工厂方法创建一个指定视图的ObjectWritter,然后通过writeValue输出结果。 输出结果: [javascript] view plain copy {"username":"isea533"} {"username":"isea533","password":"123456"} @JsonView 使用起来就是这么简单,没有太复杂的东西。 @JsonView 属性可以写在注解上,这点不知道是否类似Spring中的自定义注解,如果有了解的人可以留言,谢谢。 另外在Spring-webmvc中使用@JsonView 时要注意,虽然该注解允许指定多个视图,但是spring-webmvc只支持一个参数(视图)。
因为项目有个功能需要打印二维码,因为我比较喜欢使用html+css+js实现,所以首先想到的是jquery.qrcode.js插件,这个插件可以用canvas和table生成二维码,效果也不错,不过对中文支持有问题,这个插件默认使用canvas,所以使用IE的时候,需要指定参数render,只要参数值不是canvas就会用table生成。由于这个问题,我在github,fork了一个,做了如下的修改: [javascript] view plain copy //true if support function canvasSupport() { return !!document.createElement('canvas').getContext; } return this.each(function(){ //if the browser not support canvas,then table. if(!canvasSupport()){ options.render = "table"; } var element = options.render == "canvas" ? createCanvas() : createTable(); $(element).appendTo(this); }); 修改后就不需要指定render参数,如果不支持canvas就会用table. 使用canvas有个缺点就是网页打印的时候显示不出来...这个问题好像已经有解决办法了,我没有去找,我直接用的table。不过打印似乎仍然有问题。 jquery.qrcode.js插件地址:https://github.com/jeromeetienne/jquery-qrcode js有问题,所以只能通过zxing来输出二维码,写了如下的servlet代码: [java] view plain copy @SuppressWarnings("serial") public class QrCodeServlet extends HttpServlet { private static final int BLACK = -16777216; private static final int WHITE = -1; private BufferedImage toBufferedImage(BitMatrix matrix) { int width = matrix.getWidth(); int height = matrix.getHeight(); BufferedImage image = new BufferedImage(width, height,BufferedImage.TYPE_INT_ARGB); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE); } } return image; } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { String content = req.getParameter("m"); if(content==null||content.equals("")){ resp.setContentType("text/plain;charset=UTF-8"); resp.getOutputStream().write("二维码内容不能为空!".getBytes("utf-8")); resp.getOutputStream().close(); } int imgWidth = 110; int imgHeight = 110; String width = req.getParameter("w"); String height = req.getParameter("h"); if(width!=null&&!width.equals("")){ try { imgWidth = Integer.parseInt(width); } catch (Exception e) {} } if(height!=null&&!height.equals("")){ try { imgHeight = Integer.parseInt(height); } catch (Exception e) {} } BitMatrix byteMatrix; Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>(); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); byteMatrix = new MultiFormatWriter().encode( new String(content.getBytes("UTF-8"),"ISO-8859-1"), BarcodeFormat.QR_CODE, imgWidth, imgHeight, hints); BufferedImage image = toBufferedImage(byteMatrix); resp.setContentType("image/png"); ImageIO.write(image, "png", resp.getOutputStream()); } catch (Exception e) { } } } 参数m必须有,参数h、w可选,默认的宽高为110px。 由于zxing默认的编码为ISO-8859-1,所以使用其他编码的时候会出现乱码,即使执行其他的编码方式,还是有问题,如果转换为ISO-8859-1就没有乱码。 还有一个很重要的内容,就是使用zxing的时候必须导出png格式的二维码,导出jpg格式的时候,颜色就不是黑白的,很让人费解,希望有人能给出原因。 二维码效果图:
上一篇到440行返回结果result. 返回result之后,使用result去获取ModelAndView,执行方法getModelAndView: 该方法主要通过result返回值来判断: 当前方法是带ResponseBody注解的,所以执行到这里: 进去方法: 这里是根据http类型做出相应的输出: 用户配置的: 接收的类型: 写的方法: 这里配置的json格式,所以会进入JSON方法: 写入ResponseBody后,返回mav: 最后返回之前调用handler的地方,之后会有一些不同种类的拦截器方法: 拦截器如: 还有一些处理不同异常情况的拦截器。 系统默认都会去执行一个拦截器,这个拦截器基本上都是空方法,是一个private类: 返回调用doDIspatch的地方: 返回到doService的地方: 最后回到httpservlet的service方法: 到这里就完成了一次完整的调用过程。 可以发现,整个过程的流程是比较清晰,程序启动时会根据mvc的配置和spring配置来处理配置信息和注解的类。 Servlet处理请求,通过request(主要是url)来获取handler,之后最主要的一个部分就是获取需要注入的参数,最后调用用户方法,处理返回结果。 整个过程中麻烦的地方就是在一些细节的处理上,这些细节未必一开始就有的,一开始应该是一个主要的流程,后续发现问题或者为了通用性做的改进。
在使用Json传值并且使用@RequestBody注解的时候需要注意一些问题: 一个方法中只能有一个@RequestBody注解。 默认情况下@RequestBody标注的对象必须包含前台传来的所有字段。 第一条容易理解,因为RequestBody就是request的inputStream,这个流在第一次使用该注解后会关闭,后面的都会报错(stream closed)。 第二条如果没有包含前台传来的字段,就会报错:Unrecognized field xxx , not marked as ignorable,这是因为MappingJacksonHttpMessageConverter默认要求必须存在相应的字段。如果没有前台传来的某个字段,就会报错。。 解决方法有很多,可以增加一个字段来接收前台传来的这个值,如果存在多个字段,这种方式很不好(就算一个字段,如果没用,新增字段也不好)。 或者在前台往后台传值的时候,去掉无用的字段。这样还能减少网络传输的大小。 还有一些方法,这些方法主要是使用Jackson提供的json注解。 @JsonIgnore注解用来忽略某些字段,可以用在Field或者Getter方法上,用在Setter方法时,和Filed效果一样。这个注解只能用在POJO存在的字段要忽略的情况,不能满足现在需要的情况。 @JsonIgnoreProperties(ignoreUnknown = true),将这个注解写在类上之后,就会忽略类中不存在的字段,可以满足当前的需要。这个注解还可以指定要忽略的字段。使用方法如下: @JsonIgnoreProperties({ "internalId", "secretKey" }) 指定的字段不会被序列化和反序列化。
通过配置全局的日期转换来避免使用麻烦的注解。 首先用到了一个简单的日期工具类DateUtil.java [java] view plain copy /** * DateUtil类 * * @author liuzh */ public class DateUtil { public static final String Y_M_D = "yyyy-MM-dd"; public static final String Y_M_D_HM = "yyyy-MM-dd HH:mm"; public static final String Y_M_D_HMS = "yyyy-MM-dd HH:mm:ss"; public static final String YMD = "yyyyMMdd"; public static final String YMDHM = "yyyyMMddHHmm"; public static final String YMDHMS = "yyyyMMddHHmmss"; public static final String ymd = "yyyy/MM/dd"; public static final String ymd_HM = "yyyy/MM/dd HH:mm"; public static final String ymd_HMS = "yyyy/MM/dd HH:mm:ss"; /** * 智能转换日期 * * @param date * @return */ public static String smartFormat(Date date) { String dateStr = null; if (date == null) { dateStr = ""; } else { try { dateStr = formatDate(date, Y_M_D_HMS); //时分秒 if (dateStr.endsWith(" 00:00:00")) { dateStr = dateStr.substring(0, 10); } //时分 else if (dateStr.endsWith("00:00")) { dateStr = dateStr.substring(0, 16); } //秒 else if (dateStr.endsWith(":00")) { dateStr = dateStr.substring(0, 16); } } catch (Exception ex) { throw new IllegalArgumentException("转换日期失败: " + ex.getMessage(), ex); } } return dateStr; } /** * 智能转换日期 * * @param text * @return */ public static Date smartFormat(String text) { Date date = null; try { if (text == null || text.length() == 0) { date = null; } else if (text.length() == 10) { date = formatStringToDate(text, Y_M_D); } else if (text.length() == 13) { date = new Date(Long.parseLong(text)); } else if (text.length() == 16) { date = formatStringToDate(text, Y_M_D_HM); } else if (text.length() == 19) { date = formatStringToDate(text, Y_M_D_HMS); } else { throw new IllegalArgumentException("日期长度不符合要求!"); } } catch (Exception e) { throw new IllegalArgumentException("日期转换失败!"); } return date; } /** * 获取当前日期 * @param format * @return * @throws Exception */ public static String getNow(String format) throws Exception{ return formatDate(new Date(), format); } /** * 格式化日期格式 * * @param argDate * @param argFormat * @return 格式化后的日期字符串 */ public static String formatDate(Date argDate, String argFormat) throws Exception { if (argDate == null) { throw new Exception("参数[日期]不能为空!"); } if (StringUtils.isEmpty(argFormat)) { argFormat = Y_M_D; } SimpleDateFormat sdfFrom = new SimpleDateFormat(argFormat); return sdfFrom.format(argDate).toString(); } /** * 把字符串格式化成日期 * * @param argDateStr * @param argFormat * @return */ public static Date formatStringToDate(String argDateStr, String argFormat) throws Exception { if (argDateStr == null || argDateStr.trim().length() < 1) { throw new Exception("参数[日期]不能为空!"); } String strFormat = argFormat; if (StringUtils.isEmpty(strFormat)) { strFormat = Y_M_D; if (argDateStr.length() > 16) { strFormat = Y_M_D_HMS; } else if (argDateStr.length() > 10) { strFormat = Y_M_D_HM; } } SimpleDateFormat sdfFormat = new SimpleDateFormat(strFormat); //严格模式 sdfFormat.setLenient(false); try { return sdfFormat.parse(argDateStr); } catch (ParseException e) { throw new Exception(e); } } } 需要用到的是两个智能转换日期的方法。关于转换的格式和规则,请看这两个方法,如果不符合你需要的,可以自行修改。 然后继承SimpleDateFormat写一个智能转换日期的类SmartDateFormat [java] view plain copy /** * Description: 智能日期转换 * Author: liuzh */ public class SmartDateFormat extends SimpleDateFormat { @Override public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) { return new StringBuffer(DateUtil.smartFormat(date)); } @Override public Date parse(String text) throws ParseException { return DateUtil.smartFormat(text); } } 这里重写了两个方法,这两个方法是互相转换的方法,直接调用的DateUtil提供的两个智能转换的方法。 最后在Spring MVC的xml中配置: [html] view plain copy <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"> <property name="objectMapper"> <bean class="org.codehaus.jackson.map.ObjectMapper"> <property name="dateFormat"> <!-- 智能日期转换 --> <bean class="packageName.SmartDateFormat"/> </property> </bean> </property> </bean> 这段代码主要是针对MappingJacksonHttpMessageConverter进行配置。经过这样的配置之后,Spring就能自动根据日期样式进行转换了。 至于“智能”转换,完全就是简单的if/else判断,可以查看最上面的代码。
类型有很多,这里只用日期为例说明。 在Spring MVC中存在两大类的类型转换,一类是Json,一个是Spring的Binder转换。 JSON: 使用Json转换时,可以如下使用: [java] view plain copy public class Test { private Date createdate; @JsonSerialize(using = DateYMDHMSJsonSerializer.class) public Date getCreatedate() { return createdate; } @JsonDeserialize(using = DateYMDHMSJsonDeserializer.class) public void setCreatedate(Date createdate) { this.createdate = createdate; } } 可以看到这里使用了两个Json转换的注解: 第一个@JsonSerialize是转换为字符串,主要是后台传递给前台时的日期格式; 第二个@JsonDeserialize是转换字符串为日期类型,主要是从前台往后台传递时的日期。 两个具体转换类的实现: [java] view plain copy /** * Description: 日期转换 - "yyyy-MM-dd HH:mm:ss" * Author: liuzh * Update: liuzh(2014-04-17 10:59) */ public class DateYMDHMSJsonSerializer extends JsonSerializer<Date>{ @Override public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { try { jsonGenerator.writeString(DateUtil.formatDate(date, DateUtil.DATE_FORMAT_TIME_T)); } catch (BusinessException e) { jsonGenerator.writeString(String.valueOf(date.getTime())); } } } [java] view plain copy /** * Description: 日期转换 - "yyyy-MM-dd HH:mm:ss" * Author: liuzh * Update: liuzh(2014-04-17 10:59) */ public class DateYMDHMSJsonDeserializer extends JsonDeserializer<Date> { @Override public Date deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { try { return DateUtil.formatStringToDate(jp.getText(), DateUtil.DATE_FORMAT_TIME_T); } catch (BusinessException e) { return new Date(jp.getLongValue()); } } } 其中DateUtil是一个对日期格式转换的工具类,使用的SimpleDateFormat进行转换。 Binder: 这种类型转换的时候,使用的是Spring的参数绑定,代码如下: [java] view plain copy /** * Description: 全局类型转换 * Author: liuzh * Update: liuzh(2014-05-26 13:08) */ public class GlobalDataBinder implements WebBindingInitializer { /** * 智能日期转换,针对四种格式日期: * 1.2014-05-26 * 2.1401951570548 * 3.2014-05-26 00:00 * 4.2014-05-26 00:00:00 */ private class SmartDateEditor extends PropertyEditorSupport { /** * 根据2014-05-26 00:00:00长度来判断选择哪种转换方式 */ @Override public void setAsText(String text) throws IllegalArgumentException { if (text == null || text.length() == 0) { setValue(null); } else { try { if (text.length() == 10) { setValue(DateUtil.formatStringToDate(text, DateUtil.DATE_FORMAT_YYYYMMDD)); } else if (text.length() == 13) { setValue(new Date(Long.parseLong(text))); } else if (text.length() == 16) { setValue(DateUtil.formatStringToDate(text, DateUtil.DATE_FORMAT_TIME_R)); } else if (text.length() == 19) { setValue(DateUtil.formatStringToDate(text, DateUtil.DATE_FORMAT_TIME_T)); } else { throw new IllegalArgumentException("转换日期失败: 日期长度不符合要求!"); } } catch (Exception ex) { throw new IllegalArgumentException("转换日期失败: " + ex.getMessage(), ex); } } } /** * 转换为日期字符串 */ @Override public String getAsText() { Date value = (Date) getValue(); String dateStr = null; if (value == null) { return ""; } else { try { dateStr = DateUtil.formatDate(value, DateUtil.DATE_FORMAT_TIME_T); if (dateStr.endsWith(" 00:00:00")) { dateStr = dateStr.substring(0, 10); } else if (dateStr.endsWith(":00")) { dateStr = dateStr.substring(0, 16); } return dateStr; } catch (Exception ex) { throw new IllegalArgumentException("转换日期失败: " + ex.getMessage(), ex); } } } } @Override public void initBinder(WebDataBinder binder, WebRequest request) { //日期格式转换 binder.registerCustomEditor(Date.class, new SmartDateEditor()); } } 这里对日期类型进行了一些判断来特殊处理。该类需要在Spring的xml进行配置: [html] view plain copy <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="webBindingInitializer"> <bean class="com.easternie.sys.common.GlobalDataBinder"/> </property> </bean> 通过这种配置之后,Spring就能对日期进行智能转换了。
用过Maven的应该都遇到过,当网速不好或者源有问题的时候,Maven的依赖包经常下载失败。 下载失败后在本地仓库对应的文件夹中有一个以.lastUpdated结尾的文件,如果不手动删除这个文件,就不能重新更新依赖,重新下载对应的jar包。 一般情况下遇到的时候可能直接手动找到目录删除。 当出现很多这样的情况时,一个个找起来也很麻烦。 因此本文提供一个小工具,就是一段Java代码,通过这段代码来删除。 public class CleanMvn { public static void main(String[] args){ if(args.length != 1){ print("使用方法错误,方法需要一个参数,参数为mvn本地仓库的路径"); } findAndDelete(new File(args[0]); } public static boolean findAndDelete(File file){ if(!file.exists()){ } else if(file.isFile()){ if(file.getName.endsWith("lastUpdated")){ deleteFile(file.getParentFile()); return true; } } else if(file.isDirectory()){ File[] files = file.listFiles(); for(File f : files){ if(findAndDelete(f)){ break; } } } return false; } public static void deleteFile(File file){ if(!file.exists()){ } else if(file.isFile()){ print("删除文件:" + file.getAbsolutePath()); file.delete(); } else if(file.isDirectory()){ File[] files = file.listFiles(); for(File f : files){ deleteFile(f); } print("删除文件夹:" + file.getAbsolutePath()); print("===================================="); file.delete(); } } public static void print(String msg){ System.out.println(msg); } } 可以在IDE中指定参数后运行这段代码,例如直接调方法findAndDelete(new File("d:\\.m2\\repository")); 或者在命令行下执行这段代码: 首先javac CleanMvn.java编译为.class文件。 然后java CleanMvn d:\.m2\repository通过后面的参数来删除本地仓库中无效的文件。
MyBatis 3.3.1版本新功能示例 MyBatis3.3.1更新日志: https://github.com/mybatis/mybatis-3/issues?q=milestone%3A3.3.1 这里不对更新做翻译或者其他详细介绍。 这个更新除了一些bug修复,还有两个新增的功能: 增加了对批量插入回写自增主键的功能 增加了注解引用@Results的功能 下面通过简单例子来介绍这两个功能,为了例子的简洁,这里都使用注解实现的,没有用XML,批量插入的例子很容易就能变成XML形式的,大家自己尝试。 先看基础的表和对应的POJO。 city表: <code class="language-sql hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-operator" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">SET</span> FOREIGN_KEY_CHECKS=<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>;</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">-- ----------------------------</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">-- Table structure for city</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">-- ----------------------------</span> <span class="hljs-operator" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">DROP</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">TABLE</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">IF</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">EXISTS</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">`city`</span>;</span> <span class="hljs-operator" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">CREATE</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">TABLE</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">`city`</span> ( <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">`id`</span> bigint(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">20</span>) <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">NOT</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">NULL</span> AUTO_INCREMENT, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">`name`</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">varchar</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">255</span>) <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">CHARACTER</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">SET</span> utf8 <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">DEFAULT</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">NULL</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">`state`</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">varchar</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">255</span>) <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">CHARACTER</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">SET</span> utf8 <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">DEFAULT</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">NULL</span>, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">PRIMARY</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">KEY</span> (<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">`id`</span>) ) ENGINE=InnoDB AUTO_INCREMENT=<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">DEFAULT</span> CHARSET=latin1;</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">-- ----------------------------</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">-- Records of city</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">-- ----------------------------</span> <span class="hljs-operator" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">INSERT</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">INTO</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">`city`</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">VALUES</span> (<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'1'</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'石家庄'</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'河北'</span>);</span> <span class="hljs-operator" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">INSERT</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">INTO</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">`city`</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">VALUES</span> (<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'2'</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'邯郸'</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'河北'</span>);</span> </code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li></ul> city对象: <code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">City2</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> Integer id; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> String cityName; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> String cityState; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-title" style="box-sizing: border-box;">City2</span>() { } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-title" style="box-sizing: border-box;">City2</span>(String cityName, String cityState) { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>.cityName = cityName; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>.cityState = cityState; } <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//省略setter,getter</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> String <span class="hljs-title" style="box-sizing: border-box;">toString</span>() { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"City2{"</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"id="</span> + id + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">", cityName='"</span> + cityName + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'\''</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">", cityState='"</span> + cityState + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'\''</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'}'</span>; } }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li></ul> 定义如下MyBatis331Mapper接口 <code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** * mybatis3.3.1版本新增功能测试 * *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @author</span> liuzh *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @since</span> 2016-03-06 17:22 */</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">interface</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MyBatis331Mapper</span> {</span> <span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** * 批量插入 * *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @param</span> cities *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @return</span> */</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Insert</span>(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"<script>"</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"insert into city (id, name, state) values "</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"<foreach collection=\"list\" item=\"city\" separator=\",\" >"</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"(#{city.id}, #{city.cityName}, #{city.cityState})"</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"</foreach>"</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"</script>"</span>) <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Options</span>(useGeneratedKeys = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>, keyProperty = <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"id"</span>) <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> insertCities(List<City2> cities); <span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** * 根据主键查询一个 * *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @param</span> id *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @return</span> */</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Results</span>(id = <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"cityResult"</span>, value = { <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Result</span>(property = <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"id"</span>, column = <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"id"</span>, id = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>), <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Result</span>(property = <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"cityName"</span>, column = <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"name"</span>, id = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>), <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Result</span>(property = <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"cityState"</span>, column = <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"state"</span>, id = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>) }) <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Select</span>(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"select id, name, state from city where id = #{id}"</span>) City2 selectByCityId(Integer id); <span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** * 查询全部,引用上面的Results * *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @return</span> */</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@ResultMap</span>(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"cityResult"</span>) <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Select</span>(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"select id, name, state from city"</span>) List<City2> selectAll(); }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li><li style="box-sizing: border-box; padding: 0px 5px;">40</li><li style="box-sizing: border-box; padding: 0px 5px;">41</li><li style="box-sizing: border-box; padding: 0px 5px;">42</li><li style="box-sizing: border-box; padding: 0px 5px;">43</li><li style="box-sizing: border-box; padding: 0px 5px;">44</li><li style="box-sizing: border-box; padding: 0px 5px;">45</li><li style="box-sizing: border-box; padding: 0px 5px;">46</li></ul> 这里详细说一下这两个新功能的用法。 先看批量插入的例子 <code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Insert</span>(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"<script>"</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"insert into city (id, name, state) values "</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"<foreach collection=\"list\" item=\"city\" separator=\",\" >"</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"(#{city.id}, #{city.cityName}, #{city.cityState})"</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"</foreach>"</span> + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"</script>"</span>) <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Options</span>(useGeneratedKeys = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>, keyProperty = <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"id"</span>) <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> insertCities(List<City2> cities);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li></ul> 首先接口参数只能有一个(默认情况下),如果你参数有多个,那么要返回主键的那个List必须加注解@Param("list")或者在参数Map中对应的key为"list"。这一点很重要,只有看源码才能了解(当然除了"list"还有另外的名字,例如支持数组的"array"),参考Jdbc3KeyGenerator类中的这段代码: <code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (parameterMap.containsKey(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"collection"</span>)) { parameters = (Collection) parameterMap.get(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"collection"</span>); } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">else</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (parameterMap.containsKey(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"list"</span>)) { parameters = (List) parameterMap.get(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"list"</span>); } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">else</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (parameterMap.containsKey(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"array"</span>)) { parameters = Arrays.asList((Object[]) parameterMap.get(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"array"</span>)); }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li></ul> 然后就是必须使用useGeneratedKeys的方式,注解使用下面的方式: <code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Options</span>(useGeneratedKeys = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>, keyProperty = <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"id"</span>)</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul> XML使用类似下面的方式: <code class="language-xml hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">insert</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">id</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"insertList"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">useGeneratedKeys</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"true"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">keyProperty</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"id"</span>></span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul> 只要注意上面这几点,批量插入应该就能返回自增的值了。 注意:大家应该能理解,自增的不一定是主键,而且一个表中可能有多个自增的值。这些情况下都能获取到,keyProperty需要设置多个属性值,逗号隔开即可。 再看引用@Results (此功能是否为新增功能,我并不确定,因为我平时不用注解) 用MyBatis的人中,使用注解的是少数,但是有些企业由于领导或者别的原因,会限制必须用注解。 这对一些复杂的情况来说,使用起来不如XML的方便,但是不得不用。 以前如果返回一个对象的属性需要配置映射,那么每个对象上都需要这段重复的代码,看起来很乱很麻烦。 在上面的例子中,在selectByCityId上定义了Results,在下面的方法selectAll上通过@ResultMap("cityResult")直接引用的上面的Results。这个功能在使用的时候没有特别注意的地方。 测试 写个简单的测试,代码如下: <code class="language-java hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@RunWith</span>(SpringJUnit4ClassRunner.class) <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@WebAppConfiguration</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Transactional</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@SpringApplicationConfiguration</span>(Application.class) <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">MyBatis331Test</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> Logger logger = LoggerFactory.getLogger(getClass()); <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Autowired</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> MyBatis331Mapper mapper; <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Test</span> <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Rollback</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">testInsertList</span>() { List<City2> city2List = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> ArrayList<City2>(); city2List.add(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> City2(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"石家庄"</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"河北"</span>)); city2List.add(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> City2(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"邯郸"</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"河北"</span>)); city2List.add(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> City2(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"秦皇岛"</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"河北"</span>)); Assert.assertEquals(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3</span>, mapper.insertCities(city2List)); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">for</span> (City2 c2 : city2List) { logger.info(c2.toString()); Assert.assertNotNull(c2.getId()); } } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Test</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">testSelectById</span>(){ City2 city2 = mapper.selectByCityId(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>); logger.info(city2.toString()); Assert.assertNotNull(city2); Assert.assertNotNull(city2.getCityName()); Assert.assertNotNull(city2.getCityState()); } <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Test</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">testSelectAll</span>(){ List<City2> city2List = mapper.selectAll(); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">for</span>(City2 c2 : city2List){ logger.info(c2.toString()); Assert.assertNotNull(c2); Assert.assertNotNull(c2.getCityName()); Assert.assertNotNull(c2.getCityState()); } } }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li><li style="box-sizing: border-box; padding: 0px 5px;">40</li><li style="box-sizing: border-box; padding: 0px 5px;">41</li><li style="box-sizing: border-box; padding: 0px 5px;">42</li><li style="box-sizing: border-box; padding: 0px 5px;">43</li><li style="box-sizing: border-box; padding: 0px 5px;">44</li><li style="box-sizing: border-box; padding: 0px 5px;">45</li></ul> 第一个测试方法输出的部分日志如下: <code class="language-sql hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background: transparent;">==> Preparing: <span class="hljs-operator" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">insert</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">into</span> city (id, name, state) <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">values</span> (?, ?, ?) , (?, ?, ?) , (?, ?, ?) ==> Parameters: <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span>, 石家庄(String), 河北(String), <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span>, 邯郸(String), 河北(String), <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span>, 秦皇岛(String), 河北(String) <== Updates: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3</span> City2{id=<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6</span>, cityName=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'石家庄'</span>, cityState=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'河北'</span>} City2{id=<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">7</span>, cityName=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'邯郸'</span>, cityState=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'河北'</span>} City2{id=<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">8</span>, cityName=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'秦皇岛'</span>, cityState=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'河北'</span>}</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li></ul> 后两个方法输出的部分日志如下: <code class="language-sql hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background: transparent;">==> Preparing: <span class="hljs-operator" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">select</span> id, name, state <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">from</span> city <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">where</span> id = ? ==> Parameters: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">Integer</span>) <== Total: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span> City2{id=<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>, cityName=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'石家庄'</span>, cityState=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'河北'</span>} ==> Preparing: <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">select</span> id, name, state <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">from</span> city ==> Parameters: <== Total: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span> City2{id=<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>, cityName=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'石家庄'</span>, cityState=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'河北'</span>} City2{id=<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>, cityName=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'邯郸'</span>, cityState=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'河北'</span>}</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul> 注:由于批量插入事务并没有提交,因此这里查询出来的结果就是表中原有的两条数据。 最后 为了方便尝试上面的代码,可以直接查看下面项目的src/test: MyBatis-Spring-Boot: https://github.com/abel533/MyBatis-Spring-Boot 另外mybatis-spring项目也同时更新到了1.2.4,这个版本对于使用SpringBoot的开发人员非常有用,这个版本解决了mybatis的循环依赖异常,如果你在使用SpringBoot,赶紧升级到最新的版本试试吧。
上一篇讲到了association的关联结果查询,这里讲association的关联的嵌套查询,这种方式用起来很容易,和关联结果查询相比缺点就是会执行关联SQL,增加一定的查询。 关联的嵌套查询 属性 描述 column 来自数据库的类名,或重命名的列标签。这和通常传递给resultSet.getString(columnName)方法的字符串是相同的。column注 意 : 要 处 理 复 合 主 键 , 你 可 以 指 定 多 个 列 名 通 过 column= ”{prop1=col1,prop2=col2} ” 这种语法来传递给嵌套查询语 句。这会引起prop1 和 prop2 以参数对象形式来设置给目标嵌套查询语句。 select 另外一个映射语句的 ID,可以加载这个属性映射需要的复杂类型。获取的在列属性中指定的列的值将被传递给目标 select 语句作为参数。表格后面有一个详细的示例。select注 意 : 要 处 理 复 合 主 键 , 你 可 以 指 定 多 个 列 名 通 过 column= ”{prop1=col1,prop2=col2} ” 这种语法来传递给嵌套查询语 句。这会引起prop1 和 prop2 以参数对象形式来设置给目标嵌套查询语句。 下面是测试用的Mapper.xml文件: [html] view plain copy <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.apache.ibatis.submitted.associationtype.Mapper"> <resultMap id="sampleHashResult" type="hashmap"> <result property="f1" column="f1" /> <result property="f2" column="f2" /> <association property="a1" javaType="java.lang.String" column="{param1=f1}" select="associationTest" /> <association property="a2" javaType="java.lang.String" column="{param1=f1}" select="associationTest" /> </resultMap> <select id="getUser" resultMap="sampleHashResult"> SELECT id as f1, name as f2 from users </select> <select id="associationTest" resultType="java.lang.String"> select id from users </select> </mapper> 下面是测试用的代码: [java] view plain copy public class AssociationTypeTest { private static SqlSessionFactory sqlSessionFactory; @BeforeClass public static void setUp() throws Exception { // create a SqlSessionFactory Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/associationtype/mybatis-config.xml"); sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); reader.close(); // populate in-memory database SqlSession session = sqlSessionFactory.openSession(); Connection conn = session.getConnection(); reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/associationtype/CreateDB.sql"); ScriptRunner runner = new ScriptRunner(conn); runner.setLogWriter(null); runner.runScript(reader); reader.close(); session.close(); } @Test public void shouldGetAUser() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { List<Map> results = sqlSession.selectList("getUser"); for (Map r : results) { Assert.assertEquals(String.class, r.get("a1").getClass()); Assert.assertEquals(String.class, r.get("a2").getClass()); } } finally { sqlSession.close(); } } } 下面是mybatis的配置文件: [html] view plain copy <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"> <property name="" value="" /> </transactionManager> <dataSource type="UNPOOLED"> <property name="driver" value="org.hsqldb.jdbcDriver" /> <property name="url" value="jdbc:hsqldb:mem:associationtype" /> <property name="username" value="sa" /> </dataSource> </environment> </environments> <mappers> <mapper resource="org/apache/ibatis/submitted/associationtype/Mapper.xml" /> </mappers> </configuration> 最后是测试用的表结构和数据: [sql] view plain copy drop table users if exists; create table users ( id int, name varchar(20) ); insert into users (id, name) values(1, 'User1'); 程序运行情况: 可以看到这里的结果results完全符合。 下面来分析执行过程: 首先系统调用的getUser方法,执行下面的SQL: [sql] view plain copy SELECT id as f1, name as f2 from users 执行完上面的SQL之后(在上面Mapper.xml的配置中column="{param1=f1}"并没有起到作用,如果往users表中插入多条数据,这里肯定会报错。先不说这种情况。),再通过嵌套对象的SQL来执行,最终返回上面的结果。 下面在上面测试的基础上进行修改(下面代码只显示改动过的地方): [sql] view plain copy insert into users (id, name) values(1, 'User1'); insert into users (id, name) values(2, 'User2'); insert into users (id, name) values(3, 'User3'); [html] view plain copy <resultMap id="sampleHashResult" type="hashmap"> <result property="f1" column="f1" /> <result property="f2" column="f2" /> <association property="a1" javaType="java.lang.String" column="{id=f1}" select="associationTest" /> <association property="a2" javaType="java.lang.String" column="{id=f2}" select="associationTest" /> </resultMap> <select id="getUser" resultMap="sampleHashResult"> SELECT 1 as f1, 2 as f2 from (VALUES(0)) </select> <select id="associationTest" resultType="java.lang.String"> select name from users where id = #{id} </select> 在users表中增加了两条数据。 association中一个改为id=f1,一个改为id=f2 将associationTest中的sql改为select name from users where id = #[id},将getUser改为了select 1 as f1,2 as f2 from (values(0) 注:这里的from (values(0))类似于oracle中的from dual 执行结果如下: 这里需要注意的地方: column="{id=f1}",在column中id是要执行SQL中接收到的参数,f1是原SQL中的查询结果。这里是将原SQL查询结果放到将要执行SQL中当条件,当有多个条件时,可以用逗号隔开。 上面讲的用法还是太简单,因为执行的SQL返回的是String,现在我们创建一个Users类,然后将返回结果改为POJO类。 Users类: XML: [html] view plain copy <resultMap id="sampleHashResult" type="hashmap"> <result property="f1" column="f1" /> <result property="f2" column="f2" /> <association property="a1" javaType="org.apache.ibatis.submitted.associationtype.Users" column="{id=f1}" select="associationTest" /> <association property="a2" javaType="org.apache.ibatis.submitted.associationtype.Users" column="{id=f2}" select="associationTest" /> </resultMap> <select id="associationTest" resultType="org.apache.ibatis.submitted.associationtype.Users"> select * from users where id = #{id} </select> xml中只是将原来的java.lang.String改为了Users类。 执行测试方法,返回结果如下: 是不是发现改成POJO对象也很容易,而且这里相对于关联结果查询不需要配置resultMap,方便很多。到这里,关于association的内容就讲完了,接下来会介绍其他Mybatis的有关内容。 如果有association方面问题可以参考(或在此留言): http://mybatis.github.io/mybatis-3/zh/sqlmap-xml.html 本节源码请看官方git: https://github.com/mybatis/mybatis-3/tree/master/src/test/java/org/apache/ibatis/submitted/associationtype
接下来的文章中,关于Mybatis的示例,全部来自于Mybatis代码中的单元测试代码,通过这些代码能够学习Mybatis中很有用的知识,这些内容在doc文档中可能只是简单提到了,或者有一些文字说明,通过这些单元测试能更直观的了解如何在Mybatis使用这些内容。 这一节内容为Association关联的结果查询,就是在查询出结果后,根据查询的列和resultMap定义的对应关系,来创建对象并写入值。 association – 一个复杂的类型关联;许多结果将包成这种类型 嵌入结果映射 – 结果映射自身的关联,或者参考一个 (注:“参考一个”,这里参考一个是通过对象的Key来唯一确定的,如果Key值一样,就直接用已经存在的这个对象。) association是resultMap中的一个配置选项,下面是用到的类的UML图: Car对象中包含了Engine和Brakes两个对象。Mapper是接口对象。AssociationTest是该测试对象。 SQL表结构和数据: [sql] view plain copy drop table cars if exists; create table cars ( carid integer, cartype varchar(20), enginetype varchar(20), enginecylinders integer, brakestype varchar(20) ); insert into cars (carid, cartype, enginetype, enginecylinders, brakestype) values(1, 'VW', 'Diesel', 4, null); insert into cars (carid, cartype, enginetype, enginecylinders, brakestype) values(2, 'Opel', null, null, 'drum'); insert into cars (carid, cartype, enginetype, enginecylinders, brakestype) values(3, 'Audi', 'Diesel', 4, 'disk'); insert into cars (carid, cartype, enginetype, enginecylinders, brakestype) values(4, 'Ford', 'Gas', 8, 'drum'); Mapper.xml文件: [html] view plain copy <mapper namespace="org.apache.ibatis.submitted.associationtest.Mapper"> <resultMap type="org.apache.ibatis.submitted.associationtest.Car" id="carResult"> <id column="carid" property="id"/> <result column="cartype" property="type"/> <association property="engine" resultMap="engineResult"/> <association property="brakes" resultMap="brakesResult"/> </resultMap> <resultMap type="org.apache.ibatis.submitted.associationtest.Engine" id="engineResult"> <result column="enginetype" property="type"/> <result column="enginecylinders" property="cylinders"/> </resultMap> <resultMap type="org.apache.ibatis.submitted.associationtest.Brakes" id="brakesResult"> <result column="brakesType" property="type"/> </resultMap> <select id="getCars" resultMap="carResult"> select * from cars </select> <select id="getCarsNonUnique" resultMap="carResult"> select 1 as carid, cartype, enginetype, enginecylinders, brakestype from cars </select> <select id="getCars2" resultMap="carResult"> select 1 as carid, cartype, enginetype, enginecylinders, brakestype from cars where carid in (1,2) </select> </mapper> 其中的一个测试用例: [java] view plain copy @Test public void shouldGetAllCars() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { Mapper mapper = sqlSession.getMapper(Mapper.class); List<Car> cars = mapper.getCars(); Assert.assertEquals(4, cars.size()); Assert.assertEquals("VW", cars.get(0).getType()); Assert.assertNotNull(cars.get(0).getEngine()); Assert.assertNull(cars.get(0).getBrakes()); Assert.assertEquals("Opel", cars.get(1).getType()); Assert.assertNull(cars.get(1).getEngine()); Assert.assertNotNull(cars.get(1).getBrakes()); } finally { sqlSession.close(); } } cars返回值: association是嵌套查询中最简单的一种情况,像上述例子中,一般我们都会用一个Car对面包含所有的属性,这里的例子使用了嵌套对象,使对像的结构更鲜明。不过一般情况下很少会拆分一个对象为多个,用的多的时候是多表查询的嵌套。 上面XML中的 carResult和engieResult,brakesResult都是分别定义,carResult引用了另外两个resultMap。 对于不需要重用嵌套对象的情况,还可以直接这么写,把上面的XML修改后: [html] view plain copy <resultMap type="org.apache.ibatis.submitted.associationtest.Car" id="carResult"> <id column="carid" property="id"/> <result column="cartype" property="type"/> <association property="engine" javaType="org.apache.ibatis.submitted.associationtest.Engine"> <result column="enginetype" property="type"/> <result column="enginecylinders" property="cylinders"/> </association> <association property="brakes" resultMap="brakesResult"/> </resultMap> 为了对比和区分,这里指修改了Engine,在association元素上增加了属性javaType,元素内增加了result映射。 如果有association方面问题可以参考(或在此留言): http://mybatis.github.io/mybatis-3/zh/sqlmap-xml.html 本节源码请看官方git: https://github.com/mybatis/mybatis-3/tree/master/src/test/java/org/apache/ibatis/submitted/associationtest
SelectKey在Mybatis中是为了解决Insert数据时不支持主键自动生成的问题,他可以很随意的设置生成主键的方式。 不管SelectKey有多好,尽量不要遇到这种情况吧,毕竟很麻烦。 属性 描述 keyProperty selectKey 语句结果应该被设置的目标属性。 resultType 结果的类型。MyBatis 通常可以算出来,但是写上也没有问题。MyBatis 允许任何简单类型用作主键的类型,包括字符串。 order 这可以被设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它会首先选择主键,设置 keyProperty 然后执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 元素-这和如 Oracle 数据库相似,可以在插入语句中嵌入序列调用。 statementType 和前面的相 同,MyBatis 支持 STATEMENT ,PREPARED 和CALLABLE 语句的映射类型,分别代表 PreparedStatement 和CallableStatement 类型。 SelectKey需要注意order属性,像Mysql一类支持自动增长类型的数据库中,order需要设置为after才会取到正确的值。 像Oracle这样取序列的情况,需要设置为before,否则会报错。 另外在用Spring管理事务时,SelectKey和插入在同一事务当中,因而Mysql这样的情况由于数据未插入到数据库中,所以是得不到自动增长的Key。取消事务管理就不会有问题。 下面是一个xml和注解的例子,SelectKey很简单,两个例子就够了: [html] view plain copy <insert id="insert" parameterType="map"> insert into table1 (name) values (#{name}) <selectKey resultType="java.lang.Integer" keyProperty="id"> CALL IDENTITY() </selectKey> </insert> 上面xml的传入参数是map,selectKey会将结果放到入参数map中。用POJO的情况一样,但是有一点需要注意的是,keyProperty对应的字段在POJO中必须有相应的setter方法,setter的参数类型还要一致,否则会报错。 [java] view plain copy @Insert("insert into table2 (name) values(#{name})") @SelectKey(statement="call identity()", keyProperty="nameId", before=false, resultType=int.class) int insertTable2(Name name); 上面是注解的形式。
foreach一共有三种类型,分别为List,[](array),Map三种。 foreach的第一篇用来将List和数组(array)。 下面表格是我总结的各个属性的用途和注意点。 foreach属性 属性 描述 item 循环体中的具体对象。支持属性的点路径访问,如item.age,item.info.details。 具体说明:在list和数组中是其中的对象,在map中是value。 该参数为必选。 collection 要做foreach的对象,作为入参时,List<?>对象默认用list代替作为键,数组对象有array代替作为键,Map对象没有默认的键。 当然在作为入参时可以使用@Param("keyName")来设置键,设置keyName后,list,array将会失效。 除了入参这种情况外,还有一种作为参数对象的某个字段的时候。举个例子: 如果User有属性List ids。入参是User对象,那么这个collection = "ids" 如果User有属性Ids ids;其中Ids是个对象,Ids有个属性List id;入参是User对象,那么collection = "ids.id" 上面只是举例,具体collection等于什么,就看你想对那个元素做循环。 该参数为必选。 separator 元素之间的分隔符,例如在in()的时候,separator=","会自动在元素中间用“,“隔开,避免手动输入逗号导致sql错误,如in(1,2,)这样。该参数可选。 open foreach代码的开始符号,一般是(和close=")"合用。常用在in(),values()时。该参数可选。 close foreach代码的关闭符号,一般是)和open="("合用。常用在in(),values()时。该参数可选。 index 在list和数组中,index是元素的序号,在map中,index是元素的key,该参数可选。 下面是测试。 SQL [sql] view plain copy drop table users if exists; create table users ( id int, name varchar(20) ); insert into users (id, name) values(1, 'User1'); insert into users (id, name) values(2, 'User2'); insert into users (id, name) values(3, 'User3'); insert into users (id, name) values(4, 'User4'); insert into users (id, name) values(5, 'User5'); insert into users (id, name) values(6, 'User6'); User类 Mapper.xml [html] view plain copy <select id="countByUserList" resultType="_int" parameterType="list"> select count(*) from users <where> id in <foreach item="item" collection="list" separator="," open="(" close=")" index=""> #{item.id, jdbcType=NUMERIC} </foreach> </where> </select> 测试代码: [java] view plain copy @Test public void shouldHandleComplexNullItem() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { Mapper mapper = sqlSession.getMapper(Mapper.class); User user1 = new User(); user1.setId(2); user1.setName("User2"); List<User> users = new ArrayList<User>(); users.add(user1); users.add(null); int count = mapper.countByUserList(users); Assert.assertEquals(1, count); } finally { sqlSession.close(); } } 测试日志: DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter. DEBUG [main] - Opening JDBC Connection DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4b83b34e] DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4b83b34e] DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4b83b34e] DEBUG [main] - Opening JDBC Connection DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@763d1932] DEBUG [main] - ==> Preparing: select count(*) from users WHERE id in ( ? , ? ) DEBUG [main] - ==> Parameters: 2(Integer), null DEBUG [main] - <== Total: 1 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@763d1932] DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@763d1932] 上面这个例子是List的,但是和数组的情况基本一样,所以不针对数组进行测试了。可以看到这个例子的内容是很简单的,实际上List,array,map也可以互相嵌套,可以用多个foreach去执行,如果想看这样一个例子,可以移步这里: 新人求解问题哦,被卡了两天了,悲伤.. 上面这个问题就遇到了list,map一起用的问题,3楼是问题的答案,可以参考一看。 由于map的key,value比较特殊,所以下次再说。
Mybatis极其(最)简(好)单(用)的一个分页插件 http://blog.csdn.net/isea533/article/details/23831273 这里说最好用,绝对不是吹的,不过有好多人都不理解为什么要用这个插件,自己手写分页sql不是挺好吗...... 所以我特地写这样一个例子来讲为什么最好用。 假设我们已经写好了Mapper的接口和xml,如下: [java] view plain copy public interface SysLoginLogMapper { /** * 根据查询条件查询登录日志 * @param logip * @param username * @param loginDate * @param exitDate * @return */ List<SysLoginLog> findSysLoginLog(@Param("logip") String logip, @Param("username") String username, @Param("loginDate") String loginDate, @Param("exitDate") String exitDate, @Param("logerr") String logerr); } [html] view plain copy <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.easternie.sys.dao.SysLoginLogMapper"> <select id="findSysLoginLog" resultType="com.easternie.sys.vo.model.SysLoginLog"> select * from sys_login_log a <if test="username != null and username != ''"> left join sys_user b on a.userid = b.userid </if> <where> <if test="logip!=null and logip != ''"> a.logip like '%'||#{logip}||'%' </if> <if test="username != null and username != ''"> and (b.username like '%'||#{username}||'%' or b.realname like '%'||#{username}||'%') </if> <if test="loginDate!=null and loginDate!=''"> and to_date(substr(a.logindate,0,10),'yyyy-MM-dd') = to_date(#{loginDate},'yyyy-MM-dd') </if> <if test="exitDate!=null and exitDate!=''"> and to_date(substr(a.EXITDATE,0,10),'yyyy-MM-dd') = to_date(#{exitDate},'yyyy-MM-dd') </if> <if test="logerr!=null and logerr!=''"> and a.logerr like '%'||#{logerr}||'%' </if> </where> order by logid desc </select> </mapper> 虽然是举个简单例子,但是这里的xml并没那么简单。 如果你已经有一些例如上面这些现成的Mybatis方法了,我现在想对这个查询进行分页,我该怎么办呢? 如果是手写SQL,我需要增加两个接口,一个查询count总数,一个改为分页形式的。需要在xml中,复制粘贴,然后改改语句,似乎也不是很难。你是这样做的吗? 如果使用这个插件,我需要做什么呢??? 对Mybatis已经写好的这些方法来说,我什么都不需要改。 但是Service层可能需要动一下。具体上面这个例子。看下面的Service层调用代码。 不需要分页时候的代码: [java] view plain copy public List<SysLoginLog> findSysLoginLog(String loginIp, String username, String loginDate, String exitDate, String logerr) throws BusinessException { return sysLoginLogMapper.findSysLoginLog(loginIp, username, loginDate, exitDate, logerr); } 增加分页功能之后的代码: [java] view plain copy public PageHelper.Page<SysLoginLog> findSysLoginLog(String loginIp, String username, String loginDate, String exitDate, String logerr, int pageNumber, int pageSize) throws BusinessException { PageHelper.startPage(pageNumber,pageSize); sysLoginLogMapper.findSysLoginLog(loginIp, username, loginDate, exitDate, logerr); return PageHelper.endPage(); } 相比较而言: 返回值从List<SysLoginLog>改成了PageHelper.Page<SysLoginLog> 入参增加了两个,pageNumber和pageSize 然后过程代码中,先调用了 [java] view plain copy PageHelper.startPage(pageNumber,pageSize); startPage是告诉拦截器说我要开始分页了。分页参数是这两个。 然后调用原来的Mybatis代码: [java] view plain copy sysLoginLogMapper.findSysLoginLog(loginIp, username, loginDate, exitDate, logerr); 这里没有接收返回值,会不会觉得奇怪?实际上PageHelper已经自动接收了返回值。通过下面的代码可以取出返回值: [java] view plain copy PageHelper.endPage(); 同时endPage告诉拦截器说我结束分页了,不需要你了。
极其方便的使用Mybatis单表的增删改查 项目地址:http://git.oschina.net/free/Mapper 优点? 不客气的说,使用这个通用Mapper甚至能改变你对Mybatis单表基础操作不方便的想法,使用它你能简单的使用单表的增删改查,包含动态的增删改查. 程序使用拦截器实现具体的执行Sql,完全使用原生的Mybatis进行操作. 你还在因为数据库表变动重新生成xml吗?还是要手动修改自动生成的insert|update|delete的xml呢?赶紧使用通用Mapper,表的变动只需要实体类保持一致,不用管基础的xml,你不止会拥有更多的时间陪老婆|孩子|女朋友|打DOTA,你也不用做哪些繁琐无聊的事情,感兴趣了吗?继续看如何使用吧!!相信这个通用的Mapper会让你更方便的使用Mybatis,这是一个强大的Mapper!!! 不管你信不信,这个项目的测试代码中没有一个Mapper的xml配置文件,但是却可以做到每个Mapper对应上百行xml才能完成的许多功能.没有了这些基础xml信息的干扰,你将会拥有清晰干净的Mapper.xml. 发现BUG可以提Issue,可以给我发邮件,可以加我QQ,可以进Mybatis群讨论. 作者QQ: 120807756 作者邮箱: abel533@gmail.com Mybatis工具群: 211286137 (Mybatis相关工具插件等等) 推荐使用Mybatis分页插件:PageHelper分页插件 如何使用? 下面是通用Mapper的配置方法,还会提到Spring中的配置方法.还有和PageHelper分页插件集成的配置方式. 1. 引入通用Mapper的代码 将本项目中的4个代码文件(EntityHelper,Mapper,MapperHelper,MapperInterceptor)复制到你自己的项目中. 项目依赖于JPA的注解,需要引入persistence-api-1.0.jar或者添加Maven依赖: <dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> <version>1.0</version> </dependency> 2. 配置Mapper拦截器 在mybatis-config.xml中添加如下配置: <plugins> <plugin interceptor="com.github.abel533.mapper.MapperInterceptor"> <!--================================================--> <!--可配置参数说明(一般无需修改)--> <!--================================================--> <!--UUID生成策略--> <!--配置UUID生成策略需要使用OGNL表达式--> <!--默认值32位长度:@java.util.UUID@randomUUID().toString().replace("-", "")--> <!--<property name="UUID" value="@java.util.UUID@randomUUID().toString()"/>--> <!--主键自增回写方法,默认值CALL IDENTITY(),适应于大多数数据库--> <!--<property name="IDENTITY" value="CALL IDENTITY()"/>--> <!--主键自增回写方法执行顺序,默认AFTER,可选值为(BEFORE|AFTER)--> <!--<property name="ORDER" value="AFTER"/>--> </plugin> </plugins> 可配置参数一般情况下不需要修改,直接像下面这样一行即可: <plugin interceptor="com.github.abel533.mapper.MapperInterceptor"></plugin> 附:Spring配置相关 如果你使用Spring的方式来配置该拦截器,你可以像下面这样: <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations"> <array> <value>classpath:mapper/*.xml</value> </array> </property> <property name="typeAliasesPackage" value="com.isea533.mybatis.model"/> <property name="plugins"> <array> <-- 主要看这里 --> <bean class="com.isea533.mybatis.mapperhelper.MapperInterceptor"/> </array> </property> </bean> 只需要像上面这样配置一个bean即可. 如果你同时使用了PageHelper分页插件,可以像下面这样配置: <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations"> <array> <value>classpath:mapper/*.xml</value> </array> </property> <property name="typeAliasesPackage" value="com.isea533.mybatis.model"/> <property name="plugins"> <array> <bean class="com.isea533.mybatis.pagehelper.PageHelper"> <property name="properties"> <value> dialect=hsqldb reasonable=true </value> </property> </bean> <bean class="com.isea533.mybatis.mapperhelper.MapperInterceptor"/> </array> </property> </bean> 一定要注意PageHelper和MapperInterceptor这两者的顺序不能颠倒. 如果你想配置MapperInterceptor的参数,可以像PageHelper中的properties参数这样配置 3. 继承通用的Mapper<T>,必须指定泛型<T> 例如下面的例子: public interface UserInfoMapper extends Mapper<UserInfo> { //其他必须手写的接口... } 一旦继承了Mapper<T>,继承的Mapper就拥有了以下通用的方法: //根据实体类不为null的字段进行查询,条件全部使用=号and条件 List<T> select(T record); //根据实体类不为null的字段查询总数,条件全部使用=号and条件 int selectCount(T record); //根据主键进行查询,必须保证结果唯一 //单个字段做主键时,可以直接写主键的值 //联合主键时,key可以是实体类,也可以是Map T selectByPrimaryKey(Object key); //插入一条数据 //支持Oracle序列,UUID,类似Mysql的INDENTITY自动增长(自动回写) //优先使用传入的参数值,参数值空时,才会使用序列、UUID,自动增长 int insert(T record); //插入一条数据,只插入不为null的字段,不会影响有默认值的字段 //支持Oracle序列,UUID,类似Mysql的INDENTITY自动增长(自动回写) //优先使用传入的参数值,参数值空时,才会使用序列、UUID,自动增长 int insertSelective(T record); //根据实体类中字段不为null的条件进行删除,条件全部使用=号and条件 int delete(T key); //通过主键进行删除,这里最多只会删除一条数据 //单个字段做主键时,可以直接写主键的值 //联合主键时,key可以是实体类,也可以是Map int deleteByPrimaryKey(Object key); //根据主键进行更新,这里最多只会更新一条数据 //参数为实体类 int updateByPrimaryKey(T record); //根据主键进行更新 //只会更新不是null的数据 int updateByPrimaryKeySelective(T record); 4. 泛型(实体类)<T>的类型必须符合要求 实体类按照如下规则和数据库表进行转换,注解全部是JPA中的注解: 表名默认使用类名,驼峰转下划线,如UserInfo默认对应的表名为user_info. 表名可以使用@Table(name = "tableName")进行指定,对不符合第一条默认规则的可以通过这种方式指定表名. 字段默认和@Column一样,都会作为表字段,表字段默认为Java对象的Field名字驼峰转下划线形式. 可以使用@Column(name = "fieldName")指定不符合第3条规则的字段名 使用@Transient注解可以忽略字段,添加该注解的字段不会作为表字段使用. 建议一定是有一个@Id注解作为主键的字段,可以有多个@Id注解的字段作为联合主键. 默认情况下,实体类中如果不存在包含@Id注解的字段,所有的字段都会作为主键字段进行使用(这种效率极低). 实体类可以继承使用,可以参考测试代码中的com.github.abel533.model.UserLogin2类. 由于基本类型,如int作为实体类字段时会有默认值0,而且无法消除,所以实体类中建议不要使用基本类型. 除了上面提到的这些,Mapper还提供了序列(支持Oracle)、UUID(任意数据库,字段长度32)、主键自增(类似Mysql,Hsqldb)三种方式,其中序列和UUID可以配置多个,主键自增只能配置一个。 这三种方式不能同时使用,同时存在时按照 序列>UUID>主键自增的优先级进行选择.下面是具体配置方法: 使用序列可以添加如下的注解: //可以用于数字类型,字符串类型(需数据库支持自动转型)的字段 @SequenceGenerator(name="Any",sequenceName="seq_userid") @Id private Integer id; 使用UUID时: //可以用于任意字符串类型长度超过32位的字段 @GeneratedValue(generator = "UUID") private String countryname; 使用主键自增: //不限于@Id注解的字段,但是一个实体类中只能存在一个(继承关系中也只能存在一个) @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; 5. 将继承的Mapper接口添加到Mybatis配置中 例如本项目测试中的配置: <mappers> <mapper class="com.github.abel533.mapper.CountryMapper" /> <mapper class="com.github.abel533.mapper.Country2Mapper" /> <mapper class="com.github.abel533.mapper.CountryTMapper" /> <mapper class="com.github.abel533.mapper.CountryUMapper" /> <mapper class="com.github.abel533.mapper.CountryIMapper" /> <mapper class="com.github.abel533.mapper.UserInfoMapper" /> <mapper class="com.github.abel533.mapper.UserLoginMapper" /> <mapper class="com.github.abel533.mapper.UserLogin2Mapper" /> </mappers> 附:Spring配置相关 如果你在Spring中配置Mapper接口,不需要像上面这样一个个配置,只需要有下面的这个扫描Mapper接口的这个配置即可: <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.isea533.mybatis.mapper"/> </bean> 6. 代码中使用 例如下面这个简单的例子: SqlSession sqlSession = MybatisHelper.getSqlSession(); try { //获取Mapper UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class); UserInfo userInfo = new UserInfo(); userInfo.setUsername("abel533"); userInfo.setPassword("123456"); userInfo.setUsertype("2"); userInfo.setEmail("abel533@gmail.com"); //新增一条数据 Assert.assertEquals(1, mapper.insert(userInfo)); //ID回写,不为空 Assert.assertNotNull(userInfo.getId()); //6是当前的ID Assert.assertEquals(6, (int)userInfo.getId()); //通过主键删除新增的数据 Assert.assertEquals(1,mapper.deleteByPrimaryKey(userInfo)); } finally { sqlSession.close(); } 另一个例子: SqlSession sqlSession = MybatisHelper.getSqlSession(); try { //获取Mapper CountryMapper mapper = sqlSession.getMapper(CountryMapper.class); //查询总数 Assert.assertEquals(183, mapper.selectCount(new Country())); //查询100 Country country = mapper.selectByPrimaryKey(100); //根据主键删除 Assert.assertEquals(1, mapper.deleteByPrimaryKey(100)); //查询总数 Assert.assertEquals(182, mapper.selectCount(new Country())); //插入 Assert.assertEquals(1, mapper.insert(country)); } finally { sqlSession.close(); } 附:Spring使用相关 直接在需要的地方注入Mapper继承的接口即可,和一般情况下的使用没有区别. 关于和Spring结合的例子,可以看下面的地址: https://github.com/abel533/Mybatis-Spring
</p><p>优先级 操作符 含义 关联性 用法 </p>---------------------------------------------------------------- 1 [ ] 数组下标 左 array_name[expr] . 成员选择 左 object.member ( ) 方法参数 左 method_name(expr_list) ( ) 实例构造 左 class_name(expr_list) ++ 后缀自增 左 lvalue++ -- 后缀自减 左 lvalue-- 2 ++ 前缀自增 右 ++rvalue -- 前缀自减 右 --lvalue ~ 按位取反 右 ~expr ! 逻辑非 右 !expr + 一元加 右 +expr - 一元减 右 -expr 3 ( ) 强制转换 右 (type)expr new 对象实例化 右 new type() new type(expr_list) new type[expr] 4 * 乘 左 expr * expr / 除 左 expr / expr % 求余 左 expr % expr 5 + 加 左 expr + expr - 减 左 expr - expr + 字符串连接 左 strExpr + strExpr 6 >> 有符号右移 左 expr >> distance >>> 无符号右移 左 expr >>> distance 7 < 小于 左 expr < expr <= 小于等于 左 expr <= expr > 大于 左 expr > expr >= 大于等于 左 expr >= expr instanceof 类型比较 左 ref instanceof refType == 等于 左 expr == expr != 不等于 左 expr != expr 8 & 整数按位与 左 integralExpr & integralExpr & 布尔与 左 booleanExpr & booleanExpr 9 ^ 整数按位异或 左 integralExpr ^ integralExpr ^ 布尔异或 左 booleanExpr ^ booleanExpr 10 | 整数按位或 左 integralExpr | integralExpr | 布尔或 左 booleanExpr | booleanExpr 11 && 逻辑与 左 booleanExpr && booleanExpr 12 || 逻辑或 左 booleanExpr || booleanExpr 13 ? : 条件运算 右 booleanExpr ? expr : expr 14 = 赋值 右 lvalue = expr *= 乘赋值 右 lvalue *= expr /= 除赋值 右 lvalue /= expr %= 模赋值 右 lvalue %= expr += 加赋值 右 lvalue += expr += 字符串连接赋值 右 lvalue += expr -= 减赋值 右 lvalue -= expr <<= 左移赋值 右 lvalue <<= expr >>= 有符号右移赋值 右 lvalue >>= expr >>>= 无符号右移赋值 右 lvalue >>>= expr &= 整数按位与赋值 右 lvalue &= expr &= 布尔与赋值 右 lvalue &= expr |= 整数按位或赋值 右 lvalue |= expr |= 布尔或赋值 右 lvalue |= expr ^= 整数按位异或赋值 右 lvalue ^= expr ^= 布尔异或赋值 右 lvalue ^= expr
深入了解MyBatis二级缓存 一、创建Cache的完整过程 我们从SqlSessionFactoryBuilder解析mybatis-config.xml配置文件开始: Reader reader = Resources.getResourceAsReader("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); 然后是: XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); 看parser.parse()方法: parseConfiguration(parser.evalNode("/configuration")); 看处理Mapper.xml文件的位置: mapperElement(root.evalNode("mappers")); 看处理Mapper.xml的XMLMapperBuilder: XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); 继续看parse方法: configurationElement(parser.evalNode("/mapper")); 到这里: String namespace = context.getStringAttribute("namespace"); if (namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); 从这里看到namespace就是xml中<mapper>元素的属性。然后下面是先后处理的cache-ref和cache,后面的cache会覆盖前面的cache-ref,但是如果一开始cache-ref没有找到引用的cache,他就不会被覆盖,会一直到最后处理完成为止,最后如果存在cache,反而会被cache-ref覆盖。这里是不是看着有点晕、有点乱?所以千万别同时配置这两个,实际上也很少有人会这么做。 看看MyBatis如何处理<cache/>: private void cacheElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } } 从源码可以看到MyBatis读取了那些属性,而且很容易可以到这些属性的默认值。 创建Java的cache对象方法为builderAssistant.useNewCache,我们看看这段代码: public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { typeClass = valueOrDefault(typeClass, PerpetualCache.class); evictionClass = valueOrDefault(evictionClass, LruCache.class); Cache cache = new CacheBuilder(currentNamespace) .implementation(typeClass) .addDecorator(evictionClass) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); configuration.addCache(cache); currentCache = cache; return cache; } 从调用该方法的地方,我们可以看到并没有使用返回值cache,在后面的过程中创建MappedStatement的时候使用了currentCache。 二、使用Cache过程 在系统中,使用Cache的地方在CachingExecutor中: @Override public <E> List<E> query( MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); 获取cache后,先判断是否有二级缓存。 只有通过<cache/>,<cache-ref/>或@CacheNamespace,@CacheNamespaceRef标记使用缓存的Mapper.xml或Mapper接口(同一个namespace,不能同时使用)才会有二级缓存。 if (cache != null) { 如果cache存在,那么会根据sql配置(<insert>,<select>,<update>,<delete>的flushCache属性来确定是否清空缓存。 flushCacheIfRequired(ms); 然后根据xml配置的属性useCache来判断是否使用缓存(resultHandler一般使用的默认值,很少会null)。 if (ms.isUseCache() && resultHandler == null) { 确保方法没有Out类型的参数,mybatis不支持存储过程的缓存,所以如果是存储过程,这里就会报错。 ensureNoOutParams(ms, parameterObject, boundSql); 没有问题后,就会从cache中根据key来取值: @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); 如果没有缓存,就会执行查询,并且将查询结果放到缓存中。 if (list == null) { list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } 返回结果 return list; } } 没有缓存时,直接执行查询 return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } 在上面的代码中tcm.putObject(cache, key, list);这句代码是缓存了结果。但是实际上直到sqlsession关闭,MyBatis才以序列化的形式保存到了一个Map(默认的缓存配置)中。 三、Cache使用时的注意事项 1. 只能在【只有单表操作】的表上使用缓存 不只是要保证这个表在整个系统中只有单表操作,而且和该表有关的全部操作必须全部在一个namespace下。 2. 在可以保证查询远远大于insert,update,delete操作的情况下使用缓存 这一点不需要多说,所有人都应该清楚。记住,这一点需要保证在1的前提下才可以! 四、避免使用二级缓存 可能会有很多人不理解这里,二级缓存带来的好处远远比不上他所隐藏的危害。 缓存是以namespace为单位的,不同namespace下的操作互不影响。 insert,update,delete操作会清空所在namespace下的全部缓存。 通常使用MyBatis Generator生成的代码中,都是各个表独立的,每个表都有自己的namespace。 为什么避免使用二级缓存 在符合【Cache使用时的注意事项】的要求时,并没有什么危害。 其他情况就会有很多危害了。 针对一个表的某些操作不在他独立的namespace下进行。 例如在UserMapper.xml中有大多数针对user表的操作。但是在一个XXXMapper.xml中,还有针对user单表的操作。 这会导致user在两个命名空间下的数据不一致。如果在UserMapper.xml中做了刷新缓存的操作,在XXXMapper.xml中缓存仍然有效,如果有针对user的单表查询,使用缓存的结果可能会不正确。 更危险的情况是在XXXMapper.xml做了insert,update,delete操作时,会导致UserMapper.xml中的各种操作充满未知和风险。 有关这样单表的操作可能不常见。但是你也许想到了一种常见的情况。 多表操作一定不能使用缓存 为什么不能? 首先不管多表操作写到那个namespace下,都会存在某个表不在这个namespace下的情况。 例如两个表:role和user_role,如果我想查询出某个用户的全部角色role,就一定会涉及到多表的操作。 <code class="language-xml hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">select</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">id</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"selectUserRoles"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">resultType</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"UserRoleVO"</span>></span> select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid} <span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"></<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">select</span>></span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul> 像上面这个查询,你会写到那个xml中呢?? 不管是写到RoleMapper.xml还是UserRoleMapper.xml,或者是一个独立的XxxMapper.xml中。如果使用了二级缓存,都会导致上面这个查询结果可能不正确。 如果你正好修改了这个用户的角色,上面这个查询使用缓存的时候结果就是错的。 这点应该很容易理解。 在我看来,就以MyBatis目前的缓存方式来看是无解的。多表操作根本不能缓存。 如果你让他们都使用同一个namespace(通过<cache-ref>)来避免脏数据,那就失去了缓存的意义。 看到这里,实际上就是说,二级缓存不能用。整篇文章介绍这么多也没什么用了。 五、挽救二级缓存? 想更高效率的使用二级缓存是解决不了了。 但是解决多表操作避免脏数据还是有法解决的。解决思路就是通过拦截器判断执行的sql涉及到那些表(可以用jsqlparser解析),然后把相关表的缓存自动清空。但是这种方式对缓存的使用效率是很低的。 设计这样一个插件是相当复杂的,既然我没想着去实现,就不废话了。 最后还是建议,放弃二级缓存,在业务层使用可控制的缓存代替更好。
功能很简单,这里使用了jQuery的方法,因此依赖于jQuery。 如果存在多项name相同的表单对象,会使用","英文逗号隔开。 完整代码: <pre name="code" class="javascript">//从from获取数据,转为对象 function fromToJson(form) { var result = {}; var fieldArray = $('#' + form).serializeArray(); for (var i = 0; i < fieldArray.length; i++) { var field = fieldArray[i]; if (field.name in result) { result[field.name] += ',' + field.value; } else { result[field.name] = field.value; } } return result; } 通过代码也可以看出入参是form表单的id属性值。其他情况可以自行修改。 下面是效果图: 上面是表单内容,下面是调用js方法后的结果:
Apache Lucene 5.x 集成中文分词库 IKAnalyzer 前面写过 Apache Lucene 5.x版本 示例,为了支持中文分词,我们可以使用中文分词库 IKAnalyzer。 由于IKAnalyzer使用的是4.x版本的Analyzer接口,该接口和5.x版本不兼容,因此,如果想要在5.x版本中使用IKAnalyzer,我们还需要自己来实现5.x版本的接口。 通过看源码,发现需要修改两个接口的类。 第一个是Tokenizer接口,我们写一个IKTokenizer5x: /** * 支持5.x版本的IKTokenizer * * @author liuzh */ public class IKTokenizer5x extends Tokenizer { private IKSegmenter _IKImplement; private final CharTermAttribute termAtt = (CharTermAttribute)this.addAttribute(CharTermAttribute.class); private final OffsetAttribute offsetAtt = (OffsetAttribute)this.addAttribute(OffsetAttribute.class); private final TypeAttribute typeAtt = (TypeAttribute)this.addAttribute(TypeAttribute.class); private int endPosition; public IKTokenizer5x() { this._IKImplement = new IKSegmenter(this.input, true); } public IKTokenizer5x(boolean useSmart) { this._IKImplement = new IKSegmenter(this.input, useSmart); } public IKTokenizer5x(AttributeFactory factory) { super(factory); this._IKImplement = new IKSegmenter(this.input, true); } public boolean incrementToken() throws IOException { this.clearAttributes(); Lexeme nextLexeme = this._IKImplement.next(); if(nextLexeme != null) { this.termAtt.append(nextLexeme.getLexemeText()); this.termAtt.setLength(nextLexeme.getLength()); this.offsetAtt.setOffset(nextLexeme.getBeginPosition(), nextLexeme.getEndPosition()); this.endPosition = nextLexeme.getEndPosition(); this.typeAtt.setType(nextLexeme.getLexemeTypeString()); return true; } else { return false; } } public void reset() throws IOException { super.reset(); this._IKImplement.reset(this.input); } public final void end() { int finalOffset = this.correctOffset(this.endPosition); this.offsetAtt.setOffset(finalOffset, finalOffset); } } 该类只是在IKTokenizer基础上做了简单修改,和原方法相比修改了public IKTokenizer(Reader in, boolean useSmart)这个构造方法,不在需要Reader参数。 另一个接口就是Analyzer的IKAnalyzer5x: /** * 支持5.x版本的IKAnalyzer * * @author liuzh */ public class IKAnalyzer5x extends Analyzer { private boolean useSmart; public boolean useSmart() { return this.useSmart; } public void setUseSmart(boolean useSmart) { this.useSmart = useSmart; } public IKAnalyzer5x() { this(false); } public IKAnalyzer5x(boolean useSmart) { this.useSmart = useSmart; } @Override protected TokenStreamComponents createComponents(String fieldName) { IKTokenizer5x _IKTokenizer = new IKTokenizer5x(this.useSmart); return new TokenStreamComponents(_IKTokenizer); } } 这个类的接口由 protected TokenStreamComponents createComponents(String fieldName, Reader in) 变成了 protected TokenStreamComponents createComponents(String fieldName) 方法的实现中使用了上面创建的IKTokenizer5x。 定义好上面的类后,在Lucene中使用IKAnalyzer5x即可。 针对IKAnalyzer5x我们写个简单测试: /** * IKAnalyzer5x 测试 * * @author liuzh */ public class IKAnalyzer5xTest { public static void main(String[] args) throws IOException { Analyzer analyzer = new IKAnalyzer5x(true); TokenStream ts = analyzer.tokenStream("field", new StringReader( "IK Analyzer 是一个开源的,基于java语言开发的轻量级的中文分词工具包。" + "从2006年12月推出1.0版开始, IKAnalyzer已经推出了4个大版本。" + "最初,它是以开源项目Luence为应用主体的," + "结合词典分词和文法分析算法的中文分词组件。从3.0版本开始," + "IK发展为面向Java的公用分词组件,独立于Lucene项目," + "同时提供了对Lucene的默认优化实现。在2012版本中," + "IK实现了简单的分词歧义排除算法," + "标志着IK分词器从单纯的词典分词向模拟语义分词衍化。")); OffsetAttribute offsetAtt = ts.addAttribute(OffsetAttribute.class); try { ts.reset(); while (ts.incrementToken()) { System.out.println(offsetAtt.toString()); } ts.end(); } finally { ts.close(); } } } 输出结果: ik analyzer 是 一个 开源 的 基于 java 语言 开发 的 轻量级 的 中文 分词 工具包 从 2006年 12月 推出 1.0版 由于结果较长,省略后面的输出内容。
一搭建环境 1.1 JDK 6+ 1.2 Ant 1.8.1+ 1.3 Eclipse 3.7+ 1.4 Activiti -eclipse designer插件安装 1.4.1 先安装GEF插件 1.4.2 安装SVN插件 1.4.3安装Maven插件 1.4.4 最后安装Activiti -eclipse designer 二开始activiti 5.12.1的第一个demo 2.1 建立activiti-demo工程,选择Activiti Project 2.2 将activiti-demo工程的数据库整合为MySQL 2.3 设计activiti-demo工程的流程 以上详细描述参照Activiti 简易教程一 version5.10(http://blog.csdn.net/yangyi22/article/details/9225849) 测试类代码 DemoProcessTest.java [java] view plain copy package main.java; import java.io.FileInputStream; import java.util.List; import org.activiti.engine.HistoryService; import org.activiti.engine.ProcessEngine; import org.activiti.engine.ProcessEngines; import org.activiti.engine.RepositoryService; import org.activiti.engine.RuntimeService; import org.activiti.engine.TaskService; import org.activiti.engine.history.HistoricProcessInstance; import org.activiti.engine.runtime.ProcessInstance; import org.activiti.engine.task.Task; public class DemoProcessTest { // diagrams实际路径 private static String realPath = "D:\\Java~coding~site\\J2EE-IDE\\Workspace\\workspace[indigo-jee]" + "\\activiti-512demo\\src\\main\\resources\\diagrams"; public static void main(String[] args) throws Exception { // 创建 Activiti流程引擎 //方式一 自动寻找activiti.cfg.xml ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine(); //方式二 指定加载activiti.cfg.xml // ProcessEngine processEngine = ProcessEngineConfiguration // .createProcessEngineConfigurationFromResource("activiti.cfg.xml") // .buildProcessEngine(); // 取得 Activiti 服务 RepositoryService repositoryService = processEngine.getRepositoryService(); RuntimeService runtimeService = processEngine.getRuntimeService(); // 部署流程定义 repositoryService .createDeployment() .addInputStream("DemoProcess.bpmn",new FileInputStream(realPath + "\\DemoProcess.bpmn")) .addInputStream("DemoProcess.png", new FileInputStream(realPath + "\\DemoProcess.png")) .deploy(); // 启动流程实例 ProcessInstance instance = processEngine .getRuntimeService().startProcessInstanceByKey("DemoProcess"); String procId = instance.getId(); System.out.println("procId:"+ procId); // 获得第一个任务 TaskService taskService = processEngine.getTaskService(); List<Task> tasks = taskService.createTaskQuery().taskDefinitionKey("firstTask").list(); for (Task task : tasks) { System.out.println("Following task is: taskID -" +task.getId()+" taskName -"+ task.getName()); // 认领任务 taskService.claim(task.getId(), "testUser"); } // 查看testUser 现在是否能够获取到该任务 tasks = taskService.createTaskQuery().taskAssignee("testUser").list(); for (Task task : tasks) { System.out.println("Task for testUser: " + task.getName()); // 完成任务 taskService.complete(task.getId()); } System.out.println("Number of tasks for testUser: " + taskService.createTaskQuery().taskAssignee("testUser").count()); // 获取并认领第二个任务 tasks = taskService.createTaskQuery().taskDefinitionKey("secondTask").list(); for (Task task : tasks) { System.out.println("Following task is : taskID -" +task.getId()+" taskName -"+ task.getName()); taskService.claim(task.getId(), "testUser"); } //完成第二个任务结束结束流程 for (Task task : tasks) { taskService.complete(task.getId()); } // 核实流程是否结束 HistoryService historyService = processEngine.getHistoryService(); HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(procId).singleResult(); System.out.println("Process instance end time: " + historicProcessInstance.getEndTime()); } } 依赖jar包说明 Activiti5.10以后的版本已经去掉第三方的jar包,从官网下载的Activiti5.12.1的依赖包仅仅包含activiti相关的jar,目录libs下的jar包如下: 运行测试代码,会发现少了很多依赖包,我的处理的方法是直接从5.10版本导入缺少的jar包,导入后测试代码通过。 jar包: 教程二完毕。
相信每个涉及到用户的系统都有一套用户权限管理平台或者模块,用来维护用户以及在系统内的功能、数据权限,我们使用的Activiti工作流引擎配套设计了包括User、Group的Identify模块,怎么和业务数据同步呢,这个问题是每个新人必问的问题之一,下面介绍几种同步方案,最后总结比较。 方案一:调用IdentifyService接口完成同步 参考IdentifyService接口Javadoc:http://www.activiti.org/javadocs/org/activiti/engine/IdentityService.html 接口定义: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package com.foo.arch.service.id; import java.util.List; import com.foo.arch.entity.id.User; import com.foo.arch.service.ServiceException; /** * 维护用户、角色、权限接口 * * @author HenryYan * */ public interface AccountService { /** * 添加用户并[同步其他数据库] * <ul> * <li>step 1: 保存系统用户,同时设置和部门的关系</li> * <li>step 2: 同步用户信息到activiti的identity.User,同时设置角色</li> * </ul> * * @param user 用户对象 * @param orgId 部门ID * @param roleIds 角色ID集合 * @param synToActiviti 是否同步到Activiti数据库,通过配置文件方式设置,使用属性:account.user.add.syntoactiviti * @throws OrganizationNotFoundException 关联用户和部门的时候从数据库查询不到哦啊部门对象 * @throws Exception 其他未知异常 */ public void save(User user, Long orgId, List<long> roleIds, boolean synToActiviti) throws OrganizationNotFoundException, ServiceException, Exception; /** * 删除用户 * @param userId 用户ID * @param synToActiviti 是否同步到Activiti数据库,通过配置文件方式设置,使用属性:account.user.add.syntoactiviti * @throws Exception */ public void delete(Long userId, boolean synToActiviti) throws ServiceException, Exception; /** * 同步用户、角色数据到工作流 * @throws Exception */ public void synAllUserAndRoleToActiviti() throws Exception; /** * 删除工作流引擎Activiti的用户、角色以及关系 * @throws Exception */ public void deleteAllActivitiIdentifyData() throws Exception; } </long> 同步单个接口实现片段: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 @Service @Transactional public class AccountServiceImpl implements AccountService { /** * 保存用户信息,并且同步用户信息到activiti的identity.User和identify.Group * @param user 用户对象{@link User} * @param roleIds 用户拥有的角色ID集合 * @param synToActiviti 是否同步数据到Activiti * @see Role */ public void saveUser(User user, List<long> roleIds, boolean synToActiviti) { String userId = ObjectUtils.toString(user.getId()); // 保存系统用户 accountManager.saveEntity(user); // 同步数据到Activiti Identify模块 if (synToActiviti) { UserQuery userQuery = identityService.createUserQuery(); List<org.activiti.engine.identity.user> activitiUsers = userQuery.userId(userId).list(); if (activitiUsers.size() == 1) { updateActivitiData(user, roleIds, activitiUsers.get(0)); } else if (activitiUsers.size() > 1) { String errorMsg = "发现重复用户:id=" + userId; logger.error(errorMsg); throw new RuntimeException(errorMsg); } else { newActivitiUser(user, roleIds); } } } /** * 添加工作流用户以及角色 * @param user 用户对象{@link User} * @param roleIds 用户拥有的角色ID集合 */ private void newActivitiUser(User user, List<long> roleIds) { String userId = user.getId().toString(); // 添加用户 saveActivitiUser(user); // 添加membership addMembershipToIdentify(roleIds, userId); } /** * 添加一个用户到Activiti {@link org.activiti.engine.identity.User} * @param user 用户对象, {@link User} */ private void saveActivitiUser(User user) { String userId = user.getId().toString(); org.activiti.engine.identity.User activitiUser = identityService.newUser(userId); cloneAndSaveActivitiUser(user, activitiUser); logger.debug("add activiti user: {}", ToStringBuilder.reflectionToString(activitiUser)); } /** * 添加Activiti Identify的用户于组关系 * @param roleIds 角色ID集合 * @param userId 用户ID */ private void addMembershipToIdentify(List<long> roleIds, String userId) { for (Long roleId : roleIds) { Role role = roleManager.getEntity(roleId); logger.debug("add role to activit: {}", role); identityService.createMembership(userId, role.getEnName()); } } /** * 更新工作流用户以及角色 * @param user 用户对象{@link User} * @param roleIds 用户拥有的角色ID集合 * @param activitiUser Activiti引擎的用户对象,{@link org.activiti.engine.identity.User} */ private void updateActivitiData(User user, List<long> roleIds, org.activiti.engine.identity.User activitiUser) { String userId = user.getId().toString(); // 更新用户主体信息 cloneAndSaveActivitiUser(user, activitiUser); // 删除用户的membership List<group> activitiGroups = identityService.createGroupQuery().groupMember(userId).list(); for (Group group : activitiGroups) { logger.debug("delete group from activit: {}", ToStringBuilder.reflectionToString(group)); identityService.deleteMembership(userId, group.getId()); } // 添加membership addMembershipToIdentify(roleIds, userId); } /** * 使用系统用户对象属性设置到Activiti User对象中 * @param user 系统用户对象 * @param activitiUser Activiti User */ private void cloneAndSaveActivitiUser(User user, org.activiti.engine.identity.User activitiUser) { activitiUser.setFirstName(user.getName()); activitiUser.setLastName(StringUtils.EMPTY); activitiUser.setPassword(StringUtils.EMPTY); activitiUser.setEmail(user.getEmail()); identityService.saveUser(activitiUser); } @Override public void delete(Long userId, boolean synToActiviti, boolean synToChecking) throws ServiceException, Exception { // 查询需要删除的用户对象 User user = accountManager.getEntity(userId); if (user == null) { throw new ServiceException("删除用户时,找不到ID为" + userId + "的用户"); } /** * 同步删除Activiti User Group */ if (synToActiviti) { // 同步删除Activiti User List<role> roleList = user.getRoleList(); for (Role role : roleList) { identityService.deleteMembership(userId.toString(), role.getEnName()); } // 同步删除Activiti User identityService.deleteUser(userId.toString()); } // 删除本系统用户 accountManager.deleteUser(userId); // 删除考勤机用户 if (synToChecking) { checkingAccountManager.deleteEntity(userId); } } } </role></group></long></long></long></org.activiti.engine.identity.user></long> 同步全部数据接口实现片段: 同步全部数据步骤: 删除Activiti的User、Group、Membership数据 复制Role对象数据到Group 复制用户数据以及Membership数据 ActivitiIdentifyCommonDao.java ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class ActivitiIdentifyCommonDao { protected Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private JdbcTemplate jdbcTemplate; /** * 删除用户和组的关系 */ public void deleteAllUser() { String sql = "delete from ACT_ID_USER"; jdbcTemplate.execute(sql); logger.debug("deleted from activiti user."); } /** * 删除用户和组的关系 */ public void deleteAllRole() { String sql = "delete from ACT_ID_GROUP"; jdbcTemplate.execute(sql); logger.debug("deleted from activiti group."); } /** * 删除用户和组的关系 */ public void deleteAllMemerShip() { String sql = "delete from ACT_ID_MEMBERSHIP"; jdbcTemplate.execute(sql); logger.debug("deleted from activiti membership."); } } ActivitiIdentifyService.java ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class ActivitiIdentifyService extends AbstractBaseService { @Autowired protected ActivitiIdentifyCommonDao activitiIdentifyCommonDao; /** * 删除用户和组的关系 */ public void deleteAllUser() { activitiIdentifyCommonDao.deleteAllUser(); } /** * 删除用户和组的关系 */ public void deleteAllRole() { activitiIdentifyCommonDao.deleteAllRole(); } /** * 删除用户和组的关系 */ public void deleteAllMemerShip() { activitiIdentifyCommonDao.deleteAllMemerShip(); } } AccountServiceImpl.java ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class AccountServiceImpl implements AccountService { @Override public void synAllUserAndRoleToActiviti() throws Exception { // 清空工作流用户、角色以及关系 deleteAllActivitiIdentifyData(); // 复制角色数据 synRoleToActiviti(); // 复制用户以及关系数据 synUserWithRoleToActiviti(); } /** * 复制用户以及关系数据 */ private void synUserWithRoleToActiviti() { List<user> allUser = accountManager.getAll(); for (User user : allUser) { String userId = user.getId().toString(); // 添加一个用户到Activiti saveActivitiUser(user); // 角色和用户的关系 List<role> roleList = user.getRoleList(); for (Role role : roleList) { identityService.createMembership(userId, role.getEnName()); logger.debug("add membership {user: {}, role: {}}", userId, role.getEnName()); } } } /** * 同步所有角色数据到{@link Group} */ private void synRoleToActiviti() { List<role> allRole = roleManager.getAll(); for (Role role : allRole) { String groupId = role.getEnName().toString(); Group group = identityService.newGroup(groupId); group.setName(role.getName()); group.setType(role.getType()); identityService.saveGroup(group); } } @Override public void deleteAllActivitiIdentifyData() throws Exception { activitiIdentifyService.deleteAllMemerShip(); activitiIdentifyService.deleteAllRole(); activitiIdentifyService.deleteAllUser(); } } </role></role></user> 方案二:自定义SessionFactory 引擎内部与数据库交互使用的是MyBatis,对不同的表的CRUD操作均由一个对应的实体管理器(XxxEntityManager,有接口和实现类),引擎的7个Service接口在需要CRUD实体时会根据接口获取注册的实体管理器实现类(初始化引擎时用Map对象维护两者的映射关系),并且引擎允许我们覆盖内部的实体管理器,查看源码后可以知道有关Identity操作的两个接口分别为:UserIdentityManager和GroupIdentityManager。 查看引擎配置对象ProcessEngineConfigurationImpl类可以找到一个名称为“customSessionFactories”的属性,该属性可以用来自定义SessionFactory(每一个XXxManager类都是一个Session<实现Session接口>,由SessionFactory来管理),为了能替代内部的实体管理器的实现我们可以自定义一个SessionFactory并注册到引擎。 这种自定义SessionFactory的方式适用于公司内部有独立的身份系统或者公共的身份模块的情况,所有和用户、角色、权限的服务均通过一个统一的接口获取,而业务系统则不保存这些数据,此时引擎的身份模块表就没必要存在(ACT_ID_*)。 下面是有关customSessionFactories的示例配置。 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> ... <property name="customSessionFactories"> <list> <bean class="me.kafeitu.activiti.xxx.CustomUserEntityManagerFactory"> <property name="customUserEntityManager"> <bean class="me.kafeitu.activiti.xxx.CustomUserEntityManager"> <property name="CustomUserManager" ref="CustomUserManager"> </property></bean> </property> </bean> <bean class="me.kafeitu.activiti.xxx.CustomGroupEntityManagerFactory"> <property name="customGroupEntityManager"> <bean class="me.kafeitu.activiti.xxx.CustomGroupEntityManager"> <property name="customRoleManager" ref="customRoleManager"> </property></bean> </property> </bean> </list> </property> ... </bean> <bean id="customUserManager" class="me.kafeitu.activiti.xxx.impl.AiaUserManagerImpl"> <property name="dao"> <bean class="me.kafeitu.activiti.xxx.impl.CustomUserDaoImpl"></bean> </property> <property name="identityService" ref="identityService"> </property></bean> <bean id="customRoleManager" class="me.kafeitu.activiti.chapter19.identity.impl.AiaRoleManagerImpl"> <property name="dao"> <bean class="me.kafeitu.activiti.xxx.impl.CustomRoleDaoImpl"></bean> </property> </bean> 以用户操作为例介绍一下如何自定义一个SessionFactory。 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class CustomUserEntityManagerFactory implements SessionFactory { private CustomUserEntityManager customUserEntityManager; public void setCustomUserEntityManager(CustomUserEntityManager customUserEntityManager) { this.customUserEntityManager = customUserEntityManager; } @Override public Class<!--?--> getSessionType() { // 返回引擎的实体管理器接口 return UserIdentityManager.class; } @Override public Session openSession() { return customUserEntityManager; } } ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class CustomUserEntityManager extends UserEntityManager { // 这个Bean就是公司提供的统一身份访问接口,可以覆盖UserEntityManager的任何方法用公司内部的统一接口提供服务 private CustomUserManager customUserManager; @Override public Boolean checkPassword(String userId, String password) { CustomUser customUser = customUserManager.get(new Long(userId)); return CustomUser.getPassword().equals(password); } public void setCustomUserManager(CustomUserManager customUserManager) { this.customUserManager = customUserManager; } } 方案三:用视图覆盖同名的ACT_ID_系列表 此方案和第二种类似,放弃使用系列表:ACT_ID_,创建同名的视图。 1.删除已创建的ACT_ID_*表 创建视图必须删除引擎自动创建的ACT_ID_*表,否则不能创建视图。 2.创建视图: ACT_ID_GROUP ACT_ID_INFO ACT_ID_MEMBERSHIP ACT_ID_USER 创建的视图要保证数据类型一致,例如用户的ACT_ID_MEMBERSHIP表的两个字段都是字符型,一般系统中都是用NUMBER作为用户、角色的主键类型,所以创建视图的时候要把数字类型转换为字符型。 3.修改引擎默认配置 在引擎配置中设置属性dbIdentityUsed为false即可。 ? 1 2 3 4 5 <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> ... <property name="dbIdentityUsed" value="false"> ... </property></bean> 总结 方案一:通过数据推送方式同步数据到引擎的身份表,需要把数据备份到引擎的身份表或者公司有平台或者WebService推送用户数据的推荐使用 方案二:自定义SessionFactory,非侵入式替换接口实现,对于公司内部有统一身份访问接口的推荐使用 方案三:不需要编写Java代码,只需要创建同名视图即可
今天闲来无事,在微博上看到一个关于用java实现的一个发送手机短信的程序,看了看,写的不太相信,闲的没事,把他整理下来,以后可能用得着 JAVA发送手机短信,流传有几种方法:(1)使用webservice接口发送手机短信,这个可以使用sina提供的webservice进行发送,但是需要进行注册;(2)使用短信mao的方式进行短信的发送,这种方式应该是比较的常用,前提是需要购买硬件设备,呵呵(3)使用中国网建提供的SMS短信平台(申请账号地址:http://sms.webchinese.cn/default.shtml) 本程序主要是运用了中国网建提供的SMS短信平台,这个短信平台基于java提供个专门的接口,话不多说。,上代码,有代码有真相,呵呵 [java] view plain copy print? package com.text; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.httpclient.methods.PostMethod; public class SendMsg_webchinese { public static void main(String[] args) throws Exception { HttpClient client = new HttpClient(); PostMethod post = new PostMethod("http://sms.webchinese.cn/web_api/"); post.addRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=gbk");// 在头文件中设置转码 NameValuePair[] data = { new NameValuePair("Uid", "cshxxxxxxxx"), // 注册的用户名 new NameValuePair("Key", "53295058d1c46710666a"), // 注册成功后,登录网站使用的密钥 new NameValuePair("smsMob", "187xxxxxxx"), // 手机号码 new NameValuePair("smsText", "以后给我老实点哈。。。。听话。。。") };//设置短信内容 [java] view plain copy print? post.setRequestBody(data); client.executeMethod(post); Header[] headers = post.getResponseHeaders(); int statusCode = post.getStatusCode(); System.out.println("statusCode:" + statusCode); for (Header h : headers) { System.out.println(h.toString()); } String result = new String(post.getResponseBodyAsString().getBytes( "gbk")); System.out.println(result); post.releaseConnection(); } 运行本程序首先的代入三个jar包: commons-codec-1.4 commons-httpclient-3.1 commons-logging-1.1.1 请自行下载,呵呵 GBK编码发送接口地址: http://gbk.sms.webchinese.cn/?Uid=本站用户名&Key=接口安全密码&smsMob=手机号码&smsText=短信内容 UTF-8编码发送接口地址: http://utf8.sms.webchinese.cn/?Uid=本站用户名&Key=接口安全密码&smsMob=手机号码&smsText=短信内容获取短信数量接口地址(UTF8): http://sms.webchinese.cn/web_api/SMS/?Action=SMS_Num&Uid=本站用户名&Key=接口安全密码获取短信数量接口地址(GBK):http://sms.webchinese.cn/web_api/SMS/GBK/?Action=SMS_Num&Uid=本站用户名&Key=接口安全密码 短信发送后返回值 说 明 -1 没有该用户账户 -2 密钥不正确(不是用户密码) -3 短信数量不足 -11 该用户被禁用 -14 短信内容出现非法字符 -41 手机号码为空 -42 短信内容为空 大于0 短信发送数量 注:上面的用户名和密码是我原先申请的,不知道为什么被停用了,在运行本程序之前请先到SMS短信平台去申请一个用户名和密码。 附: 1. ASP 调用例子<% '常用函数 '输入url目标网页地址,返回值getHTTPPage是目标网页的html代码 function getHTTPPage(url) dim Http set Http=server.createobject("MSXML2.XMLHTTP") Http.open "GET",url,false Http.send() if Http.readystate<>4 then exit function end if getHTTPPage=bytesToBSTR(Http.responseBody,"GB2312") set http=nothing if err.number<>0 then err.Clear end function Function BytesToBstr(body,Cset) dim objstream set objstream = Server.CreateObject("adodb.stream") objstream.Type = 1 objstream.Mode =3 objstream.Open objstream.Write body objstream.Position = 0 objstream.Type = 2 objstream.Charset = Cset BytesToBstr = objstream.ReadText objstream.Close set objstream = nothing End Function '自已组合一下提交的URL加入自己的账号和密码 sms_url="http://sms.webchinese.cn/web_api/?Uid=账号&Key=接口密钥&smsMob=手机号码&smsText=短信内容" response.write getHTTPPage(sms_url) %> 2.C# 调用 //需要用到的命名空间 using System.Net; using System.IO; using System.Text; //调用时只需要把拼成的URL传给该函数即可。判断返回值即可 public string GetHtmlFromUrl(string url) { string strRet = null; if(url==null || url.Trim().ToString()=="") { return strRet; } string targeturl = url.Trim().ToString(); try { HttpWebRequest hr = (HttpWebRequest)WebRequest.Create(targeturl); hr.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"; hr.Method = "GET"; hr.Timeout = 30 * 60 * 1000; WebResponse hs = hr.GetResponse(); Stream sr = hs.GetResponseStream(); StreamReader ser = new StreamReader(sr, Encoding.Default); strRet = ser.ReadToEnd(); } catch (Exception ex) { strRet = null; } return strRet; } 3.JAVA调用 import java.io.UnsupportedEncodingException; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.httpclient.methods.PostMethod; public class SendMsg_webchinese { public static void main(String[] args)throws Exception { HttpClient client = new HttpClient(); PostMethod post = new PostMethod("http://gbk.sms.webchinese.cn"); post.addRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=gbk");//在头文件中设置转码 NameValuePair[] data ={ new NameValuePair("Uid", "本站用户名"),new NameValuePair("Key", "接口安全密码"),new NameValuePair("smsMob","手机号码"),new NameValuePair("smsText","短信内容")}; post.setRequestBody(data); client.executeMethod(post); Header[] headers = post.getResponseHeaders(); int statusCode = post.getStatusCode(); System.out.println("statusCode:"+statusCode); for(Header h : headers) { System.out.println(h.toString()); } String result = new String(post.getResponseBodyAsString().getBytes("gbk")); System.out.println(result); post.releaseConnection(); } } jar包下载commons-logging-1.1.1.jarcommons-httpclient-3.1.jarcommons-codec-1.4.jar 4.PHP $url='http://sms.webchinese.cn/web_api/?Uid=账号&Key=接口密钥&smsMob=手机号码&smsText=短信内容'; echo Get($url); function Get($url) { if(function_exists('file_get_contents')) { $file_contents = file_get_contents($url); } else { $ch = curl_init(); $timeout = 5; curl_setopt ($ch, CURLOPT_URL, $url); curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt ($ch, CURLOPT_CONNECTTIMEOUT, $timeout); $file_contents = curl_exec($ch); curl_close($ch); } return $file_contents; } 5.VB.NET '调用发送短信,NoList接收号码.多个之间用,分开,Memo内容70字 Public Function SendSMS(ByVal NoList As String, ByVal Memo As String) As String Dim Url As String = "http://sms.webchinese.cn/web_api/?Uid=账号&Key=接口密钥&smsMob=手机号码&smsText=短信内容" Dim webClient As New Net.WebClient() Try 'Dim responseData As Byte() = Dim srcString As String = webClient.DownloadString(Url) Return srcString Catch Return "-444" End Try End Function
实现了 JavaMail 中邮件内容的创建、邮件的发送,现在就看看怎样接收邮件了。 邮件的接收与邮件的发送的基本操作步骤很类似,邮件的发送需要用到 Transport 类,邮件的接收则需要使用 Store 类,而不管是发送还是接收, Session 类和 Properties 类都是必需的。使用 Properties 对象设置连接 SMTP 服务器、 POP3 服务器的主机名、协议等,通过 Properties 对象获取应用于整个邮件程序所必须的 Session 对象,它保存了建立网络连接的会话信息,保持了邮件程序与服务器通信的环境信息。 不同的是: 邮件的接收中还要用到 Folder 类,它表示邮件夹,这是邮件的接收比邮件的发送多出来的一个类。现在有些邮箱时支持把邮件分开放在各个用户命名的邮件夹中,邮件夹里面就有好多邮件了。 各个类的操作流程: 1、创建一个 Properties 对象,该类在 java.util 包中,以键-值对的形式设置邮件接收中需要用到的传输协议,如 POP3 协议,此外还可以设置想要连接的 POP3 服务器的主机名; 2、先使用 Session 类中静态的 getInstance() 或getDefaultInstance() 获得自身对象,此时调用这两个方法时应该传入上面创建的 Properties 对象; 3、再用 Session 对象调用 getStore() 方法获得 Store 抽象类的具体实现子类对象,如 POP3Store 类,不过这不用我们关心,Session 对象会根据 Properties 对象中已经设置好的连接协议进行创建并返回; 4、使用得到的 Store 对象通过 Store.getFolder() 方法获得邮箱中的邮件夹 Folder 对象,它包含了邮箱中的所有邮件,因此使用 Folder 对象的 Folder.getMessages() 方法则可以返回邮件夹中的所有邮件 Message 对象了 5、获得了 Message 对象之后,怎么处理里面的内容则是邮件解析的工作了,不过在这里我们还是可以打印出邮件内的原始内容。 程序要求: 获得邮箱中的所有邮件,打印出邮件的发件人地址、主题,并由用户选择是否打开邮件(目前的程序只能打开邮件的原始内容)。我们由于测试的邮箱为 testhao@126.com ,用户名为 testhao ,密码为 123456 ,需要连接的 POP3 服务器为 pop3.126.com 。我们先用其他邮箱向该邮箱发送一封简单的纯文本邮件,如下图: 实现代码: import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.Properties; import javax.mail.Folder; import javax.mail.Message; import javax.mail.Session; import javax.mail.Store; /** * 简单的邮件接收程序,打印出邮件的原始内容 * @author haolloyin */ public class SimpleStoreMails { public static void main(String[] args) throws Exception { // 连接pop3服务器的主机名、协议、用户名、密码 String pop3Server = "pop3.126.com"; String protocol = "pop3"; String user = "testhao"; String pwd = "123456"; // 创建一个有具体连接信息的Properties对象 Properties props = new Properties(); props.setProperty("mail.store.protocol", protocol); props.setProperty("mail.pop3.host", pop3Server); // 使用Properties对象获得Session对象 Session session = Session.getInstance(props); session.setDebug(true); // 利用Session对象获得Store对象,并连接pop3服务器 Store store = session.getStore(); store.connect(pop3Server, user, pwd); // 获得邮箱内的邮件夹Folder对象,以"只读"打开 Folder folder = store.getFolder("inbox"); folder.open(Folder.READ_ONLY); // 获得邮件夹Folder内的所有邮件Message对象 Message [] messages = folder.getMessages(); int mailCounts = messages.length; for(int i = 0; i < mailCounts; i++) { String subject = messages[i].getSubject(); String from = (messages[i].getFrom()[0]).toString(); System.out.println("第 " + (i+1) + "封邮件的主题:" + subject); System.out.println("第 " + (i+1) + "封邮件的发件人地址:" + from); System.out.println("是否打开该邮件(yes/no)?:"); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String input = br.readLine(); if("yes".equalsIgnoreCase(input)) { // 直接输出到控制台中 messages[i].writeTo(System.out); } } folder.close(false); store.close(); } } 测试结果: 1、用户输入是否打开邮件 2、邮件的原始内容,由于并未解析邮件,所以是一些被编码过的内容,需要解析才能读懂。 至此,简单的邮件接收程序就完成了,这与之前用 Windows 下的 telnet 程序在控制台敲入一条条命令来接收邮件相比,简单得多了。
java 代码 import java.io.*; import java.text.*; import java.util.*; import javax.mail.*; import javax.mail.internet.*; /** * 有一封邮件就需要建立一个ReciveMail对象 */ public class ReciveOneMail { private MimeMessage mimeMessage = null; private String saveAttachPath = ""; //附件下载后的存放目录 private StringBuffer bodytext = new StringBuffer();//存放邮件内容 private String dateformat = "yy-MM-dd HH:mm"; //默认的日前显示格式 public ReciveOneMail(MimeMessage mimeMessage) { this.mimeMessage = mimeMessage; } public void setMimeMessage(MimeMessage mimeMessage) { this.mimeMessage = mimeMessage; } /** * 获得发件人的地址和姓名 */ public String getFrom() throws Exception { InternetAddress address[] = (InternetAddress[]) mimeMessage.getFrom(); String from = address[0].getAddress(); if (from == null) from = ""; String personal = address[0].getPersonal(); if (personal == null) personal = ""; String fromaddr = personal + "<" + from + ">"; return fromaddr; } /** * 获得邮件的收件人,抄送,和密送的地址和姓名,根据所传递的参数的不同 "to"----收件人 "cc"---抄送人地址 "bcc"---密送人地址 */ public String getMailAddress(String type) throws Exception { String mailaddr = ""; String addtype = type.toUpperCase(); InternetAddress[] address = null; if (addtype.equals("TO") || addtype.equals("CC")|| addtype.equals("BCC")) { if (addtype.equals("TO")) { address = (InternetAddress[]) mimeMessage.getRecipients(Message.RecipientType.TO); } else if (addtype.equals("CC")) { address = (InternetAddress[]) mimeMessage.getRecipients(Message.RecipientType.CC); } else { address = (InternetAddress[]) mimeMessage.getRecipients(Message.RecipientType.BCC); } if (address != null) { for (int i = 0; i < address.length; i++) { String email = address[i].getAddress(); if (email == null) email = ""; else { email = MimeUtility.decodeText(email); } String personal = address[i].getPersonal(); if (personal == null) personal = ""; else { personal = MimeUtility.decodeText(personal); } String compositeto = personal + "<" + email + ">"; mailaddr += "," + compositeto; } mailaddr = mailaddr.substring(1); } } else { throw new Exception("Error emailaddr type!"); } return mailaddr; } /** * 获得邮件主题 */ public String getSubject() throws MessagingException { String subject = ""; try { subject = MimeUtility.decodeText(mimeMessage.getSubject()); if (subject == null) subject = ""; } catch (Exception exce) {} return subject; } /** * 获得邮件发送日期 */ public String getSentDate() throws Exception { Date sentdate = mimeMessage.getSentDate(); SimpleDateFormat format = new SimpleDateFormat(dateformat); return format.format(sentdate); } /** * 获得邮件正文内容 */ public String getBodyText() { return bodytext.toString(); } /** * 解析邮件,把得到的邮件内容保存到一个StringBuffer对象中,解析邮件 主要是根据MimeType类型的不同执行不同的操作,一步一步的解析 */ public void getMailContent(Part part) throws Exception { String contenttype = part.getContentType(); int nameindex = contenttype.indexOf("name"); boolean conname = false; if (nameindex != -1) conname = true; System.out.println("CONTENTTYPE: " + contenttype); if (part.isMimeType("text/plain") && !conname) { bodytext.append((String) part.getContent()); } else if (part.isMimeType("text/html") && !conname) { bodytext.append((String) part.getContent()); } else if (part.isMimeType("multipart/*")) { Multipart multipart = (Multipart) part.getContent(); int counts = multipart.getCount(); for (int i = 0; i < counts; i++) { getMailContent(multipart.getBodyPart(i)); } } else if (part.isMimeType("message/rfc822")) { getMailContent((Part) part.getContent()); } else {} } /** * 判断此邮件是否需要回执,如果需要回执返回"true",否则返回"false" */ public boolean getReplySign() throws MessagingException { boolean replysign = false; String needreply[] = mimeMessage .getHeader("Disposition-Notification-To"); if (needreply != null) { replysign = true; } return replysign; } /** * 获得此邮件的Message-ID */ public String getMessageId() throws MessagingException { return mimeMessage.getMessageID(); } /** * 【判断此邮件是否已读,如果未读返回返回false,反之返回true】 */ public boolean isNew() throws MessagingException { boolean isnew = false; Flags flags = ((Message) mimeMessage).getFlags(); Flags.Flag[] flag = flags.getSystemFlags(); System.out.println("flags's length: " + flag.length); for (int i = 0; i < flag.length; i++) { if (flag[i] == Flags.Flag.SEEN) { isnew = true; System.out.println("seen Message......."); break; } } return isnew; } /** * 判断此邮件是否包含附件 */ public boolean isContainAttach(Part part) throws Exception { boolean attachflag = false; String contentType = part.getContentType(); if (part.isMimeType("multipart/*")) { Multipart mp = (Multipart) part.getContent(); for (int i = 0; i < mp.getCount(); i++) { BodyPart mpart = mp.getBodyPart(i); String disposition = mpart.getDisposition(); if ((disposition != null) && ((disposition.equals(Part.ATTACHMENT)) || (disposition .equals(Part.INLINE)))) attachflag = true; else if (mpart.isMimeType("multipart/*")) { attachflag = isContainAttach((Part) mpart); } else { String contype = mpart.getContentType(); if (contype.toLowerCase().indexOf("application") != -1) attachflag = true; if (contype.toLowerCase().indexOf("name") != -1) attachflag = true; } } } else if (part.isMimeType("message/rfc822")) { attachflag = isContainAttach((Part) part.getContent()); } return attachflag; } /** * 【保存附件】 */ public void saveAttachMent(Part part) throws Exception { String fileName = ""; if (part.isMimeType("multipart/*")) { Multipart mp = (Multipart) part.getContent(); for (int i = 0; i < mp.getCount(); i++) { BodyPart mpart = mp.getBodyPart(i); String disposition = mpart.getDisposition(); if ((disposition != null) && ((disposition.equals(Part.ATTACHMENT)) || (disposition .equals(Part.INLINE)))) { fileName = mpart.getFileName(); if (fileName.toLowerCase().indexOf("gb2312") != -1) { fileName = MimeUtility.decodeText(fileName); } saveFile(fileName, mpart.getInputStream()); } else if (mpart.isMimeType("multipart/*")) { saveAttachMent(mpart); } else { fileName = mpart.getFileName(); if ((fileName != null) && (fileName.toLowerCase().indexOf("GB2312") != -1)) { fileName = MimeUtility.decodeText(fileName); saveFile(fileName, mpart.getInputStream()); } } } } else if (part.isMimeType("message/rfc822")) { saveAttachMent((Part) part.getContent()); } } /** * 【设置附件存放路径】 */ public void setAttachPath(String attachpath) { this.saveAttachPath = attachpath; } /** * 【设置日期显示格式】 */ public void setDateFormat(String format) throws Exception { this.dateformat = format; } /** * 【获得附件存放路径】 */ public String getAttachPath() { return saveAttachPath; } /** * 【真正的保存附件到指定目录里】 */ private void saveFile(String fileName, InputStream in) throws Exception { String osName = System.getProperty("os.name"); String storedir = getAttachPath(); String separator = ""; if (osName == null) osName = ""; if (osName.toLowerCase().indexOf("win") != -1) { separator = "\\"; if (storedir == null || storedir.equals("")) storedir = "c:\\tmp"; } else { separator = "/"; storedir = "/tmp"; } File storefile = new File(storedir + separator + fileName); System.out.println("storefile's path: " + storefile.toString()); // for(int i=0;storefile.exists();i++){ // storefile = new File(storedir+separator+fileName+i); // } BufferedOutputStream bos = null; BufferedInputStream bis = null; try { bos = new BufferedOutputStream(new FileOutputStream(storefile)); bis = new BufferedInputStream(in); int c; while ((c = bis.read()) != -1) { bos.write(c); bos.flush(); } } catch (Exception exception) { exception.printStackTrace(); throw new Exception("文件保存失败!"); } finally { bos.close(); bis.close(); } } /** * PraseMimeMessage类测试 */ public static void main(String args[]) throws Exception { Properties props = System.getProperties(); props.put("mail.smtp.host", "smtp.163.com"); props.put("mail.smtp.auth", "true"); Session session = Session.getDefaultInstance(props, null); URLName urln = new URLName("pop3", "pop3.163.com", 110, null, "xiangzhengyan", "pass"); Store store = session.getStore(urln); store.connect(); Folder folder = store.getFolder("INBOX"); folder.open(Folder.READ_ONLY); Message message[] = folder.getMessages(); System.out.println("Messages's length: " + message.length); ReciveOneMail pmm = null; for (int i = 0; i < message.length; i++) { System.out.println("======================"); pmm = new ReciveOneMail((MimeMessage) message[i]); System.out.println("Message " + i + " subject: " + pmm.getSubject()); System.out.println("Message " + i + " sentdate: "+ pmm.getSentDate()); System.out.println("Message " + i + " replysign: "+ pmm.getReplySign()); System.out.println("Message " + i + " hasRead: " + pmm.isNew()); System.out.println("Message " + i + " containAttachment: "+ pmm.isContainAttach((Part) message[i])); System.out.println("Message " + i + " form: " + pmm.getFrom()); System.out.println("Message " + i + " to: "+ pmm.getMailAddress("to")); System.out.println("Message " + i + " cc: "+ pmm.getMailAddress("cc")); System.out.println("Message " + i + " bcc: "+ pmm.getMailAddress("bcc")); pmm.setDateFormat("yy年MM月dd日 HH:mm"); System.out.println("Message " + i + " sentdate: "+ pmm.getSentDate()); System.out.println("Message " + i + " Message-ID: "+ pmm.getMessageId()); // 获得邮件内容=============== pmm.getMailContent((Part) message[i]); System.out.println("Message " + i + " bodycontent: \r\n" + pmm.getBodyText()); pmm.setAttachPath("c:\\"); pmm.saveAttachMent((Part) message[i]); } } }
A 概念 最常用的 3 个概念 sequence 序列,对应java 里的list 、数组等非键值对的集合 hash 键值对的集合 namespace 对一个ftl 文件的引用, 利用这个名字可以访问到该ftl 文件的资源 B 指令 if, else, elseif 语法 Java代码 <#if condition> ... <#elseif condition2> ... <#elseif condition3> ... ... <#else> ... </#if> <#if condition> ... <#elseif condition2> ... <#elseif condition3> ... ... <#else> ... </#if> 用例 Freemarker代码 <#if x = 1> x is 1 </#if> <#if x = 1> x is 1 <#else> x is not 1 </#if> <#if x = 1> x is 1 </#if> <#if x = 1> x is 1 <#else> x is not 1 </#if> switch, case, default, break 语法 Freemarker代码 <#switch value> <#case refValue1> ... <#break> <#case refValue2> ... <#break> ... <#case refValueN> ... <#break> <#default> ... </#switch> <#switch value> <#case refValue1> ... <#break> <#case refValue2> ... <#break> ... <#case refValueN> ... <#break> <#default> ... </#switch> 用例 字符串 Freemarker代码 <#switch being.size> <#case "small"> This will be processed if it is small <#break> <#case "medium"> This will be processed if it is medium <#break> <#case "large"> This will be processed if it is large <#break> <#default> This will be processed if it is neither </#switch> <#switch being.size> <#case "small"> This will be processed if it is small <#break> <#case "medium"> This will be processed if it is medium <#break> <#case "large"> This will be processed if it is large <#break> <#default> This will be processed if it is neither </#switch> 数字 Freemarker代码 <#switch x> <#case x = 1> 1 <#case x = 2> 2 <#default> d </#switch> <#switch x> <#case x = 1> 1 <#case x = 2> 2 <#default> d </#switch> 如果x=1 输出 1 2, x=2 输出 2, x=3 输出d list, break 语法 Freemarker代码 <#list sequence as item> ... <#if item = "spring"><#break></#if> ... </#list> <#list sequence as item> ... <#if item = "spring"><#break></#if> ... </#list> 关键字 item_index:是list当前值的下标 item_has_next:判断list是否还有值 用例 Freemarker代码 <#assign seq = ["winter", "spring", "summer", "autumn"]> <#list seq as x> ${x_index + 1}. ${x}<#if x_has_next>,</#if> </#list> <#assign seq = ["winter", "spring", "summer", "autumn"]> <#list seq as x> ${x_index + 1}. ${x}<#if x_has_next>,</#if> </#list> 输出: 1.winter, 2.spring, 3.summer, 4.autumn include 语法 Freemarker代码 <#include filename> <#include filename> 或则 Java代码 <#include filename options> <#include filename options> options包含两个属性 encoding="GBK" 编码格式 parse=true 是否作为ftl语法解析,默认是true,false就是以文本方式引入.注意在ftl文件里布尔值都是直接赋值 的如parse=true,而不是parse="true" 用例 /common/copyright.ftl 包含内容 Ftl代码 Copyright 2001-2002 ${me} All rights reserved. Copyright 2001-2002 ${me} All rights reserved. 模板文件 Java代码 <#assign me = "Juila Smith"> Some test Yeah ___________________________________________________________________________ <SPAN><STRONG><SPAN><#include "/common/copyright.ftl" encoding="GBK"></SPAN> </STRONG> </SPAN> <#assign me = "Juila Smith"> Some test Yeah ___________________________________________________________________________ <#include "/common/copyright.ftl" encoding="GBK"> 输出结果: Some test Yeah. Copyright 2001-2002 Juila Smith All rights reserved. Import 语法 Freemarker代码 <#import path as hash> <#import path as hash> 类似于java里的import,它导入文件,然后就可以在当前文件里使用被导入文件里的宏组件用例 假设mylib.ftl 里定义了宏copyright 那么我们在其他模板页面里可以这样使用 Freemarker代码 <#import "/libs/mylib.ftl" as my> <@my.copyright date="1999-2002"/> <#-- "my"在freemarker里被称作namespace --> <#import "/libs/mylib.ftl" as my> <@my.copyright date="1999-2002"/> <#-- "my"在freemarker里被称作namespace --> compress 语法 Freemarker代码 <#compress> ... </#compress> <#compress> ... </#compress> 用来压缩空白空间和空白的行 escape, noescape 语法 Freemarker代码 <#escape identifier as expression> ... <#noescape>...</#noescape> ... </#escape> <#escape identifier as expression> ... <#noescape>...</#noescape> ... </#escape> 用例 主要使用在相似的字符串变量输出,比如某一个模块的所有字符串输出都必须是html安全的,这个时候就可以使用 该表达式 Freemarker代码 <#escape x as x?html> First name: ${firstName} <#noescape>Last name: ${lastName}</#noescape> Maiden name: ${maidenName} </#escape> <#escape x as x?html> First name: ${firstName} <#noescape>Last name: ${lastName}</#noescape> Maiden name: ${maidenName} </#escape> 相同表达式 Ftl代码 First name: ${firstName?html} Last name: ${lastName } Maiden name: ${maidenName?html} First name: ${firstName?html} Last name: ${lastName } Maiden name: ${maidenName?html} assign 语法 Freemarker代码 <#assign name=value> <#-- 或则 --> <#assign name1=value1 name2=value2 ... nameN=valueN> <#-- 或则 --> <#assign same as above... in namespacehash> <#-- 或则 --> <#assign name> capture this </#assign> <#-- 或则 --> <#assign name in namespacehash> capture this </#assign> <#assign name=value> <#-- 或则 --> <#assign name1=value1 name2=value2 ... nameN=valueN> <#-- 或则 --> <#assign same as above... in namespacehash> <#-- 或则 --> <#assign name> capture this </#assign> <#-- 或则 --> <#assign name in namespacehash> capture this </#assign> 用例 生成变量,并且给变量赋值 给seasons赋予序列值 Ftl代码 <#assign seasons = ["winter", "spring", "summer", "autumn"]> <#assign seasons = ["winter", "spring", "summer", "autumn"]> 给变量test加1 Ftl代码 <#assign test = test + 1> <#assign test = test + 1> 给my namespage 赋予一个变量bgColor,下面可以通过my.bgColor来访问这个变量 Ftl代码 <#import "/mylib.ftl" as my> <#assign bgColor="red" in my> <#import "/mylib.ftl" as my> <#assign bgColor="red" in my> 将一段输出的文本作为变量保存在x里 Ftl代码 <#assign x> <#list 1..3 as n> ${n} <@myMacro /> </#list> </#assign> Number of words: ${x?word_list?size} ${x} <#assign x>Hello ${user}!</#assign> error <#assign x=" Hello ${user}!"> true <#assign x> <#list 1..3 as n> ${n} <@myMacro /> </#list> </#assign> Number of words: ${x?word_list?size} ${x} <#assign x>Hello ${user}!</#assign> error <#assign x=" Hello ${user}!"> true 同时也支持中文赋值,如: Ftl代码 <#assign 语法> java </#assign> ${语法} <#assign 语法> java </#assign> ${语法} 打印输出: java global 语法 Freemarker代码 <#global name=value> <#--或则--> <#global name1=value1 name2=value2 ... nameN=valueN> <#--或则--> <#global name> capture this </#global> <#global name=value> <#--或则--> <#global name1=value1 name2=value2 ... nameN=valueN> <#--或则--> <#global name> capture this </#global> 全局赋值语法,利用这个语法给变量赋值,那么这个变量在所有的namespace [A1] 中是可见的, 如果这个变量被当前的assign 语法覆盖 如<#global x=2> <#assign x=1> 在当前页面里x=2 将被隐藏,或者通过${.global.x} 来访问 setting 语法 Freemarker代码 <#setting name=value> <#setting name=value> 用来设置整个系统的一个环境 locale number_format boolean_format date_format , time_format , datetime_format time_zone classic_compatible 用例 假如当前是匈牙利的设置,然后修改成美国 Ftl代码 ${1.2} <#setting locale="en_US"> ${1.2} ${1.2} <#setting locale="en_US"> ${1.2} 输出 1,2 1.2 因为匈牙利是采用", "作为十进制的分隔符,美国是用". " macro, nested, return 语法 Freemarker代码 <#macro name param1 param2 ... paramN> ... <#nested loopvar1, loopvar2, ..., loopvarN> ... <#return> ... </#macro> <#macro name param1 param2 ... paramN> ... <#nested loopvar1, loopvar2, ..., loopvarN> ... <#return> ... </#macro> 用例 Ftl代码 <#macro test foo bar="Bar"[A2] baaz=-1> Test text, and the params: ${foo}, ${bar}, ${baaz} </#macro> <@test foo="a" bar="b" baaz=5*5-2/> <@test foo="a" bar="b"/> <@test foo="a" baaz=5*5-2/> <@test foo="a"/> <#macro test foo bar="Bar"[A2] baaz=-1> Test text, and the params: ${foo}, ${bar}, ${baaz} </#macro> <@test foo="a" bar="b" baaz=5*5-2/> <@test foo="a" bar="b"/> <@test foo="a" baaz=5*5-2/> <@test foo="a"/> 输出 Test text, and the params: a, b, 23 Test text, and the params: a, b, -1 Test text, and the params: a, Bar, 23 Test text, and the params: a, Bar, -1 定义循环输出的宏 Ftl代码 <#macro list title items> ${title?cap_first}: <#list items as x> *${x?cap_first} </#list> </#macro> <@list items=["mouse", "elephant", "python"] title="Animals"/> <#macro list title items> ${title?cap_first}: <#list items as x> *${x?cap_first} </#list> </#macro> <@list items=["mouse", "elephant", "python"] title="Animals"/> 输出结果: Animals: *Mouse *Elephant *Python 包含body 的宏 Ftl代码 <#macro repeat count> <#list 1..count as x> <#nested x, x/2, x==count> </#list> </#macro> <@repeat count=4 ; c halfc last> ${c}. ${halfc}<#if last> Last!</#if> </@repeat> <#macro repeat count> <#list 1..count as x> <#nested x, x/2, x==count> </#list> </#macro> <@repeat count=4 ; c halfc last> ${c}. ${halfc}<#if last> Last!</#if> </@repeat> 输出 1. 0.5 2. 1 3. 1.5 4. 2 Last! t, lt, rt 语法 Freemarkder代码 <#t> 去掉左右空白和回车换行 <#lt>去掉左边空白和回车换行 <#rt>去掉右边空白和回车换行 <#nt>取消上面的效果 <#t> 去掉左右空白和回车换行 <#lt>去掉左边空白和回车换行 <#rt>去掉右边空白和回车换行 <#nt>取消上面的效果 C 一些常用方法或注意事项 表达式转换类 ${expression} 计算expression 并输出 #{ expression } 数字计算#{ expression ;format} 安格式输出数字format 为M 和m M 表示小数点后最多的位数,m 表示小数点后最少的位数如#{121.2322;m2M2} 输出121.23 数字循环 1..5 表示从1 到5 ,原型number..number 对浮点取整数 ${123.23?int} 输出 123 给变量默认值 ${var?default("hello world")?html} 如果var is null 那么将会被hello world 替代 判断对象是不是 null Ftl代码 <#if mouse?exists> Mouse found <#else> <#if mouse?exists> Mouse found <#else> 也可以直接${mouse?if_exists})输出布尔形 -------------------------------------------- (1)解决输出中文乱码问题: freemarker乱码的原因: 没有使用正确的编码格式读取模版文件,表现为模版中的中文为乱码 解决方法:在classpath上放置一个文件freemarker.properties,在里面写上模版文件的编码方式,比如 default_encoding=UTF-8 locale=zh_CN 注意:eclipse中除了xml文件、java文件外,默认的文件格式iso8859-1 数据插入模版时,没有使用正确的编码,表现出模版中的新插入数据为乱码 解决方法:在result的配置中,指定charset,s2的FreemarkerResult.java会将charset传递freemarker <action name="ListPersons" class="ListPersons"> <result type="freemarker"> <param name="location">/pages/Person/view.ftl</param> <param name="contentType"> text/html;charset=UTF-8 </param> </result> </action> (2)提高freemarker的性能 在freemarker.properties中设置: template_update_delay=60000 避免每次请求都重新载入模版,即充分利用cached的模版 (3)尽量使用freemarker本身的提供的tag,使用S2 tags 的标签会在性能上有所损失 (4)freemarker的标签种类: ${..}:FreeMarker will replace it in the output with the actual value of the thing in the curly brackets. They are called interpolation s. # ,代表是FTL tags(FreeMarker Template Language tags) ,hey are instructions to FreeMarker and will not be printed to the output <#if ...></#if> <#list totalList as elementObject>...</#list> @ ,代表用户自定义的标签 <#-- --> 注释标签,注意不是<!-- --> (5)一些特殊的指令: r代表原样输出:${r"C:\foo\bar"} <#list ["winter", "spring", "summer", "autumn"] as x>${x}</#list> ?引出内置指令 String处理指令: html:特殊的html字符将会被转义,比如"<",处理后的结果是&lt; cap_first 、lower_case 、upper_case trim :除去字符串前后的空格 sequences处理指令 size :返回sequences的大小 numbers处理指令 int:number的整数部分,(e.g. -1.9?int is -1) (6)对于null,或者miss value,freemarker会报错 ?exists:旧版本的用法 !:default value operator,语法结构为: unsafe_expr !default_expr,比如 ${mouse!"No mouse."} 当mouse不存在时,返回default value; (product.color)!"red" 这种方式,能够处理product或者color为miss value的情况; 而product.color!"red"将只处理color为miss value的情况 ??: Missing value test operator ,测试是否为missing value unsafe_expr ?? :product.color??将只测试color是否为null (unsafe_expr )??:(product.color)??将测试product和color是否存在null Ftl代码 <#if mouse??> Mouse found <#else> No mouse found </#if> Creating mouse... <#assign mouse = "Jerry"> <#if mouse??> Mouse found <#else> No mouse found </#if> <#if mouse??> Mouse found <#else> No mouse found </#if> Creating mouse... <#assign mouse = "Jerry"> <#if mouse??> Mouse found <#else> No mouse found </#if> (7)模版值插入方式 (interpolation) 通用方式 ( Universal interpolations): ${expression } 对于字符串:只是简单输出 对于数值,会自动根据local确定格式,称为human audience,否则称为computer audience,可以"?c", 比如, <a href="/shop/details?id=${product.id ?c }">Details...</a>,因此这里的id是给浏览器使用的,不需要进行格式化,注意?c只对数值有效 对于日期,会使用默认的日期格式转换,因此需要事先设置好默认的转换格式,包括date_format , time_format ,atetime_format 对于布尔值,不能输出,会报错并停止模版的执行,比如${a = 2} 会出错,但是可以 string built-in来进行转换 数值处理,具体参考:Built-ins for numbers http://freemarker.org/docs/ref_builtins_number.html#ref_builtin_string_for_number 数值处理的例子: <#setting number_format="currency"/> <#assign answer=42/> ${answer} ${answer?string} <#-- the same as ${answer} --> ${answer?string.number} ${answer?string.currency} ${answer?string.percent} 除了使用内置的formate,可以使用任何用Java decimal number format syntax 书写的formate,比如 <#setting number_format="0.###E0"/> <#setting number_format="0"/> <#setting number_format="#"/> ${1234} ${12345?string("0.####E0")} 更加方便的格式: <#setting locale="en_US"> US people writes: ${12345678?string(",##0.00")} <#setting locale="hu"> Hungarian people writes: ${12345678?string(",##0.00")} 日期处理,参考Built-ins for dates http://freemarker.org/docs/ref_builtins_date.html#ref_builtin_string_for_date 日期处理的例子: ${openingTime?string.short} ${openingTime?string.medium} ${openingTime?string.long} ${openingTime?string.full} ${nextDiscountDay?string.short} ${nextDiscountDay?string.medium} ${nextDiscountDay?string.long} ${nextDiscountDay?string.full} ${lastUpdated?string.short} ${lastUpdated?string.medium} ${lastUpdated?string.long} ${lastUpdated?string.full} 注意: 由于java语言中的Date类型的不足,freemarker不能根据Date变量判断出变量包含的部分(日期、时间还是全部),在这种情况下,freemarker 不能正确显示出${lastUpdated?string.short} 或者 simply ${lastUpdated},因此,可以通过?date, ?time and ?datetime built-ins 来帮助freemarker来进行判断,比如${lastUpdated?datetime?string.short} 除了使用内置的日期转换格式外,可以自己指定日期的格式,使用的是Java date format syntax,比如: ${lastUpdated?string("yyyy-MM-dd HH:mm:ss zzzz")} ${lastUpdated?string("EEE, MMM d, ''yy")} ${lastUpdated?string("EEEE, MMMM dd, yyyy, hh:mm:ss a '('zzz')'")} 数值专用方式 ( Numerical interpolations):#{expression } or #{expression ; format },这是数值专用的输出方式,但是 最好使用通用方式的string built-in或者number_format 来完成转换,Numerical interpolations方式将会被停用 (8)创建自定义模版 Ftl代码 <#macro greet> <font size="+2">Hello Joe!</font> </#macro> <#macro greet> <font size="+2">Hello Joe!</font> </#macro> <#assign user="zhangsan"/> 字符串连接 ${"Hello ${user}!"} 与 ${"Hello " + user + "!"} 相同 结果:Hello zhangsan 获取字符 ${user[0]} ${user[4]} 结果:z g 序列的连接和访问 <#assign nums=["1" , "2"] + ["3" , "4"] /> ${nums[0]} 结果是 1 内置函数 html 使用实体引用替换字符串中所有HTML字符,例如,使用&amp; 替换& lower_case 将字符串转化成小写 substring index_of 例如”abcdc"?index_of("bc") 将返回1 seq_contains 序列中是否包含指定值 ${nums?seq_contains("1")?string("yes","no")} seq_index_of 第一个出现的索引 ${nums?seq_index_of("1")} 结果0 sort_by 用于散列
场景:程序员都不喜欢看文档,而更喜欢抄例子。所以,我们把平台组的组件都做成例子供别人参考。我们前端展示层使用的是freemarker,所以遇到这个问题,比如我们要让前端显示freemarker自己的源码时就有问题了(因为我们例子程序的页面也是使用freemarker)。遇到的问题如下: 1、如何显示html源码,而不是让浏览器解析这些html,方法是使用freemarker的html转义。 比如我们显示img标签,就是如此:${'<img src="xxxxxxx"/>'?html},这样最终在页面上展示的内容就是:<img src="xxxxxxx"/>,而不会被浏览器解析。 2、显示freemarker源码,比如${}符号,则可以如下: ${r'${obj.name}'},这样最终显示的结果就是:${obj.name},而不会被freemarker解析。这里要注意,前边那个标红的“r”字符是关键点。 3、自定义宏的结束符号不能出来的解决办法:比如我们自定义了个宏叫做:<@cfw.column></@cfw.column>,如果要显示源码,结束的那个符号"</@cfw.column>”是显示不出来的,这个时候可以采用字符转义的方式,比如: ${'&lt;/@cfw.column''},这样显示的结果就是:</@cfw.column>
模板技术在现代的软件开发中有着重要的地位,而目前最流行的两种模板技术恐怕要算freemarker和velocity了,webwork2.2对两者都有不错的支持,也就是说在webwork2中你可以随意选择使用freemarker或velocity作为view,模板技术作为view的好处是很多,尤其和jsp比较起来优点更大,众所周知jsp需要在第一次被执行的时候编译成servlet,那么这个过程是很慢的,当然很多应用服务器都提供预编译的功能,但是在开发的时候仍然给我们程序员带来了很多痛苦,每次修改都要多几秒钟,那在一天的开发中就有很多时间浪费在jsp的编译上了。用webwork in action的作者的话来说:“每次修改之后重新运行都要等等几秒是令人失望的,而频繁地修改jsp更是会令你的失望情绪变本加厉“。我们把模板技术引入到view中去可以带来更好的开发效率,而且模板的速度要比jsp快(虽然编译过后的jsp在速度上已经满足我的需求了,呵呵)。 当然模板技术可以用在很多领域,可不只在view那里。我们可以通过模板技术来生成xml,生成jsp,生成java文件等等,说到这里,大家通常会使用模板技术用在公司的框架里,这样就可以很快速的生成添删改查的代码,需要的只是模板,其他比如还有邮件模板等等。 以上是模板的作用,那么现在开源的模板技术有好几种,多了之后就有一个选择的问题了,如何选择一个满足自己需要的模板的呢,写了一个例子,我使用了几种设计模式来完成了这个例子,这个例子中,同时使用了freemarker和velocity,这样同学们可以通过代码很直观的比较两种模板技术,通过这个例子,我认识到freemarker在功能上要比velocity强大 1。在view层的时候,它提供了format日期和数字的功能,我想大家都有在页面上format日期或数字的经验,用jsp的同学可能对jstl的fmt标签很有感情,使用了freemarker之后也可以使用freemarker提供的功能来formmat日期和数据,这个功能我想是很贴心的 2。通过我的使用我发现freemaker的eclipseplugin要比velocity的eclipseplugin好,好在很多地方呢,freemarker的插件除了支持freemarker语法也支持html语句,而velocity的插件貌似只支持velocity的语法,html就只是用普通的文本来显示了,在这一点上freemarker占上风了 3。freemarker对jsptag的支持很好,算了,不到迫不得已还是不要这样做吧。 还有就是两者的语法格式,这一点上不同的人有不同倾向 下面就先介绍标签吧 一、FreeMarker模板文件主要有4个部分组成 1、文本,直接输出的部分 2、注释,即<#--...-->格式不会输出 3、插值(Interpolation):即${..}或者#{..}格式的部分,将使用数据模型中的部分替代输出 4、FTL指令:FreeMarker指令,和HTML标记类似,名字前加#予以区分,不会输出。 FTL指令规则 FreeMarker有三种FTL标签,这和HTML的标签是完全类似的 开始标签:<#directivename parameters> 结束标签:</#directivename> 空标签: <#directivename parameters /> 实际上,使用标签时前面的#符号也可能变成@,如果该指令是一个用户指令而不是系统内建指令时,应将#符号改为@符号 插值规则 FreeMarker的插值有如下两种类型 1、通用插值:${expr} 2、数字格式化插值:#{expr}或者#{expr;format} 通用插值,有可以分为四种情况 a、插值结果为字符串值:直接输出表达式结果 b、插值结果为数字值:根据默认格式(#setting 指令设置)将表达式结果转换成文本输出。可以使用内建的字符串函数格式单个插值,例如 <#setting number_format = "currency" /> <#assign price = 42 /> ${price} ${price?string} ${price?string.number} ${price?string.currency} ${price?string.percent} c、输出值为日期值:根据默认格式(由 #setting 指令设置)将表达式结果转换成文本输出,可以使用内建的字符串函数格式化单个插值,例如 <#assign lastUpdated = "2009-01-07 15:05"?datetime("yyyy-MM-dd HH:mm") /> ${lastUpdated?string("yyyy-MM-dd HH:mm:ss zzzz")}; ${lastUpdated?string("EEE,MMM d,yy")}; ${lastUpdated?string("EEEE,MMMM dd,yyyy,hh:mm:ss a '('zzz')'")}; ${lastUpdated?string.short}; ${lastUpdated?string.long}; ${lastUpdated?String.full}; d、插值结果为布尔值 <#assign foo=true /> ${foo?string("是foo","非foo")} 数字格式化插值 数字格式化插值可采用#{expr;format}的形式来格式化数字,其中format可以是: mX:小数部分最小X位 MX:小数部分最大X位 例如: <#assign x = 2.582 /> <#assign y =4 /> #{x;M2}; #{y;M2}; #{x;m1}; #{y;m1}; #{x;m1M2}; #{y:m1M2}; 二、表达式 表达式是FreeMarker的核心功能。表达式放置在插值语法(${...})之中时,表面需要输出表达式的值,表达式语法也可以与FreeMarker标签结合,用于控制输出 1、直接指定值 例如: a、字符串 ${'我的名字是\"yeek\"'}; ${"我的文件保存在d:\\盘"}; b、数值 c、布尔值 d、日期型 FreeMarker支持date、time、datetime三种类型,这三种类型的值无法直接指定,通常需要借助字符串的date、time、datetime三个内建函数进行转换才可以 <#assign test1 = "2009-01-22"?date("yyyy-MM-dd") />; <#assign test2 ="16:34:43"?time("HH:mm:ss") /> <#assign test2 = "2009-01-22 17:23:45"?datetime("yyyy-MM-dd HH:mm:ss") /> ${test1?string.full} e、集合 集合以方括号包括,各集合元素之间以英文逗号(,)分隔,看如下的示例: <#list["星期一",,["星期二",["星期三",["星期四",["星期五"] as x> ${s}; </#list> f、Map集合 Map对象使用花括号包括,Map中的key-value对之间以英文冒号(:)隔开,多组key-value对之间以英文逗号(,) 隔开 例如 <#assign score = {"语文":78,"数学":83,"Java":89} > <#list score?key as x> ${x}--->${score[x]}; </#list> 2、输出变量值 FreeMarker的表达式输出变量时,这些变量可以是顶层变量,也可以是Map对象中的变量,还可以是集合中的变量,并可以使用点(.)语法来访问Java对象的属性 a、顶层变量 Map root = new HashMap(); root.put("name","wenchao"); 对应顶层变量,直接使用${variableName}来输出变量值,变量名只能是数字、字母、下划线、$、@和#的组合,并不能以数字开头 b、输出集合元素 如果需要输出集合元素,则可以根据集合元素的索引来输出元素。集合元素的索引以方括号指定。 假设有集合对象为:["星期一","星期二","星期三","星期四","星期五","星期六"],该集合对象名为week, 如果需要输出星期三,则可以使用如下语法: ${week[2]} 集合里的第一个元素的索引是0 c、输出Map元素 这里的Map对象可以是直接HashMap的实例,甚至包括 JavaBean实例,对应JavaBean实例,我们一样可以把其当成属性为key,属性为value的Map实例 3、字符串操作 a、字符串链接 字符串连接有两种语法 A、使用${..}(或#{..})在字符串常量部分插入表达式的值,从而完成字符串连接 B、直接使用连接运算符(+)来连接字符串 使用第一种语法来连接字符串 ${"Hello,${user}!"} 第二种使用连接符号来连接字符串 ${"Hello,"+user+"!"}; 值的注意的是,${..}只能用于文本部分,因此,下面的代码是错误的: <#if ${isBig}>Wow!</#if> <#if "${isBig}">Wow!</#if> 应该写成: <#if isBig>Wow!</#if> b、截取字符串 Map root = new HashMap(); root.put("book","疯狂Ajax讲义"); ${book[0]} ${book[4]} ${book[1..4]} 4、集合连接运算符 这里所说的集合连接运算时将两个集合连接成一个新的集合,连接集合的运算符是+,例如 <#list ["星期一"," 星期二","星期三"]+["星期四","星期五"] as x> ${x} </#list> 5、Map连接运算符 Map对象的连接运算也是将两个Map对象连接成一个新的Map对象,Map对象的连接运算符是+。如果两个Map对象具有相同的 key,则后加入Map里的key所 对应的value替代原来key所对应的value 6、算术运算符 FreeMarker表达式中完全支持算术运算,FreeMarker支持的算术运算符包括: +,-,*,/,% 看如下代码示范 <#assign x = 5 /> ${x* -100} ${x/2} ${12%10} 在表达式中使用算术运算时要注意以下几点。 A、运算符两边的运算数必须是数字,因此下面的代码是错误的: ${3*"5"} B、使用+(既可以作为加号,也可以作为字符串连接运算符)运算时,如果一边是数字,一边是字符串,就会自动将数字转化为字符串。例如 ${3+"5"} 输出结果:35 C、使用内建的int函数可对数值取整。例如 <#assign x = 5> ${(x/2)?int} ${1.1?int} ${1.999?int} ${-1.9999?int} ${-1.1?int} 7、比较运算符 表达式中支持的比较运算符有如下几个 a、=(或者==):判断两个值是否相等. b、!=:判断两个值是否不相等 c、 >(或者gt):判断坐标值是否大于右边值 d、 >=(或者gte):判断坐标值是否大于等于右边值 e、 <(或者lt):判断左边值是否小于右边值 f、 <=(或者lte):判断左边值是否小于等于右边值 8、逻辑运算符 逻辑运算符有如下几个 a、逻辑与:&& b、逻辑或:|| c、逻辑非:! 逻辑运算符只能作用于布尔值,否则将产生错误。 9、内建函数 FreeMarker还提供了一些内建函数来转换输出,可以在任何变量后紧跟?,?后紧跟内建函数,就可通过内建函数来转换输出变量 下面是常用的内建的字符串函数 a、html:对字符串进行HTML编码 b、cap_first:将字符串第一个字母成大写 c、lower_case:将字符串转换成小写 d、upper_case:将字符串转换成大写 e、trim: 去掉字符串前后的空白字符 下面是集合的常用的内建函数 a、size: 获得序列中元素的数目 下面是数字值的常用的内建函数 a、int 取得数字的整数部分 例如 <#assign test="Tom & Jerry" /> ${test?html} ${test?upper_case?html} 10、空值处理运算符 FreeMarker对空值的处理非常严格,FreeMarker的变量必须有值,没有被赋值的变量就会抛出异常。 11、运算符优先级 三、FreeMarker 的常用指令 1、if指令 分支控制语句 语法格式如下 <#if condition> .... <#elseif condition2> ... <#elseif condition3> ... <#else> ... </#if> 2、switch、case、default、break指令 <#switch value> <#case refValue> ... <#bread> <#case refValue> ... <#bread> <#default> ... </#switch> 虽然FreeMarker提供了switch指令,但它并不推荐使用switch指令来控制也输出,而是推荐使用FreeMarker的if..elseif..else 指令来替代它。 3、list、break指令 list指令时一个典型的迭代输出指令,用于迭代输出数据模型中的集合。list指令的语法格式如下: <#list sequence as item> ... </#list> 除此之外,迭代集合对象时,还包括两个特殊的循环变量: a、item_index:当前变量的索引值。 b、item_has_next:是否存在下一个对象 也可以使用<#break>指令跳出迭代 <#list ["星期一","星期二","星期三","星期四","星期五"] as x> ${x_index +1}.${x} <#if x_has_next>,</#if> <#if x = "星期四"><#break></#if> </#list> 4、include 指令 include指令的作用类似于JSP的包含指令,用于包含指定页,include指令的语法格式如下 <#include filename [options] 在上面的语法格式中,两个参数的解释如下 a、filename:该参数指定被包含的模板文件 b、options:该参数可以省略,指定包含时的选项,包含encoding和parse两个选项,encoding指定包含页面时所使用的解码集,而parse指定被 包含是否作为FTL文件来解析。如果省略了parse选项值,则该选项值默认是true 5、 import指令 该指令用于导入FreeMarker模板中的所有变量,并将该变量放置在指定的Map对象中,import 指令的语法格式如下 <#import path as mapObject> 在上面的语法格式中,path指定要被导入的模板文件,而mapObject是一个Map对象名,通过这行代码,将导致path模板中的所有变量都被放置 在mapObject中 <#import "/lib/common.ftl" as com> 6、noparse指令 noparse指令指定FreeMarker不处理该指令里包含的内容,该指令的语法格式如下: <#noparse> ... </#noparse> 7、escape、noescape指令 8、assign指令 它用于为该模板页面创建或替换一个顶层变量 9、setting指令 该指令用于设置FreeMarker的运行环境,该指令的语法格式如下: <#setting name = value> name 的取值范围包括如下几个 locale :该选项指定该模板所用的国家/语言选项 number_format:该选项指定格式化输出数字的格式 boolean_format:该选项指定两个布尔值的语法格式,默认值是"true、false" date_format,time_format,datetime_format:该选项指定格式化输出日期的格式 time_zone: 设置格式化输出日期时所使用的时区 10、macro、nested、return指令 下面再介绍一个例子 Java代码 public class TemplateTest { /** * @param args */ public static void main(String[] args) throws Exception{ /* 准备数据 */ Map latest = new HashMap(); latest.put("url", "products/greenmouse.html"); latest.put("name", "green mouse"); Map root = new HashMap(); root.put("user", "Big Joe"); root.put("latestProduct", latest); root.put("number", new Long(2222)); root.put("date",new Date()); List listTest = new ArrayList(); listTest.add("1"); listTest.add("2"); root.put("list",listTest); TemplateEngine freemarkerEngine = (TemplateEngine)TemplateFactory.getInstance().getBean("freemarker"); freemarkerEngine.run(root);//使用freemarker模板技术 TemplateEngine velocityEngine = (TemplateEngine)TemplateFactory.getInstance().getBean("velocity"); velocityEngine.run(root);//使用velocity模板技术 } } 工厂类,用来得到模板引擎 Java代码 public class TemplateFactory { private static TemplateFactory instance; private Map objectMap; static{ instance = new TemplateFactory(); } public TemplateFactory() { super(); this.objectMap = new HashMap(); synchronized (this) { objectMap.put("freemarker", new FreemarkerTemplateEngine(){ public String getTemplatePath() { return "template"; } }); objectMap.put("velocity", new VelocityTemplateEngine()); } } public static TemplateFactory getInstance(){ return instance; } /** * 模仿spring的工厂 * @param beanName * @return */ public Object getBean(String beanName){ return objectMap.get(beanName); } } 引擎接口 Java代码 public interface TemplateEngine { void run(Map context)throws Exception; } 模板引擎的实现使用method template模式,因为有两个实现,这两个实现又存在公共的逻辑,所以选择了这个模式 Java代码 public abstract class AbstractTemplateEngine implements TemplateEngine{ public abstract String getTemplatePath(); public abstract String getTemplate(); public abstract String getEngineType(); public void run(Map context)throws Exception{ if(Constants.ENGINE_TYPE_FREEMARKER.equals(getEngineType())) executeFreemarker(context); else executeVelocity(context); } private void executeFreemarker(Map context)throws Exception{ Configuration cfg = new Configuration(); cfg.setDirectoryForTemplateLoading( new File(getTemplatePath())); cfg.setObjectWrapper(new DefaultObjectWrapper()); cfg.setCacheStorage(new freemarker.cache.MruCacheStorage(20, 250)); Template temp = cfg.getTemplate(getTemplate()); Writer out = new OutputStreamWriter(System.out); temp.process(context, out); out.flush(); } private void executeVelocity(Map root)throws Exception{ Velocity.init(); VelocityContext context = new VelocityContext(root); org.apache.velocity.Template template = null; template = Velocity.getTemplate(getTemplatePath()+getTemplate()); StringWriter sw = new StringWriter(); template.merge( context, sw ); System.out.print(sw.toString()); } } 这个是freemarker实现 Java代码 public class FreemarkerTemplateEngine extends AbstractTemplateEngine{ private static final String DEFAULT_TEMPLATE = "FreemarkerExample.ftl"; /** * 这个方法应该实现的是读取配置文件 */ public String getTemplatePath() { return null; } public void run(Map root) throws Exception{ super.run(root); } public String getTemplate() { // TODO Auto-generated method stub return DEFAULT_TEMPLATE; } public String getEngineType() { return Constants.ENGINE_TYPE_FREEMARKER; } } 这个是velocity实现 Java代码 public class VelocityTemplateEngine extends AbstractTemplateEngine{ private static final String DEFAULT_TEMPLATE = "VelocityExample.vm"; public String getTemplatePath() { return "/template/"; } public void run(Map map) throws Exception{ super.run(map); } public String getTemplate() { // TODO Auto-generated method stub return DEFAULT_TEMPLATE; } public String getEngineType() { // TODO Auto-generated method stub return Constants.ENGINE_TYPE_VELOCITY; } } 以下是模板 1,freemarker模板 Java代码 freemarker template test: string test-----------${user}-----------${number}-----------${latestProduct.url}-----------${latestProduct.name} condition test----------- <#if user == "Big Joe"> list iterator----------- <#list list as aa> ${aa} </#list> </#if> date test-----------${date?string("MMM/dd/yyyy")} 2,velocity模板 Java代码 ****************************************************************************************************************** velocity template test: string test-----------${user}-----------${number}-----------${latestProduct.url}-----------${latestProduct.name} condition test----------- #if ($user == "Big Joe") list iterator----------- #foreach( $aa in $list ) $aa #end #end date test-----------${date} 至此整个例子就结束了,这个例子比较直观的表现两种技术的应用
今天做S2SH集成的例子,所有该设置的地方都设置成了UTF-8,包括tomcat的配置文件server.xml、web.xml里增加了过滤器、struts2的i18N常量等,但控制台以及Action里打印出来的还是乱码。发觉不对劲,我就直接在Action里打印了一段中文,结果打印出来也是乱码,我就怀疑是eclipse的问题,然后到网上一搜,通过下面的方法解决了: 1、首先在Run-Run Configration-Tomcat-Arguments,在VM arguments中添加-Dfile.encoding=UTF-8,如图: 2、Common tab页,在Console encoding中选择UTF-8如图: 然后,执行程序,控制台打印出中文了。 如果要运行Debug模式,同样的方法修改Debug Configrations
在5.9及其之前的版本Activiti不支持直接部署“bpmn”为扩展名的流程,所以之前在这篇文章中讲解如何打包bar文件时要求把bpmn重名为bpmn20.xml再打包。 不是了bpmn结尾的流程定义文件之后启动流程时会提示对应的流程不存在,这是因为Activiti未能识别bpmn扩展名的文件,它不知道如何处理当然也就没有作为流程定义存储到数据,最后你也就不能启动这个流程。 2.黎明前的迷惘 所以解决这个问题的办法就是在部署时重命名资源文件,如下典型的代码: ? 1 2 3 String filename = "/Users/henryyan/project/foo.bpmn"; repositoryService.createDeployment() .addInputStream("foo.bpmn20.xml", new FileInputStream(filename)).deploy(); 上面的部署方式可以正常启动一个流程。 但是下面的代码就不能直接启动了。 ? 1 repositoryService.createDeployment().addClasspathResource("diagrams/Gateway.bpmn").deploy(); Activiti会报错信息如下: org.activiti.engine.ActivitiException: no processes deployed with key 'AutoClaimForReject' at org.activiti.engine.impl.persistence.deploy.DeploymentCache.findDeployedLatestProcessDefinitionByKey(DeploymentCache.java:63) at org.activiti.engine.impl.cmd.StartProcessInstanceCmd.execute(StartProcessInstanceCmd.java:58) at org.activiti.engine.impl.cmd.StartProcessInstanceCmd.execute(StartProcessInstanceCmd.java:31) at org.activiti.engine.impl.interceptor.CommandExecutorImpl.execute(CommandExecutorImpl.java:24) at org.activiti.engine.impl.interceptor.CommandContextInterceptor.execute(CommandContextInterceptor.java:42) at org.activiti.engine.impl.interceptor.LogInterceptor.execute(LogInterceptor.java:33) at org.activiti.engine.impl.RuntimeServiceImpl.startProcessInstanceByKey(RuntimeServiceImpl.java:54) 3.站在山坡看日出 从5.10版本开始我可以直接部署bpmn扩展名的流程定义文件了,顺便说一下bpmn是Activiti Designer 5.9(Designer的重大变更说明)之后默认的扩展名,部分设计器也是默认以bpmn作为扩展名。 现在就可以这样部署流程定义文件了: ? 1 2 3 String filename = "/Users/henryyan/project/foo.bpmn"; repositoryService.createDeployment() .addInputStream("foo.bpmn", new FileInputStream(filename)).deploy(); 当然这个看着不太爽,因为仅仅就是一个资源名称的更改(由foo.bpmn20.xml改为foo.bpmn)。 来点优雅的,借助CDI: ? 1 2 3 4 5 6 7 @Test @Deployment(resources = { "diagrams/foo.bpmn" }) public void startProcess() throws Exception { RuntimeService runtimeService = activitiRule.getRuntimeService(); ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("foo"); … }
1.简单说明Activiti Modeler Activiti Modeler是用来设计BPMN2.0规范的可视化设计器,使用开源的Signavio源码构建打包。 在Activiti 5.6版本之前安装包里面自带Activiti Modeler,之后就需要手动打包了,最近很多人询问如何打包运行,所以写此博文详细说明。 2.下载Signavio Signavio托管在googlecode上,地址:http://code.google.com/p/signavio-core-components/,可以通过Svn Checkout方式下载源码: ? 1 svn checkout http://signavio-core-components.googlecode.com/svn/trunk/ signavio-core-components 3.配置打包任务属性 Signavio是通过ant构建打包的,在打包之前需要更改一下build.properties文件的属性值。 version:这个版本随便定义,我是按照Activiti的最新版本定义的,例如当前最新的版本号是5.9; war:这个属性定义打包的war文件的名称,即:foo.war,我们设置为activiti-modeler; configuration:最重要的一个属性,定义打包的风格,默认支持三种:default, Activiti, jBPM;其实说风格也是授权,因为Activiti Modeler是Signavio捐赠给Activiti的。这里我们定义为Activiti,对应的配置目录在signavio-core-components/configuration/Activiti; host:这里顾名思义就是要配置运行时的域名以及端口号,根据自己的实际配置设置;可以定义为https协议。 fileSystemRootDirectory:重要,属性的含义是制定保存workspace工作去的目录;这个属性导致很多人启动服务失败,因为制定的目录不存在导致Signavio报错;所以要事先准备好一个目录用来保存设计的文件(流程定义);注意:不要使用反斜杠(\),而是所有的操作系统都使用正斜杠(/)来定义目录,例如:/home/henryyan/work/workspace/signavio 4.执行打包 支持多种打包、部署方式,也支持多种Web容器(JBoss、Tomcat)。 4.1 all-in-one方式 这个也是最常用的方式,在signavio-core-components目录执行命令: ant build-all-in-one-war 执行完命令之后在signavio-core-components/target目录就出生成activiti-modeler.war,现在就可以把这个war包部署到tomcat或者其他容器中运行了。 4.2 直接部署到Web容器 需要设置属性: dir-tomcat-webapps:此属性在运行下面的命令的时候会用到,含义为指定打包文件部署的容器web根目录,例如我的tomcat配置:/home/henryyan/work/tools/apache/tomcat/tomcat-6.0.32-activiti-modeler/webapps dir-jboss-webapps:同dir-tomcat-webapps,只不过容器类型不同而已。 可以通过如下命令直接打包+运行设计器: ant build-and-deploy-all-in-one-war-to-tomcat 等待任务结束之后在dir-tomcat-】webapps的属性值对应的目录中就看到了activiti-modeler.war文件了,现在你可以启动tomcat访问了。 4.3 Windows打包报错 又是烦人的编码问题,如图: 不过好在有人遇到过,我把解决办法搬过来整理分享给大家。解决办法就是设置编码为UTF-8。 官网的WIKI特别之处了UTF-8 Encoding Configuration。 A lot of users face issues regarding an invalid encoding that may result in corrupted model files. That is why it is very important that you ensure the usage of UTF-8 encoding in the whole application stack. 用编辑器打开signavio-core-components/ editor/build.xml文件。 找到<target name="com.signavio.editor.js.concat">,紧随其后添加一行配置代码:<property name="charset" value="utf-8"/>标签中的<concat destfile='${build}/oryx.debug.js'>修改为<concat destfile='${build}/oryx.debug.js' encoding="${charset}" outputencoding="${charset}">。 找到<target name='com.signavio.editor.js.compress代码处,更改次target内的<java dir="${build}" jar="${root}/lib/yuicompressor-2.4.2.jar" fork="true" failonerror="true" output='${compress.temp}'>;将其中的yuicompressor-2.4.2.jar更改为yuicompressor-2.4.7.jar。 signavio默认使用yuicompressor-2.4.2版本压缩javascript和css文件,为了解决编码问题我们需要使用最新版本替换2.4.2版本,笔者在撰稿的时候最新的yuicompressor版本为2.4.7,读者也可以直接下载最新版本。访问http://yuilibrary.com/download/yuicompressor/ 下载第一个版本的压缩包,解压提取build/yuicompressor-2.4.7.jar文件并复制到signavio-core-components/yuicompressor/editor/lib目录中。再次执行打包命令ant build-all-in-one-war一切正常,截图证明。
这个恐怕是初次接触工作流最多的话题之一了,当然这个不是针对Activiti来说的,每个工作流引擎都会支持多种方式的表单。目前大家讨论到的大概有三种。 动态表单 外置表单 普通表单 具体选择哪种方式只能读者根据自己项目的实际需求结合现有技术或者架构、平台选择!!! 1.动态表单 这是程序员最喜欢的方式,同时也是客户最讨厌的……因为表单完全没有布局,所有的表单元素都是顺序输出显示在页面。 此方式需要在流程定义文件(bpmn20.xml)中用activiti:formProperty属性定义,可以在开始事件(Start Event)和Task上设置,而且支持变量自动替换,语法就是UEL。 ? 1 2 3 4 5 6 7 8 9 10 <startevent id="startevent1" name="Start"> <extensionelements> <activiti:formproperty id="name" name="Name" type="string"></activiti:formproperty> </extensionelements> </startevent> <usertask id="usertask1" name="First Step"> <extensionelements> <activiti:formproperty id="setInFirstStep" name="SetInFirstStep" type="date"></activiti:formproperty> </extensionelements> </usertask> 下面是一个简单的动态表单的单元测试,读者可以下载运行以便更明确执行过程和判断动态表单能不能在企业项目中使用。 DymaticForm.bpmn ProcessTestDymaticForm.java 下载之后复制到eclipse工程里,更改里面的路径配置使用JUnit测试即可。 当流程需要一些特殊处理时可以借助Listener或者Delegate方式实现。 注意:表单的内容都是以key和value的形式数据保存在引擎表中!!! 2.外置表单 这种方式常用于基于工作流平台开发的方式,代码写的很少,开发人员只要把表单内容写好保存到.form文件中即可,然后配置每个节点需要的表单名称(form key),实际运行时通过引擎提供的API读取Task对应的form内容输出到页面。 此种方式对于在经常添加新流程的需求比较适用,可以快速发布新流程,把流程设计出来之后再设计表单之后两者关联就可以使用了。例如公司内部各种简单的审批流程,没有业务逻辑处理,仅仅是多级审批是否通过等等情况 当流程需要一些特殊处理时可以借助Listener或者Delegate方式实现。 Activiti Explorer就是使用的这种方式,表单信息都配置在流程定义文件中。 代码片段: ? 1 2 3 4 <process id="FormKey" name="FormKey"> <startevent id="startevent1" name="Start" activiti:formkey="diagrams/form/start.form"></startevent> … </process> 同样也提供了单元测试: FormKey.bpmn20.xml start.form first-step.form ProcessTestFormKey.java 注意:表单的内容都是以key和value的形式数据保存在引擎表中!!! 3.普通表单 这个是最灵活的一种方式,常用于业务比较复杂的系统中,或者业务比较固定不变的需求中,例如ERP系统。 普通表单的特点是把表单的内容存放在一个页面(jsp、jsf、html等)文件中,存放方式也有两种(一体式、分离式): 1.一体式:把整个流程涉及到的表单放在一个文件然后根据处理的任务名称匹配显示,kft-activiti-demo的普通表单模式就是一体式的做法,把表单内容封装在一个div里面,div的ID以节点的名称命名,点击“办理”按钮时用对话框的方式把div的内容显示给用户。 2.分离式:对于非Ajax应用来说比较常用,每个任务对应一个页面文件,点击办理的时候根据任务的ID动态指定表单页面。 本博客发布的Activiti入门Demo中有演示:Activiti快速入门项目-kft-activiti-demo 和以上两种方式比较有两点区别: 表单:和第二种外置表单类似,但是表单的显示、表单字段值填充均由开发人员写代码实现。 数据表:数据表单独设计而不是和前两种一样把数据以key、value形式保存在引擎表中。 4.从业务数据和流程关联比较 动态表单:引擎已经自动绑定在一起了,不需要额外配置。 外置表单:和业务关联是可选的,提供的例子中是没有和业务关联的,如果需要关联只需要在提交StartForm的时候设置businessKey即可。 普通表单:这个应该是必须和业务关联,否则就是无头苍蝇了……,关联方式可以参考:工作流引擎Activiti使用总结中的2.3 业务和流程的关联方式 5.结束语 技术只是辅助工具,只能决定这件事能不能做,如何选择要看应用场合,希望简单的比较能提供一点思路。 这是我使用Activiti以来对几种表单的划分,仅供参考,抛砖引玉,如果有异议请留言或者直接联系我一起探讨!
1.资源文件介绍 Activiti的流程定义文件可以直接部署bpmn20.xml、zip、bar文件,其中后面的zip和bar类型一样都是压缩文件格式,bpmn20.xml是符合bpmn2.0规范的xml定义。 今天要解决的问题就是帮助大家打包流程资源文件,其中肯能包括:bpmn20.xml、png、form等文件。 大多数开发人员都是用Activiti Designer来设计流程定义,可能业务人员使用了其他的流程设计器来描述业务,然后开发人员用Activiti Designer来“深加工”以便让计算机能读懂流程的走向及其逻辑。 如何打包是最近“Activiti中文”群里问的比较多的问题之一,因为太忙没用时间一一说明,这也是因为目前的5.9版本设计器导致的,主要是在5.9版本之前设计器会自动生成一个bpmn20.xml文件,而5.9版本中不再使用之前的.activiti文件,直接把设计与最终的流程定义文件合并为一个bpmn文件。具体的说明请参考《从Activiti Designer5.8升级到5.9遇到的问题》。 下面我们以kft-activiti-demo项目中的请假流程为例介绍如何打包,项目结构如下图。 2.打包Zip|Bar格式 2.1 手动打包 看了刚刚提到的文章应该明白leave.bpmn和在5.8版本中生成的bpmn20.xml一样,所以可以直接把leave.bpmn复制一份改名为leave.bpmn20.xml,然后手动把leave.bpmn20.xml和leave.png用压缩工具打包成leave.zip即可。 2.2 Ant脚本自动打包 用ant脚本无非就是代替手动操作让工具自动根据配置打包,我在kft-activiti-demo(master分支)项目中添加了此功能,把里面的代码拿出来分享给大家。 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <!--?xml version="1.0" encoding="UTF-8"?--> <project name="kft-activiti-demo" default="welcome"> <!-- properties from files --> <property file="${user.home}/.kafeitu/build.properties"> <property file="build.properties"> <!-- properties from key value --> <property name="workflow.diagrams" value="src/main/resources/diagrams"> <property name="workflow.deployments" value="src/main/resources/deployments"> <!-- 流程定义:每个模块的路径 --> <property name="wd.leave" value="${workflow.diagrams}/leave"> <!-- 显示欢迎信息以及操作提示 --> <target name="welcome"> <echo>Activiti演示程序,请输入命令后操作!</echo> </target> <!-- 请假流程定义打包 --> <target name="workflow.package.leave"> <echo>打包流程定义:请假(自定义表单)</echo> <copy file="${wd.leave}/leave.bpmn" tofile="${wd.leave}/leave.bpmn20.xml"> <zip destfile="${workflow.deployments}/leave.zip" basedir="${wd.leave}" update="true" includes="*.xml,*.png"> <delete file="${wd.leave}/leave.bpmn20.xml"> </delete></zip></copy></target> <!-- 流程定义打包 --> <target name="workflow.package.all" depends="workflow.package.leave"> </target> </property></property></property></property></property></project> 熟悉Ant的读者很快就能看懂这些配置信息及其目的,对于不熟悉Ant的稍微介绍一下。 第4、5行处读取一些配置信息,目前还未用到外部配置,可以先忽略; 第7~13行处用于配置一些文件的路径,其中workflow.diagrams就是bpmn和png文件所在的目录,只不过里面又根据模块细分了; 第21~26行处才是重点,首先复制bpmn文件为bpmn20.xml,然后把bpmn20.xml和png文件打包成zip文件 使用方法如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 henryyan@hy-mbp ~kad git:(master) ant workflow.package.all Buildfile: /Users/henryyan/work/projects/activiti/kft-activiti-demo/build.xml workflow.package.leave: [echo] 打包流程定义:请假(自定义表单) [copy] Copying 1 file to /Users/henryyan/work/projects/activiti/kft-activiti-demo/src/main/resources/diagrams/leave [zip] Updating zip: /Users/henryyan/work/projects/activiti/kft-activiti-demo/src/main/resources/deployments/leave.zip [delete] Deleting: /Users/henryyan/work/projects/activiti/kft-activiti-demo/src/main/resources/diagrams/leave/leave.bpmn20.xml workflow.package.leave-dynamic-from: [echo] 打包流程定义:请假(动态表单) [copy] Copying 1 file to /Users/henryyan/work/projects/activiti/kft-activiti-demo/src/main/resources/diagrams/leave-dynamic-from [zip] Updating zip: /Users/henryyan/work/projects/activiti/kft-activiti-demo/src/main/resources/deployments/leave-dynamic-from.zip [delete] Deleting: /Users/henryyan/work/projects/activiti/kft-activiti-demo/src/main/resources/diagrams/leave-dynamic-from/leave-dynamic-from.bpmn20.xml workflow.package.all: BUILD SUCCESSFUL Total time: 0 seconds 如果有外部form文件也可以加入到zip包中。 3.打包bar格式 bar文件就是zip格式的,仅仅是扩展名不同而已,所以打包bar文件直接用上面的方式,只不过把扩展名zip更好成bar就可以了。 3.1 自动打包Bar文件 如何打包Bar我就不多说了,官网的手册已经说的很详细了,请移步:http://www.activiti.org/userguide/index.html#eclipseDesignerBPMNFeatures,找到Activiti Designer deployment features有详细的说明。
1.简单介工作流引擎与Activiti 对于工作流引擎的解释请参考百度百科:工作流引擎 1.1 我与工作流引擎 在第一家公司工作的时候主要任务就是开发OA系统,当然基本都是有工作流的支持,不过当时使用的工作流引擎是公司一些牛人开发的(据说是用一个开源的引擎修改的),名称叫CoreFlow;功能相对Activiti来说比较弱,但是能满足日常的使用,当然也有不少的问题所以后来我们只能修改引擎的代码打补丁。 现在是我工作的第二家公司,因为要开发ERP、OA等系统需要使用工作流,在项目调研阶段我先搜索资料选择使用哪个开源工作流引擎,最终确定了Activiti5并基于公司的架构做了一些DEMO。 1.2 Activiti与JBPM5? 对于Activiti、jBPM4、jBPM5我们应该如何选择,在InfoQ上有一篇文章写的很好,从大的层面比较各个引擎之间的差异,请参考文章:纵观jBPM:从jBPM3到jBPM5以及Activiti5 1.3 Activiti资料 官网:http://www.activiti.org/ 下载:http://www.activiti.org/download.html 版本:Activiti的版本是从5开始的,因为Activiti是使用jBPM4的源码;版本发布:两个月发布一次。 Eclipse Plugin: http://activiti.org/designer/update/ Activit中文群:236540304 2.初次使用遇到问题收集 因为Activiti刚刚退出不久所以资料比较空缺,中文资料更是少的可怜,所以开始的时候一头雾水(虽然之前用过工作流,但是感觉差距很多),而且官方的手册还不是很全面;所以我把我在学习使用的过程遇到的一些疑问都罗列出来分享给大家;以下几点是我遇到和想到的,如果你还有什么疑问可以在评论中和我交流再补充。 2.1 部署流程图后中文乱码 乱码是一直缠绕着国人的问题,之前各个技术、工具出现乱码的问题写过很多文章,这里也不例外……,Activiti的乱码问题在流程图中。 流程图的乱码如下图所示: 解决办法有两种: 2.1.1 修改源代码方式 修改源码 org.activiti.engine.impl.bpmn.diagram.ProcessDiagramCanvas 在构造方法 public ProcessDiagramCanvas(int width, int height) 中有一行代码是设置字体的,默认是用Arial字体,这就是乱码产生的原因,把字改为本地的中文字体即可,例如: Font font = new Font("WenQuanYi Micro Hei", Font.BOLD, 11); 当然如果你有配置文件读取工具那么可以设置在*.properties文件中,我就是这么做的: Font font = new Font(PropertyFileUtil.get("activiti.diagram.canvas.font"), Font.BOLD, 11); 从5.12版本开始支持设置字体名称,在引擎中添加如下设置,在生成图片时即可使用微软雅黑设置图片中的文字。 2.1.2 使用压缩包方式部署 Activiti支持部署*.bpmn20.xml、bar、zip格式的流程定义。 使用Activit Deisigner工具设计流程图的时候会有三个类型的文件: .activiti设计工具使用的文件 .bpmn20.xml设计工具自动根据.activiti文件生成的xml文件 .png流程图图片 解决办法就是把xml文件和图片文件同时部署,因为在单独部署xml文件的时候Activiti会自动生成一张流程图的图片文件,但是这样在使用的时候坐标和图片对应不起来…… 所以把xml和图片同时部署的时候Activiti自动关联xml和图片,当需要获取图片的时候直接返回部署时压缩包里面的图片文件,而不是Activiti自动生成的图片文件 2.1.2.1 使用工具打包Bar文件 在“Package Explorer”视图中右键项目名称然后点击“Create deployment artifacts”,会在src目录中创建deployment文件夹,里面包含*.bar文件. 2.1.2.2 使用Ant脚本打包Zip文件 这也是我们采用的办法,你可以手动选择xml和png打包成zip格式的文件,也可以像我们一样采用ant target的方式打包这两个文件。
先来看一段API调用: ? 1 2 List hpis = historyService.createHistoricProcessInstanceQuery() .startedBy(userCode).list(); 查询结果为空,这是为什么? 1.原因说明 当通过runtimeService接口启动(startProcessInstance[Byxxx])流程的时候会设置一个变量,代码片段(ProcessDefinitionEntity.java#createProcessInstance): ? 85 86 87 88 89 String initiatorVariableName = (String) getProperty(BpmnParse.PROPERTYNAME_INITIATOR_VARIABLE_NAME); if (initiatorVariableName!=null) { String authenticatedUserId = Authentication.getAuthenticatedUserId(); processInstance.setVariable(initiatorVariableName, authenticatedUserId); } 从上面的代码片段中可以看出在启动流程的时候引擎会先从Authentication读取已认证用户信息;现在我们只要能设置认证用户的ID就可以了。 2.解决问题 查看API发现接口IdentityService有一个方法:setAuthenticatedUserId(String authenticatedUserId),正是这个方法在其接口实现类:org.activiti.engine.impl.IdentityServiceImpl#setAuthenticatedUserId中调用了Authentication.setAuthenticatedUserId()。 解决办法很简单只要在启动流程之前调用API即可:identityService.setAuthenticatedUserId(userId); ? 1 2 identityService.setAuthenticatedUserId(userId); processInstance = runtimeService.startProcessInstanceByKey("leave", entityId, variables); 当流程启动之后可以到表ACT_HI_PROCINST中查看字段START_USER_ID_的值来验证是否生效。 3.结束 问题很简单,但是官网的手册没有提到,希望让遇到问题的人少走弯路。
1. 引擎配置对象ProcessEngineConfiguration 引擎配置是配置Activiti的第一步,无论你使用Standalone还是Spring管理引擎都可以在配置文件中配置参数,虽然目前Activiti支持多种引擎配置对象,但是均继承自一个基础的配置对象(抽象类)org.activiti.engine.ProcessEngineConfiguration。 除了基础的引擎配置对象之外还有一下几个具体的实现,不同的场合使用不用的引擎实现,均继承自org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl StandaloneProcessEngineConfiguration:标准的单机引擎配置对象,默认读取activiti.cfg.xml文件的配置 StandaloneInMemProcessEngineConfiguration:用于测试环境,jdbcUrl配置为jdbc:h2:mem:activiti,数据库的DDL操作配置:create-drop,在日常的快速测试中经常用到 JtaProcessEngineConfiguration:顾名思义,支持JTA SpringProcessEngineConfiguration:这个恐怕是用的最多的一个,由Spring代理创建引擎,最最重要的是如果把引擎嵌入到业务系统中可以做到业务事务与引擎事务的统一管理 至于引擎中可以配置哪些属性在手册里面已经介绍了一部分,还有一部分隐藏的属性未介绍,如果有需要可以查看每个引擎中的setter方法覆盖默认值。 2.配置引擎的别名以及获取引擎对象 Activiti允许创建多个引擎,每个引擎用别名区分,可以在引擎配置对象中设置一下属性,默认的引擎别名为:default。 2.1 标准方式 ? 1 <property name="processEngineName" value="myProcessEngine"></property> 其中的myProcessEngine即为引擎的别名,当需要获取引擎对象时可以通过下面的代码获取: ? 1 ProcessEngine myProcessEngine = ProcessEngines.getProcessEngine("myProcessEngine"); 当然如果只有一个引擎获取就更简单了,下面的代码可以直接获取一个默认的引擎对象。 ? 1 2 ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine(); // 等价于 ProcessEngines.getProcessEngine("default"); 2.2 Spring方式 如果使用了Spring代理引擎可以使用“Spring”风格方式获取引擎对象,例如下面的配置: ? 1 2 3 4 5 6 7 <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> <property name="dataSource" ref="dataSource"></property> <property name="transactionManager" ref="transactionManager"></property> <property name="databaseSchemaUpdate" value="true"></property> <property name="jobExecutorActivate" value="false"></property> ... </bean> ? 1 2 3 4 5 6 7 8 9 10 11 @Controller @RequestMapping(value = "/workflow") public class ActivitiController { @Autowired ProcessEngineFactoryBean processEngineFactoryBean; @RequestMapping(value = "/print") public ModelAndView processList(HttpServletRequest request) { ProcessEngine processEngine = processEngineFactoryBean.getObject(); } } 2.3 在引擎外部设置引擎配置对象 或许这个小节的标题看不懂了。。。 原因是这样的,众所周知,在默认的配置情况下部署包含中文的流程文件会导致中文乱码(Linux、Windows,Mac平台没问题),所以需要覆盖引擎默认的字体配置属性(活动的字体与输出流文字字体),例如下面的配置: ? 1 2 3 4 5 <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> <property name="activityFontName" value="宋体"></property> <property name="labelFontName" value="宋体"></property> ... </bean> 字体名称根据平台的不同其值也不同,例如在Windows平台下可以使用诸如宋体、微软雅黑等,但是在Linux平台下引擎没有这些字体(或者名称不同)需要特殊设置,kft-activiti-demo的在线demo部署在Ubuntu Server上,而且是纯英文系统没有中文字体,为了解决部署后以及流程跟踪时的中文乱码问题我从Windows平台复制了宋体字体文件解决,字体的文件名为simsun.ttc,使用的配置如下所示: ? 1 2 3 4 5 <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> <property name="activityFontName" value="simsun"></property> <property name="labelFontName" value="simsun"></property> ... </bean> 这样就解决了部署流程时的中文乱码问题,但是没有解决流程跟踪时的乱码问题。。。 原因在于流程图生成工具类ProcessDiagramGenerator会从当前的ThreadLocal中获取引擎配置对象,但是目前仅仅是引擎自动在内部实现时把引擎配置对象设置到ThreadLocal中,所以很多人遇到在Struts(2)或者Spring MVC中直接调用下面的代码得到的图片是乱码: ? 1 InputStream imageStream = ProcessDiagramGenerator.generateDiagram(bpmnModel, "png", activeActivityIds); 既然知道引擎从ThreadLocal中获取引擎配置对象,而且我们已经获取了引擎对象(也就是说可以从中获取引擎配置对象),解决问题的办法很简单,手动把引擎配置对象设置到ThreadLocal中就解决问题了;下面的代码在kft-activiti-demo的ActivitiController类中。 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @RequestMapping(value = "/process/trace/auto/{executionId}") public void readResource(@PathVariable("executionId") String executionId, HttpServletResponse response) throws Exception { ProcessInstance processInstance = runtimeService.createProcessInstanceQuery() .processInstanceId(executionId).singleResult(); BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId()); List<string> activeActivityIds = runtimeService.getActiveActivityIds(executionId); // 不使用spring请使用下面的两行代码 // ProcessEngineImpl defaultProcessEngine = (ProcessEngineImpl) ProcessEngines.getDefaultProcessEngine(); // Context.setProcessEngineConfiguration(defaultProcessEngine.getProcessEngineConfiguration()); // 使用spring注入引擎请使用下面的这行代码 Context.setProcessEngineConfiguration(processEngine.getProcessEngineConfiguration()); InputStream imageStream = ProcessDiagramGenerator.generateDiagram(bpmnModel, "png", activeActivityIds); // 输出资源内容到相应对象 byte[] b = new byte[1024]; int len; while ((len = imageStream.read(b, 0, 1024)) != -1) { response.getOutputStream().write(b, 0, len); } } </string> 关键就在于在调用生成流程图的方法之前调用一次Context.setProcessEngineConfiguration()方法即可,这样引擎就可以获取到引擎配置对象,从而获取到自定义的字体名称属性,乱码问题自然没有了。 3. 结束语 本文大概介绍了一下引擎配置对象以及如何获取引擎对象,并且就大家关注最多的中文乱码问题给出了完美解决办法,想了解引擎配置对象的本文可以作为一个引子,打开你探索引擎内部的大门;里面有很多属性都可以配置,花点时间研究会有意外收获,引擎的架构做的很棒,想玩转它就多花点时间探索它的奥秘,通过引擎配置对象可以打造完全定制化的引擎工作方式。
1. 默认的主键生成策略 了解过Activiit表中数据的同学可能知道记录的主键ID是用自增的生成策略,这样的生成策略有两个优点: 有顺序:所有引擎的表在插入新记录时全部使用同一个ID生成器 便于记忆:因为是自增的所以有一定的顺序,便于记忆;例如业务人员让管理员删除一条数据(ID为5位左右的长度),管理员只要看一眼就可以记住 当然也有缺点: 随着时间的推移或者数据量非常大自增的ID生成策略的“便于记忆”优势也就不存在了,因为ID的位数会逐步增加(举个例子,我们公司做的一个小系统,用户量在30人左右,ID的长度已经到了7位数) 并发量高时会可能导致主键冲突 在引擎初始化的时候会注册ID生成器,看过源码的同学还可能知道有一个类:org.activiti.engine.impl.db.DbIdGenerator,这个类实现了一个接口:org.activiti.engine.impl.cfg.IdGenerator: ? 1 2 3 public interface IdGenerator { String getNextId(); } 该接口仅有一个方法,返回一个String类型的字符串,有兴趣的同学可以去看看引擎默认的生成器源码,接下来介绍如何更改引擎的主键生成器。 2. 更改默认的主键生成器 UUID是全球唯一的主键生成器,也是除自增策略之外最常用的一种,很幸运:引擎内置了UUID生成器实现。 要更改引擎默认的主键生成器很简单,只需要在配置引擎时覆盖一个属性即可,代码如下: ? 1 2 3 4 5 <bean id="uuidGenerator" class="org.activiti.engine.impl.persistence.StrongUuidGenerator"> <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> <property name="idGenerator" ref="uuidGenerator"> </property></bean> </bean> ID为“uuidGenerator”的bean对象就是引擎内部提供的UUID生成器,把Bean对象注册好以后覆盖引擎的“idGenerator”属性即可;再次启动系统后创建的新数据都会用UUID生成策略。 2.1 添加依赖 引擎提供的UUID生成器依赖fastxml的一个模块,需要在pom.xml(Maven工程)中添加如下依赖: ? 1 2 3 4 5 <dependency> <groupid>com.fasterxml.uuid</groupid> <artifactid>java-uuid-generator</artifactid> <version>3.1.3</version> </dependency> 3. 自定义ID生成器 Step 1:创建一个类实现接口org.activiti.engine.impl.cfg.IdGenerator Step 2:参考本文第2部分 ^_^
声明: 此教程适合Activiti 5.17+版本。 本博客所涉及的内容均可在kft-activiti-demo中找到。 在线demo可以访问 http://demo.kafeitu.me:8080/kft-activiti-demo 菜单路径:管理模块 -> 流程管理 -> 模型工作区,可以『创建』或者『编辑』模型 1. 简介 上一篇介绍整合Activiti Modeler《整合Activiti Modeler到业务系统(或BPM平台)》已经有2年多时间了,自从Activiti 5.17版本发布以后该教程已经不适用了,很多网友也反馈不知道怎么把Activiti Modeler整合到自己的项目中去,为此抽时间为适配5.17+版本的集成方法整理成这篇博文,希望对有需求的网友有帮助。 最新版本的kft-activiti-demo已经使用了5.17+版本的Activiti,并且集成了最新的Activiti Modeler组件,可以下载最新源码:https://github.com/henryyan/kft-activiti-demo。 1.1 新版Activiti Modeler特性 先来欣赏一下新版的界面,相比上一版漂亮了许多,调性高了~~~ 界面布局:上(工具区)、左(组件类目)、右(工作区)、右下(属性区) Activiti Modeler内部的实现上还是以oryx为图形组件为内核,用angular.js作为界面基本元素的基础组件以及调度oryx的API。 2. 官方Activiti Explorer的集成方式 先从Github下载官方Activiti源码,地址:https://github.com/Activiti/Activiti。 2.1 Activiti Exploer的内部结构-Java 源码目录(如果是zip下载请先解压缩)中找到modules/activiti-webapp-explorer2/src/main子目录,结构如下: ├── assembly ├── java │ └── org │ └── activiti ├── resources │ └── org │ └── activiti └── webapp ├── META-INF ├── VAADIN │ ├── themes │ └── widgetsets ├── WEB-INF ├── diagram-viewer │ ├── images │ └── js └── editor-app ├── configuration ├── css ├── editor ├── fonts ├── i18n ├── images ├── libs ├── partials ├── popups └── stencilsets 我们需要关注的目录是webapp/editor-app,以及java/org/activiti,目录结构: 新版本的Activiti Explorer放弃了XML方式的配置方式,采用Bean Configuration的方式代替,上图中org/activiti/explorer/conf包中就是各种配置,在org/activiti/explorer/servlet/WebConfigurer类用Servlet 3.0方式配置Servlet映射关系,映射的路径为/service/*。 2.2 Activiti Exploer的内部结构-Web 新版本Activiti Modeler的Web资源不再像旧版那么散乱,新版本只需要关注: src/main/webapp/editor-app:目录中包含设计器里面所有的资源:angular.js、oryx.js以及配套的插件及css src/main/webapp/modeler.html:设计器的主页面,用来引入各种web资源 src/main/resources/stencilset.json: bpmn标准里面各种组件的json定义,editor以import使用。 3. 整合到自己的项目中 了解过网友的需求不知道如何整合新版Activiti Modeler的原因有两个: 不知道怎么把注解的方式转换为XML方式 editor-app目录的结构位置 和自己应用的整合参数配置 3.1 Activiti Rest接口与Spring MVC配置 3.1.1 Maven依赖 Activiti Modeler对后台服务的调用通过Spring MVC方式实现,所有的Rest资源统一使用注解RestController标注,所以在整合到自己项目的时候需要依赖Spring MVC,Modeler模块使用的后台服务都存放在activiti-modeler模块中,在自己的项目中添加依赖: <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-modeler</artifactId> <version>5.19.0</version> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-diagram-rest</artifactId> <version>5.19.0</version> </dependency> 模块作用: activiti-modeler模块提供模型先关的操作:创建、保存、转换json与xml格式等 activiti-diagram-rest模块用来处理流程图有关的功能:流程图布局(layout)、节点高亮等 3.1.2 准备基础服务类 复制文件(https://github.com/henryyan/kft-activiti-demo/tree/master/src/main/java/org/activiti/explorer) 里面的java文件到自己项目中。 3.1.3 Activiti Spring配置 创建文件src/main/resources/beans/beans-activiti.xml定义Activiti引擎的beans: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd"> <context:component-scan base-package="org.activiti.conf,org.activiti.rest.editor"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <!-- 单例json对象 --> <bean id="objectMapper" class="com.fasterxml.jackson.databind.ObjectMapper"/> <!-- 引擎内部提供的UUID生成器,依赖fastxml的java-uuid-generator模块 --> <bean id="uuidGenerator" class="org.activiti.engine.impl.persistence.StrongUuidGenerator" /> <!-- Activiti begin --> <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> <property name="dataSource" ref="dataSource"/> <property name="transactionManager" ref="transactionManager"/> <property name="databaseSchemaUpdate" value="true"/> <property name="jobExecutorActivate" value="true"/> </bean> <bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean"> <property name="processEngineConfiguration" ref="processEngineConfiguration"/> </bean> <!-- 7大接口 --> <bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService"/> <bean id="runtimeService" factory-bean="processEngine" factory-method="getRuntimeService"/> <bean id="formService" factory-bean="processEngine" factory-method="getFormService"/> <bean id="identityService" factory-bean="processEngine" factory-method="getIdentityService"/> <bean id="taskService" factory-bean="processEngine" factory-method="getTaskService"/> <bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService"/> <bean id="managementService" factory-bean="processEngine" factory-method="getManagementService"/> </beans> 在spring初始化的时候引入即可,例如在web.xml中使用通配符方式: <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:/beans/beans-*.xml</param-value> </context-param> 3.1.4 Spring MVC配置 创建文件WEB-INF/spring-mvc-modeler.xml,内容如下: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd"> <!-- 自动扫描且只扫描@Controller --> <context:component-scan base-package="org.activiti.rest.editor,org.activiti.rest.diagram"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /> </context:component-scan> <mvc:annotation-driven /> </beans> 上面XML中告知spring mvc扫描路径为** 3.1.5 web.xml中配置Servlet服务 在web.xml中配置下面的Servlet: <servlet> <servlet-name>ModelRestServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-mvc-modeler.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>ModelRestServlet</servlet-name> <url-pattern>/service/*</url-pattern> </servlet-mapping> 3.1.6 模型设计器的Web资源 直接从Activiti Explorer中复制文件modeler.html文件到src/main/webapp目录即可,该文件会引入定义基本的布局(div)、引入css以及js文件。 修改editor-app/app-cfg.js文件的contextRoot属性为自己的应用名称,例如/kft-activiti-demo/service 3.1.7 模型控制器 在《整合Activiti Modeler到业务系统(或BPM平台)》中已经介绍过ModelController类的作用了,这里需要在基础上稍微做一点调整: create方法中在创建完Model后跳转页面由service/editor?id=改为modeler.html?modelId= 当从模型列表编辑某一个模型时也需要把路径修改为modeler.html?modelId= 4. 整合Activiti Rest 有了Activiti Modeler的基础只需要依葫芦画瓢即可。 4.1 Maven依赖 <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-rest</artifactId> <version>5.19.0</version> </dependency> 4.3 Activiti组件包扫描 文件src/main/resources/beans/beans-activiti.xmlcontext:component-scan标签的base-package属性中添加org.activiti.rest.service包,包里面包含了所有Rest API的接口Rest Controller。 4.4 添加Rest安全认证组件 package org.activiti.conf; import org.activiti.rest.security.BasicAuthenticationProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @Configuration @EnableWebSecurity @EnableWebMvcSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Bean public AuthenticationProvider authenticationProvider() { return new BasicAuthenticationProvider(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authenticationProvider(authenticationProvider()) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and() .httpBasic(); } } 4.5 spring mvc配置文件 创建文件WEB-INF/spring-mvc-rest.xml: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd"> <!-- 自动扫描且只扫描@Controller --> <context:component-scan base-package="org.activiti.rest"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /> </context:component-scan> <mvc:annotation-driven /> </beans> 4.6 配置Servlet映射 <servlet> <servlet-name>RestServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-mvc-rest.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>RestServlet</servlet-name> <url-pattern>/rest/*</url-pattern> </servlet-mapping> 4.7 访问Rest接口 现在启动应用可以访问 http://localhost:8080/your-app/rest/management/properties 以Rest方式查看引擎的属性列表,如果在网页中访问会提示输入用户名密码;也可以访问在线demo测试
首先这是一篇迟来的教程,因为从5.12版本(目前最新版本为5.15.1)开始就已经提供了Diagram Viewer这个流程图跟踪组件,不管如何总归有人需要用到,所以我觉得还是要和大家分享一下。 1. 前言 目前被大家所采用的流程图跟踪有两种方式: 一种是由引擎后台提供图片,可以把当前节点标记用红色 一种是比较灵活的方式,先用引擎接口获取流程图(原图),然后再通过解析引擎的Activity对象逐个解析(主要是判断哪个是当前节点),最后把这些对象组成一个集合转换成JSON格式的数据输出给前端,用Javascript和Css技术实现流程的跟踪 这两种方式在kft-activiti-demo中都有演示,这里就不介绍了,参考流程跟踪部门代码即可。 2. Diagram Viewer简介 Diagram Viewer是官方在5.12版本中添加的新组件,以Raphaël为基础库,用REST(参考:《如何使用Activiti Rest模块》)方式获取JSON数据生成流程图并把流程的处理过程用不同的颜色加以标注,最终的效果如下图所示。 在应用中使用时也很方便,把这个组件的源码复制到项目中再配置一个REST拦截器,最后拼接一个URL即可;举个例子: http://demo.kafeitu.me/kft-activiti-demo/diagram-viewer/index.html?processDefinitionId=leave-jpa:1:22&processInstanceId=27 这个URL中有两个参数: processDefinitionId: 流程定义ID processInstanceId: 流程实例ID 3. 集成Diagram Viewer 3.1 创建REST路由类 REST路由类源码在官方的Activiti Explorer里面有提供,代码如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package org.activiti.explorer.rest; import org.activiti.rest.common.api.DefaultResource; import org.activiti.rest.common.application.ActivitiRestApplication; import org.activiti.rest.common.filter.JsonpFilter; import org.activiti.rest.diagram.application.DiagramServicesInit; import org.activiti.rest.editor.application.ModelerServicesInit; import org.restlet.Restlet; import org.restlet.routing.Router; public class ExplorerRestApplication extends ActivitiRestApplication { public ExplorerRestApplication() { super(); } /** * Creates a root Restlet that will receive all incoming calls. */ @Override public synchronized Restlet createInboundRoot() { Router router = new Router(getContext()); router.attachDefault(DefaultResource.class); ModelerServicesInit.attachResources(router); DiagramServicesInit.attachResources(router); JsonpFilter jsonpFilter = new JsonpFilter(getContext()); jsonpFilter.setNext(router); return jsonpFilter; } } 把这个路由配置到web.xml中: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <servlet> <servlet-name>ExplorerRestletServlet</servlet-name> <servlet-class>org.restlet.ext.servlet.ServerServlet</servlet-class> <init-param> <!-- Application class name --> <param-name>org.restlet.application</param-name> <param-value>org.activiti.explorer.rest.ExplorerRestApplication</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>ExplorerRestletServlet</servlet-name> <url-pattern>/service/*</url-pattern> </servlet-mapping> 3.2 复制Diagram Viewer组件 在官方提供的Zip文件(可以从www.activiti.org/download.html下载)中有一个wars目录,用压缩工具解压activiti-explorer.war文件,目录结构如下图: 把diagram-viewer复制到项目的webapp目录(或者是WebRoot目录)下,在项目中需要跟踪的地方拼接访问diagram-viewer/index.html的URL即可,别忘记了刚刚介绍的两个重要参数。 http://demo.kafeitu.me/kft-activiti-demo/diagram-viewer/index.html?processDefinitionId=leave-jpa:1:22&processInstanceId=27 URL中有两个参数: processDefinitionId: 流程定义ID processInstanceId: 流程实例ID 这是一个独立的页面,你可以直接打开它或者把它嵌入在一个对话框里面(kft-activiti-demo就是用的嵌入方式)。
自己开发的小站,页面访问查询的速度一直不让人满意,刚好今天有时间,就决定对它优化一下。 因为在本地开时发,查询的速度是相当快的,一开始就以为是mysql版本的问题,本地是MariaDB 5.5,服务器上是mysql 5.1, 将服务器上的数据弄到本地导了一份,居然发现一样变慢了,平均查询一个文章要1-2秒,列表超过2秒以上,才几千的数据这么慢肯定是哪里出了问题。 一点一点的定位打印日志,最后发现,居然是一条使用了exists的sql语句,查询就用了2秒,也就是说时间都花在这个上了,其它的基本都可以忽略了,语句如下: select * from TERM t where exists (select t2.TERM_ID from ASS_POST_TERM t2 where t.TERM_ID = t2.TERM_ID and t2.POST_ID = ?) 文章和分类的关联,这个语句没什么特别的地方,分开执行都是零点零几毫秒,拼在一起居然要2秒,不可思议。 难道是这个exists影响了吗?将exist换成in试一下: select * from TERM t where t.TERM_ID in (select t2.TERM_ID from ASS_POST_TERM t2 where t2.POST_ID = ?) 立马见效,查询时间从2秒提升为零点1毫秒左右,还真是这个exists的缘故。 网上都说exists的效率要高于in或才not in,看来也不尽然,具体情况还得具体分析啊,像在这里就比in差了20倍不止。 看来要好好研究一个exists这个关键字了。
前面已经讲到了spring 3整合Quartz 2来实现时任务,其实从spring 3开始,它本身就已经自带了一套自主开发的定时任务工具Spring-Task,可以将它看成是一个轻量级的Quartz,而且使用起来十分简单,除spring相关的包外不需要额外的包,支持注解和配置文件两种形式。 第一种:配置文件方式 第一步:编写作业类,它是一个普通的Java类,不需要继承和实现任何类和接口: @Service public class TaskJob { public void job1() { System.out.println("任务成功运行。。。"); } } 第二步:在spring配置文件头中添加spring-task的命名空间及描述: <beans xmlns="http://www.springframework.org/schema/beans" xmlns:task="http://www.springframework.org/schema/task" ... xsi:schemaLocation="http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd"> 第三步:spring配置文件中设置具体的任务: <task:scheduled-tasks> <task:scheduled ref="taskJob" method="job1" cron="0 * * * * ?"/> </task:scheduled-tasks> <context:component-scan base-package=" com.task " /> 说明:ref参数指定的即任务类,method指定的即需要运行的方法,cron及cronExpression表达式,具体写法这里就不介绍了。 <context:component-scan base-package="com.task" />这个配置不消多说了,spring扫描注解用的。 到这里配置就完成了,是不是很简单。 第二种:使用注解形式 从spring 2.5开始,可以方便的使用注解来声明bean,对于定时任务,同样提供了注解@Scheduled,我们该注解的定义: @Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Scheduled { public abstract String cron(); public abstract long fixedDelay(); public abstract long fixedRate(); } 可以看出该注解可以接收三个参数,分别表示的意思是: cron:指定cron表达式 fixedDelay:官方文档解释:An interval-based trigger where the interval is measured from the completion time of the previous task. The time unit value is measured in milliseconds.即表示从上一个任务完成开始到下一个任务开始的间隔,单位是毫秒。 fixedRate:官方文档解释:An interval-based trigger where the interval is measured from the start time of the previous task. The time unit value is measured in milliseconds.即从上一个任务开始到下一个任务开始的间隔,单位是毫秒。 下面我们使用注解来实现一下看看: 第一步:还是编写我们的任务类,和上面基本一样,只不过方法上添加了@Scheduled注解。 @Component("taskJob") public class TaskJob { @Scheduled(cron = "0 0 3 * * ?") public void run() { System.out.println("任务成功运行。。。"); } } 第二步:同样需要在spring配置文件头中添加spring-task的命名空间及描述,另外添加扫描spring-task的配置:<beans xmlns="http://www.springframework.org/schema/beans" xmlns:task="http://www.springframework.org/schema/task" ... xsi:schemaLocation="http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd"> ... <!-- 开启这个配置,spring才能识别@Scheduled注解 --> <task:annotation-driven/> ... </beans> 配置完毕,我们的任务已经可以运行了。当然你也可以把cron参数换成另外的两个,自己尝试一下吧。spring-task还有很多的参数,这里就不一一解释了,具体可以查看官方的文档。
到这里,功能上我们已经全实现了。 但是有时候我们的项目不是部署在一台机器上的,而是一个集群环境,往往我们的定时任务只需要一台机器执行就够了。 那么我们怎么样来实现这种集群环境下的定时任务运行呢? 前面说的支持幂等性可以在一定程序上解决这个问题,网上有版本使用数据库加锁的方式也可以,当然,还可以借助zookeeper等方式来实现更强大的分布式锁。 我在这里主要说的方式并不直接涉及到这个集群的问题,而是讨论这个定时任务运行的架构该如何来搭建,当然集群问题将自然而然得到解决。 在我的思维中,定时任务的架构应该是这样子的,见下图: 有一个集中管理的定时任务中心,所有的定时任务信息都在这里创建、保存并被运行,但是没有具体的业务,所有的业务都在具体的项目中,这样它的资源是非常省的。 当定时任务到了运行的时间,它的职责就是连接消息中心,通过消息中心向外发布一个的消息,可以带上运行的任务信息等参数,至于谁来消费这个消息执行业务它就不关心了。 当项目有定时任务的需求时,只需关注它本身的业务逻辑而不必去写定时任务的代码,只需要向消息中心订阅相应的消息,在接收到消息后执行业务代码即可。 这样定时任务和具体的项目基本就解耦了,当有新项目加入进来时只需要订阅一个消息就能实现定时任务。 在集群环境下,可以根据需要(一台或多台执行)设置消息的消费模式,像metaQ就支持集群中的某一台消费消息,当然,稳妥起见前面说的幂等性还是必不可少的。 这样上面说的问题是不是也得到解决了呢? 当然,具体场景还得具体分析,只有适合的才是最好的。如果项目只要二三台机器就能搞定,显然这个方案是得不尝失的 如果你有更好的想法或者方案,欢迎加QQ群进行交流探讨:466355109!
之前已经把功能基本都实现了,这里我们再来优化一下代码。 我们发现,在创建、修改、和删除定时任务时,对于quartz的操作其实是可以封装成一个简单的工具辅助类的,如创建的代码可以抽取成: /** * 创建定时任务 * * @param scheduler the scheduler * @param jobName the job name * @param jobGroup the job group * @param cronExpression the cron expression * @param isSync the is sync * @param param the param */ public static void createScheduleJob(Scheduler scheduler, String jobName, String jobGroup, String cronExpression, boolean isSync, Object param) { //同步或异步 Class<? extends Job> jobClass = isSync ? JobSyncFactory.class : JobFactory.class; //构建job信息 JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroup).build(); //放入参数,运行时的方法可以获取 jobDetail.getJobDataMap().put(ScheduleJobVo.JOB_PARAM_KEY, param); //表达式调度构建器 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression); //按新的cronExpression表达式构建一个新的trigger CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup) .withSchedule(scheduleBuilder).build(); try { scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException e) { LOG.error("创建定时任务失败", e); throw new ScheduleException("创建定时任务失败"); } } 把任务的具体信息包括Scheduler都使用参数方式传入。 看过前面文章的同学或许还记得,quartz在spring中需要声明的对象只剩下一行: <bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean" /> 既然这个schedulerFactoryBean只是在spring中声明一下,并没有做特殊的操作,在辅助的工具类中直接使用单例模式创建一个不是更好,还能少传一个参数? 你想的没错,这种方式的确更好还能解耦,但是我们来看一下SchedulerFactoryBean类的代码: public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBean<Scheduler>, BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean, SmartLifecycle { //...... } 它实现了spring的FactoryBean接口,也就是说它在创建时并不是简单的new而已,还夹杂了一些其它的复杂行为,所以我们也没必要特地的去怎么怎么样,还是在spring中声明一下吧。 另外,我们看它的getObject()方法: public Scheduler getObject() { return this.scheduler; } 发现它实际返回的已经是Scheduler对象,既然如此在我们类中就不必注入schedulerFactoryBean再调用getScheduler()这么麻烦了,可以直接声明Scheduler对象:@Service public class ScheduleJobServiceImpl implements ScheduleJobService { /** 调度Bean */ @Autowired private Scheduler scheduler; //...... } 当然你注入schedulerFactoryBean也不会有错,看过spring源码的同学应该立马就能明白这是getBean("bean")和getBean("&bean")的区别了。 另外说说前面漏掉的两个地方。 一、更新任务 先前我们在更新任务时,虽然更新了定时任务的执行时间,但是并没有对参数进行更新,即使用context.getMergedJobDataMap().get(...)方法获取到的参数还是旧的。 假设我们更新了任务的时间表达式,任务已按新的时间表达式在执行,但在获取到参数后发现时间表达式还是原来的。 尝试对参数进行更新,使用如下代码: JobDetail jobDetail = scheduler.getJobDetail(getJobKey(jobName, jobGroup)); //jobDetail = jobDetail.getJobBuilder().ofType(jobClass).build(); //更新参数 实际测试中发现无法更新 JobDataMap jobDataMap = jobDetail.getJobDataMap(); jobDataMap.put(ScheduleJobVo.JOB_PARAM_KEY, param); jobDetail.getJobBuilder().usingJobData(jobDataMap); 发现无法更新,试过其它几个api发现都不行,没有办法,最后采用了先删除任务再进行创建的方式来迂回实现参数的更新。demo中更新任务有直接修改方式和删除修改方式,区别就在这里。 二、任务的同步和异步 同步和异步在quartz 2.2的版本中对于使用者来说区别只在于是否在job类上添加了@DisallowConcurrentExecution注解。 按时这个特点我们建立两个job的实现工厂类,在其中一个类上添加注解@DisallowConcurrentExecution,然后可以根据添加任务时的参数来确定具体使用哪个: //同步或异步 Class<? extends Job> jobClass = isSync ? JobSyncFactory.class : JobFactory.class; 需要注意在定时任务运行时更新是没有办法改变同步和异步的。 接下来说说我在整合使用时碰到的一些已知问题。 一、更新任务时参数问题。也就是前面说的无法更新任务中传入的参数。 二、同步或异步在定时任务运行时修改是不能改变的,这个在前面也提到了。 三、在定时任务运行时修改,可能会该让任务长时间处于线程阻塞状态,即BLOCKED状态,即使你的任务中只有简单的一行System.out输出。要使它恢复也很简单,删除重建即可。 四、定时任务运行两次的问题。这个也是网上传的最多的问题,这里来着重的说一下。 网上流传引起该问题的原因目前主要有两个说法: 1 spring配置文件加载了多次,导致quartz的bean被实例化多次而导致任务多次执行。 2 tomcat的webapps目录问题。tomcat运行时加载了两次配置文件导致任务多次执行。 这两个说法在我的demo中应该并不存在,但为了验证我也尝试了下。 不使用tomcat,在main方法中用编程的方式启动spring,甚至不使用spring,直接用quartz官方给出的代码: try { // Grab the Scheduler instance from the Factory Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); // and start it off scheduler.start(); scheduler.shutdown(); } catch (SchedulerException se) { se.printStackTrace(); } 问题还是存在,这就说明不是配置文件加载的问题了,这应该是quartz本身存在的一个bug,而且这个多次运行是很有规律的,基本按如下套路走: 定为5秒运行一次,一切正常,没有多次执行现象发生。 定为10秒运行一次,一切正常,没有多次执行现象发生。 定为29秒运行一次,运行时一次正常,一次不正常。 定为59秒运行一次,运行时一次正常,一次不正常。 以上是我实测得出的,再长时间就没测了,毕竟太耗时。在有运行两次的现象时都是间隔的,即一次正常一次不正常这种方式。 既然推断是quartz本身存在bug,那我们又要如何解决这个问题了? 其实在我个人看来,这个问题是无关紧要的,为什么说无关紧要呢?这就涉及到你项目业务设计的是否完善,代码是否健壮了。 一个设计良好的业务方法,特别是那些供外部调用的接口或方法,应该都支持幂等性,何为幂等性?即这个方法同样的参数至少在一个时间区间内,我调用1次和调用10次100次,结果都是一样的。 支持了幂等性,前面说的运行两次的情况是不是就无关紧要了?在有些定时任务为分布式设计的系统(后面会探讨)中,为了确保定时任务的执行甚至会故意人为的去调用两次。 当然支持幂等性最好是在进入方法时就判断,发现已经执行过时就立即返回而不是真的再去同样的结果再执行一遍,以节省资源。
前面我们已经完成了spring 3和quartz 2的整合以及动态添加定时任务,我们接着来完善它,使之能支持更多的操作,例如暂停、恢复、修改等。 在动态添加定时任务中其实已经涉及到了其中的一些代码,这里我们再来细化的理一理。先来看一下我们初步要实现的目标效果图,这里我们只在内存中操作,并没有把quartz的任何信息保存到数据库,即使用的是RAMJobStore,当然如果你有需要,可以实现成JDBCJobStore,那样任务信息将会更全面,貌似还有专门的监控工具,不过本人没有用过: 如上图,我们要先列出计划中的定时任务以及正在执行中的定时任务,这里的正在执行中指的是任务已经触发线程还没执行完的情况。比如每天2点执行一个数据导入操作,这个操作执行时间需要5分钟,在这5分钟之内这个任务才是运行中的任务。当任务正常时可以使用暂停按钮,任务暂停时可以使用恢复按钮。 trigger各状态说明: None:Trigger已经完成,且不会在执行,或者找不到该触发器,或者Trigger已经被删除 NORMAL:正常状态 PAUSED:暂停状态 COMPLETE:触发器完成,但是任务可能还正在执行中 BLOCKED:线程阻塞状态 ERROR:出现错误 计划中的任务 指那些已经添加到quartz调度器的任务,因为quartz并没有直接提供这样的查询接口,所以我们需要结合JobKey和Trigger来实现,核心代码: Scheduler scheduler = schedulerFactoryBean.getScheduler(); GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup(); Set<JobKey> jobKeys = scheduler.getJobKeys(matcher); List<ScheduleJob> jobList = new ArrayList<ScheduleJob>(); for (JobKey jobKey : jobKeys) { List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey); for (Trigger trigger : triggers) { ScheduleJob job = new ScheduleJob(); job.setJobName(jobKey.getName()); job.setJobGroup(jobKey.getGroup()); job.setDesc("触发器:" + trigger.getKey()); Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey()); job.setJobStatus(triggerState.name()); if (trigger instanceof CronTrigger) { CronTrigger cronTrigger = (CronTrigger) trigger; String cronExpression = cronTrigger.getCronExpression(); job.setCronExpression(cronExpression); } jobList.add(job); } } 上面代码中的jobList就是我们需要的计划中的任务列表,需要注意一个job可能会有多个trigger的情况,在下面讲到的立即运行一次任务的时候,会生成一个临时的trigger也会出现在这。这里把一个Job有多个trigger的情况看成是多个任务。我们前面包括在实际项目中一般用到的都是CronTrigger ,所以这里我们着重处理了下CronTrigger的情况。 运行中的任务 实现和计划中的任务类似,核心代码: Scheduler scheduler = schedulerFactoryBean.getScheduler(); List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs(); List<ScheduleJob> jobList = new ArrayList<ScheduleJob>(executingJobs.size()); for (JobExecutionContext executingJob : executingJobs) { ScheduleJob job = new ScheduleJob(); JobDetail jobDetail = executingJob.getJobDetail(); JobKey jobKey = jobDetail.getKey(); Trigger trigger = executingJob.getTrigger(); job.setJobName(jobKey.getName()); job.setJobGroup(jobKey.getGroup()); job.setDesc("触发器:" + trigger.getKey()); Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey()); job.setJobStatus(triggerState.name()); if (trigger instanceof CronTrigger) { CronTrigger cronTrigger = (CronTrigger) trigger; String cronExpression = cronTrigger.getCronExpression(); job.setCronExpression(cronExpression); } jobList.add(job); } 暂停任务 这个比较简单,核心代码: Scheduler scheduler = schedulerFactoryBean.getScheduler(); JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup()); scheduler.pauseJob(jobKey); 恢复任务 和暂停任务相对,核心代码: Scheduler scheduler = schedulerFactoryBean.getScheduler(); JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup()); scheduler.resumeJob(jobKey); 删除任务 删除任务后,所对应的trigger也将被删除 Scheduler scheduler = schedulerFactoryBean.getScheduler(); JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup()); scheduler.deleteJob(jobKey); 立即运行任务 这里的立即运行,只会运行一次,方便测试时用。quartz是通过临时生成一个trigger的方式来实现的,这个trigger将在本次任务运行完成之后自动删除。trigger的key是随机生成的,例如:DEFAULT.MT_4k9fd10jcn9mg。在我的测试中,前面的DEFAULT.MT是固定的,后面部分才随机生成。 Scheduler scheduler = schedulerFactoryBean.getScheduler(); JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup()); scheduler.triggerJob(jobKey); 更新任务的时间表达式 更新之后,任务将立即按新的时间表达式执行: Scheduler scheduler = schedulerFactoryBean.getScheduler(); TriggerKey triggerKey = TriggerKey.triggerKey(scheduleJob.getJobName(), scheduleJob.getJobGroup()); //获取trigger,即在spring配置文件中定义的 bean id="myTrigger" CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); //表达式调度构建器 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob .getCronExpression()); //按新的cronExpression表达式重新构建trigger trigger = trigger.getTriggerBuilder().withIdentity(triggerKey) .withSchedule(scheduleBuilder).build(); //按新的trigger重新设置job执行 scheduler.rescheduleJob(triggerKey, trigger); 到这里,我们的spring3 整合quartz 2的定时任务功能终于是告一段落了,对常用的一些功能进行了实现,相信可以满足一般项目的需求了。