各章节内容简介:

第1章 概述 :平台简介
第2章 构建开发环境:如何构建EIP平台开发环境
第3章 平台架构:技术框架、开发模式等介绍
第4章 快速开始:以简单的案例入手、直接体验如何使用平台
第5章 组件开发:详细介绍如何开发各类组件
第6章 工作流:流程开发和管理
第7章 专题:综合功能和独立功能的讲解
第8章 案例分析:结合案例深入了解平台运行机制
第9章 应用定制:介绍针对具体项目时,可能需要作出的调整
第10章 配置文件:配置文件详解
第11章 微服务五合一部署:如何以一个应用来部署EIP系统
第12章 开发管理:基于EIP开发的项目管理参考

# 1 概述

EIP(Enterprise Information Platform)是基于Java的企业信息平台,平台有以下多种角色:

  • 流程中心
    流程集中式管理,统一的用户组织架构,统一的流程审批,统一的流程消息。业务数据由各业务模块或业务系统管理,业务系统与流程中心之间通过Restful接口集成。

  • 门户集成平台
    提供可视化门户布局、门户栏目设计,通过门户栏目可以方便快捷的集成各个系统的数据进行统一的展示、提供统一入口等。

  • 快速开发平台
    提供业务功能快速开发,以可插拔组件为核心实现业务构建自动化,在可视化环境中创建可观察、可管理的企业级应用。

# 1.1 总体结构图

  1. 负载均衡F5/Nginx
    通过F5或者Nginx提供负载均衡的服务,在部署了多个页面服务时,根据各服务的压力适应性的分发请求到相应的节点上。
  2. 页面服务Nginx
    提供页面服务及后台服务转发,页面服务即系统的前端页面(包括html、JavaScript、css、图片等资源);后台服务转发即对后台的服务(在本平台中按照restful标准提供后台服务)进行转发,当部署了API网关时可将后台服务转发的功能转移到API网关中来实现。
  3. 用户中心Tomcat/Spring Boot Service
    提供统一的用户组织管理功能。
  4. 表单设计和发布 Tomcat/Spring Boot Service
    提供业务建模、表单设计、表单运行发布的功能。
  5. 门户Tomcat/Spring Boot Service
    提供栏目管理、布局管理的功能。
  6. 流程建模Tomcat/Spring Boot Service
    提供流程设计、流程配置的功能。
  7. 流程运行Tomcat/Spring Boot Service
    提供流程发起、待办查询、任务审批、实例管理、历史查询的功能。
名词 角色 说明
F5 负载均衡 硬件实现的负载均衡
Nginx 负载均衡 软件实现的负载均衡
Nginx Web服务器 Apache也可以代替它作为Web服务器
Nginx 转发服务器
Spring Boot Service Java应用微服务 微服务可以直接在安装了jdk的物理机/虚拟机上部署运行;
微服务可以在Spring Cloud或Dubbo搭建的微服务运行环境中部署;
微服务也可以结合docker容器来部署,通过docker镜像的实例化实现部署,可以做到自动伸缩。

# 1.2 运行环境

  • JDK1.8及以上

  • 数据库:MySQL5.7及以上、Oracle 9i及以上、SQLServer 2008及以上

# 1.3 开发技能要求

  • 熟悉Java编程的基本概念:面向对象、接口、继承等;

  • 了解关系型数据库的SQL语法;

  • 有一定的Html、JavaScript、CSS基础;

  • 了解Maven构建项目的基础知识

# 2 构建开发环境

# 2.1 准备开发环境

  • 安装JDK

安装JDK1.8或以上版本,并配置JAVA_HOME变量。

  • 配置Maven

安装Maven并配置好本地仓库的位置,建议在配置文件中添加阿里云的镜像地址提升Maven构建时的效率。
修改如下图所示的配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<settings>
    <localRepository>D:/.m2/repository</localRepository>
    <mirrors>
        <mirror>
            <id>internal-repository</id>
            <name>Internal Repository Manager</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <mirrorOf>central</mirrorOf>
        </mirror>
        <mirror>
            <id>internal-repository-thirdparty</id>
            <name>internal-repository-thirdparty</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <mirrorOf>thirdparty</mirrorOf>
        </mirror>
    </mirrors>
</settings>
  • 安装开发IDE

安装开发IDE工具并配置好Java环境和Maven环境,常见的开发工具有Eclipse、MyEclipse、NetBeans、IntelliJ IDEA。

  • 安装数据库

安装数据库并初始化EIP的四个数据库,数据库的初始化脚本在如下的目录中:

按照如下步骤新建数据库并初始化数据,注意数据库的字符集设置为UTF8,总共有四个数据库需要创建:uc、form、bpm、portal。

# 2.2 导入EIP项目代码

如下图所示,为EIP的整个项目代码结构:

通过如下步骤将整个项目导入到开发工具中:

# 2.3 修改配置文件

导入项目时会自动加载Maven依赖,这可能需要一些时间,等所有的依赖加载完成以后,修改配置文件,配置文件主要有三类:base配置、服务配置和前端配置。

  • Base配置
    Base配置是通用配置,这里的配置五个微服务都会用到,如下图所示:

这个配置文件中配置了很多属性及依赖的中间件,其中redis地址的配置是必须配置准确才能正常启动系统的,如下图所示,其他的配置根据具体情况进行修改。

在实际的开发过程中可以创建多个配置文件,例如:application-dev.yml和application-test.yml,前者用在开发环境后者用在测试环境。
配置文件使用application.yml中的spring.profiles.active属性来切换或者在运行微服务时通过命令java -jar uc.jar –spring.profiles.active=test来切换。

  • 服务配置
    服务配置是指五个微服务的专属配置,例如uc服务的配置文件如下所示,核心配置属性为数据库类型、地址、用户名、密码、驱动程序、端口等属性:

  • 前端配置
    因为EIP为前后分离架构,所以前端再访问后端微服务时需要指定IP/域名和端口,如下图所示,注意前端分为管理端和应用端,相应的配置文件也有两份。

# 2.4 启动项目

  • 项目编译
    如下图所示,对整个EIP项目执行maven install命令即可与完成对整个项目的编译和发布。在开发过程中,修改了任何类库包的代码和配置时都需要重新编译和发布。

  • 启动微服务

编译好整个项目以后,需要启动五个微服务,以uc为例,按照如下步骤来启动微服务:

  • 启动页面服务
    页面服务为纯html项目,无需依赖Java运行环境,所以我们可以使用任何web服务器来启动,这里为了方便,我们仍然使用Eclipse中的配置Tomcat的方式来启动,过程如下:
    首先,确认开发工具中配置了Tomcat环境。

然后在Servers中添加一个新的server并将发布目录指向web项目中的webapp目录,接着启动这个server。现在我们可以在浏览器端访问EIP平台了。

# 3 平台架构

# 3.1 模块划分

在上图中,红色模块为类库包,蓝色的为系统包。在下表中通过对比几个方面来描述两者之间的区别与联系。

类库包(红色) 系统包(蓝色)
角色 不实现具体的业务功能,每个类库包只提供相对单一的通用功能,例如:缓存处理、附件处理、物理表操作等等。 提供具体的业务功能,比如设计一张表单、配置一个流程、添加一个用户等等。
标准 按照Java interface提供接口或者直接提供实现类供其他包使用 按照http标准提供接口服务
原则 1. 可以依赖另外一个类库包,不能依赖系统包;
2. 可以实现另外一个类库包中的接口;
3. 依赖关系只能单向,不能相互依赖,也不能循环依赖。
1. 只能依赖类库包,不能依赖另外的系统包;
2. 需要用到其他系统包的接口服务时,通过调用http服务实现。
建议 1. 每个包职责相对单一;
2. 一个通用功能有多种实现方式时,定义为接口,通过独立的包来实现接口。
有一些类库包的部分功能也需要发布为http接口服务,可以在类库包中实现功能(实现rest controller),在系统包中装配(扫描对应的controller)

# 3.2 技术框架

整个平台采用微服务架构,使用Spring Boot 2.X作为基础框架,其他主要框架如下表所示:

框架 版本 说明
Spring Boot系列 2.0.1.Release 微服务基础框架
Spring Cloud系列 2.0.1.Release 微服务治理框架
Spring Web系列 5.0.5.Release Web框架
MyBatis 3.4.5 ORM框架
PageHelper 5.1.3 分页框架
Druid 1.1.8 数据库连接池
Jackson 2.9.5 Json框架
Groovy 2.1.6 动态脚本框架
Logback 1.2.3 日志框架
Freemarker 2.3.28 模板框架
Swagger 2.8.0 API文档工具框架

# 3.3 分层结构

# 3.3.1 微服务端

微服务端采用了标准的Spring MVC三层结构,由dao manager controller标准的三层构成,另外在类库包中对外提供的接口封装了一层service方便其他类库包或系统包引用。

实体层

如下所示,实体类均继承自BaseModel,并指定泛型主键类型,实体类必须使用swagger的注解@ApiModel说明该实体类的作用,所有的属性都用注解@ApiModelProperty进行描述,对于必填的属性需要在注解中注明required=true,对于有可选值得属性可以指定allowableValues进行值范围约束。

dao层

mapper文件的命名方式为 实体类名Mapper.xml 的格式,统一放在每个包的 src/main/resources/mapper 目录下面。mapper文件中namespace指向dao类,dao的命名方式为实体类名Dao.java,如下图所示:

dao文件为一个接口,不是一个实现类,这个接口继承自MyBatisDao,在MyBatisDao中提供了基础的增删改查方法。dao有两个泛型需要指定,第一个泛型是主键的类型,第二个泛型是对应的实体类。

manager层

manager分为接口和实现类,manager的命名方式为实体类名Manager.java,例如BoDefManager接口和BoDefManagerImpl实现类,其中接口扩展自Manager,实现类继承自AbstractManagerImpl类并实现BoDefManager接口。在接口和实现类中均需指定两个泛型,第一个为主键的数据类型,第二个为对应的实体类。

controller层

controller层的命名方式为实体类Controller.java,继承自BaseController,类上有三个注解:

  • @RestController 表明其是一个restful控制类

  • @RequestMapping("/bo/def/v1/") 标明了其映射的路径

  • @Api(tags="BoDefController") 用以生成swagger在线接口文档

在每一个方法上需要添加两个注解:

  • @RequestMapping(value="list", method=RequestMethod.POST, produces={"application/json; charset=utf-8" }) 该方法对应的地址,该方法对应的restful请求类型及支持的数据格式

  • @ApiOperation(value = "获取bo定义列表(带分页信息)", httpMethod = "POST", notes = "获取bo定义列表" 用于生成swagger文档

列表方法使用QueryFilter作为入参,该参数封装了通用查询和分页信息;返回值为PageList<?>,该参数的泛型为对应的实体类,在该返回值中包含了分页信息和列表数据。
方法可以直接throws异常信息,异常消息的处理和记录会统一在BaseController中进行。

特别说明

  1. QueryFilter和PageBean等辅助查询对象只到manager层,不允许在dao层中使用;
  2. PageBean和Page都是分页辅助类,前者用作分页查询使用,后者为返回分页信息使用,构建分页查询条件时使用PageBean,不允许使用Page;
  3. 实体类需要转为json数据,json数据也需要实例化为实体类,整个项目使用jackson作为json处理框架,不允许再引入gson或fastjson;
  4. 实体类与json的转换过程中,有属性不参与转换时在该属性的get方法上添加@JsonIgnore注解;

  1. 实体类与json转换中,有的属性需要另外命名为其他名称时使用@JsonProperty注解。

# 3.3.2 页面端

前端页面采用单页面应用的结构,样式框架采用Bootstrap3 + Font-awesome图标库。JavaScript框架采用了AngularJs 1.5 + jQuery 3.1.1。
整个系统只有一个index.html是完整的页面,通用的css文件和js文件在该页面引入,如下图所示:

其他的功能页面只有html片段,通过ui-router实现前端路由。功能页面的html代码如下图所示。

在ui-router中注册路由时,可以指定页面需要加载的js、css资源,这里指定的资源采用懒加载的方式,只有访问到该页面的时候才会加载这些资源。

# 4 快速开始

本章将实现两个简单案例,来直接体验使用EIP开发的基本过程。

# 4.1 办公用品库存管理

在微服务架构的体系里面,开发业务功能时应该按照业务需求切分为不同的微服务,例如开发办公用品管理时可以构建一个办公管理的微服务,不过本章节只是一个快速示例,所以我们在已有的微服务:门户(portal)项目中开发这个小功能。至于如果构建一个新的微服务,我们将在后面的章节应用定制中详细描述。

# 4.1.1 生成物理表

按照如下结构在数据库中创建一张物理表:办公用品表

CREATE TABLE office_supplies (
	ID_  varchar(64) NOT NULL COMMENT '主键' ,
	NAME_  varchar(255) NULL COMMENT '名称' ,
	TYPE_  varchar(64) NULL COMMENT '类型' ,
	UNIT_  varchar(40) NULL COMMENT '计量单位' ,
	AMOUT_  integer(20) NULL COMMENT '库存数量' ,
	WORTH_  decimal(20,2) NOT NULL COMMENT '单件价值' ,
	PRIMARY KEY (ID_)
) COMMENT='办公用品';

# 4.1.2 代码生成

创建了物理表以后,通过代码生成器来生成代码,找到tools项目中的codegen目录。

  • 基础配置

如下图所示,是代码生成的基础配置,这里需要配置数据库的连接信息、公司简称、项目简称等信息。

  • 代码生成配置

代码的生成分为后台代码和前台代码,因为前后分离的设计模式,所以代码的生成需要分开生成,分别生成到不同的目录中。
如下图所示,在配置文件中修改开发人员信息、生成到指定目录、连接的数据库表、类名、包名等信息。

相应的,前端代码生成时按照如下图所示修改配置:

  • 生成代码

修改好配置以后,打开ant脚本面板,将codegen目录下的build.xml文件添加到ant面板。

双击生成代码的选项即可进行生成代码的操作,通过控制台面板可以查看生成结果。在代码生成的目标目录,刷新就可以查看到新生成的代码文件。

# 4.1.3 配置路由

代码生成以后,后台的微服务需要重启,前台服务不需要重启,但是在调整Html、JavaScript代码的过程中要注意浏览器端的缓存。
路由的注册需要在系统的菜单管理中进行,如下图所示,例如我要将办公用品管理放置在门户管理这个目录下,则右键门户管理这个选项,在弹出的右键菜单中选择添加资源,注意门户管理的别名是m_portal。

选择添加资源时打开了一个编辑界面,按照如下格式填写办公用品的资源信息,注意别名以上级资源的别名为前缀的,中间用‘.’连接。

模板URL填写上代码生成器生成的list页面地址,在加载资源中common.table.css和tableFixedHeight.js是列表页面的通用资源,固定的引入即可,下面的officeSuppliescontrollers.js是代码生成器生成的当前功能页面所对应的js文件,而且是按照AngularJs的规则生成的js代码,所以在引入该js文件时必须要指定其name,name的值可以查看该js文件获得,如下图所示。

# 4.1.4 访问新功能

配置好路由以后,我们可以访问新的功能了,但是因为菜单资源是在登录时从后端获取并缓存起来的,所以要查看新的功能菜单,我们需要退出并重新登录系统。
如下图所示,我们重新登录以后可以看到门户管理下面新增了一个办公用品管理的菜单,点击即可打开办公用品管理界面。

在数据库表为空的情况下是没有数据可以显示的,我们需要通过新增数据的功能录入一些数据,但是现在新增页面还未注册路由,所以点击新增是没有反应的,我们继续回到菜单管理中添加新增和查看详情的资源。

特别注意: 这里注册的编辑页面的别名是m_portal.officeSupplies.edit,而在列表页面点击添加和编辑按钮时是通过这个别名来找对应的资源的,所以需要将这个别名与js文件中调用的别名保持一致,如下图所示:

现在同样的退出系统重新登录,注意清理浏览器缓存,因为我们刚才修改的js文件可能会被浏览器缓存了。如果开发时使用的是Chrome浏览器,我们打开F12的调试窗口,在network那一栏中勾选Disable cache选项,则只要我们不关闭这个调试窗口,则浏览器就不会缓存任何js和html文件。

如下图所示,我们点击新增按钮即可打开编辑界面了,填写一些测试数据并保存,就可以在列表页面中查看到刚才新增的数据了。

至此,我们就完成了一个简单功能模块的开发,更多的功能我们可以在这些生成的代码上进行调整来实现。

# 4.2 办公用品领用流程

在上面的例子中,我们演示了开发一个功能模块,实现了基本的增删改查功能,现在我们演示开发一个功能模块并与流程进行对接,实现对办公用品的领用流程。

# 4.2.1 实现业务功能开发

开发办公用品领用的增删改查功能与上面的例子一致,这里不再复述,办公用品领用的表结构如下。

CREATE TABLE office_supplies_ask (
	ID_  varchar(64) NOT NULL COMMENT '主键' ,
	SUPPLIES_ID_  varchar(64) NOT NULL COMMENT '办公用品ID' ,
	SUPPLIES_NAME_  varchar(255) NULL COMMENT '办公用品名称' ,
	ASK_MAN_ID_  varchar(64) NOT NULL COMMENT '领用人ID' ,
	ASK_MAN_NAME_  varchar(255) NULL COMMENT '领用人姓名' ,
	AMOUT_  varchar(255) NULL COMMENT '数量' ,
	FLOW_INSTANCE_ID_  varchar(255) NULL COMMENT '关联流程实例ID' ,
	STATUS_  smallint NULL COMMENT '状态' ,
	PRIMARY KEY (ID_)
) COMMENT='办公用品领用记录';

最终实现后的效果如下图所示:

# 4.2.2 配置流程

在流程定义中添加一个新的流程:办公用品领用流程,完成流程图设计、流程节点人员设置等基础配置。
设置流程表单时,我们使用URL表单,URL地址配置为我们的业务功能的页面地址,从而实现流程审批时能够通过URL地址加载表单界面,如下图所示:

URL表单的配置有两种情况:

  • EIP系统页面
    即当前EIP系统的页面,使用EIP的路由机制注册的页面,比如这里我们配置为办公用品领用的编辑或查看页面(注意这里是把办公用品领用这个功能注册在后台菜单中了,如果要把这个功能开放给终端用户使用,需要将这个页面注册到前台菜单中)。

在URL表单中我们将地址配置为相应的别名即可,如下图所示,在别名后面通过括号可以带上参数,参数的格式为JSON格式:{id:”{pk}”,instanceId:”{instId}”}

这里的关键参数有两个:{pk}和{instId},流程启动后,这两个变量分别会被替换成业务数据主键和流程实例ID,这个替换的动作是在后台java代码中完成的,替换的代码如下所示:

  • 外部系统页面
    外部系统页面,则配置为完整的地址,例如http://www.hotent.com?id={pk}, 参数可以通过地址参数来传递。

# 4.2.3 业务中添加启动流程的功能

在办公用品领用的编辑页面添加一个按钮,如下图所示:

这个申请按钮对应的代码如下所示:

发送Ajax请求的代码基本和保存的代码是一致的,只是调用的后台的接口不同。

现在需要在后台添加一个startFlow的restful接口,后台的代码如下所示,与save接口相比只是多了一个注解@Workflow

这个注解有四个参数,其中只有flowKey是必填的,由它来指定要启动的流程,其他参数的作用如下图所示。

其中varKeys中定义的变量key必须同时满足以下两个条件时,才能成功的将业务变量传递给流程做为流程变量。

  • 实体类中有这个属性

  • 流程中定义了同名变量

流程变量的作用是可以用作流程分支条件的判断、跳转规则的判断等等,如下图所示:

# 4.2.4 填写业务数据并发起流程申请

现在打开办公用品领用管理页面点击新增按钮,填写一些业务数据并点击发起申请按钮即可同时保存业务数据和启动流程了。

在流程管理中的任务管理界面即可查看刚才产生的流程任务。

点击处理按钮即可对流程任务进行审批,任务审批界面的表单是来自业务系统的功能页面。

# 4.2.5 在业务管理页面查看流程审批情况

在业务管理页面上,用户需要查看这条业务数据所对应的审批情况,如下图所示:

可以按照如下代码实现,添加这个按钮,而且可以通过ng-if来控制这个按钮是否显示出来。当这条业务数据有对应的流程实例ID(这里是flowInstanceId)时才显示这个按钮,而且点击这个按钮时也需要通过实例ID来查看实例详情。

# 4.2.6 业务数据状态更新

如上图所示,当我们需要在业务数据中记录流程审批状态时,可以通过流程中的接口事件来实现。流程中接口的配置如下所示:

如上图所示,配置接口触发事件来调用业务模块中更新状态的restful接口。业务模块中的接口定义如下:

在流程调用这个接口时,入参是json格式的字符串,包含的属性如下所示:

# 4.2.7 URL表单的数据处理

在配置流程时,我们配置的URL表单是一个查看详情页面,在整个流程审批过程中,用户只能查看表单数据,但是实际的需求中,某些流程节点上是需要对表单进行编辑的,那么如何实现对URL表单的编辑,下面依然分为两种情况来讨论:

EIP系统页面
首先修改流程中绑定的URL表单,从之前的绑定的详情页改为编辑页,如下图所示,这里设置的是全局表单,如果想要实现在某几个指定节点让表单可以编辑,可以通过设置全局表单为详情页面,而且具体的节点表单设置为编辑页面的方式实现。

现在我们打开任务处理界面就会看到表单处于可编辑的状态。

当我们修改了表单数据并点击同意或者保存按钮时,需要更新表单数据,这里会采用两层策略来实现对表单数据的保存。

  • 通过URL表单中Form元素的submit方法
    如下图所示,当URL表单中有Form元素,而且Form元素中配置了ng-submit事件时,表单数据的更新会以submit的方式提交。这层策略的优先级最高,会优先采用这种方式提交。

  • 调用URL表单中指定的js方法
    当表单没有Form元素,或者Form元素上没有配置ng-submit事件时,会获取表单所对应的$scope,判断$scope下有没有save方法,如果有则调用该方法来更新数据。
    特别注意,如果需要在保存URL表单数据的同时将业务数据更新给相应的流程变量,需要保证当前$scope下的数据是以data变量来存放的,例如上述流程定义了两个流程变量:amout和askManId,在审批时如果修改了这两个变量的值,则当保存URL表单数据的同时,如果还需要将这两个变量的值相应的传递给流程变量,则需要确保这两个变量的值存放在$scope.data下面,即能通过$scope.data.amout和$scope.data.askManId能访问到这两个属性。

外部系统页面
外部系统的表单,因为受跨域机制的影响,只能通过iframe中父子页面使用message通讯的方式来实现。任务处理界面作为父页面会发送一个消息给子页面,在子页面可以监听这个消息,当收到这个消息时自己做表单数据更新的操作。子页面监听的代码如下:

同样的,如果需要同步更新表单数据到流程变量中,需要将表单数据传递给父页面,传递的格式如上图所示。
在EIP系统中对URL表单的处理逻辑在以下代码中:

# 4.2.8 URL表单的字段权限控制

在流程审批过程中,根据流程处于不同的节点,需要对表单上的字段实现隐藏、只读、编辑、必填这样的权限控制,可以通过URL地址上的node这个参数来实现。
在流程配置中,不同节点上配置表单时,可以设置node为不同的值,例如配置全局表单时:m_portal.officeSuppliesAskEdit({id:"{pk}",node:"global"})这里的node参数值为global。
在使用EIP系统表单时,传过来的参数会出现在$scope.pageParam属性上,例如按照上面的参数格式,我们就可以分别取到$scope.pageParam.id和$scope.pageParam.node,前者为业务数据主键,后者为当前流程所处的环节。

另外,通过$scope.pageParam.node参数我们可以实现一些显示上的优化,例如,同样是编辑页面,当我们在业务视图中打开时,需要显示标题栏和下方的按钮组,而在任务处理界面显示时需要隐藏标题栏和按钮组,这个效果可以通过$scope.pageParam.node来实现。

而对于外部系统URL表单来说,这个参数是处于URL参数中的,通过js代码可以获取到这个参数,然后自行实现相应逻辑。

# 4.3 流程使用外部系统url表单

# 4.3.1 开发外部系统页面

外部系统的表单,因为受跨域机制的影响,只能通过iframe中父子页面使用message通讯的方式来实现。任务处理界面作为父页面会发送一个消息给子页面,在子页面可以监听这个消息,当收到这个消息时子页面完成表单数据更新的操作。子页面监听消息需要引入流程系统一个JS文件(里面封装了常用交互的监听机制)且在子页面提供对应方法:

截图中的示例为发起流程或处理任务时业务系统表单与流程系统的对接,对接场景是:当在流程发起页面或任务处理页面点击提交或处理按钮时,流程系统判断当前表单是否为URL表单,如果是,则会发送消息给业务系统页面,事件类型为:saveData,hotent.helper.js监听到该事件后,会调用业务系统页面的saveData方法,即示例中最终调用了startFlow方法。startFlow方法保存当前业务数据后,将返回业务主键和业务系统编码并将信息反馈给流程系统。以下是被引入文件hotent.helper.js中封装的部分方法

# 4.3.2 流程配置

http://www.hotent.org:8880/urlform/index.html?id={pk}

{pk} 占位符是存放url表单数据的主键值,在打开审批页面时,{pk}会被替换成相应的主键值打开详情页面,如下图所示。

# 5 组件开发

EIP系统提供了一些组件,用以在开发过程中简洁、快速的实现业务功能,而且使用统一的组件可以规范我们各个功能的实现,方便后期的维护和调整。

# 5.1 数据列表

# 5.1.1 依赖的文件

如下图,数据列表依赖的资源文件,前者控制数据列表的样式,后者实现数据列表固定高度的js运算。

# 5.1.2 标准数据列表的html内容

如上图所示,是一份标准的数据列表对应的html代码,其页面效果如下图所示:

整个列表页面分为三个区域

  • ibox-title为列表头部,放置了功能按钮,快速搜索框,展开折起按钮
  • ibox-body为列表搜索区域,提供列表的精确搜索面板
  • ibox-footer为列表内容区域,提供列表数据、分页按钮、数据条数统计

# 5.1.3 各指令详细介绍

列表源指令

列表源指令为必选指令,在该div元素内的所有其他指令均依赖它,其相当于angularJsng-controller,指令属性如下:

  • ht-data-table 必填,组件指令,值为实例化名称,在当前页面的controller中可以通过$scope.dataTable访问该实例。注意当一个页面中有多个ht-data-table时,实例名称不能相同

  • url 必填,列表页面所对应的数据源

  • target 选填,列表组件为固定高度、固定列头,行数据部分滚动的效果,当列表相对于哪一个父组件固定高度时可以通过target属性来指定,该属性的值为指定元素的class,比如:target="ibox-body"

  • adjust 选填,列表固定高度时的修正值,例如:adjust="-80" adjust="77"

批量删除指令

勾选任意条数的数据后,点击该按钮可以执行删除操作,组件属性如下:

  • ht-remove-array 必填,组件指令,无需带值

  • url 必填,删除操作对应的后台地址

  • remove-key 选填,默认为id,通过列表数据中的该key执行删除操作

  • paramKey 选填,默认为ids,请求发送到后台时声明的参数名

快速搜索指令

通过该组件可以快速查询列表数据,支持同时对多个列进行查询,快速搜索会自动执行查询不需要点击查询按钮或敲击Enter键,组件属性如下:

  • ht-quick-search 必填,组件指令,其值为对哪些列进行查询,比如这里是对alias和description,多个列名之间逗号分隔

展开折起指令

显示为一个三角形箭头,点击可以显示/隐藏精确搜索面板,组件属性:

  • table-tools 必填,组件指令

精确搜索指令

对列表进行精确搜索的搜索框,属性如下:

  • ht-query 必填,组件指令,其值为要搜索的列名

  • operation 选填,默认值like,搜索条件匹配操作符,可选值有:equal, equal_ignore_case, less, great,less_equal, great_equal, not_equal, like, left_like, right_like, is_null, notnull,in, between

  • relation 选填,默认值and,搜索条件与其他条件的组合关系,可选值有:and,or,exclusion 精确搜索需要点击查询按钮,该按钮可以通过指令ht-search触发搜索操作,示例代码如下:

  • 另外提供了重置按钮,该按钮可以通过指令ht-reset实现重置操作,示例代码如下:

表格指令

表格组件,显示列表数据的表格,属性如下:

  • ht-table 必填,组件指令

  • initialize 选填,默认值true,是否初始化查询,当设置该值为false时,表格不会默认加载数据,可以在controller中添加如下代码手动加载数据

  • 或者用$scope.dataTable.query()来加载数据,但是注意,因为ht-table的加载需要一点时间,所以直接在controller中执行$scope.dataTable.query()时可能因为$scope.dataTable还未实例化导致query失败,所以要么在$scope.$on("table:ready")中查询,要么在controller中定义的方法里面通过$scope.dataTable.query()来查询。

表格行指令

表格列组件,通过组件设置列头及列数据,属性如下:

  • ng-repeat 必填,组件指令,该指令为angularJs自带指令,遍历列表数据,dataTable.rows为列表数据,dataTable为列表组件ht-data-table的实例名,rows为固定写法,返回的列表数据会绑定到rows属性上。row为每一行数据,在下面的列组件中会用到,$index为索引,表示当前是第几行数据,注意索引从0开始。

表格列指令

表格列组件,通过组件设置列头及列数据,属性如下:

  • ht-field 必填,组件指令,该列绑定的字段,值格式为row.列名,row与行组件中的row呼应

  • title 必填,列头名称

  • sortable 选填,是否支持排序

  • width 选填,列宽度,绝对宽度,值为纯数字,单位px

  • class 选填,列样式,可以通过该属性设置相对宽度,如class="col-md-2",即为按照bootstrap布局规则占据总宽度的2/12

注意,列组件中有两个特殊列:

  • 行复选框 该列可以不设置ht-fieldtitle属性,行复选框的标准写法如下,其中属性selectable属性为是否显示全选的复选框,ht-select指令为勾选单行数据的指令

  • 行数 该列可以不设置ht-field属性,通过该列在当前分页显示行数据的序号,标准写法如下:

分页指令

通过该组件可以实现对列表数据进行分页查询,不配置该组件时为全列表数据查询,组件属性如下:

  • ht-pagination 必填,组件指令

  • ht-page-size 选填,默认值20,每页条数

  • ht-displayed-pages 选填,默认值5,最多显示的分页页签数量

  • ht-show-total 选填,默认值true,是否统计总数,当设置为false,不会对列表数据统计总条数,当数据量较大时可以提升性能

  • page-changed 选填,分页事件,翻页时触发该事件,通过如下代码可以添加对该事件的监听:

  • 注意参数$pagination为固定写法,该事件的监听函数可以声明在controller中,如下所示:

  • $pagination会映射到入参p上,该参数的值为如下:

  • page-size-changed 选填,每页条数调整事件,通过如下代码可以监听该事件,该事件的监听函数可以申明在controller中,参数为$pagination,为固定写法,参数内容与pageChanged事件的参数一致。

# 5.1.4 数据列表的方法

  • query() 列表查询的方法

  • addQuery(param) 添加查询参数,param格式如下

参数param各个属性的说明如下:

property 字段名,具有唯一性,后添加的查询参数会覆盖前面添加的property相同的查询参数
value 查询值,可以是字符串、对象、数组
operation 操作符号,默认为like,可选值:["equal","equal_ignore_case", "less",      "great","less_equal", "great_equal","not_equal", "like","left_like", "right_like","is_null", "notnull","in", "between"]
relation 查询参数间的组合关系,默认为and,可选值:["and","or", "exclusion"]
  • clearQuery(property) 清除查询参数,传入property时则清除该查询参数,不传时清空所有查询参数

  • getQuerySize() 获取当前的查询参数个数

  • hasSelectedRow() 查询当前列表是否有已选中数据

  • reset() 重置查询条件并执行一次查询

  • selectRow() 获取所有已选中数据,以列表返回

  • selectRowIds(options) 获取所有已选中数据,返回其主键数组,主键默认为id,可以通过传入options来指定主键,例如:

  • removeArray(options) 删除选中数据,options为删除参数

各个属性的说明如下:

url 必填,删除操作对应的后台地址
removeKey 列表数据中通过哪个字段来执行删除操作,默认为id
paramKey 请求后台时,通过什么参数名来传递值,默认为ids,例如:/data/remove?ids=1,2,3

# 5.1.5 数据列表的事件

列表页有六个事件:

  • table:ready 表格初始化完成事件

  • dataTable:ready 列表组件初始化完成事件

  • dataTable:query:begin 列表开始查询事件

  • dataTable:query:complete 列表完成查询事件

  • dataTable:query:error 列表出错事件

  • dataTable:query:reset 列表搜索条件重置事件

对事件的监听可以通过在controller中添加监听函数实现,例如:

如上面的代码所示,监听了列表开始查询的事件,事件有两个参数

  • t 为angularJs广播事件的固定参数
  • d 为dataTable实例

注意:因为有可能在一个controller会存在一个以上的dataTable,而广播事件是通过$rootScope发布的,所以可能会监听到其他dataTable的事件,通过判断dataTablename可以区分是否为所需监听的事件。
为了避免频繁的请求后台数据,当一个列表正在查询数据时,即触发了dataTable:query:begin还未触发dataTable:query:complete时,列表不响应query()请求,这种状态下dataTable.inquiring属性的值为true,通过该属性我们可以实现一些效果,比如将查询按钮,重置按钮的状态置为不可用,查询时显示正在查询``...的过渡效果。

# 5.2 树组件

# 5.2.1 依赖的文件

文件 说明
metroStyle.css 默认使用metro风格的树组件样式
jquery.ztree.min.js zTree库
ng-ztree.js 基于zTree的AngularJs组件库

# 5.2.2 树组件应用

在左树右列表的页面中,左边以树组件展示数据分类、状态的,右边展示位数据列表,当点击左边某条数据时,右边的列表过滤为对应分类或状态的数据,效果如下:

该页面对应的html代码:

上述html代码中,在ul元素中配置了树组件,各参数说明如下:

  • ht-tree 必填,指令属性,其值为zTree的初始化setting,配置项较多,详情请查看zTree官网的API文档

  • ng-model 必填,树组件对应的列表数据

  • tree 必填,树组件初始化后的实例对象

  • tree-events 选填,事件绑定按照name:callback的方式申明,name为zTree支持的回调事件名称,callback为定义的回调函数,zTree支持的回调事件很多,详情查看zTree官网的API文档

对应的javascript代码:

# 5.2.3 树右键菜单

可以通过以下代码为树组件添加右键菜单:

  • tree-context-menu 必填,值为右键菜单项数组,在controller中指定contextMenu的值,例如$scope.contextMenu=['添加子项', '删除目录', '-', '授权'],多个菜单项用逗号分隔,-为分割线

  • before-right-click 选填,右键点击前事件,通过该事件可以实现不同节点显示不同的右键菜单项,例如在controller中通过如下代码实现父节点与子节点有不同的菜单项:

  • menu-click 选填,点击右键菜单项的事件,该事件有两个参数menu和treeNode,前者为当前点击的菜单项,后者为右键选中的树节点。根据这两个参数可以执行不同菜单项的对应逻辑,如下面的代码所示,在该方法中返回false时,不会关闭右键菜单。

# 5.3 提示信息

提示信息使用统一的dialogService服务来实现,该服务作为AngularJs的Service来定义的,使用时需要先注入,按照如下格式:

成功

提示

警告

错误

** 确认**

Tooltip

Toaster

成功消息

警告消息

自定义消息

# 5.4 对话框

需要通过弹框显示某个页面时,可以通过以下代码实现(同样需要注入dialogService服务):

上述代码中,值bo-selector为注册到ui-router中的状态路由,如下:

通过参数{pageParam: {id: 1}}可以传递值到bo-selector中访问,在bo-selector.html页面所对应的controller中可以访问该pageParam属性,另外在子页面中定义$scope.pageSure方法会在点击对话框的确定按钮时调用该方法,而该方法的返回值会返回到父页面的回调方法中。

# 5.5 侧边栏

通过侧边栏也可以显示另一个页面,调用的代码如下:

这个方法中,第一个参数bo-detail同样为在ui-router中注册的状态,第二个参数是配置对象,各配置属性介绍如下:

  • bodyClose 选填,默认为true,默认为点击非侧边栏区域时会关闭侧边栏,设置为false时不会关闭侧边栏

  • width 选填,默认为360px,侧边栏的宽度,可以设置为固定值也可以设置为百分比,如:300px,25%,calc(100% - 250px)

  • pageParam 选填,传递到子页面的参数

另外可以通过dialogService.closeSidebar()方法来关闭侧边栏,整个平台侧边栏一次只能打开一个,所以在主页面或者子页面调用这个关闭方法都会关闭当前侧边栏。

# 6 工作流

# 6.1 流程术语

序号 术语 描述
1 流程定义 流程定义又称流程模型,是用来描述业务过程的规定性文档,一个流程主要由一系列的活动和转移组成。流程定义需要遵从特定的语法规范。ACTIVITI支持BPMN2.0标准规范。流程定义有工作流引擎负责解释执行。
2 流程实例 流程实例是在流程根据流程定义产生的实例,是实例化流程定义。我们说一条流程执行完毕,意思就是流程实例生命周期结束。
3 流程任务 流程任务是指当启动流程实例以后,流程走到用户任务或会签任务时产生的需要用户审批的待办。
4 流程元素 在BPMN标准中,流程元素包括:事件、活动、网关、泳池。
5 流程环节 完成一个流程的设计以后,流程定义中的每一个元素,我们称为这个流程的一个环节。
6 发起人 发起流程实例的用户称为发起人,或者称为流程实例的创建人。
7 执行人 当一个流程任务分配给一个用户来处理时,这个人就是这个任务的执行人。
8 候选人 当一个流程任务分配给多个人来处理时,这些人就是这个任务的候选人。

# 6.2 流程扩展

在平台中我们对Activiti扩展,主要是扩展了流程的监听器,自动任务节点等。
Activiti流程定义为一个xml文件,我们可以在平台中选择流程定义BPMNXML按钮来查看这份xml文件。

我们对流程进行了扩展,添加了一系列通用的事件监听器,这些监听器的执行顺序如下图所示:

序号 事件 事件监听器
1 流程启动事件 startEventListener
2 任务创建事件 taskCreateListener
3 任务完成事件 taskCompleteListener
4 会签任务创建事件 taskSignCreateListener
5 自动任务事件 customServiceTask
6 流程结束事件 endEventListener
7 外部子流程开始事件 callSubProcessStartListener
8 外部子流程结束事件 callSubProcessEndListener
9 子流程开始事件 subProcessStartListener
10 子流程结束事件 subProcessEndListener

在代码中我们可以找到这些事件监听器的注册类:

# 6.3 流程定义扩展

Activiti流程引擎提供了流程流转的基本功能,但是涉及到一些具体的操作,流程引擎没有满足相关功能。我们在引擎的基础上做了很多的扩展,来满足这些功能需求。

如上图所示,为Activiti流程引擎的表结构,其中最左侧部分的两张表位存储信息表、三张黄色的表为流程定义表、六张浅蓝色的表为流程运行时的表、最右侧的八张绿色的表为历史表。
为了扩展更多的流程功能,我们添加了一张表BPM_DEFINITION来保存流程定义的数据。

字段名 字段类型 备注
DEF_ID_ VARCHAR 流程定义ID
NAME_ VARCHAR 流程名称
DEF_KEY_ VARCHAR 流程定义KEY
DESC_ VARCHAR 流程描述
TYPE_ID_ VARCHAR 所属分类ID
STATUS_ VARCHAR 状态。草稿、发布、禁用
TEST_STATUS_ VARCHAR 测试状态
BPMN_DEF_ID_ VARCHAR BPMN - 流程定义ID
BPMN_DEPLOY_ID_ VARCHAR BPMN - 流程发布ID
VERSION_ INT 版本 - 当前版本号
MAIN_DEF_ID_ VARCHAR 版本 - 主版本流程ID
IS_MAIN_ CHAR 版本 - 是否主版本
REASON_ VARCHAR 版本 - 变更理由
CREATE_BY_ VARCHAR 创建人ID
CREATE_TIME_ DATETIME 创建时间
CREATE_ORG_ID_ VARCHAR 创建者所属组织ID
UPDATE_BY_ VARCHAR 更新人ID
UPDATE_TIME_ DATETIME 更新时间
DESIGNER_ VARCHAR 设计器类型
SUPPORT_MOBILE_ INT 支持手机表单

这张表所对应的实体类是DefaultBpmDefinition,其提供的流程定义接口定义在BpmDefinitionManager中。

# 6.4 流程设计器

EIP提供Html5的流程设计器,这个设计器设计出来的流程定义会以Json格式提交给后台的发布或保存流程的接口,在后台以bpm_def_data这张表来保存流程定义的文件。
这张表有两个关键字段:bpmnXml和defJson,其中defJson存放的是Html5设计出来的json数据,而bpmnXml中需要存放的是Activiti所需的遵循BPMN2.0规范的xml格式数据,这里就需要将json转换为xml格式的,我们使用DefTransform接口来转换,对于json格式,我们提供了WebDefTransform这个实现类。

# 6.5 流程授权

流程设计和配置完成以后,需要发布给用户来使用,或者需要将流程的管理权限分配出去,这里就需要通过流程授权来实现。
流程授权界面如下图所示:

存放这些授权数据的表如下所示:

# 6.6 流程插件开发

如图所示,EIP中使用插件来实现流程的扩展功能。系统中包含三种类型的插件:execution(放置所有执行类插件)、task(放置所有任务类插件)、usercalc(放置所有用户计算策略插件),其类图如下:

# 6.6.1 插件的配置

以GlobalRestFulsPluginContext插件为例,在如下图所示的配置界面完成插件的配置。

在提交保存操作时,会将这些配置内容提交到后台,后台的代码会通过如下图所示的过程完成插件的解析和配置保存。

最终,在流程对应的XML文件中,会以如下图所示的片段保存这个插件配置。

# 6.6.2 插件的解析

如上图所示,在流程的配置和流程运行时,会涉及到插件的解析,通过Jaxb框架来实现JavaBean与XML之间的相互转换。

# 6.6.3 插件的执行

插件的执行通过流程事件的监听处理器来触发,如下图所示,流程启动事件、任务创建事件、任务完成事件、流程结束事件等事件中都会将ExecutionCommand设置到事件处理器中。

# 6.7 节点人员

如上图所示,节点人员的可选策略默认包含了以上选项,这些选项由如下的代码配置来限定。

这些策略可以根据需求再进行扩展,扩展的方式按照上一章节介绍的流程插件开发方式进行。不过我们也提供了人员脚本的插件,通过人员脚本可以解决绝大部分的人员查找需求。
在流程运行过程中,产生了待办任务以后,将这个待办分配给用户时,会通过以下代码完成待办任务的分配。

# 6.8 流程跳转

如果流程节点上配置了跳转规则,在该节点对应的待办任务审批时,会按照从上至下的顺序依次判定跳转规则是否成立,成立时就会跳转到规则指定的目标节点。
如下图所示,在下面的代码中对跳转的规则进行计算,如果返回了目标节点,则流程会跳转到该目标节点。

# 6.9 流程事件脚本

流程提供了事件切入口来执行脚本,这些事件包括:流程启动、任务创建、会签任务创建、任务完成、流程结束、子流程启动、子流程结束这些。
如下图所示,分别是任务类、活动类两类的监听器来触发事件脚本的执行。脚本执行过程中的上下文变量,通过vars变量传入。

# 6.10 流程表单

表单的设置与保存通过如下代码完成。

流程绑定了表单以后,在流程启动、待办审批、保存表单数据时,会对表单的数据进行保存和更新的处理,如下图所示。

# 6.11 审批时限

审批时限的设置通过下列属性进行设置。

在设置了审批时限以后,在产生待办任务时,会根据审批时限类型及时长计算这个待办任务的到期时间,并将其设置到任务到期时间这个字段上。

# 6.12 流程催办

流程的催办现在由定时任务来触发,在触发时会查询当前需要进行催办的数据,并关联到对应的待办任务,对相应的待办审批人发送催办消息,如果符合了到期规则,则会相应的执行到期动作。

# 6.13 流程操作按钮

序号 操作名称 别名 操作说明 功能分类 是否默认
1 启动 startFlow 起草
2 同意 agree 待办
3 流程图 flowImage 实例
4 打印 print 实例
5 保存草稿 saveDraft 起草
6 抄送 instanceTrans 抄送时产生知会任务,审批记录中挂载到当前的审批环节。 待办
7 沟通 startCommu 沟通时产生知会任务,沟通发起时,流程实例处理哪个环节,审批记录中就挂载到这个环节。 实例
8 流转 startTrans 待办
9 驳回 reject 驳回分为驳回上一步、驳回发起人、驳回指定节点。 待办
10 反对 oppose 只在会签时出现。 待办
11 终止 endProcess 实例
12 反馈 commu 对沟通进行反馈,注意:对于沟通任务,不做反馈也不会影响流程的审批。 待办
13 转办 delegate 待办
14 锁定和解锁 lockUnlock 当节点为用户任务,而且有多个审批候选人时,显示该按钮。 待办
15 任务延期 taskDelay 待办
16 征询 inqu 当前待办任务流转到指定的被征询人那里,被征询人处理后回到本节点。 待办
17 回复 inqu_reply 对征询进行回复。 待办
18 跟踪 follow 跟踪指定节点,当指定节点完成审批时产生知会任务。 实例
19 加签 addSign 待办

流程的操作通过构建操作命令来实现,命令对象的类图如下所示:

在执行不同的操作时,通过不同的参数构建不同的命令对象来执行相应的流程操作。以启动流程为例,构建启动流程的命令并执行流程发起的操作,如下图所示:

# 7 专题

# 7.1 认证机制

# 7.1.1.1 Jwt认证机制

JSON Web Token(JWT)是一个非常轻巧的规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。它是基于RFC 7519标准定义的一种可以安全传输的小巧和自包含的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对其进行签名。

认证时序图

如上图所示:

  1. 用户导航到登录页,输入用户名和密码,进行登录

  2. 服务器对登录用户进行认证,如果认证通过,根据用户的信息和JWT的生成规则生成JWT Token

  3. 服务器将该Token字符串返回

  4. 客户端得到Token信息,将Token存储在localStorage、sessionStorage或cookie等存储形式中。

  5. 当用户请求服务器API时,在请求的Header中加入 Authorization:Token。

  6. 服务端对此Token进行校验如下图,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出响应结果,如果不通过,返回HTTP 401。

  7. 用户进入系统,获得请求资源

相应代码

模块 文件 说明
base com.hotent.base.controller.
AuthenticationRestController.java
createAuthenticationToken方法认证用户,认证通过返回jwttoken 到前端
web manage\js\services.js Login 方法进行了用户登录请求,登录成功后将返回的信息(包含了jwt token)保存到$sessionStorage中,并且给请求都设置默认的 Authorization
base com.hotent.base.conf.
FeignConfig.java
服务之间的调用通过将用户请求的 header中的信息 Authorization ,设置到feign请求中
base com.hotent.base.filter.
JwtAuthorizationTokenFilter.java
服务端对此Token进行校验(包含两种token jwt和basic)

# 7.2 单点登录

单点集成原理

本系统集成了cas单点登录,由于系统是采用前后端分离的系统架构,在这里采用的是restful接口来进行单点登录认证的,所以单点服务器需要添加jar来支持restful接口认证。

在此以5.2.6版本讲解, 需要给cas server 添加支持restful接口认证的jar如上图。在本系统是通过restful接口去单点服务器认证用户,如果用户认证成功则返回jwt token给前端作为下次请求的认证信息。

在application-dev.yml中配置单点登录信息,cas. useSecurityCas 的值为false / true false 值不使用单点登录, true使用单点登录。cas.server.url 为单点登录的服务器地址

在前端需要改在的地方, 当判断用户还没有登录系统时, 前端会请求后台,获取 sso.enable配置,如果为false , 这直接路由到原来的登录页面, 如果为true,则重定向到单点登录的服务器中。

登录后再重定向到前端页面,逻辑代码在sso.js中,sso.js作为前端的统一入口,通过以下逻辑代码可以实现单点登录的跳转。

完成单点登录后,在URL地址中会携带token字符串,此时会进入router的路由守卫,如下图所示,token会被提交到validAndCompletedCurrent方法中。

在validAndCompletedCurrent方法中,会根据token是否有值来判断是否调用后台的Sso方法完成单点认证并获取到jwt。

# 7.3 权限控制

# 7.3.1 后台权限控制

将权限管理设计为独立应用,对所有的微服务的资源,和授权统一在portal服务中统一维护。在base 模块中通过接口获取权限信息后实现权限控制。

架构设计

如下图所示,权限管理提供界面维护两方面的信息。

  • 资源信息,包括页面资源和功能资源

  • 授权信息, 主要是维护角色和资源信息的映射关系,另外以接口方式提供权限信息查询。

base\src\main\java\com\hotent\base\security\HtInvocationSecurityMetadataSourceService.java 该类是返回当前请求地址,需要哪些角色的权限才能请求当前接口请求的。

base\src\main\java\com\hotent\base\security\HtDecisionManager.java 决策类,用于判断当前用户是否有权限访问当前的接口(后台请求地址)

时序说明

如下图所示,对权限控制的过程进行了说明,在第6步时实现了对前端显示的权限控制,同时权限信息已缓存到业务系统中,为了实现更安全的权限控制,在后续的用户操作中,仍可在后端对用户的动作进行权限判断。

数据库设计

如下图所示,设计了三张表来存放权限信息。

  • 页面资源表 页面资源进行严格权限控制,即未授权的页面资源,用户无法访问,用户登录时通过接口返回用户的页面资源信息。

  • 功能资源表 功能资源进行松散权限控制,即未维护到功能资源表中的功能,用户都可以访问,只有维护到功能资源表中的功能才进行权限验证,拥有权限的角色才允许访问。

  • 授权信息表 授权信息表中的页面资源别名和功能资源别名分别来自页面资源表和功能资源表和角色类表中的别名。

# 7.3.2 前端按钮权限控制

时序图

设计以及相关代码

根据后台权限控制的基础,实现前端权限按钮控制显示和隐藏。 用户登录后,返回用户权限信息到前端,前端再通过angular 指令去判断用户是否有相应的功能权限,如 ht-method-auth=”uc_role_remove”,uc_role_remove 为功能资源的别名, ht-method-auth指令会判断如果功能资源具有 uc_role_remove资源配置,并且当前用户是没有uc_role_remove 功能资源授权的角色则页面将隐藏功能按钮

文件 说明
web\src\main\webapp\manage\js\directives.js htMethodAuth 方法实现了按钮权限的控制
web\src\main\webapp\manage\js\services.js getCurrentUserMethodAuth 方法实现了用户登录后获取用户的权限信息, 用于给ht-method-auth指令做判断使用

# 7.3.3 数据级别的权限控制

系统已经实现了对页面资源、功能资源的维护,通过角色与资源的授权,可以实现不同的用户登录系统时,根据其拥有的不同角色来控制其能访问的页面和能使用的功能。

现在要更进一步,做到数据级别的权限控制,例如:现在有技术部工程师、技术部经理两个角色的用户,他们都需要查看报销列表这个页面,但是要控制工程师只能看到本人的数据,经理能看到技术部的所有数据,这就是数据级别的权限控制。
如下图所示,在维护权限映射时,映射有四种策略:本人、本部门、本部门及下属部门、指定部门。根据不同的角色选择不同的授权策略,四种策略任选其一。

表结构支持

要实现数据权限控制,需要表结构中有相应字段来支持,例如上面的四种策略需要记录这条数据的创建人、创建人所属部门才能实现。
EIP系统中的所有实体类均继承于BaseMode,我们在设计BaseMode实体时已经设计了一些公共字段,如下图所示:

在开发新的业务功能时,我们只需要在设计物理表时,添加这两个列就可以了,注意列的命名,按照EIP系统的命名规范,数据库列名为:create_by_和create_org_id_,这样在代码生成以后无需修改mapper文件,如果不按照这个命名则调整mapper文件也可以。

后台代码支持

因为数据权限控制是需要牺牲一部分性能的,而大部分模块是不需要数据权限控制的,所以我们按照约定优于配置的原则,对于需要做数据权限控制的模块添加上注解来实现数据权限控制,其他未添加注解的模块不做数据权限控制。
例如,对于用户角色列表这个方法,如果要实现数据权限控制,我们需要在这个方法上添加DataPermission注解。添加了这个注解以后我们会通过AOP来实现数据权限控制,AOP的特性让我们只需要编写通用的控制逻辑,后续开发新的业务模块时,我们唯一需要修改的代码就是添加这个注解即可。

资源维护

在资源维护界面,我们维护一个列表页面时,只维护了其html页面,对应的后台restful接口地址没有维护进来,所以需要实现数据权限控制,我们还需要在列表页面中添加一个功能资源指向这个后台功能。

角色授权资源

维护了功能资源以后,在角色授权资源界面,可以针对功能资源进行数据权限控制授权。

代码实现

首先,如下图所示,系统会初始化角色的资源授权关系到缓存中。

当用户访问某个Controller层的方法时,如果这个方法上添加了DataPermission注解,则会被Aop方法拦截,被拦截到的请求,如果传递了QueryFilter对象,则会在这个查询对象中根据数据过滤规则添加新的过滤条件,例如:添加createBy等于当前用户、createOrgId等于当前用户所属的组织等。

# 7.4 菜单管理和路由注册

# 7.5 消息队列

EIP7中集成了消息队列,目前队列在EIP7中的用途主要有两种:一种是记录日志(包括操作日志和异常日志),另一种是流程中发送流程消息。
MQ分为生产者和消费者,我们将生产者的接口定义在base模块中,如下图所示,这样在所有的微服务中都可以调用接口来发送消息到MQ中。
当然,前提条件是这个微服务引入了接口的实现类,EIP7默认提供了activemq作为实现,也可以更换为其他的中间件作为实现类。为了设计上更加松耦合,这个JmsProducer的接口我们提供了一个空实现类JmsProducerEmptyImpl,这样在微服务中不引入activemq这个类库包也可以正常运行,只不过消息没有正确的发送到mq队列而已。

MQ的消费者定义在portal这个微服务中,如下图所示,目前在消费者中针对上述两种消息分别进行处理。对于流程消息通过JmsHandler找到对应的处理器来处理(邮件、手机短信、微信消息),对于系统日志直接记录到系统日志表中。

# 7.6 内存数据库

EIP7中为了尽量提升系统的性能,将很多数据缓存到内存数据库中,不过这些数据都是热数据,即可以随时清理的数据(程序读不到内存数据库中的数据时会再次从数据库中加载数据并缓存起来)。
内存数据库的操作接口定义在base模块中,如下图所示,同样的,为了避免对ICache接口的实现类产生强依赖,我们提供了MemoryCache作为默认实现类,但是要注意MemoryCache以Map来存放数据,只在当前单个微服务中生效。

另外,为了实现对缓存数据的批量操作,我们继承ICache接口在redis这个模块中定义了RedisService这个接口,在EIP7中主要对国际化的资源进行批量操作。而基于Redis内存数据库作为中间件的实现类,是EIP7系统提供的默认实现,同样的,我们可以更改为其他的中间件作为实现类。
另外要注意,默认的redis连接为单节点的,在生产环境中,如要保证系统的高可用,需要将redis部署为集群模式(哨兵模式),那么对应的连接redis的代码需要做相应的修改,采用哨兵模式来连接redis。

# 7.7 统一日志

系统统一记录了所有用户的操作的日志,方便管理员查看请求记录。

实现方案

在系统中,添加了AOP 切面 com.hotent.base.aop.SysLogsAspect类, 拦截所有的请求,并且具有@ApiOperation 注解的请求,拦截到的请求在sysLogs方法中记录用户的操作日志。

已有微服务

在uc,portal,form,bpm-model,bpm-runtime这五个微服务中, 只需要在Controllers层添加@ApiOperation 注解,就会将用户的操作记录日志到系统日志表portal_sys_logs,在系统日志中可以查看如下图

新加的微服务

处理和已有微服务的处理方式一样(在controllers层代码添加@ApiOperation 注解)还需要修改 web\src\main\webapp\manage\views\log\LogRecordList.html 页面,在页面上添加搜索条件,该条件为新增的微服务的名称,用于分微服务来归类系统的操作日志。

修改完页面后需要在数据库表portal_sys_logs_settings插入一条数据,该表时用来配置日志信息的, 配置日志是否开始记录日志功能,日志保存时间。

如:INSERT INTO portal_sys_logs_settings (ID_, MODULE_TYPE_, STATUS_, SAVE_DAYS_, REMARK_) VALUES ('5', 'bpm-runtime-eureka', '1', '365', '流程运行微服务')。
bpm-runtime-eureka (MODULE_TYPE_)为新的微服务的名称
spring.application.name

添加定时任务删除系统操作日志

系统运行时间长了,系统日志表的数据将会越来越多,这张表的查询也将会变得越来越慢,这种情况需要定时地去清理一些以前的操作日志。在系统中,是通过配置定时任务来删除系统操作日志的。

添加定时任务

com.hotent.portal.job.SysLogsJob

添加计划任务

可以添加每个一周清理一次操作日志,会根据系统的存留时间来清楚系统日志。

# 7.8 国际化

# 7.8.1 国际化实现方案

前端页面,js文件是使用angular实现国际化方案。

引入相关文件
需要在首页index.html页面引入angular-translate.min.js, angular-translate-loader-static-files.min.js,如下图

注入translate模块
在app.js文件中的eip module 中注入pascalprecht.translate 模块,如下图:

在config.js 中配置
在config.js 中的config 方法添加使用如下方法, 需要在config方法中注入 $translateProvider

在controllers.js中的MainCtrl设置切换语言的方法
目前是在front 实现了国际化, 和语言的切换

需要在controllers.js中的MainCtrl方法添加国际化的处理。去切换语言

需要注意的是,切换语言使用$translate的use方法说明使用哪个国际化的资源文件。 如 使用i18n-zh-CN文件 , 改文件的路径是在config.js 中配置的

如当切换的语言问中文(zh-CN) , 则js , html 的国际化资源将会获取 js/i18n/i18n-zh-CN.json文件翻译系统

# 7.8.2 静态资源国际化维护

系统内置了三种语言的国际化, 简体中文(zh-CN),繁体中文(zh-TW), 英文(en-US),国际化资源都是json结构的。
如果需要再添加一种语言,则需要在 国际化》语种管理 添加一条语种

如添加韩文(ko-KR) 然后在js/i18n文件夹中添加i18n-ko-KR.json 文件, 并且把i18n-zh-CN.json的内容复制到i18n-ko-KR.json文件中,然后key 不变,修改value值为对应的韩文。

# 7.8.3 Html页面如何使用国际化资源

在html 页面中可以通过如下实现

{{ key |translate}} key 为国际化资源json文件的对应的key 值  
如:{{'login'|translate }}    {{ 'logout'| translate }}

可以参考 \web\src\main\webapp\front\views\common\chooseStyle.html 页面,切换皮肤功能页面

# 7.8.4 Js如何使用国际化

在js方法中注入$filter,使用$filter获取国际化资源如

$filter('translate')('edit_approval')
使用angular 的filter获取国际化资源。

# 7.8.5 Java 代码使用国际化

Java 代码使用的国际化资源时在资源管理中维护的,这些数据在数据库中有一份数据, 使用的数据全部从redis 缓存中获取, 后台的代码返回的国际化数据通过I18nUtil类来获取国际化资源

I18nUtil.getMessages(i18nKey, getLocale()); i18nkey,是要获取的国际化资源的可以,对应资源管理中的 资源key, getLocale()获取当前语言。在前端切换语言时,会将语言存储到localStroage中,在config.js 中的run方法中添加如下代码,用于设置当前请求接受何种语言。getLocale()方法将会获取语种。

如果其他模块没有I18nUtil 工具类,需要引入i18n模块

# 8 案例分析

# 8.1 微服务流程中心

如下图所示,EIP作为微服务架构体系,可以采用微服务的方式来构建流程中心。流程微服务注册到Eureka中,各个应用服务通过Feign Interface来调用流程微服务。
这里的Feign Interface只需要定义接口方法,并声明接口要调用的微服务的名称,在实际调用时就会自动通过Eureka寻找可用的微服务节点并完成调用。而且Feign会自动继承认证信息,同步接口调用异常,避免分布式事务问题。

示例代码如下图所示,只需要定义这份Feign Interface在应用服务中,需要在业务逻辑中启动流程时,注入这个Interface然后调用start方法即可完成流程的启动。

# 8.2 统一待办门户

一个企业中,可能有多个系统都会产生待办任务,用户需要在各个系统中进行待办处理,操作繁琐、而且容易遗漏。通过构建统一的待办门户可以解决这个问题,如下图所示,将各个系统的待办任务通过MQ统一到一起展示。

各个业务系统产生待办、完成待办时,通过MQ将待办消息发送给EIP系统,EIP会根据消息类型在数据库中对待办数据进行保存或删除等操作。用户在通过EIP的待办门户查询待办时,查看到的待办任务就是来自所有系统的待办任务,当他需要对某个待办进行查看或处理时,会跳转到对应的系统完成待办处理。

# 9 应用定制

根据项目需求,可能需要将一些功能打包在一起,以一个独立的应用来发布,所以在这个章节,我们会介绍如何进行应用的定制开发。

# 9.1 定制微服务应用

# 9.1.1 创建微服务

EIP7自带的微服务为五个,但是以系统的类库包为基础,我们可以开发出第六个、第七个微服务作为业务应用。如下图所示,在项目根目录上右键选在新建一个Maven Module,项目会自动作为EIP7的一个子模块创建。

点击Finish按钮后,新创建的maven模块会自动引入到Package Explorer中,如果没有自动引入,也可以在根目录中找到fsc目录,右键菜单中点击import完成引入。

# 9.1.2 Maven依赖

新添加的Maven模块需要以EIP7的子模块定义在pom.xml的modules列表中,所以如下图所示,确认一下已经引入fsc这个模块,注意:maven会按照modules中自上至下的顺序依次编译,所以确保fsc依赖的其他maven模块生命在fsc之上。

对于fsc模块,其pom.xml文件如下图所示,依赖配置中,spring-boot-starter-test为单元测试的依赖包。

uc-api-impl为eip7的基础依赖包,其依赖关系如下图所示:

  1. uc-api:这个包中定义了基础的用户、用户组接口模型,以及获取用户、用户组的相关接口;

  2. base:封装了登录认证、数据库连接、基础的CRUD方法、缓存操作等等(详情请查看API简介章节);

  3. uc-api-impl:uc-api的默认实现包,会通过feign调用微服务uc的相应接口来获取用户、用户组数据。

另外build命令中集成了spring-boot-maven-plugin插件,该插件提供微服务打包为可执行jar的作用,并定义了微服务应用的唯一入口,便于我们在开发工具中启动应用并进行开发及调试。
以上配置为微服务应用的基础maven配置,其余的依赖包或build插件根据模块的需求相应的添加。

com.hotent.fsc.Application为微服务的唯一入口,其代码如下所示:

在这个类上面有5个注解,起作用分别如下:

  1. SpringCloudApplication:表示这个应用是一个Spring Cloud应用;

  2. Configuration:表示该类是一个配置类;

  3. MapperScan:因为系统使用MyBatis作为框架,所以需要扫描mapper文件,该配置指定扫描哪些dao所对应的mapper文件,如果应用中开发的dao存放在其他的namespace下,则相应的添加到这里,多个namespace以逗号分割;

  4. ComponentScan:需要扫描到spring容器中的java bean,同样的有不同namespace下的类需要扫描时,均需要声明在这里;

  5. EnableFeignClients:表示系统是一个feign的客户端,需要扫描装配所有的FeignClient注解修饰的接口类。

# 9.1.3 配置文件修改

微服务需要定义一个名为application.yml的文件作为配置文件,最终该配置文件会和base模块中定义的application-dev.yml合并为一个文件作为该微服务的配置文件。当这两份配置中有相同的配置项时,以application-dev.yml中的为准。
base模块中的application-dev.yml中定义了一些通用配置项及中间件的连接信息。
各个微服务模块中的application.yml中则定义了该微服务的独有的配置项,其配置如下图所示,注意红框标识出来的内容,需要根据不同的微服务修改这些配置(每一个配置项的作用请查看章节配置文件来了解详情)。

# 9.1.4 微服务之间的调用

微服务之间的相互调用通过eureka实现服务的自动发现与注册,通过feign来实现微服务间的接口调用。Feign的接口定义在base模块中,如下图所示,以每个微服务定义一个接口,将微服务中可能被其他服务调用的接口声明在这个接口中,而且在base中定义了对于这个接口的默认实现(默认实现中直接抛出接口调用异常,因为正常情况下,feign会通过代理类调用到对应的微服务的接口,如果没有正常调用到就会调用到这个默认实现类)。
新开发的微服务,如果也有接口要给其他微服务调用,可以同样的在base中定义一个feign的接口。

# 9.2 应用后端开发

应用的后端开发,可以参考快速开始章节中的办公用品库存管理的开发样例。只不过生成出来的代码就放置在fsc这个新的微服务中了。

# 9.3 应用前端开发

应用的前端开发分为管理端和应用端。

# 9.3.1 管理端

管理端的技术栈是AngularJs,其开发过程可以参照快速开始章节的办公用品库存管理的开发样例,唯一不同的地方是:样例中新开发的功能放置在portal这个微服务中。这里生成出来的代码在fsc这个新的微服务中,所以需要修改前端调用后端的地址。如下图所示,在app.js中添加一个新的微服务的地址,而且在相应的前端html页面和js文件中,修改请求后台的url地址前缀为这个fsc的地址。

# 9.3.2 应用端

应用端的技术栈为Vue,在Vue中要构建一个功能模块的增删改查页面,需要按照以下过程进行:

首先在如下图所示的js文件中声明一个新的后端微服务地址:

开发调用后端接口的api

如下图所示,仿照uc.js在api目录中添加一个新的js文件,将需要跟后台交互的接口定义在这个js文件中。

开发访问api的vuex

系统使用vuex来管理数据,如下图所示,仿照user.js开发fsc的vuex,在vuex中通过调用上一步定义的api来与后台进行数据交互。注意vuex中的数据具有全局唯一性,而且在页面跳转时,vuex中的数据会保存自己的当前状态,所以在开发的过程中要注意数据的生命周期。

在开发调试过程中,建议安装Vue的Chrome插件,可以很方便直接的查看vuex中的数据。

开发列表页面、编辑页面、详情页面

Vue的开发中很好的遵循的组件复用的原则,所以在开发各个功能模块时,对于可复用的组件或页面,我们可以定义在components目录中,在其他功能模块中可以复用这些组件或页面。而具体的列表页面、编辑页面、详情页面可以定义在Views目录下,如下图所示,为待办任务的列表页面。

挂载vuex到页面中

在页面中要挂载Vuex的数据,按照如下图所示的方式完成,首先从vuex中import出mapState和mapActions。然后在computed中声明数据来自哪些名称空间下的哪些属性,当页面加载或者用户执行了某个操作时,通过store的dispatch命令来触发Vuex中的一个mutations,这样会触发Vuex中的数据更新,同样的在页面上就会自动更新相应的数据(Vue的开发核心理念就是这种单向流的数据状态管理模式)。

注册页面到路由中

开发好的页面,需要在router.js中注册路由,如下图所示:

# 10 配置文件

# 10.1 系统配置文件

系统的配置文件由两份构成,在base项目中定义了通用的配置项,如下图所示,application-xxx.yml格式指定了在不同环境中应用的配置文件。

在具体的微服务中,定义application.yml来配置该微服务所需要的配置项。在运行这个微服务时,最终的配置文件会将base中的配置文件(多份配置文件选择哪份由spring.profiles.active属性来决定,例如下图中这个属性为dev,则使用base中的application-dev.yml)与当前微服务的配置文件合并为一份。
合并时,application-xxx.yml的优先级会高一些,在两份配置文件中有相同配置项时,优先使用application-xxx.yml中的配置项。

# 10.2 日志配置

日志的配置通过base中的logback-spring.xml来进行配置。

# 10.3 SpringCloud的优化配置

# 10.3.1 Eureka参数调整

如下图所示为Eureka的配置参数

  1. enable-self-preservation为自我保护模式,默认为true,这里建议设置为false。设置为false时关闭自我保护,自我保护模式其作用是当eureka中注册的微服务大量下线时会触发,eureka会认为这些微服务仍然在线,导致在eureka中看到这个微服务是正常的,但实际却调用不到。

  2. eviction-interval-timer-in-ms清理间隔,建议设置为15秒。

  3. fetch-registry是否从其他的eureka上获取所注册的微服务的信息,如果eureka部署了多个节点,建议设置为true,而且defaultZone指向另外部署的eureka服务。

# 10.3.2 Feign参数调整

在微服务中配置的Feign参数如下图所示:

  1. connectTimeout是连接超时时间,根据服务器的性能和网络状况来调整,在较好的情况下,建议设置为5秒,较差的情况下可以设置为15秒;

  2. readTimeout为读取数据的超时时间,建议设置为connectTimeout的两倍。

# 10.3.3 Hystrix参数调整

Hystrix的参数配置如下图所示:

  1. maxConcurrentRequests为允许的最大并发请求量,根据系统的使用情况来调整,并发量较高时相应的调大这个参数。

  2. timeoutInMilliseconds为服务熔断的超时时间,特别注意这个参数和下面的Ribbon超时时间有关系,这个时间必须大于等于Ribbon超时时间乘以重试次数。

# 10.3.4 Ribbon参数调整

Ribbon是微服务间相互调用的负载均衡模块,其参数配置如下:

  1. connectTimeout和readTimeout为连接超时时间与读取超时时间,与上面的Feign参数一样,根据服务器性能和网络状况进行配置。

  2. MaxAutoRetries为调用某个节点超时后最大重试次数(可以看做调用第一个可用节点)。

  3. MaxAutoRetriesNextServer为调用另外一个可用节点的最大重试次数(可以看做换一个可用节点来重试,所以如果Eureka中的微服务可用节点只有一个时,这个参数是不生效的,因为没有另外一个可用节点来重试了)。

  4. Hystrix中的timeoutInMilliseconds的时间必须大于等于这里的readTimeout × (MaxAutoRetries + MaxAutoRetriesNextServer)

# 11 微服务五合一部署

在开发和部署时,按照五个微服务来分别部署运行会占用较多的内存,而且还需要部署Eureka服务实现微服务的注册与发现,所以不想以微服务来进行部署时,可以将五个服务合并为一个服务部署。五合一部署时,系统完全移除了Spring Cloud的依赖,不再需要Eureka服务。
如下图所示assembly项目为五合一的项目,其pom.xml中依赖了五个微服务,而且移除了spring Cloud相关的依赖。

微服务之间的相互调用通过Feign完成,所以在assembly项目中还需要提供对Feign接口的实现类。在实现类中,直接注入各个微服务的Controller实例来实现相应的接口。

五合一时,只能连接一个数据库,所以在部署时需要将四个数据库初始化到一个数据库里面。 多个微服务合并到一个assembly项目中,有些实现类可能会存在冲突,我们通过定义了一个注解IgnoreOnAssembly来标记这些冲突的实现类,在assembly项目的入口类中,在扫描类时忽略被注解IgnoreOnAssembly标记的类。

在打包时,我们需要修改五个微服务的pom.xml文件,注释掉生成jar包的部分。

另外,确认在项目的根目录下pom.xml文件中,assembly项目处于未注释掉的状态。

# 12 开发管理

# 12.1 编程规范

# 12.1.1 重要性

编码规范对于程序员而言尤为重要,有以下几个原因:

  • 一个软件的生命周期中,80%的花费在于维护

  • 几乎没有任何一个软件,在其整个生命周期中,一直由最初的开发人员来维护。

  • 编码规范可以改善软件的可读性,可以让程序员尽快而彻底地理解新的代码

  • 如果你将源码作为产品发布,就需要确任它是否被很好的打包并且清晰无误,一如你已构建的其它任何产品

为了执行规范,每个软件开发人员必须一致遵守编码规范

# 12.1.2 注释

Java程序有两类注释:文档注释(document comments)和实现注释(implementation comments)。
文档注释:由/** .../界定。文档注释可以通过Javadoc工具转换成HTML文件。
实现注释:也称普通注释。用以注释代码或者实现细节。普通注释是为了帮助开发者和阅读者更好地理解程序,不会出现在API文档中。其中,单行注释以“//”开头;多行注释以“/”开头,以“/”结束。
注意特别是以下代码必须写好注释:
接口注释、类注释,示例如下:

/**
 * 缓存操作接口
 * <pre>
 * 定义了增加缓存,删除缓存,清除缓存,读取缓存的接口
 * \<pre>
 * @company 广州宏天软件股份有限公司
 * @author  heyifan
 * @email   heyf@jee-soft.cn
 * @date    2018年4月9日
 */
public interface ICache<T extends Serializable> {

}

使用Eclipse作为开发工具时,可以设置注释模板,设置了模板以后在接口或类文件头部输入 /**然后回车Enter即可生成注释,设置注释模板如下图所示:

如果生成的注释中,日期格式不是中文格式,修改eclipse.ini文件-Duser.language=zh-cn。

方法的注释,示例如下:

/**
 * 添加缓存
 * @param key       缓存Key
 * @param obj     缓存值
 * @param timeout 保存时限(单位:秒)
 */
void add(String key, T obj, int timeout);
/**
 * 将查询条件生成为SQL语句
 * @param isFirst    当前是否为第一个查询条件
 * @param clazz        查询条件所对应的实体类
 * @return        SQL语句
 * @throws SystemException
 */
@SuppressWarnings("rawtypes")
public String toSql(Boolean isFirst, Class clazz) throws SystemException {

}

方法的作用,复杂时要配合<pre></pre> 或 <p></p>标签做详述,各个参数的说明,返回值的说明。
接口中的方法均需注释,类中的public protected方法必须注释,private方法除非望文生义,否则也需要注释。

# 12.1.3 格式

或许你认为“让代码能工作”才是专业开发者的头等大事,但我希望我们的团队成员能够抛掉这种想法。
我们今天实现的所有功能,都有可能在交付客户后,给客户的开发人员进行修改和扩展,代码的可读性会对以后可能发生的修改行为产生深远的影响。
代码的整洁、一致性能够在第一次被阅读时就表现出我们对细节的关注,从而让客户感受到我们的专业。而反过来,如果他们看到的是一堆鬼画符般的代码,那他们多半会认为我们对项目的其它方面的细节也漠不关心,细节决定成败。

缩进

4个空格或者1个tab作为一个缩进排版的单位,每一个层级与上一层级相比往右缩进一单位。

private void initSqlValue(){
    if(hasInitValue){
        return;
    }
    hasInitValue = true;
    if (QueryOP.IN.equals(operation))
    {
        this.value = getInValueSql();
    }
    if(BeanUtils.isNotEmpty(value)){
        //TODO
    }
}

多个变量或参数用逗号分隔时,逗号后面留一个空格

/**
 * 获取返回对象
 * @param result  执行结果:true > 成功, false > 失败
 * @param message 返回的消息
 * @return
 */
public Map<String, Object> modelMap(Boolean result, String message){
    Map<String, Object> modelMap = modelMap();
    modelMap.put("result", result);
    modelMap.put("message", message);
    return modelMap;
}

行长度
一般情况避免一行的长度超过80个字符。
但实际开发中,也不需要限制在这个数量,超出几个字符问题不大,但如果超过90或100,则应该使用换行了。

水平对齐
对于一组声明中的变量名,或者一组赋值语句的右值。请尽量对齐。

protected String createBy;
protected String updateBy;
protected Date   createTime;
protected Date   updateTime;
protected String createOrgId;

换行
多参数换行

private static void fitCubi(Point2D.Double[] d,
                  int first,
                  int last,
                  Point2D.Double tHat1,
                  Point2D.Double tHat2) {}

条件换行

if(! attributes.containsKey(key)
  || oldValue != newValue
  || (oldValue != null && newValue != null))

三元运算符换行

return (drawBound==null)
       ? new Rectangle2D.Double(0, 0, -1, -1)
       : (Rectangle2D.Double) drawBound.clone();

# 12.1.4 命名

系统中命名使用英文单词或缩写,不要使用拼音
命名格式主要使用驼峰式命名,即多个单词构成的名称,第一个单词第一个字母小写,其它的单词第一个字母大写,比如orgName,processCmd等等。
但对于一些方法的传入参数或局部变量则可以根据实际需要来选择驼峰式命名或下划线分隔,比如weight_g(表示重量,按克计算),start_ms(开始时间,用毫秒ms表示)等。

命名的原则

  1. 选择专业的词下面提供了常用的单词和说明。单词的选择关键在于动词,名词比较容易选。

常用动词

英文 缩写 中文 英文 缩写 中文
query 查询 set 设置
find 查找 clear 清空
get 获取 dump 写入
display 显示 refresh 刷新
initialize init 初始化 send 发送
manage 管理 receive rec 接收
add 增加 retry 重试
edit 编辑 format 格式化
validate 验证 calculate calc 计算
print 页面打印 sum 总计
create 创建 evaluate eval 评估
update 更新 execute exec 执行
save 保存 start 开启
remove 删除 launch/begin/open 发动/开始/开启
rebuild 重建 stop 停止
do 操作 run 运行
import 导入 fork 分支
export 导出 complete 完成
show 显示 modify 修改
delete 删除 finish 完成

助词

英文 缩写 中文 英文 缩写 中文
abstract 抽象的 current curr 当前
first 第一个 last 最后一个
previous prev 前一个 next 下一个
  1. 避免泛泛的名字 避免使用如abc、a1、a2、tmp、temp、retval、flag、foo等等没有明显含义的名字。 除了某些特定的情况,如for循环中使用i,冒泡算法中的temp等,但是绝大多数情况,请起一个有意义的名字。

  2. 用具体的名字代替抽象的名字 比如fruit(水果)对比apple、orange;vehicle(交通工具)对比car、bike等; 除非抽象的名字用在抽象类或者接口中。

  3. 为名字附加更多的信息 诸如id、name、number、key等等变量,如果不是处在实体类中,而是某个方法内的局部变量,那么这些名字的可读性很有限。增加一些附加信息会避免误解。 如orgId、orgName、repeat_number、orgKey等等。

  4. 增加内容的限定 有些名字具备一些默认的含义,比如password,默认就是加密的密码,但是如果你希望传递的是明文的密码,那么最好加上一些限定会避免误解,比如 plaintext_password 或者 plaintextPassword。

  5. 增加单位描述 这里主要针对数值,比如某个数值表示时、分、秒、毫秒、千克、克或者其它计量单位,那么单纯一个weight、start、end等等,不足以描述清楚,而且如果弄错了单位,可能导致其它的错误。这时候可以加上单位: 如:weight_kg、weight_g、start_ms、end_s等等。

  6. 关于缩写 不要随便缩写,很容易造成理解困难。 缩写遵循以下原则:

  7. 有通用缩写采用通用缩写

  8. 无通用缩写:

  9. 单词小于等于6位,无需缩写

  10. 当整个名称的单词小于或等于3个,无需缩写

  11. 当整个名称的单词超出3个,使用单词的前四位字母作为缩写代替,然后在注释中说明

  12. 关于长名字 长名字带来丰富信息的同时,某些开发人员会认为它难以输入。但现在的IDE已经提供了很多快捷方式。比如Eclipse的:

  13. 代码模板(输入syso,自动转成System.out.println("");

  14. 补全或提示:输入前几位字母,输入alt + /,会自动补全或提示。

包命名

包名称均为小写单词,格式如下:
com.[company].[system].[module] 如:com.hotent.base.cache com.hotent.mail.linkman

类命名

  1. 类必须由大写字母开头而单词中的其他字母均为小写;

  2. 如果类名称由多个单词组成,则每个单词的首字母均应为大写例如PartyService;

  3. 如果类名称中包含单词缩写,则这个所写词的每个字母均应大写,如:XMLUtils;

  4. 由于类是设计用来代表对象的,所以在命名类时应尽量选择名词。长度不超过18位;

  5. 实现类在该接口名称后面添加后缀Impl,如MyBatisDaoImpl;

  6. 类方法命名采用动词+名词,如 sendMessage()。

# 12.1.5 单元测试

  • 程序员的价值在于和他人合作,开发出高质量的代码,而不是开发出一堆虫件(bugware)。

  • 程序员必须对自己的代码质量负责,单元测试是对自己代码质量的基本承诺。

  • 程序 = 单元测试(UT) + 代码(CODE)

  • 对于组件化的项目,除了1~2个子项目是可以通过界面进行测试之外,其它的项目均是java代码。如果一个bug不能在单元测试阶段被发现,而是在集成测试,甚至是系统测试阶段才被发现,那么排查和修复成本将是非常高昂的。

  • 不做好单元测试,就会影响团队其他人员的工作。

  • 测试人员有权利对没有做过单元测试的代码说不。

  • 其他的开发人员也有权利对没有做过单元测试的代码说不。

  • 项目经理在日常管理工作中,单元测试是重要的考察部分,也是界定出错责任的关键依据。

  • public和protected的方法都有相应的UT方法。

# 12.2 版本控制

  1. 严禁使用其他人的账号提交代码。

  2. 准备阶段

  3. 本机是否编译通过?

  4. 本机不能编译通过,不允许做提交操作。

  5. 这些代码自己是否能够看明白?

  6. 提交的代码必须是原子性的,如完成了某个功能,修复了某个bug,做了某个页面调整等。

  7. 对于只是自己使用的代码,比如private方法,可以阶段性完成即提交,但最起码自己看得明白,不是垃圾代码。

  8. 先更新、合并代码,每一次要提交前,先查看要提交的文件在SVN服务器上是否有修改。 如NetBeans就是右键点击要提交的目录,选择Subversion显示更改。

  9. 更新合并后,是否编译通过?如编译不通过,则调整代码或更新编译依赖的其它的文件。

  10. 禁止提交的文件

  11. 本地IDE自动生成的文件;

  12. 本地测试的文件,如uploads的图片、附件等。(这些目录应该设置为svn ignore)

  13. 配置文件的本地化修改。比如app.properties,使用了本机的数据库。

  14. 提交代码 —— 逐个文件提交!除了下面这些例外,其它的文件请逐个提交:

  15. 第三方组件的新增或者升级。(注:我方做个性化重构时需要逐一提交)

  16. 某个目录的文件全部是新增的。

  17. 样式设计和设计图片。

  18. 每次提交必须书写明晰的标注逐个文件提交,单独写标注,记录调整历史,方便其他开发人员更新和理解。

  19. 描述本次提交的文件变更内容。

  20. 如果修复了禅道上的bug,请附上BUG ID。

  21. 发布版本周期要短 —— 管理员打Tag这要求项目经理将每个小版本的待开发功能控制在一定数量内,保证周期要短,这样才可以经常打tag发布版本。 步子要小,这样项目才更加可控,每个版本的稳定性才更能保证。

# 12.3 系统调试方法

系统在开发调试过程中,可以通过以下方法进行前后端的调试。首先以调试模式运行项目,然后在前端通过浏览器的调试工具(以Chrome为例,F12打开调试工具)找到页面请求的后端地址。

最后再通过搜索/api/user/v1找到对应controller代码,在对应的方法处打上调试断点即可进入调试模式。

# 12.4 调优方法

系统在使用过程中,随着数据量的增加及新功能的开发,可能某些功能会出现性能的问题,例如加载数据很慢、提交和保存很慢等情况。这一类的问题通过druid来监视SQL的执行效率,通过以下访问地址可以打开druid的管理界面:http://ip:port/druid, 如下图所示,输入admin/admin可以进入管理页面。

通过分析SQL监控页面的慢查询,可以找到待优化的SQL,通过增减索引或者调整SQL语句的方式来进行优化。
另外针对数据量特别大的表,可以通过创建历史表,定期归集数据到历史表。或者创建表分区的方式来优化。